Created
July 17, 2026 13:29
-
-
Save stormslowly/b44c6b1540bc70a98a2aeaa8695e13a9 to your computer and use it in GitHub Desktop.
Repro: rspack NativeWatcher concurrent &mut race crash (<= 2.1.4, macOS FSEvents use-after-free)
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
| // Repro for a concurrent `&mut` race in NativeWatcher (rspack <= 2.1.4). | |
| // | |
| // Usage: | |
| // npm i @rspack/binding@2.1.4 | |
| // node repro-native-watcher-crash.js | |
| // | |
| // On macOS the process dies within a few iterations, typically with: | |
| // NSInvalidArgumentException: -[OS_os_log addObject:]: unrecognized selector | |
| // (use-after-free on notify's FSEvents watched-paths CFMutableArray). | |
| // Debug builds instead hit tokio's `!curr.is_join_waker_set()` assertion. | |
| const fs = require("node:fs"); | |
| const os = require("node:os"); | |
| const path = require("node:path"); | |
| const binding = require("@rspack/binding"); | |
| const ITERATIONS = 30; | |
| const WATCHES_PER_ITERATION = 5; | |
| const FILE_COUNT = 400; | |
| const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rspack-native-watcher-")); | |
| const files = []; | |
| const dirs = [dir]; | |
| for (let i = 0; i < FILE_COUNT; i++) { | |
| const sub = path.join(dir, `sub${i % 20}`); | |
| fs.mkdirSync(sub, { recursive: true }); | |
| const file = path.join(sub, `f${i}.js`); | |
| fs.writeFileSync(file, `module.exports = ${i};`); | |
| files.push(file); | |
| if (!dirs.includes(sub)) dirs.push(sub); | |
| } | |
| const startWatch = (watcher) => { | |
| watcher.watch( | |
| [files, []], | |
| [dirs, []], | |
| [[], []], | |
| BigInt(Date.now()), | |
| () => {}, | |
| () => {}, | |
| ); | |
| }; | |
| (async () => { | |
| console.log(`pid: ${process.pid}`); | |
| for (let i = 0; i < ITERATIONS; i++) { | |
| const watcher = new binding.NativeWatcher({ aggregateTimeout: 1 }); | |
| for (let j = 0; j < WATCHES_PER_ITERATION; j++) startWatch(watcher); | |
| await watcher.close(); | |
| try { | |
| startWatch(watcher); | |
| } catch (e) { | |
| /* expected: has been closed */ | |
| } | |
| console.log(`iteration ${i + 1}/${ITERATIONS} survived`); | |
| } | |
| console.log("ALL ITERATIONS SURVIVED (no crash)"); | |
| fs.rmSync(dir, { recursive: true, force: true }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment