Skip to content

Commit 9623c50

Browse files
RafaelGSStargos
authored andcommitted
permission: propagate permission model flags on spawn
Previously, only child_process.fork propagated the exec arguments (execvArgs) to the child process. This commit adds support for spawn and spawnSync to propagate permission model flags — except when they are already provided explicitly via arguments or through NODE_OPTIONS. Signed-off-by: RafaelGSS <[email protected]> PR-URL: #58853 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Ulises Gascón <[email protected]>
1 parent 710e13d commit 9623c50

File tree

7 files changed

+170
-17
lines changed

7 files changed

+170
-17
lines changed

doc/api/cli.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ Error: Cannot load native addon because loading addons is disabled.
153153

154154
<!-- YAML
155155
added: v20.0.0
156+
changes:
157+
- version: REPLACEME
158+
pr-url: https://github.com/nodejs/node/pull/58853
159+
description: When spawning process with the permission model enabled.
160+
The flags are inherit to the child Node.js process through
161+
NODE_OPTIONS environment variable.
156162
-->
157163

158164
> Stability: 1.1 - Active development
@@ -183,11 +189,15 @@ Error: Access to this API has been restricted
183189
}
184190
```
185191

186-
Unlike `child_process.spawn`, the `child_process.fork` API copies the execution
187-
arguments from the parent process. This means that if you start Node.js with the
188-
Permission Model enabled and include the `--allow-child-process` flag, calling
189-
`child_process.fork()` will propagate all Permission Model flags to the child
190-
process.
192+
The `child_process.fork()` API inherits the execution arguments from the
193+
parent process. This means that if Node.js is started with the Permission
194+
Model enabled and the `--allow-child-process` flag is set, any child process
195+
created using `child_process.fork()` will automatically receive all relevant
196+
Permission Model flags.
197+
198+
This behavior also applies to `child_process.spawn()`, but in that case, the
199+
flags are propagated via the `NODE_OPTIONS` environment variable rather than
200+
directly through the process arguments.
191201

192202
### `--allow-fs-read`
193203

doc/api/permissions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ easy to configure permissions as needed when using `npx`.
194194

195195
There are constraints you need to know before using this system:
196196

197-
* The model does not inherit to a child node process or a worker thread.
197+
* The model does not inherit to a worker thread.
198198
* When using the Permission Model the following features will be restricted:
199199
* Native modules
200200
* Child process

lib/child_process.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ const {
9595

9696
const MAX_BUFFER = 1024 * 1024;
9797

98+
const permission = require('internal/process/permission');
99+
98100
const isZOS = process.platform === 'os390';
99101
let addAbortListener;
100102

@@ -536,6 +538,31 @@ function copyProcessEnvToEnv(env, name, optionEnv) {
536538
}
537539
}
538540

541+
let permissionModelFlagsToCopy;
542+
543+
function getPermissionModelFlagsToCopy() {
544+
if (permissionModelFlagsToCopy === undefined) {
545+
permissionModelFlagsToCopy = [...permission.availableFlags(), '--permission'];
546+
}
547+
return permissionModelFlagsToCopy;
548+
}
549+
550+
function copyPermissionModelFlagsToEnv(env, key, args) {
551+
// Do not override if permission was already passed to file
552+
if (args.includes('--permission') || (env[key] && env[key].indexOf('--permission') !== -1)) {
553+
return;
554+
}
555+
556+
const flagsToCopy = getPermissionModelFlagsToCopy();
557+
for (const arg of process.execArgv) {
558+
for (const flag of flagsToCopy) {
559+
if (arg.startsWith(flag)) {
560+
env[key] = `${env[key] ? env[key] + ' ' + arg : arg}`;
561+
}
562+
}
563+
}
564+
}
565+
539566
let emittedDEP0190Already = false;
540567
function normalizeSpawnArguments(file, args, options) {
541568
validateString(file, 'file');
@@ -652,7 +679,8 @@ function normalizeSpawnArguments(file, args, options) {
652679
ArrayPrototypeUnshift(args, file);
653680
}
654681

655-
const env = options.env || process.env;
682+
// Shallow copy to guarantee changes won't impact process.env
683+
const env = options.env || { ...process.env };
656684
const envPairs = [];
657685

658686
// process.env.NODE_V8_COVERAGE always propagates, making it possible to
@@ -672,6 +700,10 @@ function normalizeSpawnArguments(file, args, options) {
672700
copyProcessEnvToEnv(env, '_EDC_SUSV3', options.env);
673701
}
674702

703+
if (permission.isEnabled()) {
704+
copyPermissionModelFlagsToEnv(env, 'NODE_OPTIONS', args);
705+
}
706+
675707
let envKeys = [];
676708
// Prototype values are intentionally included.
677709
for (const key in env) {

lib/internal/process/permission.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,14 @@ module.exports = ObjectFreeze({
3333

3434
return permission.has(scope, reference);
3535
},
36+
availableFlags() {
37+
return [
38+
'--allow-fs-read',
39+
'--allow-fs-write',
40+
'--allow-addons',
41+
'--allow-child-process',
42+
'--allow-wasi',
43+
'--allow-worker',
44+
];
45+
},
3646
});

lib/internal/process/pre_execution.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -609,15 +609,8 @@ function initializePermission() {
609609
},
610610
});
611611
} else {
612-
const availablePermissionFlags = [
613-
'--allow-fs-read',
614-
'--allow-fs-write',
615-
'--allow-addons',
616-
'--allow-child-process',
617-
'--allow-wasi',
618-
'--allow-worker',
619-
];
620-
ArrayPrototypeForEach(availablePermissionFlags, (flag) => {
612+
const { availableFlags } = require('internal/process/permission');
613+
ArrayPrototypeForEach(availableFlags(), (flag) => {
621614
const value = getOptionValue(flag);
622615
if (value.length) {
623616
throw new ERR_MISSING_OPTION('--permission');

test/parallel/test-permission-allow-child-process-cli.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const assert = require('assert');
1313
const childProcess = require('child_process');
1414
const fs = require('fs');
1515

16+
// Child Process (and fork) should inherit permission model flags
1617
if (process.argv[2] === 'child') {
1718
assert.throws(() => {
1819
fs.writeFileSync(__filename, 'should not write');
@@ -34,7 +35,12 @@ if (process.argv[2] === 'child') {
3435
// doesNotThrow
3536
childProcess.spawnSync(process.execPath, ['--version']);
3637
childProcess.execSync(...common.escapePOSIXShell`"${process.execPath}" --version`);
38+
childProcess.execFileSync(process.execPath, ['--version']);
39+
40+
// Guarantee permission model flags are inherited
3741
const child = childProcess.fork(__filename, ['child']);
3842
child.on('close', common.mustCall());
39-
childProcess.execFileSync(process.execPath, ['--version']);
43+
44+
const { status } = childProcess.spawnSync(process.execPath, [__filename, 'child']);
45+
assert.strictEqual(status, 0);
4046
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Flags: --permission --allow-child-process --allow-fs-read=* --allow-worker
2+
'use strict';
3+
4+
const common = require('../common');
5+
const { isMainThread } = require('worker_threads');
6+
7+
if (!isMainThread) {
8+
common.skip('This test only works on a main thread');
9+
}
10+
11+
const assert = require('assert');
12+
const childProcess = require('child_process');
13+
14+
{
15+
assert.ok(process.permission.has('child'));
16+
}
17+
18+
{
19+
assert.strictEqual(process.env.NODE_OPTIONS, undefined);
20+
}
21+
22+
{
23+
const { status, stdout } = childProcess.spawnSync(process.execPath,
24+
[
25+
'-e',
26+
`
27+
console.log(process.permission.has("fs.write"));
28+
console.log(process.permission.has("fs.read"));
29+
console.log(process.permission.has("child"));
30+
console.log(process.permission.has("net"));
31+
console.log(process.permission.has("worker"));
32+
`,
33+
]
34+
);
35+
const [fsWrite, fsRead, child, net, worker] = stdout.toString().split('\n');
36+
assert.strictEqual(status, 0);
37+
assert.strictEqual(fsWrite, 'false');
38+
assert.strictEqual(fsRead, 'true');
39+
assert.strictEqual(child, 'true');
40+
assert.strictEqual(net, 'false');
41+
assert.strictEqual(worker, 'true');
42+
}
43+
44+
// It should not override when --permission is passed
45+
{
46+
const { status, stdout } = childProcess.spawnSync(
47+
process.execPath,
48+
[
49+
'--permission',
50+
'--allow-fs-write=*',
51+
'-e',
52+
`
53+
console.log(process.permission.has("fs.write"));
54+
console.log(process.permission.has("fs.read"));
55+
console.log(process.permission.has("child"));
56+
console.log(process.permission.has("net"));
57+
console.log(process.permission.has("worker"));
58+
`,
59+
]
60+
);
61+
const [fsWrite, fsRead, child, net, worker] = stdout.toString().split('\n');
62+
assert.strictEqual(status, 0);
63+
assert.strictEqual(fsWrite, 'true');
64+
assert.strictEqual(fsRead, 'false');
65+
assert.strictEqual(child, 'false');
66+
assert.strictEqual(net, 'false');
67+
assert.strictEqual(worker, 'false');
68+
}
69+
70+
// It should not override when NODE_OPTIONS with --permission is passed
71+
{
72+
const { status, stdout } = childProcess.spawnSync(
73+
process.execPath,
74+
[
75+
'-e',
76+
`
77+
console.log(process.permission.has("fs.write"));
78+
console.log(process.permission.has("fs.read"));
79+
console.log(process.permission.has("child"));
80+
console.log(process.permission.has("net"));
81+
console.log(process.permission.has("worker"));
82+
`,
83+
],
84+
{
85+
env: {
86+
...process.env,
87+
'NODE_OPTIONS': '--permission --allow-fs-write=*',
88+
}
89+
}
90+
);
91+
const [fsWrite, fsRead, child, net, worker] = stdout.toString().split('\n');
92+
assert.strictEqual(status, 0);
93+
assert.strictEqual(fsWrite, 'true');
94+
assert.strictEqual(fsRead, 'false');
95+
assert.strictEqual(child, 'false');
96+
assert.strictEqual(net, 'false');
97+
assert.strictEqual(worker, 'false');
98+
}
99+
100+
{
101+
assert.strictEqual(process.env.NODE_OPTIONS, undefined);
102+
}

0 commit comments

Comments
 (0)