Files
electron/shell/app/node_main.cc
T
electron-roller[bot]andSam Attard 1f3d86bcf8 chore: bump node to v24.22.0 (main) (#55006)
* chore: bump node in DEPS to v24.22.0

* fix(patch): enable the V8 sandbox in shared-cage builds

Upstream now enables the V8 sandbox only for shared-cage builds. Keep
Electron's behaviour of enabling it whenever pointer compression is on,
matching Chromium's V8. The unofficial.gni x86 hunk landed upstream.

Ref: https://github.com/nodejs/node/pull/62237
Ref: https://github.com/nodejs/node/pull/65491

* fix(patch): use names for asymmetric key algorithms

Upstream dropped the BoringSSL guard in DhKeyGenTraits::Setup(), removed
ReadASN1Element() from ncrypto, and no longer includes <openssl/evp.h>
from env.h, so those hunks are no longer needed.

Ref: https://github.com/nodejs/node/pull/65966
Ref: https://github.com/nodejs/node/pull/66108
Ref: https://github.com/nodejs/node/pull/65484

* fix(patch): skip RSA-PSS keygen deprecation tests with BoringSSL

The skip is now upstream.

Ref: https://github.com/nodejs/node/pull/65808

* fix(patch): detach cppgc wrappers from their Realm before it is freed

Upstream's CppgcWrapperList::Cleanup() now pops nodes itself and the
wrapper owns and frees its list node, so the local PopFront/delete hunk
is dropped.

Ref: https://github.com/nodejs/node/pull/65778

* fix(patch): gate jspi flag on V8_ENABLE_WEBASSEMBLY

Ref: https://github.com/nodejs/node/pull/66082

* chore: remove upstreamed patches

- https://github.com/nodejs/node/pull/63985
- https://github.com/nodejs/node/pull/65630
- https://github.com/nodejs/node/pull/64453
- https://github.com/nodejs/node/pull/65778

* chore: update patches (trivial only)

* chore: update filenames.auto.gni

* fix(patch): discover hashes from OpenSSL providers

ncrypto's DigestCache::aliases() now has a function-local static, so
ncrypto needs the no_exit_time_destructors config too.

Ref: https://github.com/nodejs/node/pull/65484

* fix(patch): read the peer certificate chain without consuming it

sk_X509_num() returns size_t in BoringSSL.

Ref: https://github.com/nodejs/node/pull/65602

* fix(patch): fix shared buffer growability validation

Ref: https://github.com/nodejs/node/pull/65845

* chore: update patches (trivial only)

* node#65820: src: fix null pointer call when running without a startup snapshot

SnapshotData::AsEmbedderWrapper() was removed. Both call sites wrap the
embedded snapshot, so use EmbedderSnapshotData::BuiltinSnapshotData().

Ref: https://github.com/nodejs/node/pull/65820

* node#65407: src: apply IsolateSettings when using a snapshot

node::CreateEnvironment() no longer installs the per-isolate message
listener when it builds the environment from a snapshot, so stop
compensating for that and let SetIsolateUpForNode() add it.

Ref: https://github.com/nodejs/node/pull/65407

* test: drop removed wasm rab integration flag from webidl buffer source test

Chromium's V8 removed --experimental-wasm-rab-integration after shipping
it; the new test-internal-webidl-buffer-source.js passes it.

Ref: https://github.com/nodejs/node/pull/65845
Ref: https://chromium-review.googlesource.com/c/v8/v8/+/7708297

* test: skip new rmSync permission error test when the Node suite runs as root

test-fs-rmSync-special-char-additional-error.js is new in v24.22.0 and
relies on a read-only directory blocking removal, which root bypasses;
the Linux Node.js test job runs as root.

Ref: https://github.com/nodejs/node/pull/64453

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: Sam Attard <sattard@anthropic.com>
2026-10-15 23:11:35 +00:00

389 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2015 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/app/node_main.h"
#include <algorithm>
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/command_line.h"
#include "base/containers/fixed_flat_set.h"
#include "base/environment.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/strings/cstring_view.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "electron/fuses.h"
#include "electron/mas.h"
#include "gin/converter.h"
#include "gin/v8_initializer.h"
#include "shell/app/uv_task_runner.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/api/electron_bindings.h"
#include "shell/common/electron_command_line.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/node_bindings.h"
#include "shell/common/node_includes.h"
#include "shell/common/node_util.h"
#include "shell/common/options_switches.h"
#include "shell/common/platform_util.h"
#include "third_party/electron_node/src/tracing/agent.h"
#if BUILDFLAG(IS_WIN)
#include <windows.h>
#include "chrome/child/v8_crashpad_support_win.h"
#endif
#if BUILDFLAG(IS_LINUX)
#include "base/posix/global_descriptors.h"
#include "base/strings/string_number_conversions.h"
#include "components/crash/core/app/crash_switches.h" // nogncheck
#include "content/public/common/content_descriptors.h"
#endif
#if BUILDFLAG(IS_MAC)
#include "shell/common/mac/codesign_util.h"
#endif
#if !IS_MAS_BUILD()
#include "components/crash/core/app/crashpad.h" // nogncheck
#include "shell/app/electron_crash_reporter_client.h"
#include "shell/common/crash_keys.h"
#endif
namespace {
// Preparse Node.js cli options to pass to Node.js
// See https://nodejs.org/api/cli.html#cli_options
void ExitIfContainsDisallowedFlags(const std::vector<std::string>& argv) {
// Options that are unilaterally disallowed.
static constexpr auto disallowed = base::MakeFixedFlatSet<std::string_view>({
"--enable-fips",
"--force-fips",
"--openssl-config",
"--use-bundled-ca",
"--use-openssl-ca",
});
for (const auto& arg : argv) {
const auto key = std::string_view{arg}.substr(0, arg.find('='));
if (disallowed.contains(key)) {
LOG(ERROR) << "The Node.js cli flag " << key
<< " is not supported in Electron";
// Node.js returns 9 from ProcessGlobalArgs for any errors encountered
// when setting up cli flags and env vars. Since we're outlawing these
// flags (making them errors) exit with the same error code for
// consistency.
exit(9);
}
}
}
#if BUILDFLAG(IS_MAC)
// A list of node envs that may be used to inject scripts.
constexpr base::cstring_view kHijackableEnvs[] = {"NODE_OPTIONS",
"NODE_REPL_EXTERNAL_MODULE"};
// Return true if there is any env in kHijackableEnvs.
bool UnsetHijackableEnvs(base::Environment* env) {
bool has = false;
for (base::cstring_view name : kHijackableEnvs) {
if (env->HasVar(name)) {
env->UnSetVar(name);
has = true;
}
}
return has;
}
#endif
#if IS_MAS_BUILD()
void SetCrashKeyStub(const std::string& key, const std::string& value) {}
void ClearCrashKeyStub(const std::string& key) {}
#endif
v8::Local<v8::Value> GetParameters(v8::Isolate* isolate) {
std::map<std::string, std::string> keys;
#if !IS_MAS_BUILD()
electron::crash_keys::GetCrashKeys(&keys);
#endif
return gin::ConvertToV8(isolate, keys);
}
} // namespace
namespace electron {
int NodeMain() {
DCHECK(base::CommandLine::InitializedForCurrentProcess());
auto os_env = base::Environment::Create();
bool node_options_enabled = electron::fuses::IsNodeOptionsEnabled();
if (!node_options_enabled) {
os_env->UnSetVar("NODE_OPTIONS");
os_env->UnSetVar("NODE_EXTRA_CA_CERTS");
}
#if BUILDFLAG(IS_MAC)
// Capture the parent's audit token as early as possible. The audit token
// (unlike a raw PID) is bound to a single process instance.
std::optional<audit_token_t> parent_audit_token =
GetParentProcessAuditToken();
if (!parent_audit_token ||
!ProcessSignatureIsSameWithCurrentApp(*parent_audit_token)) {
// On macOS, it is forbidden to run sandboxed app with custom arguments
// from another app, i.e. args are discarded in following call:
// exec("Sandboxed.app", ["--custom-args-will-be-discarded"])
// However it is possible to bypass the restriction by abusing the node mode
// of Electron apps:
// exec("Electron.app", {env: {ELECTRON_RUN_AS_NODE: "1",
// NODE_OPTIONS: "--require 'bad.js'"}})
// To prevent Electron apps from being used to work around macOS security
// restrictions, when the parent process is not part of the app bundle, all
// environment variables that may be used to inject scripts are removed.
if (UnsetHijackableEnvs(os_env.get())) {
LOG(ERROR) << "Node.js environment variables are disabled because this "
"process is invoked by other apps.";
}
}
#endif // BUILDFLAG(IS_MAC)
#if BUILDFLAG(IS_WIN)
v8_crashpad_support::SetUp();
#endif
auto* command_line = base::CommandLine::ForCurrentProcess();
#if BUILDFLAG(IS_LINUX)
int pid = -1;
std::optional<std::string> fd_string = os_env->GetVar("CRASHDUMP_SIGNAL_FD");
std::optional<std::string> pid_string =
os_env->GetVar("CRASHPAD_HANDLER_PID");
if (fd_string && pid_string) {
int fd = -1;
DCHECK(base::StringToInt(fd_string.value(), &fd));
DCHECK(base::StringToInt(pid_string.value(), &pid));
base::GlobalDescriptors::GetInstance()->Set(kCrashDumpSignal, fd);
command_line->AppendSwitchASCII(
crash_reporter::switches::kCrashpadHandlerPid, pid_string.value());
// Following API is unsafe in multi-threaded scenario, but at this point
// we are still single threaded.
os_env->UnSetVar("CRASHDUMP_SIGNAL_FD");
os_env->UnSetVar("CRASHPAD_HANDLER_PID");
}
#endif
int exit_code = 1;
{
// Feed gin::PerIsolateData with a task runner.
uv_loop_t* loop = uv_default_loop();
auto uv_task_runner = base::MakeRefCounted<UvTaskRunner>(loop);
base::SingleThreadTaskRunner::CurrentDefaultHandle handle(uv_task_runner);
// Initialize feature list.
auto feature_list = std::make_unique<base::FeatureList>();
feature_list->InitFromCommandLine("", "");
base::FeatureList::SetInstance(std::move(feature_list));
// Explicitly register electron's builtin bindings.
NodeBindings::RegisterBuiltinBindings();
// Parse Node.js cli flags and strip out disallowed options.
std::vector<std::string> args = ElectronCommandLine::AsUtf8();
ExitIfContainsDisallowedFlags(args);
// Match the other process types (see NodeBindings::ParseNodeCliFlags); an
// explicit --js-source-phase-imports later on the command line still wins.
// Kept out of process.execArgv below so fork() does not accumulate it.
constexpr std::string_view kNoSourcePhaseImports =
"--no-js-source-phase-imports";
args.insert(args.begin() + 1, std::string(kNoSourcePhaseImports));
uint64_t process_flags =
node::ProcessInitializationFlags::kNoInitializeV8 |
node::ProcessInitializationFlags::kNoInitializeNodeV8Platform;
if (command_line->HasSwitch(switches::kNoStdioInit)) {
process_flags |= node::ProcessInitializationFlags::kNoStdioInitialization;
// remove the option to avoid node error "bad option: --no-stdio-init"
std::string option = std::string("--") + switches::kNoStdioInit;
std::erase(args, option);
} else {
#if BUILDFLAG(IS_WIN)
if (!platform_util::IsNulDeviceEnabled()) {
LOG(FATAL) << "Unable to open nul device needed for initialization,"
"aborting startup. As a workaround, try starting with --"
<< switches::kNoStdioInit;
}
#endif
}
std::shared_ptr<node::InitializationResult> result =
node::InitializeOncePerProcess(
args, static_cast<node::ProcessInitializationFlags::Flags>(
process_flags));
for (const std::string& error : result->errors())
std::cerr << args[0] << ": " << error << '\n';
if (result->early_return() != 0) {
return result->exit_code();
}
#if BUILDFLAG(IS_LINUX)
// On Linux, initialize crashpad after Nodejs init phase so that
// crash and termination signal handlers can be set by the crashpad client.
if (pid != -1) {
ElectronCrashReporterClient::Create();
crash_reporter::InitializeCrashpad(false, "node");
crash_keys::SetCrashKeysFromCommandLine(
*base::CommandLine::ForCurrentProcess());
crash_keys::SetPlatformCrashKey();
// Ensure the flags and env variable does not propagate to userland.
command_line->RemoveSwitch(crash_reporter::switches::kCrashpadHandlerPid);
}
#elif BUILDFLAG(IS_WIN) || (BUILDFLAG(IS_MAC) && !IS_MAS_BUILD())
ElectronCrashReporterClient::Create();
crash_reporter::InitializeCrashpad(false, "node");
crash_keys::SetCrashKeysFromCommandLine(
*base::CommandLine::ForCurrentProcess());
crash_keys::SetPlatformCrashKey();
#if BUILDFLAG(IS_WIN)
// libuv sets SEM_NOGPFAULTERRORBOX; clear it so Windows Error Reporting
// (and electron_wer.dll) still sees crashes crashpad cannot catch.
SetErrorMode(GetErrorMode() & ~SEM_NOGPFAULTERRORBOX);
#endif
#endif
gin::V8Initializer::LoadV8Snapshot(
gin::V8SnapshotFileType::kWithAdditionalContext);
// V8 requires a task scheduler.
base::ThreadPoolInstance::CreateAndStartWithDefaultParams("Electron");
// Allow Node.js to track the amount of time the event loop has spent
// idle in the kernel’s event provider .
uv_loop_configure(loop, UV_METRICS_IDLE_TIME);
auto tracing_agent = node::tracing::Agent::CreateDefault();
CHECK(tracing_agent);
// Initialize gin::IsolateHolder.
// Node.js now exposes fetch unconditionally, so WASM streaming (which
// relies on the fetch Response object) is always set up here.
// When this build embeds a Node startup snapshot (native builds) the
// isolate is created from it and no context exists yet; otherwise a fresh
// context was created and entered.
JavascriptEnvironment gin_env(loop, /*setup_wasm_streaming=*/true,
tracing_agent->GetTracingController());
const node::SnapshotData* const snapshot =
JavascriptEnvironment::NodeSnapshot();
v8::Isolate* isolate = gin_env.isolate();
v8::Isolate::Scope isolate_scope(isolate);
v8::Locker locker(isolate);
node::Environment* env = nullptr;
node::IsolateData* isolate_data = nullptr;
{
v8::HandleScope scope(isolate);
// With a snapshot, hand its per-isolate data to CreateIsolateData so the
// templates/primordials are deserialized, and an empty context to
// CreateEnvironment so node materializes the main context (and the whole
// bootstrapped environment) from the snapshot instead of running the
// bootstrap -- the same path the browser process takes in
// NodeBindings::CreateEnvironment.
auto snapshot_wrapper =
snapshot ? node::EmbedderSnapshotData::BuiltinSnapshotData()
: node::EmbedderSnapshotData::Pointer{};
isolate_data = node::CreateIsolateData(isolate, loop, gin_env.platform(),
/*allocator=*/nullptr,
snapshot_wrapper.get());
CHECK_NE(nullptr, isolate_data);
uint64_t env_flags = node::EnvironmentFlags::kDefaultFlags |
node::EnvironmentFlags::kHideConsoleWindows;
std::vector<std::string> exec_args = result->exec_args();
if (auto it = std::ranges::find(exec_args, kNoSourcePhaseImports);
it != exec_args.end()) {
exec_args.erase(it);
}
env = electron::util::CreateEnvironment(
isolate, isolate_data,
snapshot ? v8::Local<v8::Context>() : isolate->GetCurrentContext(),
result->args(), exec_args,
static_cast<node::EnvironmentFlags::Flags>(env_flags));
CHECK_NE(nullptr, env);
if (snapshot) {
// The snapshot's main context was created inside CreateEnvironment and
// is not entered yet; enter it for the lifetime of the process, as
// JavascriptEnvironment does for the context it creates itself (its
// destructor exits the current context).
env->context()->Enter();
}
node::SetIsolateUpForNode(isolate, node::IsolateSettings{});
gin_helper::Dictionary process(isolate, env->process_object());
process.SetMethod<&ElectronBindings::Crash>("crash");
// Setup process.crashReporter in child node processes
auto reporter = gin_helper::Dictionary::CreateEmpty(isolate);
reporter.SetMethod<&GetParameters>("getParameters");
#if IS_MAS_BUILD()
reporter.SetMethod<&SetCrashKeyStub>("addExtraParameter");
reporter.SetMethod<&ClearCrashKeyStub>("removeExtraParameter");
#else
reporter.SetMethod<&electron::crash_keys::SetCrashKey>(
"addExtraParameter");
reporter.SetMethod<&electron::crash_keys::ClearCrashKey>(
"removeExtraParameter");
#endif
process.Set("crashReporter", reporter);
}
v8::HandleScope scope(isolate);
electron::util::FeedEnvironmentCodeCache(env);
node::LoadEnvironment(env, node::StartExecutionCallback{}, &OnNodePreload);
// Potential reasons we get Nothing here may include: the env
// is stopping, or the user hooks process.emit('exit').
exit_code = node::SpinEventLoop(env).FromMaybe(1);
node::ResetStdio();
node::Stop(env, node::StopFlags::kDoNotTerminateIsolate);
node::FreeEnvironment(env);
node::FreeIsolateData(isolate_data);
}
// According to "src/gin/shell/gin_main.cc":
//
// gin::IsolateHolder waits for tasks running in ThreadPool in its
// destructor and thus must be destroyed before ThreadPool starts skipping
// CONTINUE_ON_SHUTDOWN tasks.
base::ThreadPoolInstance::Get()->Shutdown();
v8::V8::Dispose();
// Matches node::InitializeOncePerProcess() above. In particular this joins
// the off-thread CA certificate loader that `require('tls')` starts, which
// would otherwise race the static destructors run by exit() and abort().
node::TearDownOncePerProcess();
return exit_code;
}
} // namespace electron