mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
Necessarily semver-major. When `--permission` is on, every env var not matched by `--allow-env` is removed at startup. It takes names, prefix patterns (`PREFIX_*`), or `*`, repeatable or comma-sep'd. There are a range of env vars that Node.js itself uses, and a default range that are generally known to be safe in common usage. These are never scrubbed. These include things like `NODE_OPTIONS`, `NODE_EXTRA_CA_CERTS`, `PATH`, `HOME`, etc. `NODE_ENV` is not in the defaults and must be allowed explicitly. Proxy vars (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`) are also not in the defaults since they can carry credentials. When `--use-env-proxy` or `NODE_USE_ENV_PROXY` is set and any of them were removed, a single warning naming them is emitted. Env vars can be dropped at runtime after reading using `permission.drop()`. This is a stronger protection than using `process.env.FOO = undefined` because it will scrub the env var also from the environment block. On Linux, the removed entries are overwritten in the initial environment block. fs reads of any other process's /proc/<pid>/environ, ancestors included, are denied regardless of `--allow-fs-read`. A process's own is readable only with `--allow-env=*`. Symlinks are resolved before the check so paths like /dev/fd/../../<ppid>/environ are caught. The check only canonicalizes paths that statfs() reports are on procfs. On Windows, removal also clears the C runtime's copy of the environ using _wputenv_s. Reading a removed name returns undefined, warns once per name, and publishes to a diagnostics channel. Env file keys are allowed. If the user had reason to pass in an env file the assumption is they meant to allow them. File-source config (node.config.json and NODE_OPTIONS from a .env file) can only narrow the allow list. Embedders must call ScrubProcessEnvironment() themselves on startup. This is left up to the embedder to determine the exact timing but needs to be called before startup actually happens. Child processes are started with `--allow-env=*`. Those either receive the explicit env they were started with or only the env they inherit from the parent. Since the parent process is scrubbed, and the child cannot read any other process's /proc/<pid>/environ, it should never see more than the parent can. Main part of the impl was done by hand. Docs, tests, verification pass, and cleanup nits were automated. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode PR-URL: https://github.com/nodejs/node/pull/66132 Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
421 lines
15 KiB
C++
421 lines
15 KiB
C++
#ifdef NDEBUG
|
|
#undef NDEBUG
|
|
#endif
|
|
#include <assert.h>
|
|
#include "cppgc/platform.h"
|
|
#include "executable_wrapper.h"
|
|
#include "node.h"
|
|
|
|
#include <algorithm>
|
|
|
|
// Note: This file is being referred to from doc/api/embedding.md, and excerpts
|
|
// from it are included in the documentation. Try to keep these in sync.
|
|
// Snapshot support is not part of the embedder API docs yet due to its
|
|
// experimental nature, although it is of course documented in node.h.
|
|
|
|
using node::CommonEnvironmentSetup;
|
|
using node::Environment;
|
|
using node::MultiIsolatePlatform;
|
|
using v8::Context;
|
|
using v8::HandleScope;
|
|
using v8::Isolate;
|
|
using v8::Local;
|
|
using v8::Locker;
|
|
using v8::MaybeLocal;
|
|
using v8::V8;
|
|
using v8::Value;
|
|
|
|
static int RunNodeInstance(MultiIsolatePlatform* platform,
|
|
const std::vector<std::string>& args,
|
|
const std::vector<std::string>& exec_args);
|
|
static int RunSnapshotWithIsolateSettings(
|
|
MultiIsolatePlatform* platform,
|
|
const node::EmbedderSnapshotData* snapshot,
|
|
const std::vector<std::string>& args,
|
|
const std::vector<std::string>& exec_args);
|
|
|
|
// --create-v8-startup-blob <file>: a plain V8 startup blob (what V8's
|
|
// mksnapshot produces), to test building the Node.js snapshot on top of one.
|
|
static int CreateV8StartupBlob(MultiIsolatePlatform* platform,
|
|
const std::string& path) {
|
|
std::unique_ptr<v8::ArrayBuffer::Allocator> allocator(
|
|
v8::ArrayBuffer::Allocator::NewDefaultAllocator());
|
|
v8::Isolate::CreateParams params;
|
|
params.array_buffer_allocator = allocator.get();
|
|
uv_loop_t loop;
|
|
assert(uv_loop_init(&loop) == 0);
|
|
v8::Isolate* isolate = v8::Isolate::Allocate();
|
|
platform->RegisterIsolate(isolate, &loop);
|
|
v8::StartupData blob;
|
|
{
|
|
v8::SnapshotCreator creator(isolate, params);
|
|
{
|
|
v8::HandleScope handle_scope(isolate);
|
|
creator.SetDefaultContext(v8::Context::New(isolate));
|
|
}
|
|
blob =
|
|
creator.CreateBlob(v8::SnapshotCreator::FunctionCodeHandling::kClear);
|
|
}
|
|
bool platform_finished = false;
|
|
platform->AddIsolateFinishedCallback(
|
|
isolate,
|
|
[](void* data) {
|
|
bool* finished = static_cast<bool*>(data);
|
|
*finished = true;
|
|
},
|
|
&platform_finished);
|
|
platform->DisposeIsolate(isolate);
|
|
while (!platform_finished) uv_run(&loop, UV_RUN_ONCE);
|
|
uv_loop_close(&loop);
|
|
assert(blob.data != nullptr);
|
|
FILE* fp = fopen(path.c_str(), "wb");
|
|
assert(fp != nullptr);
|
|
size_t written = fwrite(blob.data, blob.raw_size, 1, fp);
|
|
assert(written == 1);
|
|
fclose(fp);
|
|
delete[] blob.data;
|
|
return 0;
|
|
}
|
|
|
|
static std::vector<char> base_blob_bytes;
|
|
static v8::StartupData base_blob{nullptr, 0};
|
|
|
|
NODE_MAIN(int argc, node::argv_type raw_argv[]) {
|
|
char** argv = nullptr;
|
|
node::FixupMain(argc, raw_argv, &argv);
|
|
|
|
std::vector<std::string> args(argv, argv + argc);
|
|
uint32_t flags =
|
|
node::ProcessInitializationFlags::kNoInitializeV8 |
|
|
node::ProcessInitializationFlags::kNoInitializeNodeV8Platform |
|
|
// This is used to test NODE_REPL_EXTERNAL_MODULE is disabled with
|
|
// kDisableNodeOptionsEnv. If other tests need NODE_OPTIONS
|
|
// support in the future, split this configuration out as a
|
|
// command line option.
|
|
node::ProcessInitializationFlags::kDisableNodeOptionsEnv |
|
|
node::ProcessInitializationFlags::kNoInitializeCppgc;
|
|
auto it =
|
|
std::find(args.begin(), args.end(), "--no-harvest-builtin-code-cache");
|
|
if (it != args.end()) {
|
|
args.erase(it);
|
|
flags |= node::ProcessInitializationFlags::kNoHarvestBuiltinCodeCache;
|
|
}
|
|
// --embedder-scrub-env=<names>: remove the environment variables other than
|
|
// the comma-separated <names> before initializing Node.js.
|
|
static constexpr std::string_view kScrubEnvFlag = "--embedder-scrub-env=";
|
|
it = std::find_if(args.begin(), args.end(), [](const std::string& arg) {
|
|
return arg.starts_with(kScrubEnvFlag);
|
|
});
|
|
if (it != args.end()) {
|
|
node::ProcessEnvironmentScrubOptions scrub_options;
|
|
const std::string names = it->substr(kScrubEnvFlag.size());
|
|
for (size_t begin = 0; begin < names.size();) {
|
|
size_t end = names.find(',', begin);
|
|
if (end == std::string::npos) end = names.size();
|
|
scrub_options.allow.push_back(names.substr(begin, end - begin));
|
|
begin = end + 1;
|
|
}
|
|
args.erase(it);
|
|
if (node::ScrubProcessEnvironment(scrub_options).IsNothing()) {
|
|
fprintf(
|
|
stderr, "%s: ScrubProcessEnvironment() failed\n", args[0].c_str());
|
|
return 1;
|
|
}
|
|
}
|
|
std::shared_ptr<node::InitializationResult> result =
|
|
node::InitializeOncePerProcess(
|
|
args, static_cast<node::ProcessInitializationFlags::Flags>(flags));
|
|
|
|
for (const std::string& error : result->errors())
|
|
fprintf(stderr, "%s: %s\n", args[0].c_str(), error.c_str());
|
|
if (result->early_return() != 0) {
|
|
return result->exit_code();
|
|
}
|
|
|
|
std::unique_ptr<MultiIsolatePlatform> platform =
|
|
MultiIsolatePlatform::Create(4);
|
|
V8::InitializePlatform(platform.get());
|
|
cppgc::InitializeProcess(platform->GetPageAllocator());
|
|
V8::Initialize();
|
|
|
|
// --embedder-run-twice: two sequential instances in one process, each
|
|
// loading (and freeing) its own copy of the snapshot.
|
|
std::vector<std::string> instance_args = result->args();
|
|
auto twice = std::find(
|
|
instance_args.begin(), instance_args.end(), "--embedder-run-twice");
|
|
int runs = 1;
|
|
if (twice != instance_args.end()) {
|
|
instance_args.erase(twice);
|
|
runs = 2;
|
|
}
|
|
int ret = 0;
|
|
for (int i = 0; i < runs && ret == 0; i++) {
|
|
ret = RunNodeInstance(platform.get(), instance_args, result->exec_args());
|
|
}
|
|
|
|
V8::Dispose();
|
|
V8::DisposePlatform();
|
|
|
|
node::TearDownOncePerProcess();
|
|
return ret;
|
|
}
|
|
|
|
int RunNodeInstance(MultiIsolatePlatform* platform,
|
|
const std::vector<std::string>& args,
|
|
const std::vector<std::string>& exec_args) {
|
|
int exit_code = 0;
|
|
|
|
// Format of the arguments of this binary:
|
|
// Building snapshot:
|
|
// embedtest js_code_to_eval arg1 arg2...
|
|
// --embedder-snapshot-blob blob-path
|
|
// --embedder-snapshot-create
|
|
// [--embedder-snapshot-as-file]
|
|
// [--without-code-cache]
|
|
// Running snapshot:
|
|
// embedtest --embedder-snapshot-blob blob-path
|
|
// [--embedder-snapshot-as-file]
|
|
// [--embedder-isolate-settings]
|
|
// arg1 arg2...
|
|
// No snapshot:
|
|
// embedtest arg1 arg2...
|
|
node::EmbedderSnapshotData::Pointer snapshot;
|
|
|
|
std::string binary_path = args[0];
|
|
std::vector<std::string> filtered_args;
|
|
bool is_building_snapshot = false;
|
|
bool snapshot_as_file = false;
|
|
bool with_isolate_settings = false;
|
|
std::optional<node::SnapshotConfig> snapshot_config;
|
|
std::string snapshot_blob_path;
|
|
for (size_t i = 0; i < args.size(); ++i) {
|
|
const std::string& arg = args[i];
|
|
if (arg == "--embedder-snapshot-create") {
|
|
is_building_snapshot = true;
|
|
} else if (arg == "--embedder-snapshot-as-file") {
|
|
snapshot_as_file = true;
|
|
} else if (arg == "--embedder-isolate-settings") {
|
|
with_isolate_settings = true;
|
|
} else if (arg == "--without-code-cache") {
|
|
if (!snapshot_config.has_value()) {
|
|
snapshot_config = node::SnapshotConfig{};
|
|
}
|
|
snapshot_config.value().flags = static_cast<node::SnapshotFlags>(
|
|
static_cast<uint32_t>(snapshot_config.value().flags) |
|
|
static_cast<uint32_t>(node::SnapshotFlags::kWithoutCodeCache));
|
|
} else if (arg == "--embedder-snapshot-blob") {
|
|
assert(i + 1 < args.size());
|
|
snapshot_blob_path = args[i + 1];
|
|
i++;
|
|
} else if (arg == "--create-v8-startup-blob") {
|
|
assert(i + 1 < args.size());
|
|
return CreateV8StartupBlob(platform, args[i + 1]);
|
|
} else if (arg == "--embedder-snapshot-base-blob") {
|
|
assert(i + 1 < args.size());
|
|
FILE* fp = fopen(args[i + 1].c_str(), "rb");
|
|
assert(fp != nullptr);
|
|
fseek(fp, 0, SEEK_END);
|
|
base_blob_bytes.resize(ftell(fp));
|
|
fseek(fp, 0, SEEK_SET);
|
|
size_t read =
|
|
fread(base_blob_bytes.data(), base_blob_bytes.size(), 1, fp);
|
|
assert(read == 1);
|
|
fclose(fp);
|
|
base_blob = {base_blob_bytes.data(),
|
|
static_cast<int>(base_blob_bytes.size())};
|
|
if (!snapshot_config.has_value()) {
|
|
snapshot_config = node::SnapshotConfig{};
|
|
}
|
|
snapshot_config.value().base_blob = &base_blob;
|
|
i++;
|
|
} else {
|
|
filtered_args.push_back(arg);
|
|
}
|
|
}
|
|
|
|
if (!snapshot_blob_path.empty() && !is_building_snapshot) {
|
|
FILE* fp = fopen(snapshot_blob_path.c_str(), "rb");
|
|
assert(fp != nullptr);
|
|
if (snapshot_as_file) {
|
|
snapshot = node::EmbedderSnapshotData::FromFile(fp);
|
|
} else {
|
|
uv_fs_t req = uv_fs_t();
|
|
int statret =
|
|
uv_fs_stat(nullptr, &req, snapshot_blob_path.c_str(), nullptr);
|
|
assert(statret == 0);
|
|
size_t filesize = req.statbuf.st_size;
|
|
uv_fs_req_cleanup(&req);
|
|
|
|
std::vector<char> vec(filesize);
|
|
size_t read = fread(vec.data(), filesize, 1, fp);
|
|
assert(read == 1);
|
|
snapshot = node::EmbedderSnapshotData::FromBlob(vec);
|
|
}
|
|
assert(snapshot);
|
|
int ret = fclose(fp);
|
|
assert(ret == 0);
|
|
}
|
|
|
|
if (is_building_snapshot) {
|
|
// It contains at least the binary path, the code to snapshot,
|
|
// and --embedder-snapshot-create (which is filtered, so at least
|
|
// 2 arguments should remain after filtering).
|
|
assert(filtered_args.size() >= 2);
|
|
// Insert an anonymous filename as process.argv[1].
|
|
filtered_args.insert(filtered_args.begin() + 1,
|
|
node::GetAnonymousMainPath());
|
|
}
|
|
|
|
if (snapshot && with_isolate_settings) {
|
|
return RunSnapshotWithIsolateSettings(
|
|
platform, snapshot.get(), filtered_args, exec_args);
|
|
}
|
|
|
|
std::vector<std::string> errors;
|
|
std::unique_ptr<CommonEnvironmentSetup> setup;
|
|
|
|
if (snapshot) {
|
|
setup = CommonEnvironmentSetup::CreateFromSnapshot(
|
|
platform, &errors, snapshot.get(), filtered_args, exec_args);
|
|
} else if (is_building_snapshot) {
|
|
if (snapshot_config.has_value()) {
|
|
setup = CommonEnvironmentSetup::CreateForSnapshotting(
|
|
platform, &errors, filtered_args, exec_args, snapshot_config.value());
|
|
} else {
|
|
setup = CommonEnvironmentSetup::CreateForSnapshotting(
|
|
platform, &errors, filtered_args, exec_args);
|
|
}
|
|
} else {
|
|
setup = CommonEnvironmentSetup::Create(
|
|
platform, &errors, filtered_args, exec_args);
|
|
}
|
|
if (!setup) {
|
|
for (const std::string& err : errors)
|
|
fprintf(stderr, "%s: %s\n", binary_path.c_str(), err.c_str());
|
|
return 1;
|
|
}
|
|
|
|
Isolate* isolate = setup->isolate();
|
|
Environment* env = setup->env();
|
|
|
|
{
|
|
Locker locker(isolate);
|
|
Isolate::Scope isolate_scope(isolate);
|
|
HandleScope handle_scope(isolate);
|
|
Context::Scope context_scope(setup->context());
|
|
|
|
MaybeLocal<Value> loadenv_ret;
|
|
if (snapshot) { // Deserializing snapshot
|
|
loadenv_ret = node::LoadEnvironment(env, node::StartExecutionCallback{});
|
|
} else if (is_building_snapshot) {
|
|
// Environment created for snapshotting must set process.argv[1] to
|
|
// the name of the main script, which was inserted above.
|
|
loadenv_ret = node::LoadEnvironment(
|
|
env,
|
|
"const assert = require('assert');"
|
|
"assert(require('v8').startupSnapshot.isBuildingSnapshot());"
|
|
"globalThis.embedVars = { nön_ascıı: '🏳️🌈' };"
|
|
"globalThis.require = require;"
|
|
"require('vm').runInThisContext(process.argv[2]);");
|
|
} else {
|
|
loadenv_ret = node::LoadEnvironment(
|
|
env,
|
|
"const publicRequire = require('module').createRequire(process.cwd() "
|
|
"+ '/');"
|
|
"globalThis.require = publicRequire;"
|
|
"globalThis.embedVars = { nön_ascıı: '🏳️🌈' };"
|
|
"require('vm').runInThisContext(process.argv[1]);");
|
|
}
|
|
|
|
if (loadenv_ret.IsEmpty()) // There has been a JS exception.
|
|
return 1;
|
|
|
|
exit_code = node::SpinEventLoop(env).FromMaybe(1);
|
|
|
|
if (!snapshot_blob_path.empty() && is_building_snapshot) {
|
|
snapshot = setup->CreateSnapshot();
|
|
assert(snapshot);
|
|
|
|
FILE* fp = fopen(snapshot_blob_path.c_str(), "wb");
|
|
assert(fp != nullptr);
|
|
if (snapshot_as_file) {
|
|
snapshot->ToFile(fp);
|
|
} else {
|
|
const std::vector<char> vec = snapshot->ToBlob();
|
|
size_t written = fwrite(vec.data(), vec.size(), 1, fp);
|
|
assert(written == 1);
|
|
}
|
|
int ret = fclose(fp);
|
|
assert(ret == 0);
|
|
}
|
|
}
|
|
|
|
node::Stop(env);
|
|
|
|
return exit_code;
|
|
}
|
|
|
|
// CommonEnvironmentSetup does not take IsolateSettings, so this goes through
|
|
// NewIsolate()/CreateIsolateData()/CreateEnvironment() directly.
|
|
static int RunSnapshotWithIsolateSettings(
|
|
MultiIsolatePlatform* platform,
|
|
const node::EmbedderSnapshotData* snapshot,
|
|
const std::vector<std::string>& args,
|
|
const std::vector<std::string>& exec_args) {
|
|
uv_loop_t loop;
|
|
int ret = uv_loop_init(&loop);
|
|
assert(ret == 0);
|
|
|
|
std::shared_ptr<node::ArrayBufferAllocator> allocator =
|
|
node::ArrayBufferAllocator::Create();
|
|
node::IsolateSettings settings;
|
|
settings.prepare_stack_trace_callback = [](Local<Context> context,
|
|
Local<Value> exception,
|
|
Local<v8::Array> trace) {
|
|
return MaybeLocal<Value>(v8::String::NewFromUtf8Literal(
|
|
v8::Isolate::GetCurrent(), "stack trace prepared by the embedder"));
|
|
};
|
|
Isolate* isolate =
|
|
node::NewIsolate(allocator, &loop, platform, snapshot, settings);
|
|
assert(isolate != nullptr);
|
|
|
|
int exit_code = 1;
|
|
{
|
|
Locker locker(isolate);
|
|
Isolate::Scope isolate_scope(isolate);
|
|
HandleScope handle_scope(isolate);
|
|
|
|
std::unique_ptr<node::IsolateData, decltype(&node::FreeIsolateData)>
|
|
isolate_data(node::CreateIsolateData(
|
|
isolate, &loop, platform, allocator.get(), snapshot),
|
|
node::FreeIsolateData);
|
|
std::unique_ptr<Environment, decltype(&node::FreeEnvironment)> env(
|
|
node::CreateEnvironment(
|
|
isolate_data.get(), Local<Context>(), args, exec_args),
|
|
node::FreeEnvironment);
|
|
assert(env);
|
|
|
|
Context::Scope context_scope(node::GetMainContext(env.get()));
|
|
if (!node::LoadEnvironment(env.get(), node::StartExecutionCallback{})
|
|
.IsEmpty()) {
|
|
exit_code = node::SpinEventLoop(env.get()).FromMaybe(1);
|
|
}
|
|
node::Stop(env.get());
|
|
}
|
|
|
|
bool platform_finished = false;
|
|
platform->AddIsolateFinishedCallback(
|
|
isolate,
|
|
[](void* data) {
|
|
bool* finished = static_cast<bool*>(data);
|
|
*finished = true;
|
|
},
|
|
&platform_finished);
|
|
platform->DisposeIsolate(isolate);
|
|
while (!platform_finished) uv_run(&loop, UV_RUN_ONCE);
|
|
ret = uv_loop_close(&loop);
|
|
assert(ret == 0);
|
|
|
|
return exit_code;
|
|
}
|