Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion lib/internal/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
ArrayPrototypeForEach,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeSlice,
AtomicsAdd,
Float64Array,
FunctionPrototypeBind,
Expand All @@ -20,6 +21,7 @@ const {
SafeMap,
String,
StringPrototypeTrim,
StringPrototypeStartsWith,
Symbol,
SymbolAsyncDispose,
SymbolFor,
Expand Down Expand Up @@ -111,6 +113,8 @@ let debug = require('internal/util/debuglog').debuglog('worker', (fn) => {
const dc = require('diagnostics_channel');
const workerThreadsChannel = dc.channel('worker_threads');

const permission = require('internal/process/permission');

let cwdCounter;
let normalizeHeapProfileOptions;
let normalizeCpuProfileOptions;
Expand Down Expand Up @@ -204,6 +208,57 @@ class HeapProfileHandle {
}
}


function ensurePermissionFlagsInExecArgv(execArgv) {
if (!permission.isEnabled() || execArgv == null) {
return execArgv;
}

const flagsToCopy = [
...permission.availableFlags(),
'--permission',
'--permission-audit',
];
const out = ArrayPrototypeSlice(execArgv);

function indexOfFlag(token) {
for (let i = 0; i < out.length; i++) {
const a = out[i];
if (a === token) {
return i;
}
if (StringPrototypeStartsWith(a, `${token}=`)) {
return i;
}
}
return -1;
}

for (let i = 0; i < process.execArgv.length; i++) {
const arg = process.execArgv[i];
for (let j = 0; j < flagsToCopy.length; j++) {
const flag = flagsToCopy[j];
if (arg === flag) {
if (indexOfFlag(flag) === -1) {
ArrayPrototypePush(out, arg);
const next = process.execArgv[i + 1];
if (next !== undefined && !StringPrototypeStartsWith(next, '-')) {
ArrayPrototypePush(out, next);
}
}
break;
}
if (StringPrototypeStartsWith(arg, `${flag}=`)) {
if (indexOfFlag(flag) === -1) {
ArrayPrototypePush(out, arg);
}
break;
}
}
}
return out;
}

class Worker extends EventEmitter {
constructor(filename, options = kEmptyObject) {
throwIfBuildingSnapshot('Creating workers');
Expand All @@ -218,6 +273,10 @@ class Worker extends EventEmitter {
if (options.execArgv)
validateArray(options.execArgv, 'options.execArgv');

let workerExecArgv = options.execArgv;
if (workerExecArgv)
workerExecArgv = ensurePermissionFlagsInExecArgv(workerExecArgv);

let argv;
if (options.argv) {
validateArray(options.argv, 'options.argv');
Expand Down Expand Up @@ -288,7 +347,7 @@ class Worker extends EventEmitter {
// Set up the C++ handle for the worker, as well as some internal wiring.
this[kHandle] = new WorkerImpl(url,
env === process.env ? null : env,
options.execArgv,
workerExecArgv,
parseResourceLimits(options.resourceLimits),
!!(options.trackUnmanagedFds ?? true),
isInternal,
Expand Down
93 changes: 93 additions & 0 deletions test/parallel/test-permission-worker-empty-execargv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict';

// Consistency under the Permission Model:
// Worker with explicit execArgv: [] must keep the same filesystem allowlist
// as a default Worker (empty/modified execArgv must not drop parent limits).

const common = require('../common');
const { isMainThread } = require('worker_threads');

if (!isMainThread) {
common.skip('This test only works on a main thread');
}

const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const tmpdir = require('../common/tmpdir');

tmpdir.refresh();

const allowed = tmpdir.path;
const deniedFile = path.join(tmpdir.path, '..', 'permission-worker-denied-file');
fs.writeFileSync(deniedFile, 'secret\n');

const workerSource = `
const { parentPort } = require('worker_threads');
const fs = require('fs');
const denied = ${JSON.stringify(deniedFile)};
let result;
try {
result = { ok: true, data: fs.readFileSync(denied, 'utf8') };
} catch (err) {
result = { ok: false, code: err.code, message: err.message };
}
parentPort.postMessage(result);
`;

function runCase(label, useEmptyExecArgv) {
const execArgvLine = useEmptyExecArgv ? 'execArgv: [],' : '';
const code = `
const { Worker } = require('worker_threads');
const w = new Worker(${JSON.stringify(workerSource)}, {
eval: true,
${execArgvLine}
});
w.on('message', (msg) => {
process.stdout.write(JSON.stringify({ label: ${JSON.stringify(label)}, msg }) + '\\n');
process.exit(0);
});
w.on('error', (err) => {
console.error(err);
process.exit(1);
});
`;
return spawnSync(
process.execPath,
[
'--permission',
`--allow-fs-read=${allowed}`,
'--allow-worker',
'-e',
code,
],
{
encoding: 'utf8',
timeout: 15000,
},
);
}

const defaultWorker = runCase('default', false);
const emptyExecArgv = runCase('empty-execArgv', true);

assert.strictEqual(defaultWorker.status, 0, defaultWorker.stderr);
assert.strictEqual(emptyExecArgv.status, 0, emptyExecArgv.stderr);

const defaultMsg = JSON.parse(defaultWorker.stdout.trim().split('\n').pop());
const emptyMsg = JSON.parse(emptyExecArgv.stdout.trim().split('\n').pop());

assert.strictEqual(
defaultMsg.msg.ok,
false,
`default Worker should deny: ${JSON.stringify(defaultMsg)}`,
);
assert.strictEqual(defaultMsg.msg.code, 'ERR_ACCESS_DENIED');

assert.strictEqual(
emptyMsg.msg.ok,
false,
`Worker with execArgv: [] should deny: ${JSON.stringify(emptyMsg)}`,
);
assert.strictEqual(emptyMsg.msg.code, 'ERR_ACCESS_DENIED');