feat: contextBridge.executeInMainWorld (#45330)

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>
This commit is contained in:
trop[bot] 2025-01-31 09:50:44 -05:00 committed by GitHub
parent 9d696ceffe
commit fa03b92f7e
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]`.
* `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
### API

View file

@ -5,13 +5,17 @@ const checkContextIsolationEnabled = () => {
};
const contextBridge: Electron.ContextBridge = {
exposeInMainWorld: (key: string, api: any) => {
exposeInMainWorld: (key, api) => {
checkContextIsolationEnabled();
return binding.exposeAPIInWorld(0, key, api);
},
exposeInIsolatedWorld: (worldId: number, key: string, api: any) => {
exposeInIsolatedWorld: (worldId, key, api) => {
checkContextIsolationEnabled();
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) => {
return binding._overrideGlobalPropertyFromIsolatedWorld(keys, getter, setter || null);
},
isInMainWorld: () => binding._isCalledFromMainWorld() as boolean
}
};
if (binding._isDebug) {

View file

@ -13,11 +13,14 @@
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/json/json_writer.h"
#include "base/trace_event/trace_event.h"
#include "content/public/renderer/render_frame.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/callback_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/promise.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_element.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 {
BASE_FEATURE(kContextBridgeMutability,
@ -133,8 +137,21 @@ v8::MaybeLocal<v8::Value> GetPrivate(v8::Local<v8::Context> context,
} // 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,
const blink::ExecutionContext* source_execution_context,
v8::Local<v8::Context> destination_context,
v8::Local<v8::Value> value,
v8::Local<v8::Value> parent_value,
@ -142,7 +159,7 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
bool support_dynamic_properties,
int recursion_depth,
BridgeErrorTarget error_target) {
TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContext");
TRACE_EVENT0("electron", "ContextBridge::PassValueToOtherContextInner");
if (recursion_depth >= kMaxRecursion) {
v8::Context::Scope error_scope(error_target == BridgeErrorTarget::kSource
? source_context
@ -245,7 +262,6 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
if (global_source_context.IsEmpty() ||
global_destination_context.IsEmpty())
return;
context_bridge::ObjectCache object_cache;
v8::MaybeLocal<v8::Value> val;
{
v8::TryCatch try_catch(isolate);
@ -253,7 +269,7 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
global_source_context.Get(isolate);
val = PassValueToOtherContext(
source_context, global_destination_context.Get(isolate), result,
source_context->Global(), &object_cache, false, 0,
source_context->Global(), false,
BridgeErrorTarget::kDestination);
if (try_catch.HasCaught()) {
if (try_catch.Message().IsEmpty()) {
@ -293,7 +309,6 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
if (global_source_context.IsEmpty() ||
global_destination_context.IsEmpty())
return;
context_bridge::ObjectCache object_cache;
v8::MaybeLocal<v8::Value> val;
{
v8::TryCatch try_catch(isolate);
@ -301,7 +316,7 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
global_source_context.Get(isolate);
val = PassValueToOtherContext(
source_context, global_destination_context.Get(isolate), result,
source_context->Global(), &object_cache, false, 0,
source_context->Global(), false,
BridgeErrorTarget::kDestination);
if (try_catch.HasCaught()) {
if (try_catch.Message().IsEmpty()) {
@ -367,8 +382,8 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
v8::Local<v8::Array> cloned_arr =
v8::Array::New(destination_context->GetIsolate(), length);
for (size_t i = 0; i < length; i++) {
auto value_for_array = PassValueToOtherContext(
source_context, destination_context,
auto value_for_array = PassValueToOtherContextInner(
source_context, source_execution_context, destination_context,
arr->Get(source_context, i).ToLocalChecked(), value, object_cache,
support_dynamic_properties, recursion_depth + 1, error_target);
if (value_for_array.IsEmpty())
@ -383,9 +398,11 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
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
blink::WebElement elem =
blink::WebElement::FromV8Value(destination_context->GetIsolate(), value);
blink::WebElement elem = blink::WebElement::FromV8Value(
destination_context->GetIsolate(), value);
if (!elem.IsNull()) {
v8::Context::Scope destination_context_scope(destination_context);
return v8::MaybeLocal<v8::Value>(
@ -400,13 +417,15 @@ v8::MaybeLocal<v8::Value> PassValueToOtherContext(
return v8::MaybeLocal<v8::Value>(
blob.ToV8Value(destination_context->GetIsolate()));
}
}
// Proxy all objects
if (IsPlainObject(value)) {
auto object_value = value.As<v8::Object>();
auto passed_value = CreateProxyForAPI(
object_value, source_context, destination_context, object_cache,
support_dynamic_properties, recursion_depth + 1, error_target);
object_value, source_context, source_execution_context,
destination_context, object_cache, support_dynamic_properties,
recursion_depth + 1, error_target);
if (passed_value.IsEmpty())
return {};
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) {
TRACE_EVENT0("electron", "ContextBridge::ProxyFunctionWrapper");
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);
// Cache duplicate arguments as the same proxied value.
context_bridge::ObjectCache object_cache;
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) {
auto arg = PassValueToOtherContext(
calling_context, func_owning_context, value,
calling_context->Global(), &object_cache, support_dynamic_properties,
0, BridgeErrorTarget::kSource);
calling_context->Global(), support_dynamic_properties,
BridgeErrorTarget::kSource, &object_cache);
if (arg.IsEmpty())
return;
proxied_args.push_back(arg.ToLocalChecked());
@ -540,11 +583,10 @@ void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info) {
v8::Local<v8::String> exception;
{
v8::TryCatch try_catch(args.isolate());
ret = PassValueToOtherContext(func_owning_context, calling_context,
maybe_return_value.ToLocalChecked(),
func_owning_context->Global(),
&object_cache, support_dynamic_properties,
0, BridgeErrorTarget::kDestination);
ret = PassValueToOtherContext(
func_owning_context, calling_context,
maybe_return_value.ToLocalChecked(), func_owning_context->Global(),
support_dynamic_properties, BridgeErrorTarget::kDestination);
if (try_catch.HasCaught()) {
did_error_converting_result = true;
if (!try_catch.Message().IsEmpty()) {
@ -576,6 +618,7 @@ 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,
@ -619,18 +662,20 @@ v8::MaybeLocal<v8::Object> CreateProxyForAPI(
v8::Local<v8::Value> getter_proxy;
v8::Local<v8::Value> setter_proxy;
if (!getter.IsEmpty()) {
if (!PassValueToOtherContext(
source_context, destination_context, getter,
api.GetHandle(), object_cache,
support_dynamic_properties, 1, error_target)
if (!PassValueToOtherContextInner(
source_context, source_execution_context,
destination_context, getter, api.GetHandle(),
object_cache, support_dynamic_properties, 1,
error_target)
.ToLocal(&getter_proxy))
continue;
}
if (!setter.IsEmpty()) {
if (!PassValueToOtherContext(
source_context, destination_context, setter,
api.GetHandle(), object_cache,
support_dynamic_properties, 1, error_target)
if (!PassValueToOtherContextInner(
source_context, source_execution_context,
destination_context, setter, api.GetHandle(),
object_cache, support_dynamic_properties, 1,
error_target)
.ToLocal(&setter_proxy))
continue;
}
@ -646,10 +691,10 @@ v8::MaybeLocal<v8::Object> CreateProxyForAPI(
if (!api.Get(key, &value))
continue;
auto passed_value = PassValueToOtherContext(
source_context, destination_context, value, api.GetHandle(),
object_cache, support_dynamic_properties, recursion_depth + 1,
error_target);
auto passed_value = PassValueToOtherContextInner(
source_context, source_execution_context, destination_context, value,
api.GetHandle(), object_cache, support_dynamic_properties,
recursion_depth + 1, error_target);
if (passed_value.IsEmpty())
return {};
proxy.Set(key, passed_value.ToLocalChecked());
@ -661,24 +706,14 @@ v8::MaybeLocal<v8::Object> CreateProxyForAPI(
namespace {
void ExposeAPIInWorld(v8::Isolate* isolate,
const int world_id,
void ExposeAPI(v8::Isolate* isolate,
v8::Local<v8::Context> source_context,
v8::Local<v8::Context> target_context,
const std::string& key,
v8::Local<v8::Value> api,
gin_helper::Arguments* args) {
TRACE_EVENT2("electron", "ContextBridge::ExposeAPIInWorld", "key", key,
"worldId", world_id);
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);
DCHECK(!target_context.IsEmpty());
v8::Context::Scope target_context_scope(target_context);
gin_helper::Dictionary global(target_context->GetIsolate(),
target_context->Global());
@ -689,17 +724,8 @@ void ExposeAPIInWorld(v8::Isolate* isolate,
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(
electron_isolated_context, target_context, api,
electron_isolated_context->Global(), &object_cache, false, 0,
source_context, target_context, api, source_context->Global(), false,
BridgeErrorTarget::kSource);
if (maybe_proxy.IsEmpty())
return;
@ -716,6 +742,52 @@ void ExposeAPIInWorld(v8::Isolate* isolate,
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,
@ -747,12 +819,10 @@ void OverrideGlobalValueFromIsolatedWorld(
{
v8::Context::Scope main_context_scope(main_context);
context_bridge::ObjectCache object_cache;
v8::Local<v8::Context> source_context = value->GetCreationContextChecked();
v8::MaybeLocal<v8::Value> maybe_proxy = PassValueToOtherContext(
source_context, main_context, value, source_context->Global(),
&object_cache, support_dynamic_properties, 1,
BridgeErrorTarget::kSource);
support_dynamic_properties, BridgeErrorTarget::kSource);
DCHECK(!maybe_proxy.IsEmpty());
auto proxy = maybe_proxy.ToLocalChecked();
@ -789,8 +859,8 @@ bool OverrideGlobalPropertyFromIsolatedWorld(
v8::Local<v8::Context> source_context =
getter->GetCreationContextChecked();
v8::MaybeLocal<v8::Value> maybe_getter_proxy = PassValueToOtherContext(
source_context, main_context, getter, source_context->Global(),
&object_cache, false, 1, BridgeErrorTarget::kSource);
source_context, main_context, getter, source_context->Global(), false,
BridgeErrorTarget::kSource);
DCHECK(!maybe_getter_proxy.IsEmpty());
getter_proxy = maybe_getter_proxy.ToLocalChecked();
}
@ -798,8 +868,8 @@ bool OverrideGlobalPropertyFromIsolatedWorld(
v8::Local<v8::Context> source_context =
getter->GetCreationContextChecked();
v8::MaybeLocal<v8::Value> maybe_setter_proxy = PassValueToOtherContext(
source_context, main_context, setter, source_context->Global(),
&object_cache, false, 1, BridgeErrorTarget::kSource);
source_context, main_context, setter, source_context->Global(), false,
BridgeErrorTarget::kSource);
DCHECK(!maybe_setter_proxy.IsEmpty());
setter_proxy = maybe_setter_proxy.ToLocalChecked();
}
@ -812,13 +882,205 @@ bool OverrideGlobalPropertyFromIsolatedWorld(
}
}
bool IsCalledFromMainWorld(v8::Isolate* isolate) {
auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global());
CHECK(render_frame);
auto* frame = render_frame->GetWebFrame();
CHECK(frame);
v8::Local<v8::Context> main_context = frame->MainWorldScriptContext();
return isolate->GetCurrentContext() == main_context;
// Serialize script to be executed in the given world.
v8::Local<v8::Value> ExecuteInWorld(v8::Isolate* isolate,
const int world_id,
gin_helper::Arguments* args) {
// Get context of caller
v8::Local<v8::Context> source_context = isolate->GetCurrentContext();
// 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
@ -835,13 +1097,12 @@ void Initialize(v8::Local<v8::Object> exports,
void* priv) {
v8::Isolate* isolate = context->GetIsolate();
gin_helper::Dictionary dict(isolate, exports);
dict.SetMethod("executeInWorld", &electron::api::ExecuteInWorld);
dict.SetMethod("exposeAPIInWorld", &electron::api::ExposeAPIInWorld);
dict.SetMethod("_overrideGlobalValueFromIsolatedWorld",
&electron::api::OverrideGlobalValueFromIsolatedWorld);
dict.SetMethod("_overrideGlobalPropertyFromIsolatedWorld",
&electron::api::OverrideGlobalPropertyFromIsolatedWorld);
dict.SetMethod("_isCalledFromMainWorld",
&electron::api::IsCalledFromMainWorld);
#if DCHECK_IS_ON()
dict.Set("_isDebug", true);
#endif

View file

@ -14,8 +14,6 @@ class Arguments;
namespace electron::api {
void ProxyFunctionWrapper(const v8::FunctionCallbackInfo<v8::Value>& info);
// Where the context bridge should create the exception it is about to throw
enum class BridgeErrorTarget {
// 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.
*/
v8::Local<v8::Value> parent_value,
context_bridge::ObjectCache* object_cache,
bool support_dynamic_properties,
int recursion_depth,
BridgeErrorTarget error_target);
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);
BridgeErrorTarget error_target,
context_bridge::ObjectCache* existing_object_cache = nullptr);
} // namespace electron::api

View file

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

View file

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

View file

@ -108,7 +108,7 @@ describe('contextBridge', () => {
};
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<{
trackedValues: number;
@ -408,6 +408,17 @@ describe('contextBridge', () => {
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 () => {
await makeBindingWindow(() => {
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;
overrideGlobalValueWithDynamicPropsFromIsolatedWorld(keys: string[], value: any): void;
overrideGlobalPropertyFromIsolatedWorld(keys: string[], getter: Function, setter?: Function): void;
isInMainWorld(): boolean;
}
}