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
145 changes: 95 additions & 50 deletions lib/child_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const {
RegExpPrototypeExec,
SafeSet,
StringPrototypeIncludes,
StringPrototypeIndexOf,
StringPrototypeSlice,
StringPrototypeToUpperCase,
SymbolDispose,
Expand Down Expand Up @@ -93,6 +94,8 @@ const {
stdioStringToArray,
} = child_process;

const { getEnvPairs } = internalBinding('process_wrap');

const MAX_BUFFER = 1024 * 1024;

const permission = require('internal/process/permission');
Expand Down Expand Up @@ -697,60 +700,74 @@ function normalizeSpawnArguments(file, args, options) {
ArrayPrototypeUnshift(args, file);
}

// Shallow copy to guarantee changes won't impact process.env
const env = options.env || { ...process.env };
const envPairs = [];

// process.env.NODE_V8_COVERAGE always propagates, making it possible to
// collect coverage for programs that spawn with white-listed environment.
copyProcessEnvToEnv(env, 'NODE_V8_COVERAGE', options.env);

if (isZOS) {
// The following environment variables must always propagate if set.
copyProcessEnvToEnv(env, '_BPXK_AUTOCVT', options.env);
copyProcessEnvToEnv(env, '_CEE_RUNOPTS', options.env);
copyProcessEnvToEnv(env, '_TAG_REDIR_ERR', options.env);
copyProcessEnvToEnv(env, '_TAG_REDIR_IN', options.env);
copyProcessEnvToEnv(env, '_TAG_REDIR_OUT', options.env);
copyProcessEnvToEnv(env, 'STEPLIB', options.env);
copyProcessEnvToEnv(env, 'LIBPATH', options.env);
copyProcessEnvToEnv(env, '_EDC_SIG_DFLT', options.env);
copyProcessEnvToEnv(env, '_EDC_SUSV3', options.env);
}
let envPairs;
if (!options.env && !permission.isEnabled()) {
// Default: the child inherits this process's environment. Take it as
// 'KEY=value' strings in one native pass over the environment block
// instead of copying process.env (one interceptor round trip and one
// getenv() scan per variable). Everything copyProcessEnvToEnv() would
// propagate below is part of it by definition, and entries of the real
// environment block cannot contain null bytes.
envPairs = getEnvPairs();
if (process.platform === 'win32') {
envPairs = dedupeWindowsEnvPairs(envPairs);
}
} else {
// Shallow copy to guarantee changes won't impact process.env
const env = options.env || { ...process.env };
envPairs = [];

// process.env.NODE_V8_COVERAGE always propagates, making it possible to
// collect coverage for programs that spawn with white-listed environment.
copyProcessEnvToEnv(env, 'NODE_V8_COVERAGE', options.env);

if (isZOS) {
// The following environment variables must always propagate if set.
copyProcessEnvToEnv(env, '_BPXK_AUTOCVT', options.env);
copyProcessEnvToEnv(env, '_CEE_RUNOPTS', options.env);
copyProcessEnvToEnv(env, '_TAG_REDIR_ERR', options.env);
copyProcessEnvToEnv(env, '_TAG_REDIR_IN', options.env);
copyProcessEnvToEnv(env, '_TAG_REDIR_OUT', options.env);
copyProcessEnvToEnv(env, 'STEPLIB', options.env);
copyProcessEnvToEnv(env, 'LIBPATH', options.env);
copyProcessEnvToEnv(env, '_EDC_SIG_DFLT', options.env);
copyProcessEnvToEnv(env, '_EDC_SUSV3', options.env);
}

if (permission.isEnabled()) {
copyPermissionModelFlagsToEnv(env, 'NODE_OPTIONS', args);
}
if (permission.isEnabled()) {
copyPermissionModelFlagsToEnv(env, 'NODE_OPTIONS', args);
}

let envKeys = [];
// Prototype values are intentionally included.
for (const key in env) {
ArrayPrototypePush(envKeys, key);
}
let envKeys = [];
// Prototype values are intentionally included.
for (const key in env) {
ArrayPrototypePush(envKeys, key);
}

if (process.platform === 'win32') {
// On Windows env keys are case insensitive. Filter out duplicates,
// keeping only the first one (in lexicographic order)
const sawKey = new SafeSet();
envKeys = ArrayPrototypeFilter(
ArrayPrototypeSort(envKeys),
(key) => {
const uppercaseKey = StringPrototypeToUpperCase(key);
if (sawKey.has(uppercaseKey)) {
return false;
}
sawKey.add(uppercaseKey);
return true;
},
);
}
if (process.platform === 'win32') {
// On Windows env keys are case insensitive. Filter out duplicates,
// keeping only the first one (in lexicographic order)
const sawKey = new SafeSet();
envKeys = ArrayPrototypeFilter(
ArrayPrototypeSort(envKeys),
(key) => {
const uppercaseKey = StringPrototypeToUpperCase(key);
if (sawKey.has(uppercaseKey)) {
return false;
}
sawKey.add(uppercaseKey);
return true;
},
);
}

for (const key of envKeys) {
const value = env[key];
if (value !== undefined) {
validateArgumentNullCheck(key, `options.env['${key}']`);
validateArgumentNullCheck(value, `options.env['${key}']`);
ArrayPrototypePush(envPairs, `${key}=${value}`);
for (const key of envKeys) {
const value = env[key];
if (value !== undefined) {
validateArgumentNullCheck(key, `options.env['${key}']`);
validateArgumentNullCheck(value, `options.env['${key}']`);
ArrayPrototypePush(envPairs, `${key}=${value}`);
}
}
}

Expand All @@ -768,6 +785,34 @@ function normalizeSpawnArguments(file, args, options) {
};
}

/**
* Windows environment variable names are case-insensitive: keep only the
* first entry for each name in lexicographic order of the names, exactly like
* the key filtering applied to a user-supplied `options.env`.
* @param {string[]} envPairs 'KEY=value' strings
* @returns {string[]}
*/
function dedupeWindowsEnvPairs(envPairs) {
const keyed = [];
for (let i = 0; i < envPairs.length; i++) {
const pair = envPairs[i];
// Names never start with '=' here (hidden variables are not enumerated),
// so the first '=' ends the name.
const eq = StringPrototypeIndexOf(pair, '=');
ArrayPrototypePush(keyed, { key: eq === -1 ? pair : StringPrototypeSlice(pair, 0, eq), pair });
}
ArrayPrototypeSort(keyed, (a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
const sawKey = new SafeSet();
const result = [];
for (let i = 0; i < keyed.length; i++) {
const uppercaseKey = StringPrototypeToUpperCase(keyed[i].key);
if (sawKey.has(uppercaseKey)) continue;
sawKey.add(uppercaseKey);
ArrayPrototypePush(result, keyed[i].pair);
}
return result;
}

function abortChildProcess(child, killSignal, reason) {
if (!child)
return;
Expand Down
58 changes: 58 additions & 0 deletions src/node_env_var.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ using v8::Boolean;
using v8::Context;
using v8::DontDelete;
using v8::DontEnum;
using v8::EscapableHandleScope;
using v8::FunctionTemplate;
using v8::HandleScope;
using v8::IndexedPropertyHandlerConfiguration;
Expand Down Expand Up @@ -47,6 +48,7 @@ class RealEnvStore final : public KVStore {
int32_t Query(const char* key) const override;
void Delete(Isolate* isolate, Local<String> key) override;
MaybeLocal<Array> Enumerate(Isolate* isolate) const override;
MaybeLocal<Array> Pairs(Isolate* isolate) const override;
};

class MapKVStore final : public KVStore {
Expand Down Expand Up @@ -219,6 +221,62 @@ MaybeLocal<Array> RealEnvStore::Enumerate(Isolate* isolate) const {
return Array::New(isolate, env_v.out(), env_v_index);
}

MaybeLocal<Array> RealEnvStore::Pairs(Isolate* isolate) const {
Mutex::ScopedLock lock(per_process::env_var_mutex);
uv_env_item_t* items;
int count;

auto cleanup = OnScopeLeave([&]() { uv_os_free_environ(items, count); });
CHECK_EQ(uv_os_environ(&items, &count), 0);

MaybeStackBuffer<Local<Value>, 256> pairs_v(count);
int pairs_v_index = 0;
std::string pair;
for (int i = 0; i < count; i++) {
#ifdef _WIN32
// If the key starts with '=' it is a hidden environment variable.
// Enumerate() skips these, so a copy of process.env never had them.
if (items[i].name[0] == '=') continue;
#endif
pair.assign(items[i].name);
pair += '=';
pair += items[i].value;
Local<Value> str;
if (!ToV8Value(isolate->GetCurrentContext(), pair, isolate).ToLocal(&str)) {
return {};
}
pairs_v[pairs_v_index++] = str;
}

return Array::New(isolate, pairs_v.out(), pairs_v_index);
}

MaybeLocal<Array> KVStore::Pairs(Isolate* isolate) const {
EscapableHandleScope scope(isolate);
Local<Context> context = isolate->GetCurrentContext();
Local<Array> keys;
if (!Enumerate(isolate).ToLocal(&keys)) return {};
uint32_t keys_length = keys->Length();
LocalVector<Value> pairs(isolate);
pairs.reserve(keys_length);
for (uint32_t i = 0; i < keys_length; i++) {
Local<Value> key;
Local<String> value;
if (!keys->Get(context, i).ToLocal(&key)) return {};
if (!key->IsString()) continue;
// A key that disappeared between Enumerate() and Get() is skipped, like an
// undefined value is when copying process.env in JS.
if (!Get(isolate, key.As<String>()).ToLocal(&value)) continue;
Local<String> pair = String::Concat(
isolate,
String::Concat(
isolate, key.As<String>(), FIXED_ONE_BYTE_STRING(isolate, "=")),
value);
pairs.push_back(pair);
}
return scope.Escape(Array::New(isolate, pairs.data(), pairs.size()));
}

std::shared_ptr<KVStore> KVStore::Clone(Isolate* isolate) const {
HandleScope handle_scope(isolate);
Local<Context> context = isolate->GetCurrentContext();
Expand Down
13 changes: 13 additions & 0 deletions src/process_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class ProcessWrap : public HandleWrap {
SetProtoMethod(isolate, constructor, "kill", Kill);

SetConstructorFunction(context, target, "Process", constructor);
SetMethodNoSideEffect(context, target, "getEnvPairs", GetEnvPairs);

Local<Object> constants = Object::New(isolate);
NODE_DEFINE_CONSTANT(constants, kProcessFlagDetached);
Expand All @@ -91,6 +92,7 @@ class ProcessWrap : public HandleWrap {
registry->Register(New);
registry->Register(Spawn);
registry->Register(Kill);
registry->Register(GetEnvPairs);
}

SET_NO_MEMORY_INFO()
Expand Down Expand Up @@ -341,6 +343,17 @@ class ProcessWrap : public HandleWrap {
args.GetReturnValue().Set(err);
}

// The current environment as ["KEY=value", ...], i.e. what a spawned
// child inherits by default, produced in one pass over the environment
// block instead of one interceptor round trip per variable.
static void GetEnvPairs(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Local<Array> pairs;
if (env->env_vars()->Pairs(env->isolate()).ToLocal(&pairs)) {
args.GetReturnValue().Set(pairs);
}
}

static void Kill(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
ProcessWrap* wrap;
Expand Down
4 changes: 4 additions & 0 deletions src/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,10 @@ class KVStore {
virtual int32_t Query(const char* key) const = 0;
virtual void Delete(v8::Isolate* isolate, v8::Local<v8::String> key) = 0;
virtual v8::MaybeLocal<v8::Array> Enumerate(v8::Isolate* isolate) const = 0;
// All entries as an array of "KEY=value" strings, in enumeration order —
// the form uv_spawn() consumes. The default implementation is
// Enumerate() + Get(); stores that can produce it in one pass override it.
virtual v8::MaybeLocal<v8::Array> Pairs(v8::Isolate* isolate) const;

virtual std::shared_ptr<KVStore> Clone(v8::Isolate* isolate) const;
virtual v8::Maybe<void> AssignFromObject(v8::Local<v8::Context> context,
Expand Down
75 changes: 75 additions & 0 deletions test/parallel/test-child-process-default-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict';
// When no `env` option is given, a child process must inherit exactly the
// parent's current environment: same variables, same values, reflecting
// runtime additions/deletions made through process.env, and (on POSIX, where
// nothing re-sorts the block) in the same order the parent enumerates it.
const common = require('../common');
const assert = require('assert');
const { spawn, spawnSync, execFileSync } = require('child_process');

// Mutate the environment at runtime in a few ways first.
process.env.TEST_DEFAULT_ENV_ADDED = 'added ünïcödé ✓';
process.env.TEST_DEFAULT_ENV_EMPTY = '';
process.env.TEST_DEFAULT_ENV_EQUALS = 'a=b=c';
process.env.TEST_DEFAULT_ENV_DELETED = 'x';
delete process.env.TEST_DEFAULT_ENV_DELETED;

function expectedEnv() {
// What `{ ...process.env }` yields, minus keys whose value is undefined
// (there are none for the real environment, but keep the definition exact).
const copy = { ...process.env };
for (const key of Object.keys(copy)) {
if (copy[key] === undefined) delete copy[key];
}
return copy;
}

const printEnv = ['-e', 'process.stdout.write(JSON.stringify([Object.keys(process.env), process.env]))'];

function check(output, label, expected = expectedEnv()) {
const [childKeys, childEnv] = JSON.parse(output);
assert.deepStrictEqual(childEnv, expected, `${label}: contents`);
assert.strictEqual(childEnv.TEST_DEFAULT_ENV_ADDED, 'added ünïcödé ✓');
assert.strictEqual(childEnv.TEST_DEFAULT_ENV_EMPTY, '');
assert.strictEqual(childEnv.TEST_DEFAULT_ENV_EQUALS, 'a=b=c');
assert.ok(!('TEST_DEFAULT_ENV_DELETED' in childEnv));
if (!common.isWindows) {
// Integer-like names are hoisted by object key ordering on both sides, so
// compare the order of the remaining names.
const nonIndex = (k) => !/^(?:0|[1-9]\d*)$/.test(k);
assert.deepStrictEqual(childKeys.filter(nonIndex), Object.keys(expected).filter(nonIndex), `${label}: order`);
}
}

// spawnSync, options omitted entirely.
check(spawnSync(process.execPath, printEnv, { encoding: 'utf8' }).stdout, 'spawnSync no options');
// Explicitly undefined / null env behave like the default.
check(spawnSync(process.execPath, printEnv, { encoding: 'utf8', env: undefined }).stdout, 'spawnSync env undefined');
check(spawnSync(process.execPath, printEnv, { encoding: 'utf8', env: null }).stdout, 'spawnSync env null');
// execFileSync goes through the same normalization.
check(execFileSync(process.execPath, printEnv, { encoding: 'utf8' }), 'execFileSync');
// A user-supplied env is still passed through as given (not merged).
{
const env = { ONLY: 'this', PATH: process.env.PATH };
const out = spawnSync(process.execPath, printEnv, { encoding: 'utf8', env }).stdout;
const [, childEnv] = JSON.parse(out);
assert.strictEqual(childEnv.ONLY, 'this');
assert.ok(!('TEST_DEFAULT_ENV_ADDED' in childEnv));
}
// Async spawn (the environment is captured at spawn() time).
{
const expectedAtSpawn = expectedEnv();
const child = spawn(process.execPath, printEnv);
let out = '';
child.stdout.setEncoding('utf8').on('data', (d) => { out += d; });
child.on('close', common.mustCall((code) => {
assert.strictEqual(code, 0);
check(out, 'spawn', expectedAtSpawn);
}));
}
// A variable added after an earlier spawn is seen by a later one (no caching).
process.env.TEST_DEFAULT_ENV_LATE = 'late';
{
const [, childEnv] = JSON.parse(spawnSync(process.execPath, printEnv, { encoding: 'utf8' }).stdout);
assert.strictEqual(childEnv.TEST_DEFAULT_ENV_LATE, 'late');
}
Loading