Skip to content

Commit 91b6cbc

Browse files
joyeecheungaduh95
authored andcommitted
cli: implement --trace-env and --trace-env-[js|native]-stack
This implements --trace-env, --trace-env-js-stack and --trace-env-native-stack CLI options which can be used to find out what environment variables are accessed and where they are accessed. PR-URL: #55604 Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent ca41743 commit 91b6cbc

19 files changed

+365
-26
lines changed

doc/api/cli.md

+42
Original file line numberDiff line numberDiff line change
@@ -2604,6 +2604,45 @@ added: v0.8.0
26042604

26052605
Print stack traces for deprecations.
26062606

2607+
### `--trace-env`
2608+
2609+
<!-- YAML
2610+
added: REPLACEME
2611+
-->
2612+
2613+
Print information about any access to environment variables done in the current Node.js
2614+
instance to stderr, including:
2615+
2616+
* The environment variable reads that Node.js does internally.
2617+
* Writes in the form of `process.env.KEY = "SOME VALUE"`.
2618+
* Reads in the form of `process.env.KEY`.
2619+
* Definitions in the form of `Object.defineProperty(process.env, 'KEY', {...})`.
2620+
* Queries in the form of `Object.hasOwn(process.env, 'KEY')`,
2621+
`process.env.hasOwnProperty('KEY')` or `'KEY' in process.env`.
2622+
* Deletions in the form of `delete process.env.KEY`.
2623+
* Enumerations inf the form of `...process.env` or `Object.keys(process.env)`.
2624+
2625+
Only the names of the environment variables being accessed are printed. The values are not printed.
2626+
2627+
To print the stack trace of the access, use `--trace-env-js-stack` and/or
2628+
`--trace-env-native-stack`.
2629+
2630+
### `--trace-env-js-stack`
2631+
2632+
<!-- YAML
2633+
added: REPLACEME
2634+
-->
2635+
2636+
In addition to what `--trace-env` does, this prints the JavaScript stack trace of the access.
2637+
2638+
### `--trace-env-native-stack`
2639+
2640+
<!-- YAML
2641+
added: REPLACEME
2642+
-->
2643+
2644+
In addition to what `--trace-env` does, this prints the native stack trace of the access.
2645+
26072646
### `--trace-event-categories`
26082647

26092648
<!-- YAML
@@ -3150,6 +3189,9 @@ one is included in the list below.
31503189
* `--tls-min-v1.3`
31513190
* `--trace-atomics-wait`
31523191
* `--trace-deprecation`
3192+
* `--trace-env-js-stack`
3193+
* `--trace-env-native-stack`
3194+
* `--trace-env`
31533195
* `--trace-event-categories`
31543196
* `--trace-event-file-pattern`
31553197
* `--trace-events-enabled`

src/debug_utils.cc

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ EnabledDebugList enabled_debug_list;
6262
using v8::Local;
6363
using v8::StackTrace;
6464

