Skip to content

Commit 4b045af

Browse files
committed
worker: fix TOCTOU race in CWD caching
The atomic counter used to signal CWD changes to worker threads was being incremented before chdir() completed, creating a race window where workers could cache stale directory paths with the new counter value. This caused process.cwd() in workers to return incorrect values until the next chdir() call. Fix by reordering operations: call originalChdir() first, then increment the counter. This ensures workers never cache stale data while believing it is current. Reported-by: Giulio Comi Reported-by: Caleb Everett
1 parent 81e05e1 commit 4b045af

File tree

2 files changed

+74
-1
lines changed

2 files changed

+74
-1
lines changed

lib/internal/worker.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ if (isMainThread) {
112112
cwdCounter = new Uint32Array(constructSharedArrayBuffer(4));
113113
const originalChdir = process.chdir;
114114
process.chdir = function(path) {
115-
AtomicsAdd(cwdCounter, 0, 1);
116115
originalChdir(path);
116+
AtomicsAdd(cwdCounter, 0, 1);
117117
};
118118
}
119119

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use strict';
2+
3+
// This test verifies that worker threads do not cache stale CWD values
4+
// after process.chdir() has completed in the main thread.
5+
//
6+
// Regression test for a TOCTOU race condition where the atomic counter
7+
// was incremented before chdir() completed, allowing workers to cache
8+
// stale directory paths.
9+
10+
const common = require('../common');
11+
const assert = require('assert');
12+
const path = require('path');
13+
const { Worker, isMainThread, parentPort } = require('worker_threads');
14+
15+
if (!isMainThread) {
16+
// Worker: respond to 'check' messages with current cwd
17+
parentPort.on('message', (msg) => {
18+
if (msg.type === 'check') {
19+
parentPort.postMessage({
20+
type: 'cwd',
21+
cwd: process.cwd(),
22+
expected: msg.expected,
23+
});
24+
}
25+
});
26+
return;
27+
}
28+
29+
// Main thread
30+
const testDir = __dirname;
31+
const parentDir = path.dirname(testDir);
32+
33+
// Ensure we start in a known directory
34+
process.chdir(testDir);
35+
36+
const worker = new Worker(__filename);
37+
38+
let checksCompleted = 0;
39+
const totalChecks = 100;
40+
41+
worker.on('message', common.mustCall((msg) => {
42+
if (msg.type === 'cwd') {
43+
// After chdir() has returned in the main thread, the worker
44+
// must see the new directory, not a stale cached value
45+
assert.strictEqual(
46+
msg.cwd,
47+
msg.expected,
48+
`Worker returned stale CWD: got "${msg.cwd}", expected "${msg.expected}"`
49+
);
50+
checksCompleted++;
51+
52+
if (checksCompleted < totalChecks) {
53+
// Alternate between directories
54+
const newDir = checksCompleted % 2 === 0 ? testDir : parentDir;
55+
process.chdir(newDir);
56+
// Immediately after chdir returns, ask worker for cwd
57+
worker.postMessage({ type: 'check', expected: newDir });
58+
} else {
59+
worker.terminate();
60+
}
61+
}
62+
}, totalChecks));
63+
64+
worker.on('online', common.mustCall(() => {
65+
// Start the test cycle
66+
process.chdir(parentDir);
67+
worker.postMessage({ type: 'check', expected: parentDir });
68+
}));
69+
70+
worker.on('exit', common.mustCall((code) => {
71+
assert.strictEqual(code, 1); // terminated
72+
assert.strictEqual(checksCompleted, totalChecks);
73+
}));

0 commit comments

Comments
 (0)