Skip to content

Commit 89f8122

Browse files
committed
fix(watcher): serialize watch and close on the native watcher
`NativeWatcher::watch` spawns a detached task that drives the FSEvents backend's `append_path` (mutating the watcher's CFMutableArray), while `NativeWatcher::close` drops the watcher (`CFRelease` of that array). Nothing kept the two apart, so the close-old-then-watch-new restart flow could free the CFArray while a watch task was still appending to it — a use-after-free that aborts the process on macOS. Guard every access to the watcher with an async mutex: a watch task acquires it before touching the watcher and holds it for the whole `watch()`, and `close` aborts the in-flight task and then acquires the same mutex before tearing the watcher down. A future can only be cancelled at an `.await`, never mid-FFI, so an aborted task always releases the guard with the watcher in a consistent state, and holding the mutex in `close` proves no task is inside `append_path`. Adds `rspack_napi::runtime::spawn_handle` to retain the JoinHandle needed to abort.
1 parent cd0781d commit 89f8122

3 files changed

Lines changed: 28 additions & 3 deletions

File tree

crates/rspack_binding_api/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ serde = { workspace = true }
117117
serde_json = { workspace = true }
118118
simd-json = { workspace = true }
119119
swc_core = { workspace = true, default-features = false, features = ["ecma_transforms_react"] }
120-
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros", "test-util", "tracing", "parking_lot"] }
120+
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros", "sync", "test-util", "tracing", "parking_lot"] }
121121
ustr = { workspace = true }
122122

123123
[package.metadata.cargo-shear]

crates/rspack_binding_api/src/native_watcher.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::{
22
boxed::Box,
33
panic::AssertUnwindSafe,
44
path::{Path, PathBuf},
5+
sync::Arc,
56
time::{Duration, SystemTime, UNIX_EPOCH},
67
};
78

@@ -59,6 +60,8 @@ pub struct NativeWatchUndelayedEvent {
5960
pub struct NativeWatcher {
6061
watcher: FsWatcher,
6162
closed: bool,
63+
watch_task: Option<tokio::task::JoinHandle<()>>,
64+
watcher_lock: Arc<tokio::sync::Mutex<()>>,
6265
}
6366

6467
fn timestamp_to_system_time(millis: u64) -> SystemTime {
@@ -81,6 +84,8 @@ impl NativeWatcher {
8184
Self {
8285
watcher,
8386
closed: false,
87+
watch_task: None,
88+
watcher_lock: Arc::new(tokio::sync::Mutex::new(())),
8489
}
8590
}
8691

@@ -110,8 +115,15 @@ impl NativeWatcher {
110115

111116
let start_time = start_time.get_u64().1;
112117

118+
if let Some(prev) = self.watch_task.take() {
119+
prev.abort();
120+
}
121+
122+
let watcher_lock = Arc::clone(&self.watcher_lock);
123+
let mut watch_task = None;
113124
reference.share_with(env, |native_watcher| {
114-
rspack_napi::runtime::spawn(async move {
125+
watch_task = Some(rspack_napi::runtime::spawn_handle(async move {
126+
let _guard = watcher_lock.lock_owned().await;
115127
native_watcher
116128
.watcher
117129
.watch(
@@ -123,9 +135,10 @@ impl NativeWatcher {
123135
Box::new(js_event_handler_undelayed),
124136
)
125137
.await
126-
});
138+
}));
127139
Ok(())
128140
})?;
141+
self.watch_task = watch_task;
129142

130143
Ok(())
131144
}
@@ -160,6 +173,10 @@ impl NativeWatcher {
160173
let shared_reference = reference.share_with(Env::from_raw(env.raw()), |native_watcher| {
161174
rspack_napi::runtime::spawn(async move {
162175
let result = AssertUnwindSafe(async {
176+
if let Some(task) = native_watcher.watch_task.take() {
177+
task.abort();
178+
}
179+
let _guard = Arc::clone(&native_watcher.watcher_lock).lock_owned().await;
163180
native_watcher
164181
.watcher
165182
.close()

crates/rspack_napi/src/runtime.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ where
3838
std::mem::drop(spawn_inner(future));
3939
}
4040

41+
pub fn spawn_handle<F>(future: F) -> tokio::task::JoinHandle<F::Output>
42+
where
43+
F: Future + Send + 'static,
44+
F::Output: Send + 'static,
45+
{
46+
spawn_inner(future)
47+
}
48+
4149
pub fn block_on<F: Future>(future: F) -> F::Output {
4250
with_runtime(|runtime| runtime.block_on(future))
4351
}

0 commit comments

Comments
 (0)