-
Notifications
You must be signed in to change notification settings - Fork 5k
Fix PathWatcherManager deadlock and UAF in deferred deinit #28088
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 2 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
6fd7fd9
Fix PathWatcherManager deadlock and UAF in deferred deinit
claude 599c9b5
[autofix.ci] apply automated fixes
autofix-ci[bot] b674d12
Fix UAF/double-free in PathWatcherManager.onError
claude 8299d81
Fix UAF and double-free in PathWatcher/Watcher cleanup
claude 5745529
Fix WatcherTrace fd leak in Watcher.deinit else branch
claude b9aba1c
Fix UAF, double-deinit, and TOCTOU races in PathWatcher
claude f692020
Fix watcher_count TOCTOU and Watcher leak on error path
claude 823908a
Fix deadlock in PathWatcherManager.deinit()
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { bunEnv, bunExe, tempDir } from "harness"; | ||
|
|
||
| describe("fs.watch deadlock", () => { | ||
| test("rapid create/close of recursive watchers should not hang", async () => { | ||
|
robobun marked this conversation as resolved.
|
||
| // Create a deep directory tree to ensure DirectoryRegisterTask runs on | ||
| // the work pool, increasing the chance of the close() racing with it. | ||
| using dir = tempDir("watch-deadlock", { | ||
| "a/b/c/d/e/f1.txt": "x", | ||
| "a/b/c/d/e/f2.txt": "x", | ||
| "a/b/c/d/f3.txt": "x", | ||
| "a/b/c/f4.txt": "x", | ||
| "a/b/f5.txt": "x", | ||
| "g/h/i/j/f6.txt": "x", | ||
| "g/h/i/f7.txt": "x", | ||
| "g/h/f8.txt": "x", | ||
| "k/l/m/f9.txt": "x", | ||
| "k/l/f10.txt": "x", | ||
| }); | ||
|
|
||
| // Spawn a subprocess that rapidly creates and closes recursive watchers. | ||
| // Before the fix, this could deadlock when: | ||
| // 1. unregisterWatcher() called deinit() while holding this.mutex | ||
| // (deinit re-acquires the same non-recursive mutex → self-deadlock) | ||
| // 2. unrefPendingTask() called deinit() while holding mutex → UAF | ||
| // 3. processWatcher held watcher.mutex while calling _decrementPathRef | ||
| // which acquires manager.mutex → AB/BA with unregisterWatcher | ||
| // | ||
| // The subprocess has a 10s timeout: if it hangs, it's a deadlock. | ||
| await using proc = Bun.spawn({ | ||
| cmd: [ | ||
| bunExe(), | ||
| "-e", | ||
| ` | ||
| const fs = require("fs"); | ||
| const dir = process.argv[1]; | ||
|
|
||
| // Timeout: if we hang for 10s, it's a deadlock | ||
| const timer = setTimeout(() => { | ||
| process.stderr.write("DEADLOCK: hung for 10 seconds\\n"); | ||
| process.exit(1); | ||
| }, 10000); | ||
| timer.unref(); | ||
|
|
||
| let done = 0; | ||
| const total = 50; | ||
|
|
||
| for (let i = 0; i < total; i++) { | ||
| // Stagger slightly to increase thread interleaving | ||
| const w = fs.watch(dir, { recursive: true }, () => {}); | ||
| // Close immediately — racing with directory scanning on worker thread | ||
| w.close(); | ||
| done++; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // If we reach here without deadlocking, success | ||
| console.log("OK " + done); | ||
| `, | ||
| String(dir), | ||
| ], | ||
| env: bunEnv, | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
|
|
||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
|
|
||
| expect(stderr.replace(/^WARNING: ASAN.*\n?/gm, "")).toBe(""); | ||
| expect(stdout).toStartWith("OK"); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.