Skip to content

Commit

Permalink
src: bootstrap prepare stack trace callback in shadow realm
Browse files Browse the repository at this point in the history
Bootstrap per-realm callbacks like `prepare_stack_trace_callback` in
the ShadowRealm. This enables stack trace decoration in the ShadowRealm.
  • Loading branch information
legendecas committed Mar 15, 2023
1 parent fa84657 commit e760ab7
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 52 deletions.
25 changes: 3 additions & 22 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
@@ -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.
//
Expand Down Expand Up @@ -35,6 +35,8 @@
// - `lib/internal/bootstrap/loaders.js`: this sets up internal binding and
// module loaders, including `process.binding()`, `process._linkedBinding()`,
// `internalBinding()` and `BuiltinModule`.
// - `lib/internal/bootstrap/realm.js`: this sets up per-realm internal states
// and callbacks, 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
Expand All @@ -52,8 +54,6 @@
// passed by `BuiltinLoader::CompileAndCall()`.
/* global process, require, internalBinding, primordials */

setupPrepareStackTrace();

const {
FunctionPrototypeCall,
JSONParse,
Expand Down Expand Up @@ -336,25 +336,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);
Expand Down
29 changes: 29 additions & 0 deletions lib/internal/bootstrap/realm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

/**
* 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 callbacks should be bootstrapped in this
* file and no globals should be exposed to the user code.
*/

setupPrepareStackTrace();

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);
}
17 changes: 10 additions & 7 deletions src/api/environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,20 @@ 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);
Local<Function> prepare;
if (current_realm != nullptr &&
current_realm->kind() != Realm::Kind::kPrincipal) {
return exception->ToString(context).FromMaybe(Local<Value>());
// If we are in a Realm that is not the principal Realm (e.g. ShadowRealm),
// 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 = current_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>());
}
Expand Down
10 changes: 5 additions & 5 deletions src/node_errors.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.
Expand Down
17 changes: 6 additions & 11 deletions src/node_realm.cc
Original file line number Diff line number Diff line change
Expand Up @@ -335,11 +335,10 @@ void PrincipalRealm::MemoryInfo(MemoryTracker* tracker) const {
}

MaybeLocal<Value> PrincipalRealm::BootstrapRealm() {
EscapableHandleScope scope(isolate_);

MaybeLocal<Value> result = ExecuteBootstrapper("internal/bootstrap/node");
HandleScope scope(isolate_);

if (result.IsEmpty()) {
if (ExecuteBootstrapper("internal/bootstrap/realm").IsEmpty() ||
ExecuteBootstrapper("internal/bootstrap/node").IsEmpty()) {
return MaybeLocal<Value>();
}

Expand All @@ -356,19 +355,15 @@ 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>();
}

auto process_state_switch_id =
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>();
}

Expand All @@ -380,7 +375,7 @@ MaybeLocal<Value> PrincipalRealm::BootstrapRealm() {
return MaybeLocal<Value>();
}

return scope.EscapeMaybe(result);
return v8::True(isolate_);
}

} // namespace node
15 changes: 8 additions & 7 deletions src/node_shadow_realm.cc
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,22 @@ 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 (ExecuteBootstrapper("internal/bootstrap/realm").IsEmpty()) {
return MaybeLocal<Value>();
}

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
Expand Down
45 changes: 45 additions & 0 deletions test/parallel/test-shadow-realm-prepare-stack-trace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Flags: --experimental-shadow-realm
'use strict';

require('../common');
const assert = require('assert');

{
// 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 String(error);
};
try {
throw new Error('boom');
} catch (e) {
e.stack;
}
`);
assert.strictEqual(stack, 'Error: boom');
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;
}
`);
const lines = stack.split('\n');
assert.strictEqual(lines[0], 'Error: boom');
assert.match(lines[1], /^ {4}at myFunc \(.*\)/);
}

0 comments on commit e760ab7

Please sign in to comment.