Skip to content

Commit cd99dc7

Browse files
lundibundicodebytere
authored andcommitted
worker: properly handle env and NODE_OPTIONS in workers
PR-URL: #31711 Fixes: #30627 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent 54caf76 commit cd99dc7

File tree

6 files changed

+193
-75
lines changed

6 files changed

+193
-75
lines changed

lib/internal/errors.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,8 +1354,8 @@ E('ERR_VM_MODULE_NOT_MODULE',
13541354
'Provided module is not an instance of Module', Error);
13551355
E('ERR_VM_MODULE_STATUS', 'Module status %s', Error);
13561356
E('ERR_WASI_ALREADY_STARTED', 'WASI instance has already started', Error);
1357-
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors) =>
1358-
`Initiated Worker with invalid execArgv flags: ${errors.join(', ')}`,
1357+
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') =>
1358+
`Initiated Worker with ${msg}: ${errors.join(', ')}`,
13591359
Error);
13601360
E('ERR_WORKER_NOT_RUNNING', 'Worker instance not running', Error);
13611361
E('ERR_WORKER_OUT_OF_MEMORY', 'Worker terminated due to reaching memory limit',

lib/internal/worker.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,16 @@ class Worker extends EventEmitter {
125125

126126
const url = options.eval ? null : pathToFileURL(filename);
127127
// Set up the C++ handle for the worker, as well as some internal wiring.
128-
this[kHandle] = new WorkerImpl(url, options.execArgv,
128+
this[kHandle] = new WorkerImpl(url,
129+
env === process.env ? null : env,
130+
options.execArgv,
129131
parseResourceLimits(options.resourceLimits));
130132
if (this[kHandle].invalidExecArgv) {
131133
throw new ERR_WORKER_INVALID_EXEC_ARGV(this[kHandle].invalidExecArgv);
132134
}
133-
if (env === process.env) {
134-
// This may be faster than manually cloning the object in C++, especially
135-
// when recursively spawning Workers.
136-
this[kHandle].cloneParentEnvVars();
137-
} else if (env !== undefined) {
138-
this[kHandle].setEnvVars(env);
135+
if (this[kHandle].invalidNodeOptions) {
136+
throw new ERR_WORKER_INVALID_EXEC_ARGV(
137+
this[kHandle].invalidNodeOptions, 'invalid NODE_OPTIONS env variable');
139138
}
140139
this[kHandle].onexit = (code, customErr) => this[kOnExit](code, customErr);
141140
this[kPort] = this[kHandle].messagePort;

src/node_worker.cc

Lines changed: 79 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <string>
1717
#include <vector>
1818

19+
using node::kAllowedInEnvironment;
1920
using node::kDisallowedInEnvironment;
2021
using v8::Array;
2122
using v8::ArrayBuffer;
@@ -46,14 +47,15 @@ Worker::Worker(Environment* env,
4647
Local<Object> wrap,
4748
const std::string& url,
4849
std::shared_ptr<PerIsolateOptions> per_isolate_opts,
49-
std::vector<std::string>&& exec_argv)
50+
std::vector<std::string>&& exec_argv,
51+
std::shared_ptr<KVStore> env_vars)
5052
: AsyncWrap(env, wrap, AsyncWrap::PROVIDER_WORKER),
5153
per_isolate_opts_(per_isolate_opts),
5254
exec_argv_(exec_argv),
5355
platform_(env->isolate_data()->platform()),
5456
start_profiler_idle_notifier_(env->profiler_idle_notifier_started()),
5557
thread_id_(Environment::AllocateThreadId()),
56-
env_vars_(env->env_vars()) {
58+
env_vars_(env_vars) {
5759
Debug(this, "Creating new worker instance with thread id %llu", thread_id_);
5860

5961
// Set up everything that needs to be set up in the parent environment.
@@ -441,6 +443,7 @@ Worker::~Worker() {
441443

442444
void Worker::New(const FunctionCallbackInfo<Value>& args) {
443445
Environment* env = Environment::GetCurrent(args);
446+
Isolate* isolate = args.GetIsolate();
444447

445448
CHECK(args.IsConstructCall());
446449

@@ -451,24 +454,81 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) {
451454

452455
std::string url;
453456
std::shared_ptr<PerIsolateOptions> per_isolate_opts = nullptr;
457+
std::shared_ptr<KVStore> env_vars = nullptr;
454458

455459
std::vector<std::string> exec_argv_out;
456-
bool has_explicit_exec_argv = false;
457460

458-
CHECK_EQ(args.Length(), 3);
461+
CHECK_EQ(args.Length(), 4);
459462
// Argument might be a string or URL
460463
if (!args[0]->IsNullOrUndefined()) {
461464
Utf8Value value(
462-
args.GetIsolate(),
463-
args[0]->ToString(env->context()).FromMaybe(Local<String>()));
465+
isolate, args[0]->ToString(env->context()).FromMaybe(Local<String>()));
464466
url.append(value.out(), value.length());
465467
}
466468

467-
if (args[1]->IsArray()) {
468-
Local<Array> array = args[1].As<Array>();
469+
if (args[1]->IsNull()) {
470+
// Means worker.env = { ...process.env }.
471+
env_vars = env->env_vars()->Clone(isolate);
472+
} else if (args[1]->IsObject()) {
473+
// User provided env.
474+
env_vars = KVStore::CreateMapKVStore();
475+
env_vars->AssignFromObject(isolate->GetCurrentContext(),
476+
args[1].As<Object>());
477+
} else {
478+
// Env is shared.
479+
env_vars = env->env_vars();
480+
}
481+
482+
if (args[1]->IsObject() || args[2]->IsArray()) {
483+
per_isolate_opts.reset(new PerIsolateOptions());
484+
485+
HandleEnvOptions(
486+
per_isolate_opts->per_env, [isolate, &env_vars](const char* name) {
487+
MaybeLocal<String> value =
488+
env_vars->Get(isolate, OneByteString(isolate, name));
489+
return value.IsEmpty() ? std::string{}
490+
: std::string(*String::Utf8Value(
491+
isolate, value.ToLocalChecked()));
492+
});
493+
494+
#ifndef NODE_WITHOUT_NODE_OPTIONS
495+
MaybeLocal<String> maybe_node_opts =
496+
env_vars->Get(isolate, OneByteString(isolate, "NODE_OPTIONS"));
497+
if (!maybe_node_opts.IsEmpty()) {
498+
std::string node_options(
499+
*String::Utf8Value(isolate, maybe_node_opts.ToLocalChecked()));
500+
std::vector<std::string> errors{};
501+
std::vector<std::string> env_argv =
502+
ParseNodeOptionsEnvVar(node_options, &errors);
503+
// [0] is expected to be the program name, add dummy string.
504+
env_argv.insert(env_argv.begin(), "");
505+
std::vector<std::string> invalid_args{};
506+
options_parser::Parse(&env_argv,
507+
nullptr,
508+
&invalid_args,
509+
per_isolate_opts.get(),
510+
kAllowedInEnvironment,
511+
&errors);
512+
if (errors.size() > 0 && args[1]->IsObject()) {
513+
// Only fail for explicitly provided env, this protects from failures
514+
// when NODE_OPTIONS from parent's env is used (which is the default).
515+
Local<Value> error;
516+
if (!ToV8Value(env->context(), errors).ToLocal(&error)) return;
517+
Local<String> key =
518+
FIXED_ONE_BYTE_STRING(env->isolate(), "invalidNodeOptions");
519+
// Ignore the return value of Set() because exceptions bubble up to JS
520+
// when we return anyway.
521+
USE(args.This()->Set(env->context(), key, error));
522+
return;
523+
}
524+
}
525+
#endif
526+
}
527+
528+
if (args[2]->IsArray()) {
529+
Local<Array> array = args[2].As<Array>();
469530
// The first argument is reserved for program name, but we don't need it
470531
// in workers.
471-
has_explicit_exec_argv = true;
472532
std::vector<std::string> exec_argv = {""};
473533
uint32_t length = array->Length();
474534
for (uint32_t i = 0; i < length; i++) {
@@ -490,8 +550,6 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) {
490550

491551
std::vector<std::string> invalid_args{};
492552
std::vector<std::string> errors{};
493-
per_isolate_opts.reset(new PerIsolateOptions());
494-
495553
// Using invalid_args as the v8_args argument as it stores unknown
496554
// options for the per isolate parser.
497555
options_parser::Parse(
@@ -518,40 +576,24 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) {
518576
USE(args.This()->Set(env->context(), key, error));
519577
return;
520578
}
521-
}
522-
if (!has_explicit_exec_argv)
579+
} else {
523580
exec_argv_out = env->exec_argv();
581+
}
524582

525-
Worker* worker =
526-
new Worker(env, args.This(), url, per_isolate_opts,
527-
std::move(exec_argv_out));
583+
Worker* worker = new Worker(env,
584+
args.This(),
585+
url,
586+
per_isolate_opts,
587+
std::move(exec_argv_out),
588+
env_vars);
528589

529-
CHECK(args[2]->IsFloat64Array());
530-
Local<Float64Array> limit_info = args[2].As<Float64Array>();
590+
CHECK(args[3]->IsFloat64Array());
591+
Local<Float64Array> limit_info = args[3].As<Float64Array>();
531592
CHECK_EQ(limit_info->Length(), kTotalResourceLimitCount);
532593
limit_info->CopyContents(worker->resource_limits_,
533594
sizeof(worker->resource_limits_));
534595
}
535596

536-
void Worker::CloneParentEnvVars(const FunctionCallbackInfo<Value>& args) {
537-
Worker* w;
538-
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
539-
CHECK(w->thread_joined_); // The Worker has not started yet.
540-
541-
w->env_vars_ = w->env()->env_vars()->Clone(args.GetIsolate());
542-
}
543-
544-
void Worker::SetEnvVars(const FunctionCallbackInfo<Value>& args) {
545-
Worker* w;
546-
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
547-
CHECK(w->thread_joined_); // The Worker has not started yet.
548-
549-
CHECK(args[0]->IsObject());
550-
w->env_vars_ = KVStore::CreateMapKVStore();
551-
w->env_vars_->AssignFromObject(args.GetIsolate()->GetCurrentContext(),
552-
args[0].As<Object>());
553-
}
554-
555597
void Worker::StartThread(const FunctionCallbackInfo<Value>& args) {
556598
Worker* w;
557599
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
@@ -722,8 +764,6 @@ void InitWorker(Local<Object> target,
722764
w->InstanceTemplate()->SetInternalFieldCount(1);
723765
w->Inherit(AsyncWrap::GetConstructorTemplate(env));
724766

725-
env->SetProtoMethod(w, "setEnvVars", Worker::SetEnvVars);
726-
env->SetProtoMethod(w, "cloneParentEnvVars", Worker::CloneParentEnvVars);
727767
env->SetProtoMethod(w, "startThread", Worker::StartThread);
728768
env->SetProtoMethod(w, "stopThread", Worker::StopThread);
729769
env->SetProtoMethod(w, "ref", Worker::Ref);

src/node_worker.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ class Worker : public AsyncWrap {
2626
v8::Local<v8::Object> wrap,
2727
const std::string& url,
2828
std::shared_ptr<PerIsolateOptions> per_isolate_opts,
29-
std::vector<std::string>&& exec_argv);
29+
std::vector<std::string>&& exec_argv,
30+
std::shared_ptr<KVStore> env_vars);
3031
~Worker() override;
3132

3233
// Run the worker. This is only called from the worker thread.

test/parallel/test-cli-node-options.js

Lines changed: 75 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,44 @@ if (process.config.variables.node_without_node_options)
77

88
const assert = require('assert');
99
const exec = require('child_process').execFile;
10+
const { Worker } = require('worker_threads');
1011

1112
const tmpdir = require('../common/tmpdir');
1213
tmpdir.refresh();
1314

1415
const printA = require.resolve('../fixtures/printA.js');
1516
const printSpaceA = require.resolve('../fixtures/print A.js');
1617

17-
expect(` -r ${printA} `, 'A\nB\n');
18-
expect(`-r ${printA}`, 'A\nB\n');
19-
expect(`-r ${JSON.stringify(printA)}`, 'A\nB\n');
20-
expect(`-r ${JSON.stringify(printSpaceA)}`, 'A\nB\n');
21-
expect(`-r ${printA} -r ${printA}`, 'A\nB\n');
22-
expect(` -r ${printA} -r ${printA}`, 'A\nB\n');
23-
expect(` --require ${printA} --require ${printA}`, 'A\nB\n');
18+
expectNoWorker(` -r ${printA} `, 'A\nB\n');
19+
expectNoWorker(`-r ${printA}`, 'A\nB\n');
20+
expectNoWorker(`-r ${JSON.stringify(printA)}`, 'A\nB\n');
21+
expectNoWorker(`-r ${JSON.stringify(printSpaceA)}`, 'A\nB\n');
22+
expectNoWorker(`-r ${printA} -r ${printA}`, 'A\nB\n');
23+
expectNoWorker(` -r ${printA} -r ${printA}`, 'A\nB\n');
24+
expectNoWorker(` --require ${printA} --require ${printA}`, 'A\nB\n');
2425
expect('--no-deprecation', 'B\n');
2526
expect('--no-warnings', 'B\n');
2627
expect('--no_warnings', 'B\n');
2728
expect('--trace-warnings', 'B\n');
2829
expect('--redirect-warnings=_', 'B\n');
2930
expect('--trace-deprecation', 'B\n');
3031
expect('--trace-sync-io', 'B\n');
31-
expect('--trace-events-enabled', 'B\n');
32+
expectNoWorker('--trace-events-enabled', 'B\n');
3233
expect('--track-heap-objects', 'B\n');
33-
expect('--throw-deprecation', 'B\n');
34-
expect('--zero-fill-buffers', 'B\n');
35-
expect('--v8-pool-size=10', 'B\n');
36-
expect('--trace-event-categories node', 'B\n');
37-
// eslint-disable-next-line no-template-curly-in-string
38-
expect('--trace-event-file-pattern {pid}-${rotation}.trace_events', 'B\n');
34+
expect('--throw-deprecation',
35+
/.*DeprecationWarning: Buffer\(\) is deprecated due to security and usability issues.*/,
36+
'new Buffer(42)',
37+
true);
38+
expectNoWorker('--zero-fill-buffers', 'B\n');
39+
expectNoWorker('--v8-pool-size=10', 'B\n');
40+
expectNoWorker('--trace-event-categories node', 'B\n');
41+
expectNoWorker(
42+
// eslint-disable-next-line no-template-curly-in-string
43+
'--trace-event-file-pattern {pid}-${rotation}.trace_events',
44+
'B\n'
45+
);
3946
// eslint-disable-next-line no-template-curly-in-string
40-
expect('--trace-event-file-pattern {pid}-${rotation}.trace_events ' +
47+
expectNoWorker('--trace-event-file-pattern {pid}-${rotation}.trace_events ' +
4148
'--trace-event-categories node.async_hooks', 'B\n');
4249
expect('--unhandled-rejections=none', 'B\n');
4350

@@ -53,24 +60,30 @@ if (common.isLinux && ['arm', 'x64'].includes(process.arch)) {
5360
}
5461

5562
if (common.hasCrypto) {
56-
expect('--use-openssl-ca', 'B\n');
57-
expect('--use-bundled-ca', 'B\n');
58-
expect('--openssl-config=_ossl_cfg', 'B\n');
63+
expectNoWorker('--use-openssl-ca', 'B\n');
64+
expectNoWorker('--use-bundled-ca', 'B\n');
65+
expectNoWorker('--openssl-config=_ossl_cfg', 'B\n');
5966
}
6067

6168
// V8 options
6269
expect('--abort_on-uncaught_exception', 'B\n');
6370
expect('--disallow-code-generation-from-strings', 'B\n');
6471
expect('--max-old-space-size=0', 'B\n');
6572
expect('--stack-trace-limit=100',
66-
/(\s*at f \(\[eval\]:1:\d*\)\r?\n){100}/,
73+
/(\s*at f \(\[(eval|worker eval)\]:1:\d*\)\r?\n)/,
6774
'(function f() { f(); })();',
6875
true);
6976
// Unsupported on arm. See https://crbug.com/v8/8713.
7077
if (!['arm', 'arm64'].includes(process.arch))
7178
expect('--interpreted-frames-native-stack', 'B\n');
7279

73-
function expect(opt, want, command = 'console.log("B")', wantsError = false) {
80+
function expectNoWorker(opt, want, command, wantsError) {
81+
expect(opt, want, command, wantsError, false);
82+
}
83+
84+
function expect(
85+
opt, want, command = 'console.log("B")', wantsError = false, testWorker = true
86+
) {
7487
const argv = ['-e', command];
7588
const opts = {
7689
cwd: tmpdir.path,
@@ -79,15 +92,52 @@ function expect(opt, want, command = 'console.log("B")', wantsError = false) {
7992
};
8093
if (typeof want === 'string')
8194
want = new RegExp(want);
82-
exec(process.execPath, argv, opts, common.mustCall((err, stdout, stderr) => {
95+
96+
const test = (type) => common.mustCall((err, stdout) => {
97+
const o = JSON.stringify(opt);
8398
if (wantsError) {
84-
stdout = stderr;
99+
assert.ok(err, `${type}: expected error for ${o}`);
100+
stdout = err.stack;
85101
} else {
86-
assert.ifError(err);
102+
assert.ifError(err, `${type}: failed for ${o}`);
87103
}
88104
if (want.test(stdout)) return;
89105

90-
const o = JSON.stringify(opt);
91-
assert.fail(`For ${o}, failed to find ${want} in: <\n${stdout}\n>`);
106+
assert.fail(
107+
`${type}: for ${o}, failed to find ${want} in: <\n${stdout}\n>`
108+
);
109+
});
110+
111+
exec(process.execPath, argv, opts, test('child process'));
112+
if (testWorker)
113+
workerTest(opts, command, wantsError, test('worker'));
114+
}
115+
116+
async function collectStream(readable) {
117+
readable.setEncoding('utf8');
118+
let data = '';
119+
for await (const chunk of readable) {
120+
data += chunk;
121+
}
122+
return data;
123+
}
124+
125+
function workerTest(opts, command, wantsError, test) {
126+
let workerError = null;
127+
const worker = new Worker(command, {
128+
...opts,
129+
execArgv: [],
130+
eval: true,
131+
stdout: true,
132+
stderr: true
133+
});
134+
worker.on('error', (err) => {
135+
workerError = err;
136+
});
137+
worker.on('exit', common.mustCall((code) => {
138+
assert.strictEqual(code, wantsError ? 1 : 0);
139+
collectStream(worker.stdout).then((stdout) => {
140+
test(workerError, stdout);
141+
});
92142
}));
93143
}

0 commit comments

Comments
 (0)