Skip to content

Conversation

@overlookmotel
Copy link
Member

@overlookmotel overlookmotel commented Aug 12, 2025

Allocator is currently Sync, but it shouldn't be - Allocator is not thread-safe and allocating into it in multiple threads can cause data corruption or, in worst case, writing out of bounds. I intend to remove the impl Sync for Allocator in a PR to follow.

The code altered in this PR was relying on Allocator being Sync. It was not actually dangerous as it stood, because access to the Allocator was synchronized by always holding a lock on the messages Mutex while using it. However, I suspect this was more by accident by design, and it was rather fragile.

This PR introduces a MessageCloner struct which provides a safe API which ensures access to the Allocator is correctly synchronized.

It's unfortunate to introduce a 2nd Mutex, and the Mutex inside MessageCloner is accessed more than necessary - on each turn of the loop in the map loops, instead of just once at the top of the loop. However, I couldn't find a way to achieve that without resorting to fragile unsafe code.

In any case, from what I can understand, the ultimate callers of both run_source and run_test_source convert the cloned Messages into other owning types (DiagnosticReport or Report) anyway. Therefore the double-conversion may be wasteful anyway, and we might be better off trying to convert to these owned types earlier, and avoid using an Allocator at all in these methods.

I also am wondering in what circumstances error messages would contain arena-allocated strings, and whether Message could instead contain Cow<'static, str>s, in which case the Allocator becomes unnecessary, and cloning would be cheaper.

Delving into the depths of the linter to understand all this is a bit beyond me, so I hope we can accept this imperfect solution for now, and refactor to remove/optimize it later on.

@github-actions github-actions bot added A-linter Area - Linter A-editor Area - Editor and Language Server C-bug Category - Bug labels Aug 12, 2025
Copy link
Member Author

overlookmotel commented Aug 12, 2025


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@codspeed-hq
Copy link

codspeed-hq bot commented Aug 12, 2025

CodSpeed Instrumentation Performance Report

Merging #13032 will not alter performance

Comparing 08-12-fix_linter_prevent_unsound_use_of_allocator_across_threads (b0558a4) with main (ab685bd)1

Summary

✅ 34 untouched benchmarks

Footnotes

  1. No successful run was found on main (b0558a4) during the generation of this report, so ab685bd was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@overlookmotel overlookmotel marked this pull request as ready for review August 12, 2025 16:10
Copilot AI review requested due to automatic review settings August 12, 2025 16:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR addresses a thread-safety issue with the Allocator type by preventing unsound use across multiple threads. The main change introduces a MessageCloner struct that provides synchronized access to an Allocator for cloning Message objects safely in multi-threaded contexts.

  • Introduces MessageCloner wrapper with mutex-based synchronization for thread-safe Allocator access
  • Updates function signatures to take &mut Allocator instead of &Allocator to enforce exclusive ownership
  • Replaces direct clone_in calls with MessageCloner::clone_message method calls

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
crates/oxc_linter/src/tester.rs Updates allocator to mutable reference and passes it to run_test_source
crates/oxc_linter/src/service/runtime.rs Implements MessageCloner struct and updates methods to use synchronized message cloning
crates/oxc_linter/src/service/mod.rs Updates method signatures to accept mutable allocator references
crates/oxc_language_server/src/linter/isolated_lint_handler.rs Updates allocator usage to mutable references throughout the handler

@Sysix Sysix removed their request for review August 12, 2025 17:53
@Sysix
Copy link
Member

Sysix commented Aug 12, 2025

Removed myself as a reviewer because alllocation and async is not my strength.
But maybe this info could still help you:
MessageWithPosition is a language server struct. I implemented the lifetime only because it reflects the Message struct.
Because of that, I needed the Allocator in the run_source for the lifetime.
Just wanted to drop this info, so you understand why this was implemented the way it was.

@overlookmotel
Copy link
Member Author

@Sysix Thanks for the context. The linter is all new to me, so I can't say I understand any of this! It looks to me like the root cause might be that Message doesn't need a lifetime, but I don't know enough to evaluate if that's correct or not.

I've written up observations in #14563 so we can investigate further later on when we have time.

For now, going to merge this.

@overlookmotel overlookmotel added the 0-merge Merge with Graphite Merge Queue label Aug 13, 2025
Copy link
Member Author

overlookmotel commented Aug 13, 2025

Merge activity

`Allocator` is currently `Sync`, but it shouldn't be - `Allocator` is not thread-safe and allocating into it in multiple threads can cause data corruption or, in worst case, writing out of bounds. I intend to remove the `impl Sync for Allocator` in a PR to follow.

The code altered in this PR was relying on `Allocator` being `Sync`. It was *not* actually dangerous as it stood, because access to the `Allocator` was synchronized by always holding a lock on the `messages` `Mutex` while using it. However, I suspect this was more by accident by design, and it was rather fragile.

This PR introduces a `MessageCloner` struct which provides a safe API which ensures access to the `Allocator` is correctly synchronized.

It's unfortunate to introduce a 2nd `Mutex`, and the `Mutex` inside `MessageCloner` is accessed more than necessary - on each turn of the loop in the `map` loops, instead of just once at the top of the loop. However, I couldn't find a way to achieve that without resorting to fragile unsafe code.

In any case, from what I can understand, the ultimate callers of both `run_source` and `run_test_source` convert the cloned `Message`s into other owning types (`DiagnosticReport` or `Report`) anyway. Therefore the double-conversion may be wasteful anyway, and we might be better off trying to convert to these owned types earlier, and avoid using an `Allocator` at all in these methods.

I also am wondering in what circumstances error messages would contain arena-allocated strings, and whether `Message` could instead contain `Cow<'static, str>`s, in which case the `Allocator` becomes unnecessary, and cloning would be cheaper.

Delving into the depths of the linter to understand all this is a bit beyond me, so I hope we can accept this imperfect solution for now, and refactor to remove/optimize it later on.
@graphite-app graphite-app bot force-pushed the 08-12-fix_linter_prevent_unsound_use_of_allocator_across_threads branch from 1585688 to b0558a4 Compare August 13, 2025 11:48
@graphite-app graphite-app bot merged commit b0558a4 into main Aug 13, 2025
27 checks passed
@graphite-app graphite-app bot deleted the 08-12-fix_linter_prevent_unsound_use_of_allocator_across_threads branch August 13, 2025 11:51
@graphite-app graphite-app bot removed the 0-merge Merge with Graphite Merge Queue label Aug 13, 2025
graphite-app bot pushed a commit that referenced this pull request Aug 15, 2025
#13132)

`MessageCloner` (introduced in #13032) is only used when `language_server` feature is enabled, or in tests. Feature-gate this code, to avoid "dead code" warnings when compiling `napi/oxlint2`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-editor Area - Editor and Language Server A-linter Area - Linter C-bug Category - Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants