Skip to content

Commit 9509faf

Browse files
saagar210claude
andcommitted
fix(src): close KB watcher start/start TOCTOU race
start_kb_watcher_impl previously performed the lifecycle steps in this order: 1. KbWatcher::new(&path) 2. watcher.start() (spawns notify threads, starts watching) 3. KB_WATCHER.lock() 4. *guard = Some(watcher) Between 2 and 3 the watcher is already running on the filesystem but unreferenced by the global slot. Two concurrent start_kb_watcher invocations — e.g., a settings "Start" click that double-fires, or a frontend that retries after a timeout while the first call is still in flight — could each pass their own check-then-act without observing each other, and both reach step 4 with their own watcher instance. The second guard assignment would overwrite the first, but the first watcher's notify threads keep running on the filesystem as an orphan with no handle for stop_kb_watcher to reach. Fix: hold the KB_WATCHER StdMutex guard across the full check/create/start/store sequence. A racing second starter now observes the populated slot and returns Ok(false) without ever constructing a second KbWatcher. KbWatcher::new and watcher.start() are synchronous, so holding the guard across them does not block other async tasks on the runtime; the tokio::spawn event-forwarder is moved outside the critical section so receiving events never contends with KB commands. Added a SAFETY-style explanatory comment inline; no API or behavior change for callers other than the correct race resolution. cargo check --all-targets clean; cargo test --lib passes 311/312 (one pre-existing #[ignore]'d model-download test). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent aee527a commit 9509faf

1 file changed

Lines changed: 23 additions & 8 deletions

File tree

src-tauri/src/commands/kb_commands.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -260,17 +260,32 @@ pub(crate) async fn start_kb_watcher_impl(
260260

261261
let validated_path = validate_stored_kb_path(&folder_path)?;
262262

263-
// Create and start watcher
264-
let mut watcher = KbWatcher::new(&validated_path).map_err(|e| e.to_string())?;
265-
let mut rx = watcher.start().map_err(|e| e.to_string())?;
266-
267-
// Store watcher instance
268-
{
263+
// Acquire the global watcher slot up-front, then create and start the
264+
// watcher *inside* the critical section. This closes a TOCTOU race
265+
// where two concurrent start_kb_watcher_impl calls could each
266+
// successfully construct + start their own KbWatcher instance before
267+
// either reached the KB_WATCHER.lock() — one of the instances would
268+
// then be overwritten in the slot while still running on the
269+
// filesystem as an orphan notify thread. With the lock held for the
270+
// full init sequence, a racing second starter observes the populated
271+
// slot and returns Ok(false) without ever creating its own watcher.
272+
//
273+
// KbWatcher::new and watcher.start() are synchronous — no .await is
274+
// performed while the StdMutex guard is held, so this is safe under
275+
// tokio's work-stealing scheduler.
276+
let mut rx = {
269277
let mut guard = KB_WATCHER.lock().map_err(|e| e.to_string())?;
278+
if guard.is_some() {
279+
return Ok(false);
280+
}
281+
let mut watcher = KbWatcher::new(&validated_path).map_err(|e| e.to_string())?;
282+
let rx = watcher.start().map_err(|e| e.to_string())?;
270283
*guard = Some(watcher);
271-
}
284+
rx
285+
};
272286

273-
// Spawn event handler
287+
// Spawn event handler outside the lock so receiving doesn't starve
288+
// other KB commands.
274289
let window_clone = window.clone();
275290
tokio::spawn(async move {
276291
while let Some(event) = rx.recv().await {

0 commit comments

Comments
 (0)