feat: contextBridge.executeInMainWorld (#45229)

This commit is contained in:
Sam Maddock 2025-01-23 21:12:46 -05:00 committed by GitHub
parent e09577b123
commit 996477152d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 528 additions and 130 deletions

View file

@ -61,6 +61,20 @@ The `contextBridge` module has the following methods:
* `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`. * `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`.
* `api` any - Your API, more information on what this API can be and how it works is available below. * `api` any - Your API, more information on what this API can be and how it works is available below.
### `contextBridge.executeInMainWorld(executionScript)` _Experimental_
<!-- TODO(samuelmaddock): add generics to map the `args` types to the `func` params -->
* `executionScript` Object
* `func` (...args: any[]) => any - A JavaScript function to execute. This function will be serialized which means
that any bound parameters and execution context will be lost.
* `args` any[] (optional) - An array of arguments to pass to the provided function. These
arguments will be copied between worlds in accordance with
[the table of supported types.](#parameter--error--return-type-support)
Returns `any` - A copy of the resulting value from executing the function in the main world.
[Refer to the table](#parameter--error--return-type-support) on how values are copied between worlds.
## Usage ## Usage
### API ### API

View file

@ -5,13 +5,17 @@ const checkContextIsolationEnabled = () => {
}; };
const contextBridge: Electron.ContextBridge = { const contextBridge: Electron.ContextBridge = {
exposeInMainWorld: (key: string, api: any) => { exposeInMainWorld: (key, api) => {
checkContextIsolationEnabled(); checkContextIsolationEnabled();
return binding.exposeAPIInWorld(0, key, api); return binding.exposeAPIInWorld(0, key, api);
}, },
exposeInIsolatedWorld: (worldId: number, key: string, api: any) => { exposeInIsolatedWorld: (worldId, key, api) => {
checkContextIsolationEnabled(); checkContextIsolationEnabled();
return binding.exposeAPIInWorld(worldId, key, api); return binding.exposeAPIInWorld(worldId, key, api);
},
executeInMainWorld: (script) => {
checkContextIsolationEnabled();
return binding.executeInWorld(0, script);
} }
}; };
@ -27,8 +31,7 @@ export const internalContextBridge = {
}, },
overrideGlobalPropertyFromIsolatedWorld: (keys: string[], getter: Function, setter?: Function) => { overrideGlobalPropertyFromIsolatedWorld: (keys: string[], getter: Function, setter?: Function) => {
return binding._overrideGlobalPropertyFromIsolatedWorld(keys, getter, setter || null); return binding._overrideGlobalPropertyFromIsolatedWorld(keys, getter, setter || null);
}, }
isInMainWorld: () => binding._isCalledFromMainWorld() as boolean
}; };
if (binding._isDebug) { if (binding._isDebug) {

View file

@ -13,11 +13,14 @@
#include "base/containers/contains.h" #include "base/containers/contains.h"
#include "base/feature_list.h" #include "base/feature_list.h"
#include "base/json/json_writer.h"
#include "base/trace_event/trace_event.h" #include "base/trace_event/trace_event.h"
#include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_frame_observer.h" #include "content/public/renderer/render_frame_observer.h"
#include "gin/converter.h"
#include "shell/common/gin_converters/blink_converter.h" #include "shell/common/gin_converters/blink_converter.h"
#include "shell/common/gin_converters/callback_converter.h" #include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h" #include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/promise.h" #include "shell/common/gin_helper/promise.h"
#include "shell/common/node_includes.h" #include "shell/common/node_includes.h"
@ -25,6 +28,7 @@
#include "third_party/blink/public/web/web_blob.h" #include "third_party/blink/public/web/web_blob.h"
#include "third_party/blink/public/web/web_element.h" #include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_local_frame.h" #include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h" // nogncheck
namespace features { namespace features {
BASE_FEATURE(kContextBridgeMutability, BASE_FEATURE(kContextBridgeMutability,
@ -133,8 +137,21 @@ v8::MaybeLocal<v8::Value> GetPrivate(v8::Local<v8::Context> context,
} // namespace } // namespace
v8::MaybeLocal<v8::Value> PassValueToOtherContext( // Forward declare methods
void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info);
v8::MaybeLocal<v8::Object> CreateProxyForAPI(
const v8::Local<v8::Object>& api_object,
const v8::Local<v8::Context>& source_context,
const blink::ExecutionContext* source_execution_context,
const v8::Local<v8::Context>& destination_context,
context_bridge::ObjectCache* object_cache,
bool support_dynamic_properties,
int recursion_depth,
BridgeErrorTarget error_target);
v8::MaybeLocal<v8::Value> PassValueToOtherContextInner(
v8::Local<v8::Context> source_context, v8::Local<v8::Context> source_context,
const blink::ExecutionContext* source_execution_context,
v8::Local<v8::Context> destination_context, v8::Local<v8::Context> destination_context,
v8::Local<v8::Value> value, v8::Local<v8::Value> value,
v8::Local<v8::Value> parent_value, v8::Local<v8::Value> parent_value,
@ -142,7 +159,7 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
bool support_dynamic_properties, bool support_dynamic_properties,
int recursion_depth, int recursion_depth,
BridgeErrorTarget error_target) { BridgeErrorTarget error_target) {
TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContext"); TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContextInner");
if (recursion_depth >= kMaxRecursion) { if (recursion_depth >= kMaxRecursion) {
v8::Context::Scope error_scope(error_target == BridgeErrorTarget::kSource v8::Context::Scope error_scope(error_target == BridgeErrorTarget::kSource
? source_context ? source_context
@ -245,7 +262,6 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
if (global_source_context.IsEmpty() || if (global_source_context.IsEmpty() ||
global_destination_context.IsEmpty()) global_destination_context.IsEmpty())
return; return;
context_bridge::ObjectCache object_cache;
v8::MaybeLocal<v8::Value> val; v8::MaybeLocal<v8::Value> val;
{ {
v8::TryCatch try_catch(isolate); v8::TryCatch try_catch(isolate);
@ -253,7 +269,7 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
global_source_context.Get(isolate); global_source_context.Get(isolate);
val = PassValueToOtherContext( val = PassValueToOtherContext(
source_context, global_destination_context.Get(isolate), result, source_context, global_destination_context.Get(isolate), result,
source_context->Global(), &object_cache, false, 0, source_context->Global(), false,
BridgeErrorTarget::kDestination); BridgeErrorTarget::kDestination);
if (try_catch.HasCaught()) { if (try_catch.HasCaught()) {
if (try_catch.Message().IsEmpty()) { if (try_catch.Message().IsEmpty()) {
@ -293,7 +309,6 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
if (global_source_context.IsEmpty() || if (global_source_context.IsEmpty() ||
global_destination_context.IsEmpty()) global_destination_context.IsEmpty())
return; return;
context_bridge::ObjectCache object_cache;
v8::MaybeLocal<v8::Value> val; v8::MaybeLocal<v8::Value> val;
{ {
v8::TryCatch try_catch(isolate); v8::TryCatch try_catch(isolate);
@ -301,7 +316,7 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
global_source_context.Get(isolate); global_source_context.Get(isolate);
val = PassValueToOtherContext( val = PassValueToOtherContext(
source_context, global_destination_context.Get(isolate), result, source_context, global_destination_context.Get(isolate), result,
source_context->Global(), &object_cache, false, 0, source_context->Global(), false,
BridgeErrorTarget::kDestination); BridgeErrorTarget::kDestination);
if (try_catch.HasCaught()) { if (try_catch.HasCaught()) {
if (try_catch.Message().IsEmpty()) { if (try_catch.Message().IsEmpty()) {
@ -367,8 +382,8 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
v8::Local<v8::Array> cloned_arr = v8::Local<v8::Array> cloned_arr =
v8::Array::New(destination_context->GetIsolate(), length); v8::Array::New(destination_context->GetIsolate(), length);
for (size_t i = 0; i < length; i++) { for (size_t i = 0; i < length; i++) {
auto value_for_array = PassValueToOtherContext( auto value_for_array = PassValueToOtherContextInner(
source_context, destination_context, source_context, source_execution_context, destination_context,
arr->Get(source_context, i).ToLocalChecked(), value, object_cache, arr->Get(source_context, i).ToLocalChecked(), value, object_cache,
support_dynamic_properties, recursion_depth + 1, error_target); support_dynamic_properties, recursion_depth + 1, error_target);
if (value_for_array.IsEmpty()) if (value_for_array.IsEmpty())
@ -383,9 +398,11 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
return v8::MaybeLocal<v8::Value>(cloned_arr); return v8::MaybeLocal<v8::Value>(cloned_arr);
} }
// Clone certain DOM APIs only within Window contexts.
if (source_execution_context->IsWindow()) {
// Custom logic to "clone" Element references // Custom logic to "clone" Element references
blink::WebElement elem = blink::WebElement elem = blink::WebElement::FromV8Value(
blink::WebElement::FromV8Value(destination_context->GetIsolate(), value); destination_context->GetIsolate(), value);
if (!elem.IsNull()) { if (!elem.IsNull()) {
v8::Context::Scope destination_context_scope(destination_context); v8::Context::Scope destination_context_scope(destination_context);
return v8::MaybeLocal<v8::Value>( return v8::MaybeLocal<v8::Value>(
@ -400,13 +417,15 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
return v8::MaybeLocal<v8::Value>( return v8::MaybeLocal<v8::Value>(
blob.ToV8Value(destination_context->GetIsolate())); blob.ToV8Value(destination_context->GetIsolate()));
} }
}
// Proxy all objects // Proxy all objects
if (IsPlainObject(value)) { if (IsPlainObject(value)) {
auto object_value = value.As<v8::Object>(); auto object_value = value.As<v8::Object>();
auto passed_value = CreateProxyForAPI( auto passed_value = CreateProxyForAPI(
object_value, source_context, destination_context, object_cache, object_value, source_context, source_execution_context,
support_dynamic_properties, recursion_depth + 1, error_target); destination_context, object_cache, support_dynamic_properties,
recursion_depth + 1, error_target);
if (passed_value.IsEmpty()) if (passed_value.IsEmpty())
return {}; return {};
return v8::MaybeLocal<v8::Value>(passed_value.ToLocalChecked()); return v8::MaybeLocal<v8::Value>(passed_value.ToLocalChecked());
@ -434,6 +453,28 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
} }
} }
v8::MaybeLocal<v8::Value> PassValueToOtherContext(
v8::Local<v8::Context> source_context,
v8::Local<v8::Context> destination_context,
v8::Local<v8::Value> value,
v8::Local<v8::Value> parent_value,
bool support_dynamic_properties,
BridgeErrorTarget error_target,
context_bridge::ObjectCache* existing_object_cache) {
TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContext");
context_bridge::ObjectCache local_object_cache;
context_bridge::ObjectCache* object_cache =
existing_object_cache ? existing_object_cache : &local_object_cache;
const blink::ExecutionContext* source_execution_context =
blink::ExecutionContext::From(source_context);
DCHECK(source_execution_context);
return PassValueToOtherContextInner(
source_context, source_execution_context, destination_context, value,
parent_value, object_cache, support_dynamic_properties, 0, error_target);
}
void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) { void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) {
TRACE_EVENT0("electron", "ContextBridge::ProxyFunctionWrapper"); TRACE_EVENT0("electron", "ContextBridge::ProxyFunctionWrapper");
CHECK(info.Data()->IsObject()); CHECK(info.Data()->IsObject());
@ -464,6 +505,8 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) {
{ {
v8::Context::Scope func_owning_context_scope(func_owning_context); v8::Context::Scope func_owning_context_scope(func_owning_context);
// Cache duplicate arguments as the same proxied value.
context_bridge::ObjectCache object_cache; context_bridge::ObjectCache object_cache;
std::vector<v8::Local<v8::Value>> original_args; std::vector<v8::Local<v8::Value>> original_args;
@ -473,8 +516,8 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) {
for (auto value : original_args) { for (auto value : original_args) {
auto arg = PassValueToOtherContext( auto arg = PassValueToOtherContext(
calling_context, func_owning_context, value, calling_context, func_owning_context, value,
calling_context->Global(), &object_cache, support_dynamic_properties, calling_context->Global(), support_dynamic_properties,
0, BridgeErrorTarget::kSource); BridgeErrorTarget::kSource, &object_cache);
if (arg.IsEmpty()) if (arg.IsEmpty())
return; return;
proxied_args.push_back(arg.ToLocalChecked()); proxied_args.push_back(arg.ToLocalChecked());
@ -540,11 +583,10 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) {
v8::Local<v8::String> exception; v8::Local<v8::String> exception;
{ {
v8::TryCatch try_catch(args.isolate()); v8::TryCatch try_catch(args.isolate());
ret = PassValueToOtherContext(func_owning_context, calling_context, ret = PassValueToOtherContext(
maybe_return_value.ToLocalChecked(), func_owning_context, calling_context,
func_owning_context->Global(), maybe_return_value.ToLocalChecked(), func_owning_context->Global(),
&object_cache, support_dynamic_properties, support_dynamic_properties, BridgeErrorTarget::kDestination);
0, BridgeErrorTarget::kDestination);
if (try_catch.HasCaught()) { if (try_catch.HasCaught()) {
did_error_converting_result = true; did_error_converting_result = true;
if (!try_catch.Message().IsEmpty()) { if (!try_catch.Message().IsEmpty()) {
@ -576,6 +618,7 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) {
v8::MaybeLocal<v8::Object> CreateProxyForAPI( v8::MaybeLocal<v8::Object> CreateProxyForAPI(
const v8::Local<v8::Object>& api_object, const v8::Local<v8::Object>& api_object,
const v8::Local<v8::Context>& source_context, const v8::Local<v8::Context>& source_context,
const blink::ExecutionContext* source_execution_context,
const v8::Local<v8::Context>& destination_context, const v8::Local<v8::Context>& destination_context,
context_bridge::ObjectCache* object_cache, context_bridge::ObjectCache* object_cache,
bool support_dynamic_properties, bool support_dynamic_properties,
@ -619,18 +662,20 @@ v8::MaybeLocal<v8::Object> CreateProxyForAPI(
v8::Local<v8::Value> getter_proxy; v8::Local<v8::Value> getter_proxy;
v8::Local<v8::Value> setter_proxy; v8::Local<v8::Value> setter_proxy;
if (!getter.IsEmpty()) { if (!getter.IsEmpty()) {
if (!PassValueToOtherContext( if (!PassValueToOtherContextInner(
source_context, destination_context, getter, source_context, source_execution_context,
api.GetHandle(), object_cache, destination_context, getter, api.GetHandle(),
support_dynamic_properties, 1, error_target) object_cache, support_dynamic_properties, 1,
error_target)
.ToLocal(&getter_proxy)) .ToLocal(&getter_proxy))
continue; continue;
} }
if (!setter.IsEmpty()) { if (!setter.IsEmpty()) {
if (!PassValueToOtherContext( if (!PassValueToOtherContextInner(
source_context, destination_context, setter, source_context, source_execution_context,
api.GetHandle(), object_cache, destination_context, setter, api.GetHandle(),
support_dynamic_properties, 1, error_target) object_cache, support_dynamic_properties, 1,
error_target)
.ToLocal(&setter_proxy)) .ToLocal(&setter_proxy))
continue; continue;
} }
@ -646,10 +691,10 @@ v8::MaybeLocal<v8::Object> CreateProxyForAPI(
if (!api.Get(key, &value)) if (!api.Get(key, &value))
continue; continue;
auto passed_value = PassValueToOtherContext( auto passed_value = PassValueToOtherContextInner(
source_context, destination_context, value, api.GetHandle(), source_context, source_execution_context, destination_context, value,
object_cache, support_dynamic_properties, recursion_depth + 1, api.GetHandle(), object_cache, support_dynamic_properties,
error_target); recursion_depth + 1, error_target);
if (passed_value.IsEmpty()) if (passed_value.IsEmpty())
return {}; return {};
proxy.Set(key, passed_value.ToLocalChecked()); proxy.Set(key, passed_value.ToLocalChecked());
@ -661,24 +706,14 @@ v8::MaybeLocal<v8::Object> CreateProxyForAPI(
namespace { namespace {
void ExposeAPIInWorld(v8::Isolate* isolate, void ExposeAPI(v8::Isolate* isolate,
const int world_id, v8::Local<v8::Context> source_context,
v8::Local<v8::Context> target_context,
const std::string& key, const std::string& key,
v8::Local<v8::Value> api, v8::Local<v8::Value> api,
gin_helper::Arguments* args) { gin_helper::Arguments* args) {
TRACE_EVENT2("electron", "ContextBridge::ExposeAPIInWorld", "key", key, DCHECK(!target_context.IsEmpty());
"worldId", world_id); v8::Context::Scope target_context_scope(target_context);
auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global());
CHECK(render_frame);
auto* frame = render_frame->GetWebFrame();
CHECK(frame);
v8::Local<v8::Context> target_context =
world_id == WorldIDs::MAIN_WORLD_ID
? frame->MainWorldScriptContext()
: frame->GetScriptContextFromWorldId(isolate, world_id);
gin_helper::Dictionary global(target_context->GetIsolate(), gin_helper::Dictionary global(target_context->GetIsolate(),
target_context->Global()); target_context->Global());
@ -689,17 +724,8 @@ void ExposeAPIInWorld(v8::Isolate* isolate,
return; return;
} }
v8::Local<v8::Context> electron_isolated_context =
frame->GetScriptContextFromWorldId(args->isolate(),
WorldIDs::ISOLATED_WORLD_ID);
{
context_bridge::ObjectCache object_cache;
v8::Context::Scope target_context_scope(target_context);
v8::MaybeLocal<v8::Value> maybe_proxy = PassValueToOtherContext( v8::MaybeLocal<v8::Value> maybe_proxy = PassValueToOtherContext(
electron_isolated_context, target_context, api, source_context, target_context, api, source_context->Global(), false,
electron_isolated_context->Global(), &object_cache, false, 0,
BridgeErrorTarget::kSource); BridgeErrorTarget::kSource);
if (maybe_proxy.IsEmpty()) if (maybe_proxy.IsEmpty())
return; return;
@ -716,6 +742,52 @@ void ExposeAPIInWorld(v8::Isolate* isolate,
global.SetReadOnlyNonConfigurable(key, proxy); global.SetReadOnlyNonConfigurable(key, proxy);
} }
// Attempt to get the target context based on the current context.
//
// For render frames, this is either the main world (0) or an arbitrary
// world ID. For service workers, Electron only supports one isolated
// context and the main worker context. Anything else is invalid.
v8::MaybeLocal<v8::Context> GetTargetContext(v8::Isolate* isolate,
const int world_id) {
v8::Local<v8::Context> source_context = isolate->GetCurrentContext();
v8::MaybeLocal<v8::Context> maybe_target_context;
blink::ExecutionContext* execution_context =
blink::ExecutionContext::From(source_context);
if (execution_context->IsWindow()) {
auto* render_frame = GetRenderFrame(source_context->Global());
CHECK(render_frame);
auto* frame = render_frame->GetWebFrame();
CHECK(frame);
maybe_target_context =
world_id == WorldIDs::MAIN_WORLD_ID
? frame->MainWorldScriptContext()
: frame->GetScriptContextFromWorldId(isolate, world_id);
} else {
NOTREACHED();
}
CHECK(!maybe_target_context.IsEmpty());
return maybe_target_context;
}
void ExposeAPIInWorld(v8::Isolate* isolate,
const int world_id,
const std::string& key,
v8::Local<v8::Value> api,
gin_helper::Arguments* args) {
TRACE_EVENT2("electron", "ContextBridge::ExposeAPIInWorld", "key", key,
"worldId", world_id);
v8::Local<v8::Context> source_context = isolate->GetCurrentContext();
CHECK(!source_context.IsEmpty());
v8::MaybeLocal<v8::Context> maybe_target_context =
GetTargetContext(isolate, world_id);
if (maybe_target_context.IsEmpty())
return;
v8::Local<v8::Context> target_context = maybe_target_context.ToLocalChecked();
ExposeAPI(isolate, source_context, target_context, key, api, args);
} }
gin_helper::Dictionary TraceKeyPath(const gin_helper::Dictionary& start, gin_helper::Dictionary TraceKeyPath(const gin_helper::Dictionary& start,
@ -747,12 +819,10 @@ void OverrideGlobalValueFromIsolatedWorld(
{ {
v8::Context::Scope main_context_scope(main_context); v8::Context::Scope main_context_scope(main_context);
context_bridge::ObjectCache object_cache;
v8::Local<v8::Context> source_context = value->GetCreationContextChecked(); v8::Local<v8::Context> source_context = value->GetCreationContextChecked();
v8::MaybeLocal<v8::Value> maybe_proxy = PassValueToOtherContext( v8::MaybeLocal<v8::Value> maybe_proxy = PassValueToOtherContext(
source_context, main_context, value, source_context->Global(), source_context, main_context, value, source_context->Global(),
&object_cache, support_dynamic_properties, 1, support_dynamic_properties, BridgeErrorTarget::kSource);
BridgeErrorTarget::kSource);
DCHECK(!maybe_proxy.IsEmpty()); DCHECK(!maybe_proxy.IsEmpty());
auto proxy = maybe_proxy.ToLocalChecked(); auto proxy = maybe_proxy.ToLocalChecked();
@ -789,8 +859,8 @@ bool OverrideGlobalPropertyFromIsolatedWorld(
v8::Local<v8::Context> source_context = v8::Local<v8::Context> source_context =
getter->GetCreationContextChecked(); getter->GetCreationContextChecked();
v8::MaybeLocal<v8::Value> maybe_getter_proxy = PassValueToOtherContext( v8::MaybeLocal<v8::Value> maybe_getter_proxy = PassValueToOtherContext(
source_context, main_context, getter, source_context->Global(), source_context, main_context, getter, source_context->Global(), false,
&object_cache, false, 1, BridgeErrorTarget::kSource); BridgeErrorTarget::kSource);
DCHECK(!maybe_getter_proxy.IsEmpty()); DCHECK(!maybe_getter_proxy.IsEmpty());
getter_proxy = maybe_getter_proxy.ToLocalChecked(); getter_proxy = maybe_getter_proxy.ToLocalChecked();
} }
@ -798,8 +868,8 @@ bool OverrideGlobalPropertyFromIsolatedWorld(
v8::Local<v8::Context> source_context = v8::Local<v8::Context> source_context =
getter->GetCreationContextChecked(); getter->GetCreationContextChecked();
v8::MaybeLocal<v8::Value> maybe_setter_proxy = PassValueToOtherContext( v8::MaybeLocal<v8::Value> maybe_setter_proxy = PassValueToOtherContext(
source_context, main_context, setter, source_context->Global(), source_context, main_context, setter, source_context->Global(), false,
&object_cache, false, 1, BridgeErrorTarget::kSource); BridgeErrorTarget::kSource);
DCHECK(!maybe_setter_proxy.IsEmpty()); DCHECK(!maybe_setter_proxy.IsEmpty());
setter_proxy = maybe_setter_proxy.ToLocalChecked(); setter_proxy = maybe_setter_proxy.ToLocalChecked();
} }
@ -812,13 +882,205 @@ bool OverrideGlobalPropertyFromIsolatedWorld(
} }
} }
bool IsCalledFromMainWorld(v8::Isolate* isolate) { // Serialize script to be executed in the given world.
auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global()); v8::Local<v8::Value> ExecuteInWorld(v8::Isolate* isolate,
CHECK(render_frame); const int world_id,
auto* frame = render_frame->GetWebFrame(); gin_helper::Arguments* args) {
CHECK(frame); // Get context of caller
v8::Local<v8::Context> main_context = frame->MainWorldScriptContext(); v8::Local<v8::Context> source_context = isolate->GetCurrentContext();
return isolate->GetCurrentContext() == main_context;
// Get execution script argument
gin_helper::Dictionary exec_script;
if (args->Length() >= 1 && !args->GetNext(&exec_script)) {
gin_helper::ErrorThrower(args->isolate()).ThrowError("Invalid script");
return v8::Undefined(isolate);
}
// Get "func" from execution script
v8::Local<v8::Function> func;
if (!exec_script.Get("func", &func)) {
gin_helper::ErrorThrower(isolate).ThrowError(
"Function 'func' is required in script");
return v8::Undefined(isolate);
}
// Get optional "args" from execution script
v8::Local<v8::Array> args_array;
v8::Local<v8::Value> args_value;
if (exec_script.Get("args", &args_value)) {
if (!args_value->IsArray()) {
gin_helper::ErrorThrower(isolate).ThrowError("'args' must be an array");
return v8::Undefined(isolate);
}
args_array = args_value.As<v8::Array>();
}
// Serialize the function
std::string function_str;
{
v8::Local<v8::String> serialized_function;
if (!func->FunctionProtoToString(isolate->GetCurrentContext())
.ToLocal(&serialized_function)) {
gin_helper::ErrorThrower(isolate).ThrowError(
"Failed to serialize function");
return v8::Undefined(isolate);
}
// If ToLocal() succeeds, this should always be a string.
CHECK(gin::Converter<std::string>::FromV8(isolate, serialized_function,
&function_str));
}
// Get the target context
v8::MaybeLocal<v8::Context> maybe_target_context =
GetTargetContext(isolate, world_id);
v8::Local<v8::Context> target_context;
if (!maybe_target_context.ToLocal(&target_context)) {
isolate->ThrowException(v8::Exception::Error(gin::StringToV8(
isolate,
base::StringPrintf("Failed to get context for world %d", world_id))));
return v8::Undefined(isolate);
}
// Compile the script
v8::Local<v8::Script> compiled_script;
{
v8::Context::Scope target_scope(target_context);
std::string error_message;
v8::MaybeLocal<v8::Script> maybe_compiled_script;
{
v8::TryCatch try_catch(isolate);
std::string return_func_code =
base::StringPrintf("(%s)", function_str.c_str());
maybe_compiled_script = v8::Script::Compile(
target_context, gin::StringToV8(isolate, return_func_code));
if (try_catch.HasCaught()) {
// Must throw outside of TryCatch scope
v8::String::Utf8Value error(isolate, try_catch.Exception());
error_message =
*error ? *error : "Unknown error during script compilation";
}
}
if (!maybe_compiled_script.ToLocal(&compiled_script)) {
isolate->ThrowException(
v8::Exception::Error(gin::StringToV8(isolate, error_message)));
return v8::Undefined(isolate);
}
}
// Run the script
v8::Local<v8::Function> copied_func;
{
v8::Context::Scope target_scope(target_context);
std::string error_message;
v8::MaybeLocal<v8::Value> maybe_script_result;
{
v8::TryCatch try_catch(isolate);
maybe_script_result = compiled_script->Run(target_context);
if (try_catch.HasCaught()) {
// Must throw outside of TryCatch scope
v8::String::Utf8Value error(isolate, try_catch.Exception());
error_message =
*error ? *error : "Unknown error during script execution";
}
}
v8::Local<v8::Value> script_result;
if (!maybe_script_result.ToLocal(&script_result)) {
isolate->ThrowException(
v8::Exception::Error(gin::StringToV8(isolate, error_message)));
return v8::Undefined(isolate);
}
if (!script_result->IsFunction()) {
isolate->ThrowException(v8::Exception::Error(
gin::StringToV8(isolate,
"Expected script to result in a function but a "
"non-function type was found")));
return v8::Undefined(isolate);
}
// Get copied function from the script result
copied_func = script_result.As<v8::Function>();
}
// Proxy args to be passed into copied function
std::vector<v8::Local<v8::Value>> proxied_args;
{
v8::Context::Scope target_scope(target_context);
bool support_dynamic_properties = false;
uint32_t args_length = args_array.IsEmpty() ? 0 : args_array->Length();
// Cache duplicate arguments as the same proxied value.
context_bridge::ObjectCache object_cache;
for (uint32_t i = 0; i < args_length; ++i) {
v8::Local<v8::Value> arg;
if (!args_array->Get(source_context, i).ToLocal(&arg)) {
gin_helper::ErrorThrower(isolate).ThrowError(
base::StringPrintf("Failed to get argument at index %d", i));
return v8::Undefined(isolate);
}
auto proxied_arg = PassValueToOtherContext(
source_context, target_context, arg, source_context->Global(),
support_dynamic_properties, BridgeErrorTarget::kSource,
&object_cache);
if (proxied_arg.IsEmpty()) {
gin_helper::ErrorThrower(isolate).ThrowError(
base::StringPrintf("Failed to proxy argument at index %d", i));
return v8::Undefined(isolate);
}
proxied_args.push_back(proxied_arg.ToLocalChecked());
}
}
// Call the function and get the result
v8::Local<v8::Value> result;
{
v8::Context::Scope target_scope(target_context);
std::string error_message;
v8::MaybeLocal<v8::Value> maybe_result;
{
v8::TryCatch try_catch(isolate);
maybe_result =
copied_func->Call(isolate, target_context, v8::Null(isolate),
proxied_args.size(), proxied_args.data());
if (try_catch.HasCaught()) {
v8::String::Utf8Value error(isolate, try_catch.Exception());
error_message =
*error ? *error : "Unknown error during function execution";
}
}
if (!maybe_result.ToLocal(&result)) {
// Must throw outside of TryCatch scope
isolate->ThrowException(
v8::Exception::Error(gin::StringToV8(isolate, error_message)));
return v8::Undefined(isolate);
}
}
// Clone the result into the source/caller context
v8::Local<v8::Value> cloned_result;
{
v8::Context::Scope source_scope(source_context);
std::string error_message;
v8::MaybeLocal<v8::Value> maybe_cloned_result;
{
v8::TryCatch try_catch(isolate);
// Pass value from target context back to source context
maybe_cloned_result = PassValueToOtherContext(
target_context, source_context, result, target_context->Global(),
false, BridgeErrorTarget::kSource);
if (try_catch.HasCaught()) {
v8::String::Utf8Value utf8(isolate, try_catch.Exception());
error_message = *utf8 ? *utf8 : "Unknown error cloning result";
}
}
if (!maybe_cloned_result.ToLocal(&cloned_result)) {
// Must throw outside of TryCatch scope
isolate->ThrowException(
v8::Exception::Error(gin::StringToV8(isolate, error_message)));
return v8::Undefined(isolate);
}
}
return cloned_result;
} }
} // namespace } // namespace
@ -835,13 +1097,12 @@ void Initialize(v8::Local<v8::Object> exports,
void* priv) { void* priv) {
v8::Isolate* isolate = context->GetIsolate(); v8::Isolate* isolate = context->GetIsolate();
gin_helper::Dictionary dict(isolate, exports); gin_helper::Dictionary dict(isolate, exports);
dict.SetMethod("executeInWorld", &electron::api::ExecuteInWorld);
dict.SetMethod("exposeAPIInWorld", &electron::api::ExposeAPIInWorld); dict.SetMethod("exposeAPIInWorld", &electron::api::ExposeAPIInWorld);
dict.SetMethod("_overrideGlobalValueFromIsolatedWorld", dict.SetMethod("_overrideGlobalValueFromIsolatedWorld",
&electron::api::OverrideGlobalValueFromIsolatedWorld); &electron::api::OverrideGlobalValueFromIsolatedWorld);
dict.SetMethod("_overrideGlobalPropertyFromIsolatedWorld", dict.SetMethod("_overrideGlobalPropertyFromIsolatedWorld",
&electron::api::OverrideGlobalPropertyFromIsolatedWorld); &electron::api::OverrideGlobalPropertyFromIsolatedWorld);
dict.SetMethod("_isCalledFromMainWorld",
&electron::api::IsCalledFromMainWorld);
#if DCHECK_IS_ON() #if DCHECK_IS_ON()
dict.Set("_isDebug", true); dict.Set("_isDebug", true);
#endif #endif

View file

@ -14,8 +14,6 @@ class Arguments;
namespace electron::api { namespace electron::api {
void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info);
// Where the context bridge should create the exception it is about to throw // Where the context bridge should create the exception it is about to throw
enum class BridgeErrorTarget { enum class BridgeErrorTarget {
// The source / calling context. This is default and correct 99% of the time, // The source / calling context. This is default and correct 99% of the time,
@ -44,19 +42,9 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
* the bridge set this to the "context" of the value. * the bridge set this to the "context" of the value.
*/ */
v8::Local<v8::Value> parent_value, v8::Local<v8::Value> parent_value,
context_bridge::ObjectCache* object_cache,
bool support_dynamic_properties, bool support_dynamic_properties,
int recursion_depth, BridgeErrorTarget error_target,
BridgeErrorTarget error_target); context_bridge::ObjectCache* existing_object_cache = nullptr);
v8::MaybeLocal<v8::Object> CreateProxyForAPI(
const v8::Local<v8::Object>& api_object,
const v8::Local<v8::Context>& source_context,
const v8::Local<v8::Context>& destination_context,
context_bridge::ObjectCache* object_cache,
bool support_dynamic_properties,
int recursion_depth,
BridgeErrorTarget error_target);
} // namespace electron::api } // namespace electron::api

View file

@ -150,13 +150,11 @@ class ScriptExecutionCallback {
"An unknown exception occurred while getting the result of the script"; "An unknown exception occurred while getting the result of the script";
{ {
v8::TryCatch try_catch(isolate); v8::TryCatch try_catch(isolate);
context_bridge::ObjectCache object_cache;
v8::Local<v8::Context> source_context = v8::Local<v8::Context> source_context =
result->GetCreationContextChecked(); result->GetCreationContextChecked();
maybe_result = maybe_result = PassValueToOtherContext(
PassValueToOtherContext(source_context, promise_.GetContext(), result, source_context, promise_.GetContext(), result,
source_context->Global(), &object_cache, source_context->Global(), false, BridgeErrorTarget::kSource);
false, 0, BridgeErrorTarget::kSource);
if (maybe_result.IsEmpty() || try_catch.HasCaught()) { if (maybe_result.IsEmpty() || try_catch.HasCaught()) {
success = false; success = false;
} }

View file

@ -611,10 +611,9 @@ void RendererClientBase::SetupMainWorldOverrides(
v8::Local<v8::Value> guest_view_internal; v8::Local<v8::Value> guest_view_internal;
if (global.GetHidden("guestViewInternal", &guest_view_internal)) { if (global.GetHidden("guestViewInternal", &guest_view_internal)) {
api::context_bridge::ObjectCache object_cache;
auto result = api::PassValueToOtherContext( auto result = api::PassValueToOtherContext(
source_context, context, guest_view_internal, source_context->Global(), source_context, context, guest_view_internal, source_context->Global(),
&object_cache, false, 0, api::BridgeErrorTarget::kSource); false, api::BridgeErrorTarget::kSource);
if (!result.IsEmpty()) { if (!result.IsEmpty()) {
isolated_api.Set("guestViewInternal", result.ToLocalChecked()); isolated_api.Set("guestViewInternal", result.ToLocalChecked());
} }

View file

@ -108,7 +108,7 @@ describe('contextBridge', () => {
}; };
const callWithBindings = (fn: Function, worldId: number = 0) => const callWithBindings = (fn: Function, worldId: number = 0) =>
worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`) : w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]); ; worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`) : w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]);
const getGCInfo = async (): Promise<{ const getGCInfo = async (): Promise<{
trackedValues: number; trackedValues: number;
@ -408,6 +408,17 @@ describe('contextBridge', () => {
expect(result).equal(true); expect(result).equal(true);
}); });
it('should proxy function arguments only once', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', (a: any, b: any) => a === b);
});
const result = await callWithBindings(async (root: any) => {
const obj = { foo: 1 };
return root.example(obj, obj);
});
expect(result).to.be.true();
});
it('should properly handle errors thrown in proxied functions', async () => { it('should properly handle errors thrown in proxied functions', async () => {
await makeBindingWindow(() => { await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', () => { throw new Error('oh no'); }); contextBridge.exposeInMainWorld('example', () => { throw new Error('oh no'); });
@ -1290,6 +1301,131 @@ describe('contextBridge', () => {
}); });
}); });
}); });
describe('executeInMainWorld', () => {
it('serializes function and proxies args', async () => {
await makeBindingWindow(async () => {
const values = [
undefined,
null,
123,
'string',
true,
[123, 'string', true, ['foo']],
() => 'string',
Symbol('foo')
];
function appendArg (arg: any) {
// @ts-ignore
globalThis.args = globalThis.args || [];
// @ts-ignore
globalThis.args.push(arg);
}
for (const value of values) {
try {
await contextBridge.executeInMainWorld({
func: appendArg,
args: [value]
});
} catch {
contextBridge.executeInMainWorld({
func: appendArg,
args: ['FAIL']
});
}
}
});
const result = await callWithBindings(() => {
// @ts-ignore
return globalThis.args.map(arg => {
// Map unserializable IPC types to their type string
if (['function', 'symbol'].includes(typeof arg)) {
return typeof arg;
} else {
return arg;
}
});
});
expect(result).to.deep.equal([
undefined,
null,
123,
'string',
true,
[123, 'string', true, ['foo']],
'function',
'symbol'
]);
});
it('allows function args to be invoked', async () => {
const donePromise = once(ipcMain, 'done');
makeBindingWindow(() => {
const uuid = crypto.randomUUID();
const done = (receivedUuid: string) => {
if (receivedUuid === uuid) {
require('electron').ipcRenderer.send('done');
}
};
contextBridge.executeInMainWorld({
func: (callback, innerUuid) => {
callback(innerUuid);
},
args: [done, uuid]
});
});
await donePromise;
});
it('proxies arguments only once', async () => {
await makeBindingWindow(() => {
const obj = {};
// @ts-ignore
globalThis.result = contextBridge.executeInMainWorld({
func: (a, b) => a === b,
args: [obj, obj]
});
});
const result = await callWithBindings(() => {
// @ts-ignore
return globalThis.result;
}, 999);
expect(result).to.be.true();
});
it('safely clones returned objects', async () => {
await makeBindingWindow(() => {
const obj = contextBridge.executeInMainWorld({
func: () => ({})
});
// @ts-ignore
globalThis.safe = obj.constructor === Object;
});
const result = await callWithBindings(() => {
// @ts-ignore
return globalThis.safe;
}, 999);
expect(result).to.be.true();
});
it('uses internal Function.prototype.toString', async () => {
await makeBindingWindow(() => {
const funcHack = () => {
// @ts-ignore
globalThis.hacked = 'nope';
};
funcHack.toString = () => '() => { globalThis.hacked = \'gotem\'; }';
contextBridge.executeInMainWorld({
func: funcHack
});
});
const result = await callWithBindings(() => {
// @ts-ignore
return globalThis.hacked;
});
expect(result).to.equal('nope');
});
});
}); });
}; };

View file

@ -63,7 +63,6 @@ declare namespace Electron {
overrideGlobalValueFromIsolatedWorld(keys: string[], value: any): void; overrideGlobalValueFromIsolatedWorld(keys: string[], value: any): void;
overrideGlobalValueWithDynamicPropsFromIsolatedWorld(keys: string[], value: any): void; overrideGlobalValueWithDynamicPropsFromIsolatedWorld(keys: string[], value: any): void;
overrideGlobalPropertyFromIsolatedWorld(keys: string[], getter: Function, setter?: Function): void; overrideGlobalPropertyFromIsolatedWorld(keys: string[], getter: Function, setter?: Function): void;
isInMainWorld(): boolean;
} }
} }