Skip to content

Commit 1585688

Browse files
committed
fix(linter): prevent unsound use of Allocator across threads
1 parent 37ebff2 commit 1585688

File tree

4 files changed

+87
-16
lines changed

4 files changed

+87
-16
lines changed

crates/oxc_language_server/src/linter/isolated_lint_handler.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@ impl IsolatedLintHandler {
9292
return None;
9393
}
9494

95-
let allocator = Allocator::default();
95+
let mut allocator = Allocator::default();
9696

97-
Some(self.lint_path(&allocator, &path, content).map_or(vec![], |errors| {
97+
Some(self.lint_path(&mut allocator, &path, content).map_or(vec![], |errors| {
9898
let mut diagnostics: Vec<DiagnosticReport> = errors
9999
.iter()
100100
.map(|e| message_with_position_to_lsp_diagnostic_report(e, uri))
@@ -143,7 +143,7 @@ impl IsolatedLintHandler {
143143

144144
fn lint_path<'a>(
145145
&mut self,
146-
allocator: &'a Allocator,
146+
allocator: &'a mut Allocator,
147147
path: &Path,
148148
source_text: Option<String>,
149149
) -> Option<Vec<MessageWithPosition<'a>>> {

crates/oxc_linter/src/service/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ impl LintService {
9696
#[cfg(feature = "language_server")]
9797
pub fn run_source<'a>(
9898
&mut self,
99-
allocator: &'a oxc_allocator::Allocator,
99+
allocator: &'a mut oxc_allocator::Allocator,
100100
) -> Vec<crate::MessageWithPosition<'a>> {
101101
self.runtime.run_source(allocator)
102102
}
@@ -105,7 +105,7 @@ impl LintService {
105105
#[cfg(test)]
106106
pub(crate) fn run_test_source<'a>(
107107
&mut self,
108-
allocator: &'a oxc_allocator::Allocator,
108+
allocator: &'a mut oxc_allocator::Allocator,
109109
check_syntax_errors: bool,
110110
tx_error: &DiagnosticSender,
111111
) -> Vec<crate::Message<'a>> {

crates/oxc_linter/src/service/runtime.rs

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,72 @@ impl RuntimeFileSystem for OsFileSystem {
173173
}
174174
}
175175

176+
/// [`MessageCloner`] is a wrapper around an `&Allocator` which allows it to be safely shared across threads,
177+
/// in order to clone [`Message`]s into it.
178+
///
179+
/// `Allocator` is not thread safe (it is not `Sync`), so cannot be shared across threads.
180+
/// It would be undefined behavior to allocate into an `Allocator` from multiple threads simultaneously.
181+
///
182+
/// `MessageCloner` ensures only one thread at a time can utilize the `Allocator`, by taking an
183+
/// exclusive `&mut Allocator` to start with, and synchronising access to the `Allocator` with a `Mutex`.
184+
///
185+
/// This type is wrapped in a module so that other code cannot access the inner `UnsafeAllocatorRef`
186+
/// directly, and must go via the [`MessageCloner::clone_message`] method.
187+
mod message_cloner {
188+
use std::sync::Mutex;
189+
190+
use oxc_allocator::{Allocator, CloneIn};
191+
192+
use crate::Message;
193+
194+
/// Unsafe wrapper around an `&Allocator` which makes it `Send`.
195+
struct UnsafeAllocatorRef<'a>(&'a Allocator);
196+
197+
// SAFETY: It is sound to implement `Send` for `UnsafeAllocatorRef` because:
198+
// * The only way to construct an `UnsafeAllocatorRef` is via `MessageCloner::new`, which takes
199+
// an exclusive `&mut Allocator`, ensuring no other references to the same `Allocator` exist.
200+
// * The lifetime `'a` ensures that the reference to the `Allocator` cannot outlive the original
201+
// mutable borrow, preventing aliasing or concurrent mutation.
202+
// * All access to the `Allocator` via `UnsafeAllocatorRef` is synchronized by a `Mutex` inside
203+
// `MessageCloner`, so only one thread can access the allocator at a time.
204+
// * The module encapsulation prevents direct access to `UnsafeAllocatorRef`, so it cannot be
205+
// misused outside of the intended, synchronized context.
206+
//
207+
// Therefore, although `Allocator` is not `Sync`, it is safe to send `UnsafeAllocatorRef` between
208+
// threads as long as it is only accessed via the `Mutex` in `MessageCloner`.
209+
unsafe impl Send for UnsafeAllocatorRef<'_> {}
210+
211+
/// Wrapper around an [`Allocator`] which allows safely using it on multiple threads to
212+
/// clone [`Message`]s into.
213+
pub struct MessageCloner<'a>(Mutex<UnsafeAllocatorRef<'a>>);
214+
215+
impl<'a> MessageCloner<'a> {
216+
/// Wrap an [`Allocator`] in a [`MessageCloner`].
217+
///
218+
/// This method takes a `&mut Allocator`, to ensure that no other references to the `Allocator`
219+
/// can exist, which guarantees no other threads can allocate with the `Allocator` while this
220+
/// `MessageCloner` exists.
221+
#[inline]
222+
#[expect(clippy::needless_pass_by_ref_mut)]
223+
pub fn new(allocator: &'a mut Allocator) -> Self {
224+
Self(Mutex::new(UnsafeAllocatorRef(allocator)))
225+
}
226+
227+
/// Clone a [`Message`] into the [`Allocator`] held by this [`MessageCloner`].
228+
///
229+
/// # Panics
230+
/// Panics if the underlying `Mutex` is poisoned.
231+
pub fn clone_message(&self, message: &Message) -> Message<'a> {
232+
// Obtain an exclusive lock on the `Mutex` during `clone_in` operation,
233+
// to ensure no other thread can be simultaneously using the `Allocator`
234+
let guard = self.0.lock().unwrap();
235+
let allocator = guard.0;
236+
message.clone_in(allocator)
237+
}
238+
}
239+
}
240+
use message_cloner::MessageCloner;
241+
176242
impl Runtime {
177243
pub(super) fn new(
178244
linter: Linter,
@@ -588,12 +654,12 @@ impl Runtime {
588654
#[cfg(feature = "language_server")]
589655
pub(super) fn run_source<'a>(
590656
&mut self,
591-
allocator: &'a oxc_allocator::Allocator,
657+
allocator: &'a mut oxc_allocator::Allocator,
592658
) -> Vec<MessageWithPosition<'a>> {
593-
use oxc_allocator::CloneIn;
594-
use oxc_data_structures::rope::Rope;
595659
use std::sync::Mutex;
596660

661+
use oxc_data_structures::rope::Rope;
662+
597663
use crate::{
598664
FixWithPosition,
599665
fixer::{Fix, PossibleFixesWithPosition},
@@ -614,6 +680,9 @@ impl Runtime {
614680
}
615681
}
616682

683+
// Wrap allocator in `MessageCloner` so can clone `Message`s into it
684+
let message_cloner = MessageCloner::new(allocator);
685+
617686
let messages = Mutex::new(Vec::<MessageWithPosition<'a>>::new());
618687
let (sender, _receiver) = mpsc::channel();
619688
rayon::scope(|scope| {
@@ -655,7 +724,7 @@ impl Runtime {
655724

656725
messages.lock().unwrap().extend(section_messages.iter().map(
657726
|message| {
658-
let message = message.clone_in(allocator);
727+
let message = message_cloner.clone_message(message);
659728

660729
let labels = &message.error.labels.clone().map(|labels| {
661730
labels
@@ -742,13 +811,15 @@ impl Runtime {
742811
#[cfg(test)]
743812
pub(super) fn run_test_source<'a>(
744813
&mut self,
745-
allocator: &'a Allocator,
814+
allocator: &'a mut Allocator,
746815
check_syntax_errors: bool,
747816
tx_error: &DiagnosticSender,
748817
) -> Vec<Message<'a>> {
749-
use oxc_allocator::CloneIn;
750818
use std::sync::Mutex;
751819

820+
// Wrap allocator in `MessageCloner` so can clone `Message`s into it
821+
let message_cloner = MessageCloner::new(allocator);
822+
752823
let messages = Mutex::new(Vec::<Message<'a>>::new());
753824
rayon::scope(|scope| {
754825
self.resolve_modules(scope, check_syntax_errors, tx_error, |me, mut module| {
@@ -773,13 +844,13 @@ impl Runtime {
773844
.map(|err| Message::new(err, PossibleFixes::None))
774845
.collect(),
775846
}
776-
.into_iter()
777-
.map(|mut message| {
847+
.iter_mut()
848+
.map(|message| {
778849
if section.source.start != 0 {
779850
message.move_offset(section.source.start)
780851
.move_fix_offset(section.source.start);
781852
}
782-
message.clone_in(allocator)
853+
message_cloner.clone_message(message)
783854
}),
784855
);
785856
}

crates/oxc_linter/src/tester.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ impl Tester {
501501
fix_kind: ExpectFixKind,
502502
fix_index: u8,
503503
) -> TestResult {
504-
let allocator = Allocator::default();
504+
let mut allocator = Allocator::default();
505505
let rule = self.find_rule().read_json(rule_config.unwrap_or_default());
506506
let mut external_plugin_store = ExternalPluginStore::default();
507507
let linter = Linter::new(
@@ -554,7 +554,7 @@ impl Tester {
554554
.with_paths(paths);
555555

556556
let (sender, _receiver) = mpsc::channel();
557-
let result = lint_service.run_test_source(&allocator, false, &sender);
557+
let result = lint_service.run_test_source(&mut allocator, false, &sender);
558558

559559
if result.is_empty() {
560560
return TestResult::Passed;

0 commit comments

Comments
 (0)