@@ -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