Skip to content

Commit 5589e02

Browse files
committed
fix(linter): prevent unsound use of Allocator across threads
1 parent 7223686 commit 5589e02

File tree

4 files changed

+76
-16
lines changed

4 files changed

+76
-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: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,61 @@ 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: Not safe by itself. `MessageCloner` handles synchronization with a `Mutex`.
198+
unsafe impl Send for UnsafeAllocatorRef<'_> {}
199+
200+
/// Wrapper around an [`Allocator`] which allows safely using it on multiple threads to
201+
/// clone [`Message`]s into.
202+
pub struct MessageCloner<'a>(Mutex<UnsafeAllocatorRef<'a>>);
203+
204+
impl<'a> MessageCloner<'a> {
205+
/// Wrap an [`Allocator`] in a [`MessageCloner`].
206+
///
207+
/// This method takes a `&mut Allocator`, to ensure that no other references to the `Allocator`
208+
/// can exist, which guarantees no other threads can allocate with the `Allocator` while this
209+
/// `MessageCloner` exists.
210+
#[inline]
211+
#[expect(clippy::needless_pass_by_ref_mut)]
212+
pub fn new(allocator: &'a mut Allocator) -> Self {
213+
Self(Mutex::new(UnsafeAllocatorRef(allocator)))
214+
}
215+
216+
/// Clone a [`Message`] into the [`Allocator`] held by this [`MessageCloner`].
217+
///
218+
/// # Panics
219+
/// Panics if the underlying `Mutex` is poisoned.
220+
pub fn clone_message(&self, message: &Message) -> Message<'a> {
221+
// Obtain an exclusive lock on the `Mutex` during `clone_in` operation,
222+
// to ensure no other thread can be simultaneously using the `Allocator`
223+
let guard = self.0.lock().unwrap();
224+
let allocator = guard.0;
225+
message.clone_in(allocator)
226+
}
227+
}
228+
}
229+
use message_cloner::MessageCloner;
230+
176231
impl Runtime {
177232
pub(super) fn new(
178233
linter: Linter,
@@ -588,12 +643,12 @@ impl Runtime {
588643
#[cfg(feature = "language_server")]
589644
pub(super) fn run_source<'a>(
590645
&mut self,
591-
allocator: &'a oxc_allocator::Allocator,
646+
allocator: &'a mut oxc_allocator::Allocator,
592647
) -> Vec<MessageWithPosition<'a>> {
593-
use oxc_allocator::CloneIn;
594-
use oxc_data_structures::rope::Rope;
595648
use std::sync::Mutex;
596649

650+
use oxc_data_structures::rope::Rope;
651+
597652
use crate::{
598653
FixWithPosition,
599654
fixer::{Fix, PossibleFixesWithPosition},
@@ -614,6 +669,9 @@ impl Runtime {
614669
}
615670
}
616671

672+
// Wrap allocator in `MessageCloner` so can clone `Message`s into it
673+
let message_cloner = MessageCloner::new(allocator);
674+
617675
let messages = Mutex::new(Vec::<MessageWithPosition<'a>>::new());
618676
let (sender, _receiver) = mpsc::channel();
619677
rayon::scope(|scope| {
@@ -655,7 +713,7 @@ impl Runtime {
655713

656714
messages.lock().unwrap().extend(section_messages.iter().map(
657715
|message| {
658-
let message = message.clone_in(allocator);
716+
let message = message_cloner.clone_message(message);
659717

660718
let labels = &message.error.labels.clone().map(|labels| {
661719
labels
@@ -742,13 +800,15 @@ impl Runtime {
742800
#[cfg(test)]
743801
pub(super) fn run_test_source<'a>(
744802
&mut self,
745-
allocator: &'a Allocator,
803+
allocator: &'a mut Allocator,
746804
check_syntax_errors: bool,
747805
tx_error: &DiagnosticSender,
748806
) -> Vec<Message<'a>> {
749-
use oxc_allocator::CloneIn;
750807
use std::sync::Mutex;
751808

809+
// Wrap allocator in `MessageCloner` so can clone `Message`s into it
810+
let message_cloner = MessageCloner::new(allocator);
811+
752812
let messages = Mutex::new(Vec::<Message<'a>>::new());
753813
rayon::scope(|scope| {
754814
self.resolve_modules(scope, check_syntax_errors, tx_error, |me, mut module| {
@@ -773,13 +833,13 @@ impl Runtime {
773833
.map(|err| Message::new(err, PossibleFixes::None))
774834
.collect(),
775835
}
776-
.into_iter()
777-
.map(|mut message| {
836+
.iter_mut()
837+
.map(|message| {
778838
if section.source.start != 0 {
779839
message.move_offset(section.source.start)
780840
.move_fix_offset(section.source.start);
781841
}
782-
message.clone_in(allocator)
842+
message_cloner.clone_message(message)
783843
}),
784844
);
785845
}

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)