Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ensure that errors thrown in the context bridge are created in the correct context #24713

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 30 additions & 8 deletions shell/renderer/api/electron_api_context_bridge.cc
Expand Up @@ -127,6 +127,22 @@ v8::MaybeLocal<v8::Value> GetPrivate(v8::Local<v8::Context> context,
gin::StringToV8(context->GetIsolate(), key)));
}

// Where the context bridge should create the exception it is about to throw
enum BridgeErrorTarget {
// The source / calling context. This is default and correct 99% of the time,
// the caller / context asking for the conversion will receive the error and
// therefore the error should be made in that context
kSource,
// The destination / target context. This should only be used when the source
// won't catch the error that results from the value it is passing over the
// bridge. This can **only** occur when returning a value from a function as
// we convert the return value after the method has terminated and execution
// has been returned to the caller. In this scenario the error will the be
// catchable in the "destination" context and therefore we create the error
// there.
kDestination
};

} // namespace

v8::MaybeLocal<v8::Value> PassValueToOtherContext(
Expand All @@ -135,10 +151,13 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
v8::Local<v8::Value> value,
context_bridge::ObjectCache* object_cache,
bool support_dynamic_properties,
int recursion_depth) {
int recursion_depth,
BridgeErrorTarget error_target = BridgeErrorTarget::kSource) {
TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContext");
if (recursion_depth >= kMaxRecursion) {
v8::Context::Scope source_scope(source_context);
v8::Context::Scope error_scope(error_target == BridgeErrorTarget::kSource
? source_context
: destination_context);
source_context->GetIsolate()->ThrowException(v8::Exception::TypeError(
gin::StringToV8(source_context->GetIsolate(),
"Electron contextBridge recursion depth exceeded. "
Expand Down Expand Up @@ -314,9 +333,12 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
// Serializable objects
blink::CloneableMessage ret;
{
v8::Context::Scope source_context_scope(source_context);
v8::Local<v8::Context> error_context =
error_target == BridgeErrorTarget::kSource ? source_context
: destination_context;
v8::Context::Scope error_scope(error_context);
// V8 serializer will throw an error if required
if (!gin::ConvertFromV8(source_context->GetIsolate(), value, &ret))
if (!gin::ConvertFromV8(error_context->GetIsolate(), value, &ret))
return v8::MaybeLocal<v8::Value>();
}

Expand Down Expand Up @@ -402,10 +424,10 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) {
if (maybe_return_value.IsEmpty())
return;

auto ret =
PassValueToOtherContext(func_owning_context, calling_context,
maybe_return_value.ToLocalChecked(),
&object_cache, support_dynamic_properties, 0);
auto ret = PassValueToOtherContext(
func_owning_context, calling_context,
maybe_return_value.ToLocalChecked(), &object_cache,
support_dynamic_properties, 0, BridgeErrorTarget::kDestination);
if (ret.IsEmpty())
return;
info.GetReturnValue().Set(ret.ToLocalChecked());
Expand Down
33 changes: 33 additions & 0 deletions spec-main/api-context-bridge-spec.ts
Expand Up @@ -486,6 +486,39 @@ describe('contextBridge', () => {
expect(threw).to.equal(true);
});

it('should copy thrown errors into the other context', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
throwNormal: () => {
throw new Error('whoops');
},
throwWeird: () => {
throw 'this is no error...'; // eslint-disable-line no-throw-literal
},
throwNotClonable: () => {
return Object(Symbol('foo'));
},
argumentConvert: () => {}
});
});
const result = await callWithBindings((root: any) => {
const getError = (fn: Function) => {
try {
fn();
} catch (e) {
return e;
}
return null;
};
const normalIsError = Object.getPrototypeOf(getError(root.example.throwNormal)) === Error.prototype;
const weirdIsError = Object.getPrototypeOf(getError(root.example.throwWeird)) === Error.prototype;
const notClonableIsError = Object.getPrototypeOf(getError(root.example.throwNotClonable)) === Error.prototype;
const argumentConvertIsError = Object.getPrototypeOf(getError(() => root.example.argumentConvert(Object(Symbol('test'))))) === Error.prototype;
return [normalIsError, weirdIsError, notClonableIsError, argumentConvertIsError];
});
expect(result).to.deep.equal([true, true, true, true], 'should all be errors in the current context');
});

it('should not leak prototypes', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
Expand Down