Skip to content

Commit 30dbc84

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 28289ea commit 30dbc84

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
@@ -1344,8 +1344,8 @@ E('ERR_VM_MODULE_NOT_MODULE',
13441344
'Provided module is not an instance of Module', Error);
13451345
E('ERR_VM_MODULE_STATUS', 'Module status %s', Error);
13461346
E('ERR_WASI_ALREADY_STARTED', 'WASI instance has already started', Error);
1347-
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors) =>
1348-
`Initiated Worker with invalid execArgv flags: ${errors.join(', ')}`,
1347+
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') =>
1348+
`Initiated Worker with ${msg}: ${errors.join(', ')}`,
13491349
Error);
13501350
E('ERR_WORKER_OUT_OF_MEMORY', 'Worker terminated due to reaching memory limit',
13511351
Error);

lib/internal/worker.js

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

125125
const url = options.eval ? null : pathToFileURL(filename);
126126
// Set up the C++ handle for the worker, as well as some internal wiring.
127-
this[kHandle] = new WorkerImpl(url, options.execArgv,
127+
this[kHandle] = new WorkerImpl(url,
128+
env === process.env ? null : env,
129+
options.execArgv,
128130
parseResourceLimits(options.resourceLimits));
129131
if (this[kHandle].invalidExecArgv) {
130132
throw new ERR_WORKER_INVALID_EXEC_ARGV(this[kHandle].invalidExecArgv);
131133
}
132-
if (env === process.env) {
133-
// This may be faster than manually cloning the object in C++, especially
134-
// when recursively spawning Workers.
135-
this[kHandle].cloneParentEnvVars();
136-
} else if (env !== undefined) {
137-
this[kHandle].setEnvVars(env);
134+
if (this[kHandle].invalidNodeOptions) {
135+
throw new ERR_WORKER_INVALID_EXEC_ARGV(
136+
this[kHandle].invalidNodeOptions, 'invalid NODE_OPTIONS env variable');
138137
}
139138
this[kHandle].onexit = (code, customErr) => this[kOnExit](code, customErr);
140139
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,15 +47,16 @@ 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
array_buffer_allocator_(ArrayBufferAllocator::Create()),
5557
start_profiler_idle_notifier_(env->profiler_idle_notifier_started()),
5658
thread_id_(Environment::AllocateThreadId()),
57-
env_vars_(env->env_vars()) {
59+
env_vars_(env_vars) {
5860
Debug(this, "Creating new worker instance with thread id %llu", thread_id_);
5961

6062
// Set up everything that needs to be set up in the parent environment.
@@ -445,6 +447,7 @@ Worker::~Worker() {
445447

446448
void Worker::New(const FunctionCallbackInfo<Value>& args) {
447449
Environment* env = Environment::GetCurrent(args);
450+
Isolate* isolate = args.GetIsolate();
448451

449452
CHECK(args.IsConstructCall());
450453

@@ -455,24 +458,81 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) {
455458

456459
std::string url;
457460
std::shared_ptr<PerIsolateOptions> per_isolate_opts = nullptr;
461+
std::shared_ptr<KVStore> env_vars = nullptr;
458462

459463
std::vector<std::string> exec_argv_out;
460-
bool has_explicit_exec_argv = false;
461464

462-
CHECK_EQ(args.Length(), 3);
465+
CHECK_EQ(args.Length(), 4);
463466
// Argument might be a string or URL
464467
if (!args[0]->IsNullOrUndefined()) {
465468
Utf8Value value(
466-
args.GetIsolate(),
467-
args[0]->ToString(env->context()).FromMaybe(Local<String>()));
469+
isolate, args[0]->ToString(env->context()).FromMaybe(Local<String>()));
468470
url.append(value.out(), value.length());
469471
}
470472

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

495555
std::vector<std::string> invalid_args{};
496556
std::vector<std::string> errors{};
497-
per_isolate_opts.reset(new PerIsolateOptions());
498-
499557
// Using invalid_args as the v8_args argument as it stores unknown
500558
// options for the per isolate parser.
501559
options_parser::Parse(
@@ -522,40 +580,24 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) {
522580
USE(args.This()->Set(env->context(), key, error));
523581
return;
524582
}
525-
}
526-
if (!has_explicit_exec_argv)
583+
} else {
527584
exec_argv_out = env->exec_argv();
585+
}
528586

529-
Worker* worker =
530-
new Worker(env, args.This(), url, per_isolate_opts,
531-
std::move(exec_argv_out));
587+
Worker* worker = new Worker(env,
588+
args.This(),
589+
url,
590+
per_isolate_opts,
591+
std::move(exec_argv_out),
592+
env_vars);
532593

533-
CHECK(args[2]->IsFloat64Array());
534-
Local<Float64Array> limit_info = args[2].As<Float64Array>();
594+
CHECK(args[3]->IsFloat64Array());
595+
Local<Float64Array> limit_info = args[3].As<Float64Array>();
535596
CHECK_EQ(limit_info->Length(), kTotalResourceLimitCount);
536597
limit_info->CopyContents(worker->resource_limits_,
537598
sizeof(worker->resource_limits_));
538599
}
539600

540-
void Worker::CloneParentEnvVars(const FunctionCallbackInfo<Value>& args) {
541-
Worker* w;
542-
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
543-
CHECK(w->thread_joined_); // The Worker has not started yet.
544-
545-
w->env_vars_ = w->env()->env_vars()->Clone(args.GetIsolate());
546-
}
547-
548-
void Worker::SetEnvVars(const FunctionCallbackInfo<Value>& args) {
549-
Worker* w;
550-
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
551-
CHECK(w->thread_joined_); // The Worker has not started yet.
552-
553-
CHECK(args[0]->IsObject());
554-
w->env_vars_ = KVStore::CreateMapKVStore();
555-
w->env_vars_->AssignFromObject(args.GetIsolate()->GetCurrentContext(),
556-
args[0].As<Object>());
557-
}
558-
559601
void Worker::StartThread(const FunctionCallbackInfo<Value>& args) {
560602
Worker* w;
561603
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
@@ -663,8 +705,6 @@ void InitWorker(Local<Object> target,
663705
w->InstanceTemplate()->SetInternalFieldCount(1);
664706
w->Inherit(AsyncWrap::GetConstructorTemplate(env));
665707

666-
env->SetProtoMethod(w, "setEnvVars", Worker::SetEnvVars);
667-
env->SetProtoMethod(w, "cloneParentEnvVars", Worker::CloneParentEnvVars);
668708
env->SetProtoMethod(w, "startThread", Worker::StartThread);
669709
env->SetProtoMethod(w, "stopThread", Worker::StopThread);
670710
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)