Skip to content

Commit be2f20d

Browse files
committed
permission: preserve parent allowlist for Worker with empty execArgv
When execArgv is set explicitly (including []), workers no longer inherit the parent's CLI flags. Re-attach Permission Model flags from the parent so empty/modified execArgv does not drop filesystem allowlists. Signed-off-by: yunshingng <yunshingng25@gmail.com>
1 parent 8b9f89f commit be2f20d

2 files changed

Lines changed: 94 additions & 1 deletion

File tree

lib/internal/worker.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const {
55
ArrayPrototypeForEach,
66
ArrayPrototypeMap,
77
ArrayPrototypePush,
8+
ArrayPrototypeSlice,
89
AtomicsAdd,
910
Float64Array,
1011
FunctionPrototypeBind,
@@ -20,6 +21,7 @@ const {
2021
SafeMap,
2122
String,
2223
StringPrototypeTrim,
24+
StringPrototypeStartsWith,
2325
Symbol,
2426
SymbolAsyncDispose,
2527
SymbolFor,
@@ -111,6 +113,8 @@ let debug = require('internal/util/debuglog').debuglog('worker', (fn) => {
111113
const dc = require('diagnostics_channel');
112114
const workerThreadsChannel = dc.channel('worker_threads');
113115

116+
const permission = require('internal/process/permission');
117+
114118
let cwdCounter;
115119
let normalizeHeapProfileOptions;
116120
let normalizeCpuProfileOptions;
@@ -204,6 +208,52 @@ class HeapProfileHandle {
204208
}
205209
}
206210

211+
212+
function ensurePermissionFlagsInExecArgv(execArgv) {
213+
if (!permission.isEnabled() || execArgv == null) {
214+
return execArgv;
215+
}
216+
const flagsToCopy = [
217+
...permission.availableFlags(),
218+
'--permission',
219+
'--permission-audit',
220+
];
221+
const out = ArrayPrototypeSlice(execArgv);
222+
const hasToken = (token) => {
223+
for (let i = 0; i < out.length; i++) {
224+
const a = out[i];
225+
if (a === token) return true;
226+
if (typeof a === 'string' && a.startsWith(`${token}=`)) return true;
227+
}
228+
return false;
229+
};
230+
for (let i = 0; i < process.execArgv.length; i++) {
231+
const arg = process.execArgv[i];
232+
for (let j = 0; j < flagsToCopy.length; j++) {
233+
const flag = flagsToCopy[j];
234+
if (arg === flag) {
235+
if (!hasToken(flag)) {
236+
ArrayPrototypePush(out, arg);
237+
const next = process.execArgv[i + 1];
238+
if (next && !StringPrototypeStartsWith(next, '-')) {
239+
ArrayPrototypePush(out, next);
240+
}
241+
}
242+
} else if (StringPrototypeStartsWith(arg, `${flag}=`)) {
243+
let present = false;
244+
for (let k = 0; k < out.length; k++) {
245+
if (out[k] === arg || StringPrototypeStartsWith(out[k], `${flag}=`)) {
246+
present = true;
247+
break;
248+
}
249+
}
250+
if (!present) ArrayPrototypePush(out, arg);
251+
}
252+
}
253+
}
254+
return out;
255+
}
256+
207257
class Worker extends EventEmitter {
208258
constructor(filename, options = kEmptyObject) {
209259
throwIfBuildingSnapshot('Creating workers');
@@ -218,6 +268,10 @@ class Worker extends EventEmitter {
218268
if (options.execArgv)
219269
validateArray(options.execArgv, 'options.execArgv');
220270

271+
let workerExecArgv = options.execArgv;
272+
if (workerExecArgv)
273+
workerExecArgv = ensurePermissionFlagsInExecArgv(workerExecArgv);
274+
221275
let argv;
222276
if (options.argv) {
223277
validateArray(options.argv, 'options.argv');
@@ -288,7 +342,7 @@ class Worker extends EventEmitter {
288342
// Set up the C++ handle for the worker, as well as some internal wiring.
289343
this[kHandle] = new WorkerImpl(url,
290344
env === process.env ? null : env,
291-
options.execArgv,
345+
workerExecArgv,
292346
parseResourceLimits(options.resourceLimits),
293347
!!(options.trackUnmanagedFds ?? true),
294348
isInternal,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
const assert = require('assert');
3+
const fs = require('fs');
4+
const path = require('path');
5+
const { spawnSync } = require('child_process');
6+
const tmpdir = require('../common/tmpdir');
7+
tmpdir.refresh();
8+
const allowed = tmpdir.path;
9+
const denied = path.join(tmpdir.path, '..', 'permission-worker-denied-file');
10+
fs.writeFileSync(denied, 'secret\n');
11+
const workerSource = `
12+
const { parentPort } = require('worker_threads');
13+
const fs = require('fs');
14+
const path = ${JSON.stringify(denied)};
15+
let result;
16+
try { result = { ok: true, data: fs.readFileSync(path, 'utf8') }; }
17+
catch (e) { result = { ok: false, code: e.code, message: e.message }; }
18+
parentPort.postMessage(result);
19+
`;
20+
function runCase(label, useEmpty) {
21+
const code = `
22+
const { Worker } = require('worker_threads');
23+
const w = new Worker(${JSON.stringify(workerSource)}, { eval: true, ${useEmpty ? 'execArgv: [],' : ''} });
24+
w.on('message', (msg) => { console.log(JSON.stringify({ label: ${JSON.stringify(label)}, msg })); process.exit(0); });
25+
w.on('error', (err) => { console.error(err); process.exit(1); });
26+
`;
27+
return spawnSync(process.execPath, ['--permission', `--allow-fs-read=${allowed}`, '--allow-worker', '-e', code], { encoding: 'utf8', timeout: 15000 });
28+
}
29+
const defaultWorker = runCase('default', false);
30+
const emptyExec = runCase('empty-execArgv', true);
31+
assert.strictEqual(defaultWorker.status, 0, defaultWorker.stderr);
32+
assert.strictEqual(emptyExec.status, 0, emptyExec.stderr);
33+
const dMsg = JSON.parse(defaultWorker.stdout.trim().split('\n').pop());
34+
const eMsg = JSON.parse(emptyExec.stdout.trim().split('\n').pop());
35+
assert.strictEqual(dMsg.msg.ok, false);
36+
assert.strictEqual(dMsg.msg.code, 'ERR_ACCESS_DENIED');
37+
assert.strictEqual(eMsg.msg.ok, false, JSON.stringify(eMsg));
38+
assert.strictEqual(eMsg.msg.code, 'ERR_ACCESS_DENIED');
39+
console.log('ok - permission inheritance consistent for empty execArgv');

0 commit comments

Comments
 (0)