src: bootstrap prepare stack trace callback in shadow realm

Bootstrap per-realm callbacks like `prepare_stack_trace_callback` in
the ShadowRealm. This enables stack trace decoration in the ShadowRealm.

PR-URL: https://github.com/nodejs/node/pull/47107
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Chengzhong Wu
2023-04-04 09:13:37 +08:00
committed by GitHub
parent edaa9f4102
commit 3a014dc38a
20 changed files with 141 additions and 83 deletions

2
.github/CODEOWNERS vendored
View File

@@ -84,7 +84,7 @@
/doc/api/module.md @nodejs/modules @nodejs/loaders
/doc/api/modules.md @nodejs/modules @nodejs/loaders
/doc/api/packages.md @nodejs/modules @nodejs/loaders
/lib/internal/bootstrap/loaders.js @nodejs/modules @nodejs/loaders
/lib/internal/bootstrap/realm.js @nodejs/modules @nodejs/loaders
/lib/internal/modules/* @nodejs/modules @nodejs/loaders
/lib/internal/process/esm_loader.js @nodejs/modules @nodejs/loaders
/lib/internal/process/execution.js @nodejs/modules @nodejs/loaders

View File

@@ -65,7 +65,7 @@ const { openSync, closeSync, readSync } = require('fs');
const { inspect } = require('internal/util/inspect');
const { isPromise, isRegExp } = require('internal/util/types');
const { EOL } = require('internal/constants');
const { BuiltinModule } = require('internal/bootstrap/loaders');
const { BuiltinModule } = require('internal/bootstrap/realm');
const { isError } = require('internal/util');
const errorCache = new SafeMap();

View File

@@ -1,6 +1,6 @@
// Hello, and welcome to hacking node.js!
//
// This file is invoked by `Realm::BootstrapNode()` in `src/node_realm.cc`,
// This file is invoked by `Realm::BootstrapRealm()` in `src/node_realm.cc`,
// and is responsible for setting up Node.js core before main scripts
// under `lib/internal/main/` are executed.
//
@@ -32,9 +32,10 @@
// `DOMException` class.
// - `lib/internal/per_context/messageport.js`: JS-side components of the
// `MessagePort` implementation.
// - `lib/internal/bootstrap/loaders.js`: this sets up internal binding and
// - `lib/internal/bootstrap/realm.js`: this sets up internal binding and
// module loaders, including `process.binding()`, `process._linkedBinding()`,
// `internalBinding()` and `BuiltinModule`.
// `internalBinding()` and `BuiltinModule`, and per-realm internal states
// and bindings, including `prepare_stack_trace_callback`.
//
// The initialization done in this script is included in both the main thread
// and the worker threads. After this, further initialization is done based
@@ -52,8 +53,6 @@
// passed by `BuiltinLoader::CompileAndCall()`.
/* global process, require, internalBinding, primordials */
setupPrepareStackTrace();
const {
FunctionPrototypeCall,
JSONParse,
@@ -336,25 +335,6 @@ process.emitWarning = emitWarning;
// Note: only after this point are the timers effective
}
function setupPrepareStackTrace() {
const {
setEnhanceStackForFatalException,
setPrepareStackTraceCallback,
} = internalBinding('errors');
const {
prepareStackTrace,
fatalExceptionStackEnhancers: {
beforeInspector,
afterInspector,
},
} = require('internal/errors');
// Tell our PrepareStackTraceCallback passed to the V8 API
// to call prepareStackTrace().
setPrepareStackTraceCallback(prepareStackTrace);
// Set the function used to enhance the error stack for printing
setEnhanceStackForFatalException(beforeInspector, afterInspector);
}
function setupProcessObject() {
const EventEmitter = require('events');
const origProcProto = ObjectGetPrototypeOf(process);

View File

@@ -1,3 +1,8 @@
// This file is executed in every realm that is created by Node.js, including
// the context of main thread, worker threads, and ShadowRealms.
// Only per-realm internal states and bindings should be bootstrapped in this
// file and no globals should be exposed to the user code.
//
// This file creates the internal module & binding loaders used by built-in
// modules. In contrast, user land modules are loaded using
// lib/internal/modules/cjs/loader.js (CommonJS Modules) or
@@ -30,7 +35,7 @@
// so they can be loaded faster without the cost of I/O. This class makes the
// lib/internal/*, deps/internal/* modules and internalBinding() available by
// default to core modules, and lets the core modules require itself via
// require('internal/bootstrap/loaders') even when this file is not written in
// require('internal/bootstrap/realm') even when this file is not written in
// CommonJS style.
//
// Other objects:
@@ -178,7 +183,7 @@ let internalBinding;
};
}
const loaderId = 'internal/bootstrap/loaders';
const selfId = 'internal/bootstrap/realm';
const {
builtinIds,
compileFunction,
@@ -235,7 +240,7 @@ class BuiltinModule {
static exposeInternals() {
for (const { 0: id, 1: mod } of BuiltinModule.map) {
// Do not expose this to user land even with --expose-internals.
if (id !== loaderId) {
if (id !== selfId) {
mod.canBeRequiredByUsers = true;
}
}
@@ -354,7 +359,7 @@ const loaderExports = {
};
function requireBuiltin(id) {
if (id === loaderId) {
if (id === selfId) {
return loaderExports;
}
@@ -374,5 +379,27 @@ function requireWithFallbackInDeps(request) {
return requireBuiltin(request);
}
function setupPrepareStackTrace() {
const {
setEnhanceStackForFatalException,
setPrepareStackTraceCallback,
} = internalBinding('errors');
const {
prepareStackTrace,
fatalExceptionStackEnhancers: {
beforeInspector,
afterInspector,
},
} = requireBuiltin('internal/errors');
// Tell our PrepareStackTraceCallback passed to the V8 API
// to call prepareStackTrace().
setPrepareStackTraceCallback(prepareStackTrace);
// Set the function used to enhance the error stack for printing
setEnhanceStackForFatalException(beforeInspector, afterInspector);
}
// Store the internal loaders in C++.
setInternalLoaders(internalBinding, requireBuiltin);
// Setup per-realm bindings.
setupPrepareStackTrace();

View File

@@ -10,7 +10,7 @@ const {
} = primordials;
const binding = internalBinding('mksnapshot');
const { BuiltinModule } = require('internal/bootstrap/loaders');
const { BuiltinModule } = require('internal/bootstrap/realm');
const {
getEmbedderEntryFunction,
compileSerializeMain,

View File

@@ -75,7 +75,7 @@ module.exports = {
initializeCJS,
};
const { BuiltinModule } = require('internal/bootstrap/loaders');
const { BuiltinModule } = require('internal/bootstrap/realm');
const {
maybeCacheSourceMap,
} = require('internal/source_map/source_map_cache');

View File

@@ -190,7 +190,7 @@ class Hooks {
filename: '<preload>',
},
);
const { BuiltinModule } = require('internal/bootstrap/loaders');
const { BuiltinModule } = require('internal/bootstrap/realm');
// We only allow replacing the importMetaInitializer during preload;
// after preload is finished, we disable the ability to replace it.
//

View File

@@ -24,7 +24,7 @@ const {
StringPrototypeStartsWith,
} = primordials;
const internalFS = require('internal/fs/utils');
const { BuiltinModule } = require('internal/bootstrap/loaders');
const { BuiltinModule } = require('internal/bootstrap/realm');
const {
realpathSync,
statSync,

View File

@@ -17,7 +17,7 @@ const {
ERR_MANIFEST_DEPENDENCY_MISSING,
ERR_UNKNOWN_BUILTIN_MODULE,
} = require('internal/errors').codes;
const { BuiltinModule } = require('internal/bootstrap/loaders');
const { BuiltinModule } = require('internal/bootstrap/realm');
const { validateString } = require('internal/validators');
const path = require('path');

View File

@@ -351,7 +351,7 @@ function initializeReport() {
function setupDebugEnv() {
require('internal/util/debuglog').initializeDebugEnv(process.env.NODE_DEBUG);
if (getOptionValue('--expose-internals')) {
require('internal/bootstrap/loaders').BuiltinModule.exposeInternals();
require('internal/bootstrap/realm').BuiltinModule.exposeInternals();
}
}

View File

@@ -150,7 +150,7 @@ const {
const assert = require('internal/assert');
const { BuiltinModule } = require('internal/bootstrap/loaders');
const { BuiltinModule } = require('internal/bootstrap/realm');
const {
validateObject,
validateString,

View File

@@ -96,7 +96,7 @@ const {
globalThis,
} = primordials;
const { BuiltinModule } = require('internal/bootstrap/loaders');
const { BuiltinModule } = require('internal/bootstrap/realm');
const {
makeRequireFunction,
addBuiltinLibsToObject,

View File

@@ -67,17 +67,18 @@ MaybeLocal<Value> PrepareStackTraceCallback(Local<Context> context,
if (env == nullptr) {
return exception->ToString(context).FromMaybe(Local<Value>());
}
// TODO(legendecas): Per-realm prepareStackTrace callback.
// If we are in a Realm that is not the principal Realm (e.g. ShadowRealm),
// skip the prepareStackTrace callback to avoid passing the JS objects (
// the exception and trace) across the realm boundary with the
// `Error.prepareStackTrace` override.
Realm* current_realm = Realm::GetCurrent(context);
if (current_realm != nullptr &&
current_realm->kind() != Realm::Kind::kPrincipal) {
return exception->ToString(context).FromMaybe(Local<Value>());
Realm* realm = Realm::GetCurrent(context);
Local<Function> prepare;
if (realm != nullptr) {
// If we are in a Realm, call the realm specific prepareStackTrace callback
// to avoid passing the JS objects (the exception and trace) across the
// realm boundary with the `Error.prepareStackTrace` override.
prepare = realm->prepare_stack_trace_callback();
} else {
// The context is created with ContextifyContext, call the principal
// realm's prepareStackTrace callback.
prepare = env->principal_realm()->prepare_stack_trace_callback();
}
Local<Function> prepare = env->prepare_stack_trace_callback();
if (prepare.IsEmpty()) {
return exception->ToString(context).FromMaybe(Local<Value>());
}

View File

@@ -356,9 +356,9 @@ MaybeLocal<Function> BuiltinLoader::LookupAndCompile(Local<Context> context,
std::vector<Local<String>> parameters;
Isolate* isolate = context->GetIsolate();
// Detects parameters of the scripts based on module ids.
// internal/bootstrap/loaders: process, getLinkedBinding,
// getInternalBinding, primordials
if (strcmp(id, "internal/bootstrap/loaders") == 0) {
// internal/bootstrap/realm: process, getLinkedBinding,
// getInternalBinding, primordials
if (strcmp(id, "internal/bootstrap/realm") == 0) {
parameters = {
FIXED_ONE_BYTE_STRING(isolate, "process"),
FIXED_ONE_BYTE_STRING(isolate, "getLinkedBinding"),
@@ -414,9 +414,9 @@ MaybeLocal<Value> BuiltinLoader::CompileAndCall(Local<Context> context,
// BuiltinLoader::LookupAndCompile().
std::vector<Local<Value>> arguments;
// Detects parameters of the scripts based on module ids.
// internal/bootstrap/loaders: process, getLinkedBinding,
// getInternalBinding, primordials
if (strcmp(id, "internal/bootstrap/loaders") == 0) {
// internal/bootstrap/realm: process, getLinkedBinding,
// getInternalBinding, primordials
if (strcmp(id, "internal/bootstrap/realm") == 0) {
Local<Value> get_linked_binding;
Local<Value> get_internal_binding;
if (!NewFunctionTemplate(isolate, binding::GetLinkedBinding)

View File

@@ -960,9 +960,9 @@ void PerIsolateMessageListener(Local<Message> message, Local<Value> error) {
}
void SetPrepareStackTraceCallback(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Realm* realm = Realm::GetCurrent(args);
CHECK(args[0]->IsFunction());
env->set_prepare_stack_trace_callback(args[0].As<Function>());
realm->set_prepare_stack_trace_callback(args[0].As<Function>());
}
static void SetSourceMapsEnabled(const FunctionCallbackInfo<Value>& args) {
@@ -987,11 +987,11 @@ static void SetMaybeCacheGeneratedSourceMap(
static void SetEnhanceStackForFatalException(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Realm* realm = Realm::GetCurrent(args);
CHECK(args[0]->IsFunction());
CHECK(args[1]->IsFunction());
env->set_enhance_fatal_stack_before_inspector(args[0].As<Function>());
env->set_enhance_fatal_stack_after_inspector(args[1].As<Function>());
realm->set_enhance_fatal_stack_before_inspector(args[0].As<Function>());
realm->set_enhance_fatal_stack_after_inspector(args[1].As<Function>());
}
// Side effect-free stringification that will never throw exceptions.

View File

@@ -179,7 +179,7 @@ MaybeLocal<Value> Realm::RunBootstrapping() {
CHECK(!has_run_bootstrapping_code());
Local<Value> result;
if (!ExecuteBootstrapper("internal/bootstrap/loaders").ToLocal(&result) ||
if (!ExecuteBootstrapper("internal/bootstrap/realm").ToLocal(&result) ||
!BootstrapRealm().ToLocal(&result)) {
return MaybeLocal<Value>();
}
@@ -306,11 +306,9 @@ void PrincipalRealm::MemoryInfo(MemoryTracker* tracker) const {
}
MaybeLocal<Value> PrincipalRealm::BootstrapRealm() {
EscapableHandleScope scope(isolate_);
HandleScope scope(isolate_);
MaybeLocal<Value> result = ExecuteBootstrapper("internal/bootstrap/node");
if (result.IsEmpty()) {
if (ExecuteBootstrapper("internal/bootstrap/node").IsEmpty()) {
return MaybeLocal<Value>();
}
@@ -327,9 +325,7 @@ MaybeLocal<Value> PrincipalRealm::BootstrapRealm() {
auto thread_switch_id =
env_->is_main_thread() ? "internal/bootstrap/switches/is_main_thread"
: "internal/bootstrap/switches/is_not_main_thread";
result = ExecuteBootstrapper(thread_switch_id);
if (result.IsEmpty()) {
if (ExecuteBootstrapper(thread_switch_id).IsEmpty()) {
return MaybeLocal<Value>();
}
@@ -337,9 +333,7 @@ MaybeLocal<Value> PrincipalRealm::BootstrapRealm() {
env_->owns_process_state()
? "internal/bootstrap/switches/does_own_process_state"
: "internal/bootstrap/switches/does_not_own_process_state";
result = ExecuteBootstrapper(process_state_switch_id);
if (result.IsEmpty()) {
if (ExecuteBootstrapper(process_state_switch_id).IsEmpty()) {
return MaybeLocal<Value>();
}
@@ -351,7 +345,7 @@ MaybeLocal<Value> PrincipalRealm::BootstrapRealm() {
return MaybeLocal<Value>();
}
return scope.EscapeMaybe(result);
return v8::True(isolate_);
}
} // namespace node

View File

@@ -1,20 +1,25 @@
#include "node_shadow_realm.h"
#include "env-inl.h"
#include "node_errors.h"
namespace node {
namespace shadow_realm {
using v8::Context;
using v8::EscapableHandleScope;
using v8::HandleScope;
using v8::Local;
using v8::MaybeLocal;
using v8::Value;
using TryCatchScope = node::errors::TryCatchScope;
// static
ShadowRealm* ShadowRealm::New(Environment* env) {
ShadowRealm* realm = new ShadowRealm(env);
env->AssignToContext(realm->context(), realm, ContextInfo(""));
// We do not expect the realm bootstrapping to throw any
// exceptions. If it does, exit the current Node.js instance.
TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal);
if (realm->RunBootstrapping().IsEmpty()) {
delete realm;
return nullptr;
@@ -91,21 +96,19 @@ PER_REALM_STRONG_PERSISTENT_VALUES(V)
#undef V
v8::MaybeLocal<v8::Value> ShadowRealm::BootstrapRealm() {
EscapableHandleScope scope(isolate_);
MaybeLocal<Value> result = v8::True(isolate_);
HandleScope scope(isolate_);
// Skip "internal/bootstrap/node" as it installs node globals and per-isolate
// callbacks like PrepareStackTraceCallback.
// callbacks.
if (!env_->no_browser_globals()) {
result = ExecuteBootstrapper("internal/bootstrap/web/exposed-wildcard");
if (result.IsEmpty()) {
if (ExecuteBootstrapper("internal/bootstrap/web/exposed-wildcard")
.IsEmpty()) {
return MaybeLocal<Value>();
}
}
return result;
return v8::True(isolate_);
}
} // namespace shadow_realm

View File

@@ -7,16 +7,16 @@ const assert = require('assert');
assert.throws(
() => {
require('internal/bootstrap/loaders');
require('internal/bootstrap/realm');
}, {
code: 'MODULE_NOT_FOUND',
message: /Cannot find module 'internal\/bootstrap\/loaders'/
message: /Cannot find module 'internal\/bootstrap\/realm'/
}
);
assert.throws(
() => {
const source = 'module.exports = require("internal/bootstrap/loaders")';
const source = 'module.exports = require("internal/bootstrap/realm")';
const { internalBinding } = require('internal/test/binding');
internalBinding('natives').owo = source;
require('owo');

View File

@@ -16,7 +16,7 @@ async function runTest() {
await session.waitForNotification((notification) => {
// The main assertion here is that we do hit the loader script first.
return notification.method === 'Debugger.scriptParsed' &&
notification.params.url === 'node:internal/bootstrap/loaders';
notification.params.url === 'node:internal/bootstrap/realm';
});
await session.waitForNotification('Debugger.paused');

View File

@@ -0,0 +1,53 @@
// Flags: --experimental-shadow-realm
'use strict';
require('../common');
const assert = require('assert');
let principalRealmPrepareStackTraceCalled = false;
Error.prepareStackTrace = (error, trace) => {
principalRealmPrepareStackTraceCalled = true;
return `${String(error)}\n at ${trace.join('\n at ')}`;
};
{
// Validates inner Error.prepareStackTrace can not leak into the outer realm.
const shadowRealm = new ShadowRealm();
const stack = shadowRealm.evaluate(`
Error.prepareStackTrace = (error, trace) => {
globalThis.leaked = 'inner';
return 'from shadow realm';
};
try {
throw new Error('boom');
} catch (e) {
e.stack;
}
`);
assert.ok(!principalRealmPrepareStackTraceCalled);
assert.strictEqual(stack, 'from shadow realm');
assert.strictEqual('leaked' in globalThis, false);
}
{
// Validates stacks can be generated in the ShadowRealm.
const shadowRealm = new ShadowRealm();
const stack = shadowRealm.evaluate(`
function myFunc() {
throw new Error('boom');
}
try {
myFunc();
} catch (e) {
e.stack;
}
`);
assert.ok(!principalRealmPrepareStackTraceCalled);
const lines = stack.split('\n');
assert.strictEqual(lines[0], 'Error: boom');
assert.match(lines[1], /^ {4}at myFunc \(.*\)/);
}