From 4b045af249d0a0cb306976ce57ee883017473938 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 23 Jan 2026 19:31:14 +0100 Subject: [PATCH] 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 --- lib/internal/worker.js | 2 +- .../test-worker-cwd-no-stale-cache.js | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-worker-cwd-no-stale-cache.js diff --git a/lib/internal/worker.js b/lib/internal/worker.js index 08e87d07e7eb80..2a4caed82cf7c5 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -112,8 +112,8 @@ if (isMainThread) { cwdCounter = new Uint32Array(constructSharedArrayBuffer(4)); const originalChdir = process.chdir; process.chdir = function(path) { - AtomicsAdd(cwdCounter, 0, 1); originalChdir(path); + AtomicsAdd(cwdCounter, 0, 1); }; } diff --git a/test/parallel/test-worker-cwd-no-stale-cache.js b/test/parallel/test-worker-cwd-no-stale-cache.js new file mode 100644 index 00000000000000..2ddeee2c10fd11 --- /dev/null +++ b/test/parallel/test-worker-cwd-no-stale-cache.js @@ -0,0 +1,73 @@ +'use strict'; + +// This test verifies that worker threads do not cache stale CWD values +// after process.chdir() has completed in the main thread. +// +// Regression test for a TOCTOU race condition where the atomic counter +// was incremented before chdir() completed, allowing workers to cache +// stale directory paths. + +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); +const { Worker, isMainThread, parentPort } = require('worker_threads'); + +if (!isMainThread) { + // Worker: respond to 'check' messages with current cwd + parentPort.on('message', (msg) => { + if (msg.type === 'check') { + parentPort.postMessage({ + type: 'cwd', + cwd: process.cwd(), + expected: msg.expected, + }); + } + }); + return; +} + +// Main thread +const testDir = __dirname; +const parentDir = path.dirname(testDir); + +// Ensure we start in a known directory +process.chdir(testDir); + +const worker = new Worker(__filename); + +let checksCompleted = 0; +const totalChecks = 100; + +worker.on('message', common.mustCall((msg) => { + if (msg.type === 'cwd') { + // After chdir() has returned in the main thread, the worker + // must see the new directory, not a stale cached value + assert.strictEqual( + msg.cwd, + msg.expected, + `Worker returned stale CWD: got "${msg.cwd}", expected "${msg.expected}"` + ); + checksCompleted++; + + if (checksCompleted < totalChecks) { + // Alternate between directories + const newDir = checksCompleted % 2 === 0 ? testDir : parentDir; + process.chdir(newDir); + // Immediately after chdir returns, ask worker for cwd + worker.postMessage({ type: 'check', expected: newDir }); + } else { + worker.terminate(); + } + } +}, totalChecks)); + +worker.on('online', common.mustCall(() => { + // Start the test cycle + process.chdir(parentDir); + worker.postMessage({ type: 'check', expected: parentDir }); +})); + +worker.on('exit', common.mustCall((code) => { + assert.strictEqual(code, 1); // terminated + assert.strictEqual(checksCompleted, totalChecks); +}));