fix: ensure that errors thrown in the context bridge are created in the correct context (#24534)

This commit is contained in:
Samuel Attard 2020-07-23 14:32:38 -07:00 committed by GitHub
parent b500294c1d
commit 5cfe956fe1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 63 additions and 8 deletions

View file

@ -127,6 +127,22 @@ v8::MaybeLocal<v8::Value> GetPrivate(v8::Local<v8::Context> context,
gin::StringToV8(context->GetIsolate(), key))); 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 } // namespace
v8::MaybeLocal<v8::Value> PassValueToOtherContext( v8::MaybeLocal<v8::Value> PassValueToOtherContext(
@ -135,10 +151,13 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
v8::Local<v8::Value> value, v8::Local<v8::Value> value,
context_bridge::ObjectCache* object_cache, context_bridge::ObjectCache* object_cache,
bool support_dynamic_properties, bool support_dynamic_properties,
int recursion_depth) { int recursion_depth,
BridgeErrorTarget error_target = BridgeErrorTarget::kSource) {
TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContext"); TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContext");
if (recursion_depth >= kMaxRecursion) { 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( source_context->GetIsolate()->ThrowException(v8::Exception::TypeError(
gin::StringToV8(source_context->GetIsolate(), gin::StringToV8(source_context->GetIsolate(),
"Electron contextBridge recursion depth exceeded. " "Electron contextBridge recursion depth exceeded. "
@ -314,9 +333,12 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
// Serializable objects // Serializable objects
blink::CloneableMessage ret; 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 // 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>(); return v8::MaybeLocal<v8::Value>();
} }
@ -402,10 +424,10 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) {
if (maybe_return_value.IsEmpty()) if (maybe_return_value.IsEmpty())
return; return;
auto ret = auto ret = PassValueToOtherContext(
PassValueToOtherContext(func_owning_context, calling_context, func_owning_context, calling_context,
maybe_return_value.ToLocalChecked(), maybe_return_value.ToLocalChecked(), &object_cache,
&object_cache, support_dynamic_properties, 0); support_dynamic_properties, 0, BridgeErrorTarget::kDestination);
if (ret.IsEmpty()) if (ret.IsEmpty())
return; return;
info.GetReturnValue().Set(ret.ToLocalChecked()); info.GetReturnValue().Set(ret.ToLocalChecked());

View file

@ -500,6 +500,39 @@ describe('contextBridge', () => {
expect(threw).to.equal(true); 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 () => { it('should not leak prototypes', async () => {
await makeBindingWindow(() => { await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', { contextBridge.exposeInMainWorld('example', {