65-
void EnabledDebugList::Parse(std::shared_ptr<KVStore> env_vars) {
65+
void EnabledDebugList::Parse(Environment* env) {
6666
std::string cats;
67-
credentials::SafeGetenv("NODE_DEBUG_NATIVE", &cats, env_vars);
67+
credentials::SafeGetenv("NODE_DEBUG_NATIVE", &cats, env);
6868
Parse(cats);
6969
}
7070

src/debug_utils.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ class NODE_EXTERN_PRIVATE EnabledDebugList {
7474
return enabled_[static_cast<unsigned int>(category)];
7575
}
7676

77-
// Uses NODE_DEBUG_NATIVE to initialize the categories. The env_vars variable
77+
// Uses NODE_DEBUG_NATIVE to initialize the categories. env->env_vars()
7878
// is parsed if it is not a nullptr, otherwise the system environment
7979
// variables are parsed.
80-
void Parse(std::shared_ptr<KVStore> env_vars);
80+
void Parse(Environment* env);
8181

8282
private:
8383
// Enable all categories matching cats.

src/env.cc

+9-6
Original file line numberDiff line numberDiff line change
@@ -875,9 +875,6 @@ Environment::Environment(IsolateData* isolate_data,
875875
EnvironmentFlags::kOwnsInspector;
876876
}
877877

878-
set_env_vars(per_process::system_environment);
879-
enabled_debug_list_.Parse(env_vars());
880-
881878
// We create new copies of the per-Environment option sets, so that it is
882879
// easier to modify them after Environment creation. The defaults are
883880
// part of the per-Isolate option set, for which in turn the defaults are
@@ -887,6 +884,13 @@ Environment::Environment(IsolateData* isolate_data,
887884
inspector_host_port_ = std::make_shared<ExclusiveAccess<HostPort>>(
888885
options_->debug_options().host_port);
889886

887+
set_env_vars(per_process::system_environment);
888+
// This should be done after options is created, so that --trace-env can be
889+
// checked when parsing NODE_DEBUG_NATIVE. It should also be done after
890+
// env_vars() is set so that the parser uses values from env->env_vars()
891+
// which may or may not be the system environment variable store.
892+
enabled_debug_list_.Parse(this);
893+
890894
heap_snapshot_near_heap_limit_ =
891895
static_cast<uint32_t>(options_->heap_snapshot_near_heap_limit);
892896

@@ -1115,8 +1119,7 @@ void Environment::InitializeLibuv() {
11151119

11161120
void Environment::InitializeCompileCache() {
11171121
std::string dir_from_env;
1118-
if (!credentials::SafeGetenv(
1119-
"NODE_COMPILE_CACHE", &dir_from_env, env_vars()) ||
1122+
if (!credentials::SafeGetenv("NODE_COMPILE_CACHE", &dir_from_env, this) ||
11201123
dir_from_env.empty()) {
11211124
return;
11221125
}
@@ -1128,7 +1131,7 @@ CompileCacheEnableResult Environment::EnableCompileCache(
11281131
CompileCacheEnableResult result;
11291132
std::string disable_env;
11301133
if (credentials::SafeGetenv(
1131-
"NODE_DISABLE_COMPILE_CACHE", &disable_env, env_vars())) {
1134+
"NODE_DISABLE_COMPILE_CACHE", &disable_env, this)) {
11321135
result.status = CompileCacheEnableStatus::DISABLED;
11331136
result.message = "Disabled by NODE_DISABLE_COMPILE_CACHE";
11341137
Debug(this,

src/node.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1045,7 +1045,7 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
10451045
if (!(flags & ProcessInitializationFlags::kNoParseGlobalDebugVariables)) {
10461046
// Initialized the enabled list for Debug() calls with system
10471047
// environment variables.
1048-
per_process::enabled_debug_list.Parse(per_process::system_environment);
1048+
per_process::enabled_debug_list.Parse(nullptr);
10491049
}
10501050

10511051
PlatformInit(flags);

src/node_credentials.cc

+24-9
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,7 @@ static bool HasOnly(int capability) {
7272
// process only has the capability CAP_NET_BIND_SERVICE set. If the current
7373
// process does not have any capabilities set and the process is running as
7474
// setuid root then lookup will not be allowed.
75-
bool SafeGetenv(const char* key,
76-
std::string* text,
77-
std::shared_ptr<KVStore> env_vars) {
75+
bool SafeGetenv(const char* key, std::string* text, Environment* env) {
7876
#if !defined(__CloudABI__) && !defined(_WIN32)
7977
#if defined(__linux__)
8078
if ((!HasOnly(CAP_NET_BIND_SERVICE) && linux_at_secure()) ||
@@ -87,14 +85,31 @@ bool SafeGetenv(const char* key,
8785

8886
// Fallback to system environment which reads the real environment variable
8987
// through uv_os_getenv.
90-
if (env_vars == nullptr) {
88+
std::shared_ptr<KVStore> env_vars;
89+
if (env == nullptr) {
9190
env_vars = per_process::system_environment;
91+
} else {
92+
env_vars = env->env_vars();
9293
}
9394

9495
std::optional<std::string> value = env_vars->Get(key);
95-
if (!value.has_value()) return false;
96-
*text = value.value();
97-
return true;
96+
97+
bool has_env = value.has_value();
98+
if (has_env) {
99+
*text = value.value();
100+
}
101+
102+
auto options =
103+
(env != nullptr ? env->options()
104+
: per_process::cli_options->per_isolate->per_env);
105+
106+
if (options->trace_env) {
107+
fprintf(stderr, "[--trace-env] get environment variable \"%s\"\n", key);
108+
109+
PrintTraceEnvStack(options);
110+
}
111+
112+
return has_env;
98113
}
99114

100115
static void SafeGetenv(const FunctionCallbackInfo<Value>& args) {
@@ -103,7 +118,7 @@ static void SafeGetenv(const FunctionCallbackInfo<Value>& args) {
103118
Isolate* isolate = env->isolate();
104119
Utf8Value strenvtag(isolate, args[0]);
105120
std::string text;
106-
if (!SafeGetenv(*strenvtag, &text, env->env_vars())) return;
121+
if (!SafeGetenv(*strenvtag, &text, env)) return;
107122
Local<Value> result =
108123
ToV8Value(isolate->GetCurrentContext(), text).ToLocalChecked();
109124
args.GetReturnValue().Set(result);
@@ -117,7 +132,7 @@ static void GetTempDir(const FunctionCallbackInfo<Value>& args) {
117132

118133
// Let's wrap SafeGetEnv since it returns true for empty string.
119134
auto get_env = [&dir, &env](std::string_view key) {
120-
USE(SafeGetenv(key.data(), &dir, env->env_vars()));
135+
USE(SafeGetenv(key.data(), &dir, env));
121136
return !dir.empty();
122137
};
123138

src/node_env_var.cc

+60-2
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,19 @@ Maybe<void> KVStore::AssignToObject(v8::Isolate* isolate,
337337
return JustVoid();
338338
}
339339

340+
void PrintTraceEnvStack(Environment* env) {
341+
PrintTraceEnvStack(env->options());
342+
}
343+
344+
void PrintTraceEnvStack(std::shared_ptr<EnvironmentOptions> options) {
345+
if (options->trace_env_native_stack) {
346+
DumpNativeBacktrace(stderr);
347+
}
348+
if (options->trace_env_js_stack) {
349+
DumpJavaScriptBacktrace(stderr);
350+
}
351+
}
352+
340353
static Intercepted EnvGetter(Local<Name> property,
341354
const PropertyCallbackInfo<Value>& info) {
342355
Environment* env = Environment::GetCurrent(info);
@@ -348,7 +361,18 @@ static Intercepted EnvGetter(Local<Name> property,
348361
CHECK(property->IsString());
349362
MaybeLocal<String> value_string =
350363
env->env_vars()->Get(env->isolate(), property.As<String>());
351-
if (!value_string.IsEmpty()) {
364+
365+
bool has_env = !value_string.IsEmpty();
366+
if (env->options()->trace_env) {
367+
Utf8Value key(env->isolate(), property.As<String>());
368+
fprintf(stderr,
369+
"[--trace-env] get environment variable \"%.*s\"\n",
370+
static_cast<int>(key.length()),
371+
key.out());
372+
PrintTraceEnvStack(env);
373+
}
374+
375+
if (has_env) {
352376
info.GetReturnValue().Set(value_string.ToLocalChecked());
353377
return Intercepted::kYes;
354378
}
@@ -386,6 +410,14 @@ static Intercepted EnvSetter(Local<Name> property,
386410
}
387411

388412
env->env_vars()->Set(env->isolate(), key, value_string);
413+
if (env->options()->trace_env) {
414+
Utf8Value key_utf8(env->isolate(), key);
415+
fprintf(stderr,
416+
"[--trace-env] set environment variable \"%.*s\"\n",
417+
static_cast<int>(key_utf8.length()),
418+
key_utf8.out());
419+
PrintTraceEnvStack(env);
420+
}
389421

390422
return Intercepted::kYes;
391423
}
@@ -396,7 +428,18 @@ static Intercepted EnvQuery(Local<Name> property,
396428
CHECK(env->has_run_bootstrapping_code());
397429
if (property->IsString()) {
398430
int32_t rc = env->env_vars()->Query(env->isolate(), property.As<String>());
399-
if (rc != -1) {
431+
bool has_env = (rc != -1);
432+
433+
if (env->options()->trace_env) {
434+
Utf8Value key_utf8(env->isolate(), property.As<String>());
435+
fprintf(stderr,
436+
"[--trace-env] query environment variable \"%.*s\": %s\n",
437+
static_cast<int>(key_utf8.length()),
438+
key_utf8.out(),
439+
has_env ? "is set" : "is not set");
440+
PrintTraceEnvStack(env);
441+
}
442+
if (has_env) {
400443
// Return attributes for the property.
401444
info.GetReturnValue().Set(v8::None);
402445
return Intercepted::kYes;
@@ -411,6 +454,15 @@ static Intercepted EnvDeleter(Local<Name> property,
411454
CHECK(env->has_run_bootstrapping_code());
412455
if (property->IsString()) {
413456
env->env_vars()->Delete(env->isolate(), property.As<String>());
457+
458+
if (env->options()->trace_env) {
459+
Utf8Value key_utf8(env->isolate(), property.As<String>());
460+
fprintf(stderr,
461+
"[--trace-env] delete environment variable \"%.*s\"\n",
462+
static_cast<int>(key_utf8.length()),
463+
key_utf8.out());
464+
PrintTraceEnvStack(env);
465+
}
414466
}
415467

416468
// process.env never has non-configurable properties, so always
@@ -423,6 +475,12 @@ static void EnvEnumerator(const PropertyCallbackInfo<Array>& info) {
423475
Environment* env = Environment::GetCurrent(info);
424476
CHECK(env->has_run_bootstrapping_code());
425477

478+
if (env->options()->trace_env) {
479+
fprintf(stderr, "[--trace-env] enumerate environment variables\n");
480+
481+
PrintTraceEnvStack(env);
482+
}
483+
426484
info.GetReturnValue().Set(
427485
env->env_vars()->Enumerate(env->isolate()));
428486
}

src/node_internals.h

+4-3
Original file line numberDiff line numberDiff line change
@@ -321,11 +321,12 @@ class ThreadPoolWork {
321321
#endif // defined(__POSIX__) && !defined(__ANDROID__) && !defined(__CloudABI__)
322322

323323
namespace credentials {
324-
bool SafeGetenv(const char* key,
325-
std::string* text,
326-
std::shared_ptr<KVStore> env_vars = nullptr);
324+
bool SafeGetenv(const char* key, std::string* text, Environment* env = nullptr);
327325
} // namespace credentials
328326

327+
void PrintTraceEnvStack(Environment* env);
328+
void PrintTraceEnvStack(std::shared_ptr<EnvironmentOptions> options);
329+
329330
void DefineZlibConstants(v8::Local<v8::Object> target);
330331
v8::Isolate* NewIsolate(v8::Isolate::CreateParams* params,
331332
uv_loop_t* event_loop,

src/node_options.cc

+18
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,24 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
775775
"show stack traces on promise initialization and resolution",
776776
&EnvironmentOptions::trace_promises,
777777
kAllowedInEnvvar);
778+
779+
AddOption("--trace-env",
780+
"Print accesses to the environment variables",
781+
&EnvironmentOptions::trace_env,
782+
kAllowedInEnvvar);
783+
Implies("--trace-env-js-stack", "--trace-env");
784+
Implies("--trace-env-native-stack", "--trace-env");
785+
AddOption("--trace-env-js-stack",
786+
"Print accesses to the environment variables and the JavaScript "
787+
"stack trace",
788+
&EnvironmentOptions::trace_env_js_stack,
789+
kAllowedInEnvvar);
790+
AddOption(
791+
"--trace-env-native-stack",
792+
"Print accesses to the environment variables and the native stack trace",
793+
&EnvironmentOptions::trace_env_native_stack,
794+
kAllowedInEnvvar);
795+
778796
AddOption("--experimental-default-type",
779797
"set module system to use by default",
780798
&EnvironmentOptions::type,

src/node_options.h

+3
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@ class EnvironmentOptions : public Options {
210210
bool trace_uncaught = false;
211211
bool trace_warnings = false;
212212
bool trace_promises = false;
213+
bool trace_env = false;
214+
bool trace_env_js_stack = false;
215+
bool trace_env_native_stack = false;
213216
bool extra_info_on_fatal_exception = true;
214217
std::string unhandled_rejections;
215218
std::vector<std::string> userland_loaders;

src/path.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ std::string PathResolve(Environment* env,
114114
// a UNC path at this points, because UNC paths are always absolute.
115115
std::string resolvedDevicePath;
116116
const std::string envvar = "=" + resolvedDevice;
117-
credentials::SafeGetenv(envvar.c_str(), &resolvedDevicePath);
117+
credentials::SafeGetenv(envvar.c_str(), &resolvedDevicePath, env);
118118
path = resolvedDevicePath.empty() ? cwd : resolvedDevicePath;
119119

120120
// Verify that a cwd was found and that it actually points

test/fixtures/process-env/define.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Object.defineProperty(process.env, 'FOO', {
2+
configurable: true,
3+
enumerable: true,
4+
writable: true,
5+
value: 'FOO',
6+
});

test/fixtures/process-env/delete.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
delete process.env.FOO;
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Object.keys(process.env);
2+
3+
const env = { ...process.env };

test/fixtures/process-env/get.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const foo = process.env.FOO;
2+
const bar = process.env.BAR;

test/fixtures/process-env/query.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const foo = 'FOO' in process.env;
2+
const bar = Object.hasOwn(process.env, 'BAR');
3+
const baz = process.env.hasOwnProperty('BAZ');

test/fixtures/process-env/set.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
process.env.FOO = "FOO";

0 commit comments

Comments
 (0)