Browse Source

feat: add a new contextBridge module (#20789)

* feat: add a new contextBridge module (#20307)

* feat: add a new contextBridge module

* chore: fix docs linting

* feat: add support for function arguments being proxied

* chore: ensure that contextBridge can only be used when contextIsolation is enabled

* docs: getReverseBinding can be null

* docs: fix broken links in md file

* feat: add support for promises in function parameters

* fix: linting failure for explicit constructor

* Update atom_api_context_bridge.cc

* chore: update docs and API design as per feedback

* refactor: remove reverse bindings and handle GC'able functions across the bridge

* chore: only expose debugGC in testing builds

* fix: do not proxy promises as objects

* spec: add complete spec coverage for contextBridge

* spec: add tests for null/undefined and the anti-overwrite logic

* chore: fix linting

* spec: add complex nested back-and-forth function calling

* fix: expose contextBridge in sandboxed renderers

* refactor: improve security of default_app using the new contextBridge module

* s/bindAPIInMainWorld/exposeInMainWorld

* chore: sorry for this commit, its a big one, I fixed like everything and refactored a lot

* chore: remove PassedValueCache as it is unused now

Values transferred from context A to context B are now cachde in the RenderFramePersistenceStore

* chore: move to anonymous namespace

* refactor: remove PassValueToOtherContextWithCache

* chore: remove commented unused code blocks

* chore: remove .only

* chore: remote commented code

* refactor: extract RenderFramePersistenceStore

* spec: ensure it works with numbered keys

* fix: handle number keys correctly

* fix: sort out the linter

* spec: update default_app asar spec for removed file

* refactor: change signatures to return v8 objects directly rather than the mate dictionary handle

* refactor: use the v8 serializer to support cloneable buffers and other object types

* chore: fix linting

* fix: handle hash collisions with a linked list in the map

* fix: enforce a recursion limit on the context bridge

* chore: fix linting

* chore: remove TODO

* chore: adapt for PR feedback

* chore: remove .only

* chore: clean up docs and clean up the proxy map when objects are released

* chore: ensure we cache object values that are cloned through the V8 serializer

* docs: mark contextBridge as experimental (#20638)

* docs: mark contextBridge as experimental

This commit didn't make it to the original PR, quick addition here

* Update context-bridge.md

* chore: update for 7-0-x differences

* chore: update callback header

* chore: add v8 serializer converter, cherry picked from 2fad53e66b1a2cb6f7dad88fe9bb62d7a461fe98

* chore: update for 7-0-x differences
Samuel Attard 5 years ago
parent
commit
976c7d122a

+ 4 - 2
default_app/index.html

@@ -2,10 +2,9 @@
 
 <head>
   <title>Electron</title>
-  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'" />
+  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-6PH54BfkNq/EMMhUY7nhHf3c+AxloOwfy7hWyT01CM8='; style-src 'self'; img-src 'self'; connect-src 'self'" />
   <link href="./styles.css" type="text/css" rel="stylesheet" />
   <link href="./octicon/build.css" type="text/css" rel="stylesheet" />
-  <script defer src="./index.js"></script>
 </head>
 
 <body>
@@ -84,6 +83,9 @@
       </div>
     </div>
   </nav>
+  <script>
+    window.electronDefaultApp.initialize()
+  </script>
 </body>
 
 </html>

+ 0 - 30
default_app/index.ts

@@ -1,30 +0,0 @@
-async function getOcticonSvg (name: string) {
-  try {
-    const response = await fetch(`octicon/${name}.svg`)
-    const div = document.createElement('div')
-    div.innerHTML = await response.text()
-    return div
-  } catch {
-    return null
-  }
-}
-
-async function loadSVG (element: HTMLSpanElement) {
-  for (const cssClass of element.classList) {
-    if (cssClass.startsWith('octicon-')) {
-      const icon = await getOcticonSvg(cssClass.substr(8))
-      if (icon) {
-        for (const elemClass of element.classList) {
-          icon.classList.add(elemClass)
-        }
-        element.before(icon)
-        element.remove()
-        break
-      }
-    }
-  }
-}
-
-for (const element of document.querySelectorAll<HTMLSpanElement>('.octicon')) {
-  loadSVG(element)
-}

+ 35 - 2
default_app/preload.ts

@@ -1,4 +1,31 @@
-import { ipcRenderer } from 'electron'
+import { ipcRenderer, contextBridge } from 'electron'
+
+async function getOcticonSvg (name: string) {
+  try {
+    const response = await fetch(`octicon/${name}.svg`)
+    const div = document.createElement('div')
+    div.innerHTML = await response.text()
+    return div
+  } catch {
+    return null
+  }
+}
+
+async function loadSVG (element: HTMLSpanElement) {
+  for (const cssClass of element.classList) {
+    if (cssClass.startsWith('octicon-')) {
+      const icon = await getOcticonSvg(cssClass.substr(8))
+      if (icon) {
+        for (const elemClass of element.classList) {
+          icon.classList.add(elemClass)
+        }
+        element.before(icon)
+        element.remove()
+        break
+      }
+    }
+  }
+}
 
 async function initialize () {
   const electronPath = await ipcRenderer.invoke('bootstrap')
@@ -15,6 +42,12 @@ async function initialize () {
   replaceText('.node-version', `Node v${process.versions.node}`)
   replaceText('.v8-version', `v8 v${process.versions.v8}`)
   replaceText('.command-example', `${electronPath} path-to-app`)
+
+  for (const element of document.querySelectorAll<HTMLSpanElement>('.octicon')) {
+    loadSVG(element)
+  }
 }
 
-document.addEventListener('DOMContentLoaded', initialize)
+contextBridge.exposeInMainWorld('electronDefaultApp', {
+  initialize
+})

+ 111 - 0
docs/api/context-bridge.md

@@ -0,0 +1,111 @@
+# contextBridge
+
+> Create a safe, bi-directional, synchronous bridge across isolated contexts
+
+Process: [Renderer](../glossary.md#renderer-process)
+
+An example of exposing an API to a renderer from an isolated preload script is given below:
+
+```javascript
+// Preload (Isolated World)
+const { contextBridge, ipcRenderer } = require('electron')
+
+contextBridge.exposeInMainWorld(
+  'electron',
+  {
+    doThing: () => ipcRenderer.send('do-a-thing')
+  }
+)
+```
+
+```javascript
+// Renderer (Main World)
+
+window.electron.doThing()
+```
+
+## Glossary
+
+### Main World
+
+The "Main World" is the javascript context that your main renderer code runs in.  By default the page you load in your renderer
+executes code in this world.
+
+### Isolated World
+
+When `contextIsolation` is enabled in your `webPreferences` your `preload` scripts run in an "Isolated World".  You can read more about
+context isolation and what it affects in the [BrowserWindow](browser-window.md) docs.
+
+## Methods
+
+The `contextBridge` module has the following methods:
+
+### `contextBridge.exposeInMainWorld(apiKey, api)` _Experimental_
+
+* `apiKey` String - The key to inject the API onto `window` with.  The API will be accessible on `window[apiKey]`.
+* `api` Record<String, any> - Your API object, more information on what this API can be and how it works is available below.
+
+## Usage
+
+### API Objects
+
+The `api` object provided to [`exposeInMainWorld`](#contextbridgeexposeinmainworldapikey-api-experimental) must be an object
+whose keys are strings and values are a `Function`, `String`, `Number`, `Array`, `Boolean` or another nested object that meets the same conditions.
+
+`Function` values are proxied to the other context and all other values are **copied** and **frozen**.  I.e. Any data / primitives sent in
+the API object become immutable and updates on either side of the bridge do not result in an update on the other side.
+
+An example of a complex API object is shown below.
+
+```javascript
+const { contextBridge } = require('electron')
+
+contextBridge.exposeInMainWorld(
+  'electron',
+  {
+    doThing: () => ipcRenderer.send('do-a-thing'),
+    myPromises: [Promise.resolve(), Promise.reject(new Error('whoops'))],
+    anAsyncFunction: async () => 123,
+    data: {
+      myFlags: ['a', 'b', 'c'],
+      bootTime: 1234
+    },
+    nestedAPI: {
+      evenDeeper: {
+        youCanDoThisAsMuchAsYouWant: {
+          fn: () => ({
+            returnData: 123
+          })
+        }
+      }
+    }
+  }
+)
+```
+
+### API Functions
+
+`Function` values that you bind through the `contextBridge` are proxied through Electron to ensure that contexts remain isolated.  This
+results in some key limitations that we've outlined below.
+
+#### Parameter / Error / Return Type support
+
+Because parameters, errors and return values are **copied** when they are sent over the bridge there are only certain types that can be used.
+At a high level if the type you want to use can be serialized and un-serialized into the same object it will work.  A table of type support
+has been included below for completeness.
+
+| Type | Complexity | Parameter Support | Return Value Support | Limitations |
+| ---- | ---------- | ----------------- | -------------------- | ----------- |
+| `String` | Simple | ✅ | ✅ | N/A |
+| `Number` | Simple | ✅ | ✅ | N/A |
+| `Boolean` | Simple | ✅ | ✅ | N/A |
+| `Object` | Complex | ✅ | ✅ | Keys must be supported "Simple" types in this table.  Values must be supported in this table.  Prototype modifications are dropped.  Sending custom classes will copy values but not the prototype. |
+| `Array` | Complex | ✅ | ✅ | Same limitations as the `Object` type |
+| `Error` | Complex | ✅ | ✅ | Errors that are thrown are also copied, this can result in the message and stack trace of the error changing slightly due to being thrown in a different context |
+| `Promise` | Complex | ✅ | ✅ | Promises are only proxied if they are a the return value or exact parameter.  Promises nested in arrays or obejcts will be dropped. |
+| `Function` | Complex | ✅ | ✅ | Prototype modifications are dropped.  Sending classes or constructors will not work. |
+| [Cloneable Types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) | Simple | ✅ | ✅ | See the linked document on cloneable types |
+| `Symbol` | N/A | ❌ | ❌ | Symbols cannot be copied across contexts so they are dropped |
+
+
+If the type you care about is not in the above table it is probably not supported.

+ 4 - 0
filenames.auto.gni

@@ -14,6 +14,7 @@ auto_filenames = {
     "docs/api/clipboard.md",
     "docs/api/command-line.md",
     "docs/api/content-tracing.md",
+    "docs/api/context-bridge.md",
     "docs/api/cookies.md",
     "docs/api/crash-reporter.md",
     "docs/api/debugger.md",
@@ -140,6 +141,7 @@ auto_filenames = {
     "lib/common/error-utils.ts",
     "lib/common/is-promise.ts",
     "lib/common/web-view-methods.ts",
+    "lib/renderer/api/context-bridge.ts",
     "lib/renderer/api/crash-reporter.js",
     "lib/renderer/api/desktop-capturer.ts",
     "lib/renderer/api/ipc-renderer.js",
@@ -299,6 +301,7 @@ auto_filenames = {
     "lib/common/is-promise.ts",
     "lib/common/reset-search-paths.ts",
     "lib/common/web-view-methods.ts",
+    "lib/renderer/api/context-bridge.ts",
     "lib/renderer/api/crash-reporter.js",
     "lib/renderer/api/desktop-capturer.ts",
     "lib/renderer/api/exports/electron.js",
@@ -348,6 +351,7 @@ auto_filenames = {
     "lib/common/init.ts",
     "lib/common/is-promise.ts",
     "lib/common/reset-search-paths.ts",
+    "lib/renderer/api/context-bridge.ts",
     "lib/renderer/api/crash-reporter.js",
     "lib/renderer/api/desktop-capturer.ts",
     "lib/renderer/api/exports/electron.js",

+ 4 - 1
filenames.gni

@@ -1,7 +1,6 @@
 filenames = {
   default_app_ts_sources = [
     "default_app/default_app.ts",
-    "default_app/index.ts",
     "default_app/main.ts",
     "default_app/preload.ts",
   ]
@@ -537,6 +536,10 @@ filenames = {
     "shell/common/promise_util.cc",
     "shell/common/skia_util.h",
     "shell/common/skia_util.cc",
+    "shell/renderer/api/context_bridge/render_frame_context_bridge_store.cc",
+    "shell/renderer/api/context_bridge/render_frame_context_bridge_store.h",
+    "shell/renderer/api/atom_api_context_bridge.cc",
+    "shell/renderer/api/atom_api_context_bridge.h",
     "shell/renderer/api/atom_api_renderer_ipc.cc",
     "shell/renderer/api/atom_api_spell_check_client.cc",
     "shell/renderer/api/atom_api_spell_check_client.h",

+ 20 - 0
lib/renderer/api/context-bridge.ts

@@ -0,0 +1,20 @@
+const { hasSwitch } = process.electronBinding('command_line')
+const binding = process.electronBinding('context_bridge')
+
+const contextIsolationEnabled = hasSwitch('context-isolation')
+
+const checkContextIsolationEnabled = () => {
+  if (!contextIsolationEnabled) throw new Error('contextBridge API can only be used when contextIsolation is enabled')
+}
+
+const contextBridge = {
+  exposeInMainWorld: (key: string, api: Record<string, any>) => {
+    checkContextIsolationEnabled()
+    return binding.exposeAPIInMainWorld(key, api)
+  },
+  debugGC: () => binding._debugGCMaps({})
+}
+
+if (!binding._debugGCMaps) delete contextBridge.debugGC
+
+export default contextBridge

+ 1 - 0
lib/renderer/api/module-list.js

@@ -8,6 +8,7 @@ const enableRemoteModule = v8Util.getHiddenValue(global, 'enableRemoteModule')
 // Renderer side modules, please sort alphabetically.
 // A module is `enabled` if there is no explicit condition defined.
 module.exports = [
+  { name: 'contextBridge', loader: () => require('./context-bridge') },
   { name: 'crashReporter', loader: () => require('./crash-reporter') },
   { name: 'ipcRenderer', loader: () => require('./ipc-renderer') },
   { name: 'webFrame', loader: () => require('./web-frame') }

+ 4 - 0
lib/sandboxed_renderer/api/module-list.js

@@ -3,6 +3,10 @@
 const features = process.electronBinding('features')
 
 module.exports = [
+  {
+    name: 'contextBridge',
+    load: () => require('@electron/internal/renderer/api/context-bridge')
+  },
   {
     name: 'crashReporter',
     load: () => require('@electron/internal/renderer/api/crash-reporter')

+ 2 - 2
native_mate/native_mate/arguments.h

@@ -27,7 +27,7 @@ class Arguments {
 
   template <typename T>
   bool GetHolder(T* out) {
-    return ConvertFromV8(isolate_, info_->Holder(), out);
+    return mate::ConvertFromV8(isolate_, info_->Holder(), out);
   }
 
   template <typename T>
@@ -53,7 +53,7 @@ class Arguments {
       return false;
     }
     v8::Local<v8::Value> val = (*info_)[next_];
-    bool success = ConvertFromV8(isolate_, val, out);
+    bool success = mate::ConvertFromV8(isolate_, val, out);
     if (success)
       next_++;
     return success;

+ 17 - 0
native_mate/native_mate/dictionary.h

@@ -40,6 +40,12 @@ class Dictionary {
 
   static Dictionary CreateEmpty(v8::Isolate* isolate);
 
+  bool Has(base::StringPiece key) const {
+    v8::Local<v8::Context> context = isolate_->GetCurrentContext();
+    v8::Local<v8::String> v8_key = StringToV8(isolate_, key);
+    return internal::IsTrue(GetHandle()->Has(context, v8_key));
+  }
+
   template <typename T>
   bool Get(base::StringPiece key, T* out) const {
     // Check for existence before getting, otherwise this method will always
@@ -102,6 +108,17 @@ class Dictionary {
     return !result.IsNothing() && result.FromJust();
   }
 
+  template <typename T>
+  bool SetReadOnlyNonConfigurable(base::StringPiece key, T val) {
+    v8::Local<v8::Value> v8_value;
+    if (!TryConvertToV8(isolate_, val, &v8_value))
+      return false;
+    v8::Maybe<bool> result = GetHandle()->DefineOwnProperty(
+        isolate_->GetCurrentContext(), StringToV8(isolate_, key), v8_value,
+        static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete));
+    return !result.IsNothing() && result.FromJust();
+  }
+
   template <typename T>
   bool SetMethod(base::StringPiece key, const T& callback) {
     return GetHandle()

+ 175 - 0
shell/common/native_mate_converters/blink_converter.cc

@@ -6,14 +6,18 @@
 
 #include <algorithm>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "content/public/browser/native_web_keyboard_event.h"
 #include "gin/converter.h"
+#include "mojo/public/cpp/base/values_mojom_traits.h"
+#include "mojo/public/mojom/base/values.mojom.h"
 #include "native_mate/dictionary.h"
 #include "shell/common/keyboard_util.h"
+#include "shell/common/native_mate_converters/value_converter.h"
 #include "third_party/blink/public/platform/web_input_event.h"
 #include "third_party/blink/public/platform/web_mouse_event.h"
 #include "third_party/blink/public/platform/web_mouse_wheel_event.h"
@@ -527,4 +531,175 @@ bool Converter<network::mojom::ReferrerPolicy>::FromV8(
   return true;
 }
 
+namespace {
+constexpr uint8_t kNewSerializationTag = 0;
+constexpr uint8_t kOldSerializationTag = 1;
+
+class V8Serializer : public v8::ValueSerializer::Delegate {
+ public:
+  explicit V8Serializer(v8::Isolate* isolate,
+                        bool use_old_serialization = false)
+      : isolate_(isolate),
+        serializer_(isolate, this),
+        use_old_serialization_(use_old_serialization) {}
+  ~V8Serializer() override = default;
+
+  bool Serialize(v8::Local<v8::Value> value, blink::CloneableMessage* out) {
+    serializer_.WriteHeader();
+    if (use_old_serialization_) {
+      WriteTag(kOldSerializationTag);
+      if (!WriteBaseValue(value)) {
+        isolate_->ThrowException(
+            mate::StringToV8(isolate_, "An object could not be cloned."));
+        return false;
+      }
+    } else {
+      WriteTag(kNewSerializationTag);
+      bool wrote_value;
+      v8::TryCatch try_catch(isolate_);
+      if (!serializer_.WriteValue(isolate_->GetCurrentContext(), value)
+               .To(&wrote_value)) {
+        try_catch.Reset();
+        if (!V8Serializer(isolate_, true).Serialize(value, out)) {
+          try_catch.ReThrow();
+          return false;
+        }
+        return true;
+      }
+      DCHECK(wrote_value);
+    }
+
+    std::pair<uint8_t*, size_t> buffer = serializer_.Release();
+    DCHECK_EQ(buffer.first, data_.data());
+    out->encoded_message = base::make_span(buffer.first, buffer.second);
+    out->owned_encoded_message = std::move(data_);
+
+    return true;
+  }
+
+  bool WriteBaseValue(v8::Local<v8::Value> object) {
+    base::Value value;
+    if (!ConvertFromV8(isolate_, object, &value)) {
+      return false;
+    }
+    mojo::Message message = mojo_base::mojom::Value::SerializeAsMessage(&value);
+
+    serializer_.WriteUint32(message.data_num_bytes());
+    serializer_.WriteRawBytes(message.data(), message.data_num_bytes());
+    return true;
+  }
+
+  void WriteTag(uint8_t tag) { serializer_.WriteRawBytes(&tag, 1); }
+
+  // v8::ValueSerializer::Delegate
+  void* ReallocateBufferMemory(void* old_buffer,
+                               size_t size,
+                               size_t* actual_size) override {
+    DCHECK_EQ(old_buffer, data_.data());
+    data_.resize(size);
+    *actual_size = data_.capacity();
+    return data_.data();
+  }
+
+  void FreeBufferMemory(void* buffer) override {
+    DCHECK_EQ(buffer, data_.data());
+    data_ = {};
+  }
+
+  void ThrowDataCloneError(v8::Local<v8::String> message) override {
+    isolate_->ThrowException(v8::Exception::Error(message));
+  }
+
+ private:
+  v8::Isolate* isolate_;
+  std::vector<uint8_t> data_;
+  v8::ValueSerializer serializer_;
+  bool use_old_serialization_;
+};
+
+class V8Deserializer : public v8::ValueDeserializer::Delegate {
+ public:
+  V8Deserializer(v8::Isolate* isolate, const blink::CloneableMessage& message)
+      : isolate_(isolate),
+        deserializer_(isolate,
+                      message.encoded_message.data(),
+                      message.encoded_message.size(),
+                      this) {}
+
+  v8::Local<v8::Value> Deserialize() {
+    v8::EscapableHandleScope scope(isolate_);
+    auto context = isolate_->GetCurrentContext();
+    bool read_header;
+    if (!deserializer_.ReadHeader(context).To(&read_header))
+      return v8::Null(isolate_);
+    DCHECK(read_header);
+    uint8_t tag;
+    if (!ReadTag(&tag))
+      return v8::Null(isolate_);
+    switch (tag) {
+      case kNewSerializationTag: {
+        v8::Local<v8::Value> value;
+        if (!deserializer_.ReadValue(context).ToLocal(&value)) {
+          return v8::Null(isolate_);
+        }
+        return scope.Escape(value);
+      }
+      case kOldSerializationTag: {
+        v8::Local<v8::Value> value;
+        if (!ReadBaseValue(&value)) {
+          return v8::Null(isolate_);
+        }
+        return scope.Escape(value);
+      }
+      default:
+        NOTREACHED() << "Invalid tag: " << tag;
+        return v8::Null(isolate_);
+    }
+  }
+
+  bool ReadTag(uint8_t* tag) {
+    const void* tag_bytes;
+    if (!deserializer_.ReadRawBytes(1, &tag_bytes))
+      return false;
+    *tag = *reinterpret_cast<const uint8_t*>(tag_bytes);
+    return true;
+  }
+
+  bool ReadBaseValue(v8::Local<v8::Value>* value) {
+    uint32_t length;
+    const void* data;
+    if (!deserializer_.ReadUint32(&length) ||
+        !deserializer_.ReadRawBytes(length, &data)) {
+      return false;
+    }
+    mojo::Message message(
+        base::make_span(reinterpret_cast<const uint8_t*>(data), length), {});
+    base::Value out;
+    if (!mojo_base::mojom::Value::DeserializeFromMessage(std::move(message),
+                                                         &out)) {
+      return false;
+    }
+    *value = ConvertToV8(isolate_, out);
+    return true;
+  }
+
+ private:
+  v8::Isolate* isolate_;
+  v8::ValueDeserializer deserializer_;
+};
+
+}  // namespace
+
+v8::Local<v8::Value> Converter<blink::CloneableMessage>::ToV8(
+    v8::Isolate* isolate,
+    const blink::CloneableMessage& in) {
+  return V8Deserializer(isolate, in).Deserialize();
+}
+
+bool Converter<blink::CloneableMessage>::FromV8(v8::Isolate* isolate,
+                                                v8::Handle<v8::Value> val,
+                                                blink::CloneableMessage* out) {
+  return V8Serializer(isolate).Serialize(val, out);
+}
+
 }  // namespace mate

+ 10 - 0
shell/common/native_mate_converters/blink_converter.h

@@ -6,6 +6,7 @@
 #define SHELL_COMMON_NATIVE_MATE_CONVERTERS_BLINK_CONVERTER_H_
 
 #include "native_mate/converter.h"
+#include "third_party/blink/public/common/messaging/cloneable_message.h"
 #include "third_party/blink/public/platform/web_cache.h"
 #include "third_party/blink/public/platform/web_input_event.h"
 #include "third_party/blink/public/web/web_context_menu_data.h"
@@ -131,6 +132,15 @@ struct Converter<network::mojom::ReferrerPolicy> {
                      network::mojom::ReferrerPolicy* out);
 };
 
+template <>
+struct Converter<blink::CloneableMessage> {
+  static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
+                                   const blink::CloneableMessage& in);
+  static bool FromV8(v8::Isolate* isolate,
+                     v8::Local<v8::Value> val,
+                     blink::CloneableMessage* out);
+};
+
 v8::Local<v8::Value> EditFlagsToV8(v8::Isolate* isolate, int editFlags);
 v8::Local<v8::Value> MediaFlagsToV8(v8::Isolate* isolate, int mediaFlags);
 

+ 1 - 0
shell/common/node_bindings.cc

@@ -66,6 +66,7 @@
   V(atom_common_screen)              \
   V(atom_common_shell)               \
   V(atom_common_v8_util)             \
+  V(atom_renderer_context_bridge)    \
   V(atom_renderer_ipc)               \
   V(atom_renderer_web_frame)
 

+ 10 - 0
shell/common/promise_util.h

@@ -119,6 +119,16 @@ class Promise {
     return GetInner()->Reject(GetContext(), v8::Undefined(isolate()));
   }
 
+  v8::Maybe<bool> Reject(v8::Local<v8::Value> exception) {
+    v8::HandleScope handle_scope(isolate());
+    v8::MicrotasksScope script_scope(isolate(),
+                                     v8::MicrotasksScope::kRunMicrotasks);
+    v8::Context::Scope context_scope(
+        v8::Local<v8::Context>::New(isolate(), GetContext()));
+
+    return GetInner()->Reject(GetContext(), exception);
+  }
+
   // Please note that using Then is effectively the same as calling .then
   // in javascript.  This means (a) it is not type safe and (b) please note
   // it is NOT type safe.

+ 513 - 0
shell/renderer/api/atom_api_context_bridge.cc

@@ -0,0 +1,513 @@
+// Copyright (c) 2019 Slack Technologies, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/renderer/api/atom_api_context_bridge.h"
+
+#include <set>
+#include <utility>
+#include <vector>
+
+#include "base/no_destructor.h"
+#include "base/strings/string_number_conversions.h"
+#include "shell/common/api/object_life_monitor.h"
+#include "shell/common/native_mate_converters/blink_converter.h"
+#include "shell/common/native_mate_converters/callback.h"
+#include "shell/common/native_mate_converters/once_callback.h"
+#include "shell/common/promise_util.h"
+
+namespace electron {
+
+namespace api {
+
+namespace {
+
+static int kMaxRecursion = 1000;
+
+content::RenderFrame* GetRenderFrame(const v8::Local<v8::Object>& value) {
+  v8::Local<v8::Context> context = value->CreationContext();
+  if (context.IsEmpty())
+    return nullptr;
+  blink::WebLocalFrame* frame = blink::WebLocalFrame::FrameForContext(context);
+  if (!frame)
+    return nullptr;
+  return content::RenderFrame::FromWebFrame(frame);
+}
+
+std::map<content::RenderFrame*, context_bridge::RenderFramePersistenceStore*>&
+GetStoreMap() {
+  static base::NoDestructor<std::map<
+      content::RenderFrame*, context_bridge::RenderFramePersistenceStore*>>
+      store_map;
+  return *store_map;
+}
+
+context_bridge::RenderFramePersistenceStore* GetOrCreateStore(
+    content::RenderFrame* render_frame) {
+  auto it = GetStoreMap().find(render_frame);
+  if (it == GetStoreMap().end()) {
+    auto* store = new context_bridge::RenderFramePersistenceStore(render_frame);
+    GetStoreMap().emplace(render_frame, store);
+    return store;
+  }
+  return it->second;
+}
+
+// Sourced from "extensions/renderer/v8_schema_registry.cc"
+// Recursively freezes every v8 object on |object|.
+bool DeepFreeze(const v8::Local<v8::Object>& object,
+                const v8::Local<v8::Context>& context,
+                std::set<int> frozen = std::set<int>()) {
+  int hash = object->GetIdentityHash();
+  if (frozen.find(hash) != frozen.end())
+    return true;
+  frozen.insert(hash);
+
+  v8::Local<v8::Array> property_names =
+      object->GetOwnPropertyNames(context).ToLocalChecked();
+  for (uint32_t i = 0; i < property_names->Length(); ++i) {
+    v8::Local<v8::Value> child =
+        object->Get(context, property_names->Get(context, i).ToLocalChecked())
+            .ToLocalChecked();
+    if (child->IsObject() && !child->IsTypedArray()) {
+      if (!DeepFreeze(v8::Local<v8::Object>::Cast(child), context, frozen))
+        return false;
+    }
+  }
+  return mate::internal::IsTrue(
+      object->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen));
+}
+
+bool IsPlainObject(const v8::Local<v8::Value>& object) {
+  if (!object->IsObject())
+    return false;
+
+  return !(object->IsNullOrUndefined() || object->IsDate() ||
+           object->IsArgumentsObject() || object->IsBigIntObject() ||
+           object->IsBooleanObject() || object->IsNumberObject() ||
+           object->IsStringObject() || object->IsSymbolObject() ||
+           object->IsNativeError() || object->IsRegExp() ||
+           object->IsPromise() || object->IsMap() || object->IsSet() ||
+           object->IsMapIterator() || object->IsSetIterator() ||
+           object->IsWeakMap() || object->IsWeakSet() ||
+           object->IsArrayBuffer() || object->IsArrayBufferView() ||
+           object->IsArray() || object->IsDataView() ||
+           object->IsSharedArrayBuffer() || object->IsProxy() ||
+           object->IsWebAssemblyCompiledModule() ||
+           object->IsModuleNamespaceObject());
+}
+
+bool IsPlainArray(const v8::Local<v8::Value>& arr) {
+  if (!arr->IsArray())
+    return false;
+
+  return !arr->IsTypedArray();
+}
+
+class FunctionLifeMonitor final : public ObjectLifeMonitor {
+ public:
+  static void BindTo(v8::Isolate* isolate,
+                     v8::Local<v8::Object> target,
+                     context_bridge::RenderFramePersistenceStore* store,
+                     size_t func_id) {
+    new FunctionLifeMonitor(isolate, target, store, func_id);
+  }
+
+ protected:
+  FunctionLifeMonitor(v8::Isolate* isolate,
+                      v8::Local<v8::Object> target,
+                      context_bridge::RenderFramePersistenceStore* store,
+                      size_t func_id)
+      : ObjectLifeMonitor(isolate, target), store_(store), func_id_(func_id) {}
+  ~FunctionLifeMonitor() override = default;
+
+  void RunDestructor() override { store_->functions().erase(func_id_); }
+
+ private:
+  context_bridge::RenderFramePersistenceStore* store_;
+  size_t func_id_;
+};
+
+}  // namespace
+
+template <typename Sig>
+v8::Local<v8::Value> BindRepeatingFunctionToV8(
+    v8::Isolate* isolate,
+    const base::RepeatingCallback<Sig>& val) {
+  auto translater =
+      base::BindRepeating(&mate::internal::NativeFunctionInvoker<Sig>::Go, val);
+  return mate::internal::CreateFunctionFromTranslater(isolate, translater,
+                                                      false);
+}
+
+v8::MaybeLocal<v8::Value> PassValueToOtherContext(
+    v8::Local<v8::Context> source_context,
+    v8::Local<v8::Context> destination_context,
+    v8::Local<v8::Value> value,
+    context_bridge::RenderFramePersistenceStore* store,
+    int recursion_depth) {
+  if (recursion_depth >= kMaxRecursion) {
+    v8::Context::Scope source_scope(source_context);
+    {
+      source_context->GetIsolate()->ThrowException(v8::Exception::TypeError(
+          mate::StringToV8(source_context->GetIsolate(),
+                           "Electron contextBridge recursion depth exceeded.  "
+                           "Nested objects "
+                           "deeper than 1000 are not supported.")));
+      return v8::MaybeLocal<v8::Value>();
+    }
+  }
+  // Check Cache
+  auto cached_value = store->GetCachedProxiedObject(value);
+  if (!cached_value.IsEmpty()) {
+    return cached_value;
+  }
+
+  // Proxy functions and monitor the lifetime in the new context to release
+  // the global handle at the right time.
+  if (value->IsFunction()) {
+    auto func = v8::Local<v8::Function>::Cast(value);
+    v8::Global<v8::Function> global_func(source_context->GetIsolate(), func);
+    v8::Global<v8::Context> global_source(source_context->GetIsolate(),
+                                          source_context);
+
+    size_t func_id = store->take_func_id();
+    store->functions()[func_id] =
+        std::make_tuple(std::move(global_func), std::move(global_source));
+    v8::Context::Scope destination_scope(destination_context);
+    {
+      v8::Local<v8::Value> proxy_func = BindRepeatingFunctionToV8(
+          destination_context->GetIsolate(),
+          base::BindRepeating(&ProxyFunctionWrapper, store, func_id));
+      FunctionLifeMonitor::BindTo(destination_context->GetIsolate(),
+                                  v8::Local<v8::Object>::Cast(proxy_func),
+                                  store, func_id);
+      store->CacheProxiedObject(value, proxy_func);
+      return v8::MaybeLocal<v8::Value>(proxy_func);
+    }
+  }
+
+  // Proxy promises as they have a safe and guaranteed memory lifecycle
+  if (value->IsPromise()) {
+    v8::Context::Scope destination_scope(destination_context);
+    {
+      auto source_promise = v8::Local<v8::Promise>::Cast(value);
+      auto* proxied_promise =
+          new util::Promise(destination_context->GetIsolate());
+      v8::Local<v8::Promise> proxied_promise_handle =
+          proxied_promise->GetHandle();
+
+      auto then_cb = base::BindOnce(
+          [](util::Promise* proxied_promise, v8::Isolate* isolate,
+             v8::Global<v8::Context> global_source_context,
+             v8::Global<v8::Context> global_destination_context,
+             context_bridge::RenderFramePersistenceStore* store,
+             v8::Local<v8::Value> result) {
+            auto val = PassValueToOtherContext(
+                global_source_context.Get(isolate),
+                global_destination_context.Get(isolate), result, store, 0);
+            if (!val.IsEmpty())
+              proxied_promise->Resolve(val.ToLocalChecked());
+            delete proxied_promise;
+          },
+          proxied_promise, destination_context->GetIsolate(),
+          v8::Global<v8::Context>(source_context->GetIsolate(), source_context),
+          v8::Global<v8::Context>(destination_context->GetIsolate(),
+                                  destination_context),
+          store);
+      auto catch_cb = base::BindOnce(
+          [](util::Promise* proxied_promise, v8::Isolate* isolate,
+             v8::Global<v8::Context> global_source_context,
+             v8::Global<v8::Context> global_destination_context,
+             context_bridge::RenderFramePersistenceStore* store,
+             v8::Local<v8::Value> result) {
+            auto val = PassValueToOtherContext(
+                global_source_context.Get(isolate),
+                global_destination_context.Get(isolate), result, store, 0);
+            if (!val.IsEmpty())
+              proxied_promise->Reject(val.ToLocalChecked());
+            delete proxied_promise;
+          },
+          proxied_promise, destination_context->GetIsolate(),
+          v8::Global<v8::Context>(source_context->GetIsolate(), source_context),
+          v8::Global<v8::Context>(destination_context->GetIsolate(),
+                                  destination_context),
+          store);
+
+      ignore_result(source_promise->Then(
+          source_context,
+          v8::Local<v8::Function>::Cast(
+              mate::ConvertToV8(destination_context->GetIsolate(), then_cb)),
+          v8::Local<v8::Function>::Cast(
+              mate::ConvertToV8(destination_context->GetIsolate(), catch_cb))));
+
+      store->CacheProxiedObject(value, proxied_promise_handle);
+      return v8::MaybeLocal<v8::Value>(proxied_promise_handle);
+    }
+  }
+
+  // Errors aren't serializable currently, we need to pull the message out and
+  // re-construct in the destination context
+  if (value->IsNativeError()) {
+    v8::Context::Scope destination_context_scope(destination_context);
+    return v8::MaybeLocal<v8::Value>(v8::Exception::Error(
+        v8::Exception::CreateMessage(destination_context->GetIsolate(), value)
+            ->Get()));
+  }
+
+  // Manually go through the array and pass each value individually into a new
+  // array so that functions deep inside arrays get proxied or arrays of
+  // promises are proxied correctly.
+  if (IsPlainArray(value)) {
+    v8::Context::Scope destination_context_scope(destination_context);
+    {
+      v8::Local<v8::Array> arr = v8::Local<v8::Array>::Cast(value);
+      size_t length = arr->Length();
+      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,
+            arr->Get(source_context, i).ToLocalChecked(), store,
+            recursion_depth + 1);
+        if (value_for_array.IsEmpty())
+          return v8::MaybeLocal<v8::Value>();
+
+        if (!mate::internal::IsTrue(
+                cloned_arr->Set(destination_context, static_cast<int>(i),
+                                value_for_array.ToLocalChecked()))) {
+          return v8::MaybeLocal<v8::Value>();
+        }
+      }
+      store->CacheProxiedObject(value, cloned_arr);
+      return v8::MaybeLocal<v8::Value>(cloned_arr);
+    }
+  }
+
+  // Proxy all objects
+  if (IsPlainObject(value)) {
+    auto object_value = v8::Local<v8::Object>::Cast(value);
+    auto passed_value =
+        CreateProxyForAPI(object_value, source_context, destination_context,
+                          store, recursion_depth + 1);
+    if (passed_value.IsEmpty())
+      return v8::MaybeLocal<v8::Value>();
+    return v8::MaybeLocal<v8::Value>(passed_value.ToLocalChecked());
+  }
+
+  // Serializable objects
+  blink::CloneableMessage ret;
+  {
+    v8::Context::Scope source_context_scope(source_context);
+    {
+      // V8 serializer will throw an error if required
+      if (!mate::ConvertFromV8(source_context->GetIsolate(), value, &ret))
+        return v8::MaybeLocal<v8::Value>();
+    }
+  }
+
+  v8::Context::Scope destination_context_scope(destination_context);
+  {
+    v8::Local<v8::Value> cloned_value =
+        mate::ConvertToV8(destination_context->GetIsolate(), ret);
+    store->CacheProxiedObject(value, cloned_value);
+    return v8::MaybeLocal<v8::Value>(cloned_value);
+  }
+}
+
+v8::Local<v8::Value> ProxyFunctionWrapper(
+    context_bridge::RenderFramePersistenceStore* store,
+    size_t func_id,
+    mate::Arguments* args) {
+  // Context the proxy function was called from
+  v8::Local<v8::Context> calling_context = args->isolate()->GetCurrentContext();
+  // Context the function was created in
+  v8::Local<v8::Context> func_owning_context =
+      std::get<1>(store->functions()[func_id]).Get(args->isolate());
+
+  v8::Context::Scope func_owning_context_scope(func_owning_context);
+  {
+    v8::Local<v8::Function> func =
+        (std::get<0>(store->functions()[func_id])).Get(args->isolate());
+
+    std::vector<v8::Local<v8::Value>> original_args;
+    std::vector<v8::Local<v8::Value>> proxied_args;
+    args->GetRemaining(&original_args);
+
+    for (auto value : original_args) {
+      auto arg = PassValueToOtherContext(calling_context, func_owning_context,
+                                         value, store, 0);
+      if (arg.IsEmpty())
+        return v8::Undefined(args->isolate());
+      proxied_args.push_back(arg.ToLocalChecked());
+    }
+
+    v8::MaybeLocal<v8::Value> maybe_return_value;
+    bool did_error = false;
+    std::string error_message;
+    {
+      v8::TryCatch try_catch(args->isolate());
+      maybe_return_value = func->Call(func_owning_context, func,
+                                      proxied_args.size(), proxied_args.data());
+      if (try_catch.HasCaught()) {
+        did_error = true;
+        auto message = try_catch.Message();
+
+        if (message.IsEmpty() ||
+            !mate::ConvertFromV8(args->isolate(), message->Get(),
+                                 &error_message)) {
+          error_message =
+              "An unknown exception occurred in the isolated context, an error "
+              "occurred but a valid exception was not thrown.";
+        }
+      }
+    }
+
+    if (did_error) {
+      v8::Context::Scope calling_context_scope(calling_context);
+      {
+        args->ThrowError(error_message);
+        return v8::Local<v8::Object>();
+      }
+    }
+
+    if (maybe_return_value.IsEmpty())
+      return v8::Undefined(args->isolate());
+
+    auto ret =
+        PassValueToOtherContext(func_owning_context, calling_context,
+                                maybe_return_value.ToLocalChecked(), store, 0);
+    if (ret.IsEmpty())
+      return v8::Undefined(args->isolate());
+    return ret.ToLocalChecked();
+  }
+}
+
+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::RenderFramePersistenceStore* store,
+    int recursion_depth) {
+  mate::Dictionary api(source_context->GetIsolate(), api_object);
+  mate::Dictionary proxy =
+      mate::Dictionary::CreateEmpty(destination_context->GetIsolate());
+  store->CacheProxiedObject(api.GetHandle(), proxy.GetHandle());
+  auto maybe_keys = api.GetHandle()->GetOwnPropertyNames(
+      source_context,
+      static_cast<v8::PropertyFilter>(v8::ONLY_ENUMERABLE | v8::SKIP_SYMBOLS),
+      v8::KeyConversionMode::kConvertToString);
+  if (maybe_keys.IsEmpty())
+    return v8::MaybeLocal<v8::Object>(proxy.GetHandle());
+  auto keys = maybe_keys.ToLocalChecked();
+
+  v8::Context::Scope destination_context_scope(destination_context);
+  {
+    uint32_t length = keys->Length();
+    std::string key_str;
+    for (uint32_t i = 0; i < length; i++) {
+      v8::Local<v8::Value> key =
+          keys->Get(destination_context, i).ToLocalChecked();
+      // Try get the key as a string
+      if (!mate::ConvertFromV8(api.isolate(), key, &key_str)) {
+        continue;
+      }
+      v8::Local<v8::Value> value;
+      if (!api.Get(key_str, &value))
+        continue;
+
+      auto passed_value =
+          PassValueToOtherContext(source_context, destination_context, value,
+                                  store, recursion_depth + 1);
+      if (passed_value.IsEmpty())
+        return v8::MaybeLocal<v8::Object>();
+      proxy.Set(key_str, passed_value.ToLocalChecked());
+    }
+
+    return proxy.GetHandle();
+  }
+}
+
+#ifdef DCHECK_IS_ON
+mate::Dictionary DebugGC(mate::Dictionary empty) {
+  auto* render_frame = GetRenderFrame(empty.GetHandle());
+  auto* store = GetOrCreateStore(render_frame);
+  mate::Dictionary ret = mate::Dictionary::CreateEmpty(empty.isolate());
+  ret.Set("functionCount", store->functions().size());
+  auto* proxy_map = store->proxy_map();
+  ret.Set("objectCount", proxy_map->size() * 2);
+  int live_from = 0;
+  int live_proxy = 0;
+  for (auto iter = proxy_map->begin(); iter != proxy_map->end(); iter++) {
+    auto* node = iter->second;
+    while (node) {
+      if (!std::get<0>(node->pair).IsEmpty())
+        live_from++;
+      if (!std::get<1>(node->pair).IsEmpty())
+        live_proxy++;
+      node = node->next;
+    }
+  }
+  ret.Set("liveFromValues", live_from);
+  ret.Set("liveProxyValues", live_proxy);
+  return ret;
+}
+#endif
+
+void ExposeAPIInMainWorld(const std::string& key,
+                          v8::Local<v8::Object> api_object,
+                          mate::Arguments* args) {
+  auto* render_frame = GetRenderFrame(api_object);
+  CHECK(render_frame);
+  context_bridge::RenderFramePersistenceStore* store =
+      GetOrCreateStore(render_frame);
+  auto* frame = render_frame->GetWebFrame();
+  CHECK(frame);
+  v8::Local<v8::Context> main_context = frame->MainWorldScriptContext();
+  mate::Dictionary global(main_context->GetIsolate(), main_context->Global());
+
+  if (global.Has(key)) {
+    args->ThrowError(
+        "Cannot bind an API on top of an existing property on the window "
+        "object");
+    return;
+  }
+
+  v8::Local<v8::Context> isolated_context = frame->WorldScriptContext(
+      args->isolate(), electron::World::ISOLATED_WORLD);
+
+  v8::Context::Scope main_context_scope(main_context);
+  {
+    v8::MaybeLocal<v8::Object> maybe_proxy =
+        CreateProxyForAPI(api_object, isolated_context, main_context, store, 0);
+    if (maybe_proxy.IsEmpty())
+      return;
+    auto proxy = maybe_proxy.ToLocalChecked();
+    if (!DeepFreeze(proxy, main_context))
+      return;
+
+    global.SetReadOnlyNonConfigurable(key, proxy);
+  }
+}
+
+}  // namespace api
+
+}  // namespace electron
+
+namespace {
+
+void Initialize(v8::Local<v8::Object> exports,
+                v8::Local<v8::Value> unused,
+                v8::Local<v8::Context> context,
+                void* priv) {
+  v8::Isolate* isolate = context->GetIsolate();
+  mate::Dictionary dict(isolate, exports);
+  dict.SetMethod("exposeAPIInMainWorld", &electron::api::ExposeAPIInMainWorld);
+#ifdef DCHECK_IS_ON
+  dict.SetMethod("_debugGCMaps", &electron::api::DebugGC);
+#endif
+}
+
+}  // namespace
+
+NODE_LINKED_MODULE_CONTEXT_AWARE(atom_renderer_context_bridge, Initialize)

+ 41 - 0
shell/renderer/api/atom_api_context_bridge.h

@@ -0,0 +1,41 @@
+// Copyright (c) 2019 Slack Technologies, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef SHELL_RENDERER_API_ATOM_API_CONTEXT_BRIDGE_H_
+#define SHELL_RENDERER_API_ATOM_API_CONTEXT_BRIDGE_H_
+
+#include <map>
+#include <string>
+#include <tuple>
+
+#include "content/public/renderer/render_frame.h"
+#include "content/public/renderer/render_frame_observer.h"
+#include "native_mate/converter.h"
+#include "native_mate/dictionary.h"
+#include "shell/common/node_includes.h"
+#include "shell/renderer/api/context_bridge/render_frame_context_bridge_store.h"
+#include "shell/renderer/atom_render_frame_observer.h"
+#include "third_party/blink/public/web/web_local_frame.h"
+
+namespace electron {
+
+namespace api {
+
+v8::Local<v8::Value> ProxyFunctionWrapper(
+    context_bridge::RenderFramePersistenceStore* store,
+    size_t func_id,
+    mate::Arguments* args);
+
+v8::MaybeLocal<v8::Object> CreateProxyForAPI(
+    const v8::Local<v8::Object>& api_object,
+    const v8::Local<v8::Context>& source_context,
+    const v8::Local<v8::Context>& target_context,
+    context_bridge::RenderFramePersistenceStore* store,
+    int recursion_depth);
+
+}  // namespace api
+
+}  // namespace electron
+
+#endif  // SHELL_RENDERER_API_ATOM_API_CONTEXT_BRIDGE_H_

+ 145 - 0
shell/renderer/api/context_bridge/render_frame_context_bridge_store.cc

@@ -0,0 +1,145 @@
+// Copyright (c) 2019 Slack Technologies, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/renderer/api/context_bridge/render_frame_context_bridge_store.h"
+
+#include <utility>
+
+#include "shell/common/api/object_life_monitor.h"
+
+namespace electron {
+
+namespace api {
+
+namespace context_bridge {
+
+namespace {
+
+class CachedProxyLifeMonitor final : public ObjectLifeMonitor {
+ public:
+  static void BindTo(v8::Isolate* isolate,
+                     v8::Local<v8::Object> target,
+                     RenderFramePersistenceStore* store,
+                     WeakGlobalPairNode* node,
+                     int hash) {
+    new CachedProxyLifeMonitor(isolate, target, store, node, hash);
+  }
+
+ protected:
+  CachedProxyLifeMonitor(v8::Isolate* isolate,
+                         v8::Local<v8::Object> target,
+                         RenderFramePersistenceStore* store,
+                         WeakGlobalPairNode* node,
+                         int hash)
+      : ObjectLifeMonitor(isolate, target),
+        store_(store),
+        node_(node),
+        hash_(hash) {}
+
+  void RunDestructor() override {
+    if (node_->detached) {
+      delete node_;
+    }
+    if (node_->prev) {
+      node_->prev->next = node_->next;
+    }
+    if (node_->next) {
+      node_->next->prev = node_->prev;
+    }
+    if (!node_->prev && !node_->next) {
+      // Must be a single length linked list
+      store_->proxy_map()->erase(hash_);
+    }
+    node_->detached = true;
+  }
+
+ private:
+  RenderFramePersistenceStore* store_;
+  WeakGlobalPairNode* node_;
+  int hash_;
+};
+
+}  // namespace
+
+WeakGlobalPairNode::WeakGlobalPairNode(WeakGlobalPair pair) {
+  this->pair = std::move(pair);
+}
+
+WeakGlobalPairNode::~WeakGlobalPairNode() {
+  if (next) {
+    delete next;
+  }
+}
+
+RenderFramePersistenceStore::RenderFramePersistenceStore(
+    content::RenderFrame* render_frame)
+    : content::RenderFrameObserver(render_frame) {}
+
+RenderFramePersistenceStore::~RenderFramePersistenceStore() = default;
+
+void RenderFramePersistenceStore::OnDestruct() {
+  delete this;
+}
+
+void RenderFramePersistenceStore::CacheProxiedObject(
+    v8::Local<v8::Value> from,
+    v8::Local<v8::Value> proxy_value) {
+  if (from->IsObject() && !from->IsNullOrUndefined()) {
+    auto obj = v8::Local<v8::Object>::Cast(from);
+    int hash = obj->GetIdentityHash();
+    auto global_from = v8::Global<v8::Value>(v8::Isolate::GetCurrent(), from);
+    auto global_proxy =
+        v8::Global<v8::Value>(v8::Isolate::GetCurrent(), proxy_value);
+    // Do not retain
+    global_from.SetWeak();
+    global_proxy.SetWeak();
+    auto iter = proxy_map_.find(hash);
+    auto* node = new WeakGlobalPairNode(
+        std::make_tuple(std::move(global_from), std::move(global_proxy)));
+    CachedProxyLifeMonitor::BindTo(v8::Isolate::GetCurrent(), obj, this, node,
+                                   hash);
+    CachedProxyLifeMonitor::BindTo(v8::Isolate::GetCurrent(),
+                                   v8::Local<v8::Object>::Cast(proxy_value),
+                                   this, node, hash);
+    if (iter == proxy_map_.end()) {
+      proxy_map_.emplace(hash, node);
+    } else {
+      WeakGlobalPairNode* target = iter->second;
+      while (target->next) {
+        target = target->next;
+      }
+      target->next = node;
+      node->prev = target;
+    }
+  }
+}
+
+v8::MaybeLocal<v8::Value> RenderFramePersistenceStore::GetCachedProxiedObject(
+    v8::Local<v8::Value> from) {
+  if (!from->IsObject() || from->IsNullOrUndefined())
+    return v8::MaybeLocal<v8::Value>();
+
+  auto obj = v8::Local<v8::Object>::Cast(from);
+  int hash = obj->GetIdentityHash();
+  auto iter = proxy_map_.find(hash);
+  if (iter == proxy_map_.end())
+    return v8::MaybeLocal<v8::Value>();
+  WeakGlobalPairNode* target = iter->second;
+  while (target) {
+    auto from_cmp = std::get<0>(target->pair).Get(v8::Isolate::GetCurrent());
+    if (from_cmp == from) {
+      if (std::get<1>(target->pair).IsEmpty())
+        return v8::MaybeLocal<v8::Value>();
+      return std::get<1>(target->pair).Get(v8::Isolate::GetCurrent());
+    }
+    target = target->next;
+  }
+  return v8::MaybeLocal<v8::Value>();
+}
+
+}  // namespace context_bridge
+
+}  // namespace api
+
+}  // namespace electron

+ 71 - 0
shell/renderer/api/context_bridge/render_frame_context_bridge_store.h

@@ -0,0 +1,71 @@
+// Copyright (c) 2019 Slack Technologies, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef SHELL_RENDERER_API_CONTEXT_BRIDGE_RENDER_FRAME_CONTEXT_BRIDGE_STORE_H_
+#define SHELL_RENDERER_API_CONTEXT_BRIDGE_RENDER_FRAME_CONTEXT_BRIDGE_STORE_H_
+
+#include <map>
+#include <tuple>
+
+#include "content/public/renderer/render_frame.h"
+#include "content/public/renderer/render_frame_observer.h"
+#include "shell/renderer/atom_render_frame_observer.h"
+#include "third_party/blink/public/web/web_local_frame.h"
+
+namespace electron {
+
+namespace api {
+
+namespace context_bridge {
+
+using FunctionContextPair =
+    std::tuple<v8::Global<v8::Function>, v8::Global<v8::Context>>;
+
+using WeakGlobalPair = std::tuple<v8::Global<v8::Value>, v8::Global<v8::Value>>;
+
+struct WeakGlobalPairNode {
+  explicit WeakGlobalPairNode(WeakGlobalPair pair_);
+  ~WeakGlobalPairNode();
+  WeakGlobalPair pair;
+  bool detached = false;
+  struct WeakGlobalPairNode* prev = nullptr;
+  struct WeakGlobalPairNode* next = nullptr;
+};
+
+class RenderFramePersistenceStore final : public content::RenderFrameObserver {
+ public:
+  explicit RenderFramePersistenceStore(content::RenderFrame* render_frame);
+  ~RenderFramePersistenceStore() override;
+
+  // RenderFrameObserver implementation.
+  void OnDestruct() override;
+
+  size_t take_func_id() { return next_func_id_++; }
+
+  std::map<size_t, FunctionContextPair>& functions() { return functions_; }
+  std::map<int, WeakGlobalPairNode*>* proxy_map() { return &proxy_map_; }
+
+  void CacheProxiedObject(v8::Local<v8::Value> from,
+                          v8::Local<v8::Value> proxy_value);
+  v8::MaybeLocal<v8::Value> GetCachedProxiedObject(v8::Local<v8::Value> from);
+
+ private:
+  // func_id ==> { function, owning_context }
+  std::map<size_t, FunctionContextPair> functions_;
+  size_t next_func_id_ = 1;
+
+  // proxy maps are weak globals, i.e. these are not retained beyond
+  // there normal JS lifetime.  You must check IsEmpty()
+
+  // object_identity ==> [from_value, proxy_value]
+  std::map<int, WeakGlobalPairNode*> proxy_map_;
+};
+
+}  // namespace context_bridge
+
+}  // namespace api
+
+}  // namespace electron
+
+#endif  // SHELL_RENDERER_API_CONTEXT_BRIDGE_RENDER_FRAME_CONTEXT_BRIDGE_STORE_H_

+ 680 - 0
spec-main/api-context-bridge-spec.ts

@@ -0,0 +1,680 @@
+import { contextBridge, BrowserWindow, ipcMain } from 'electron'
+import { expect } from 'chai'
+import * as fs from 'fs-extra'
+import * as os from 'os'
+import * as path from 'path'
+
+import { closeWindow } from './window-helpers'
+import { emittedOnce } from './events-helpers'
+
+const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'context-bridge')
+
+describe('contextBridge', () => {
+  let w: BrowserWindow
+  let dir: string
+
+  afterEach(async () => {
+    await closeWindow(w)
+    if (dir) await fs.remove(dir)
+  })
+
+  it('should not be accessible when contextIsolation is disabled', async () => {
+    w = new BrowserWindow({
+      show: false,
+      webPreferences: {
+        contextIsolation: false,
+        preload: path.resolve(fixturesPath, 'can-bind-preload.js')
+      }
+    })
+    const [,bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => w.loadFile(path.resolve(fixturesPath, 'empty.html')))
+    expect(bound).to.equal(false)
+  })
+
+  it('should be accessible when contextIsolation is enabled', async () => {
+    w = new BrowserWindow({
+      show: false,
+      webPreferences: {
+        contextIsolation: true,
+        preload: path.resolve(fixturesPath, 'can-bind-preload.js')
+      }
+    })
+    const [,bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => w.loadFile(path.resolve(fixturesPath, 'empty.html')))
+    expect(bound).to.equal(true)
+  })
+
+  const generateTests = (useSandbox: boolean) => {
+    describe(`with sandbox=${useSandbox}`, () => {
+      const makeBindingWindow = async (bindingCreator: Function) => {
+        const preloadContent = `const electron_1 = require('electron');
+        ${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc');
+        const gc=require('vm').runInNewContext('gc');
+        electron_1.contextBridge.exposeInMainWorld('GCRunner', {
+          run: () => gc()
+        });`}
+        (${bindingCreator.toString()})();`
+        const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-spec-preload-'))
+        dir = tmpDir
+        await fs.writeFile(path.resolve(tmpDir, 'preload.js'), preloadContent)
+        w = new BrowserWindow({
+          show: false,
+          webPreferences: {
+            contextIsolation: true,
+            nodeIntegration: true,
+            sandbox: useSandbox,
+            preload: path.resolve(tmpDir, 'preload.js')
+          }
+        })
+        await w.loadFile(path.resolve(fixturesPath, 'empty.html'))
+      }
+    
+      const callWithBindings = async (fn: Function) => {
+        return await w.webContents.executeJavaScript(`(${fn.toString()})(window)`)
+      }
+    
+      const getGCInfo = async (): Promise<{
+        functionCount: number
+        objectCount: number
+        liveFromValues: number
+        liveProxyValues: number
+      }> => {
+        const [,info] = await emittedOnce(ipcMain, 'gc-info', () => w.webContents.send('get-gc-info'))
+        return info
+      }
+    
+      it('should proxy numbers', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            myNumber: 123,
+          })
+        })
+        const result = await callWithBindings((root: any) => {
+          return root.example.myNumber
+        })
+        expect(result).to.equal(123)
+      })
+    
+      it('should make properties unwriteable', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            myNumber: 123,
+          })
+        })
+        const result = await callWithBindings((root: any) => {
+          root.example.myNumber = 456
+          return root.example.myNumber
+        })
+        expect(result).to.equal(123)
+      })
+    
+      it('should proxy strings', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            myString: 'my-words',
+          })
+        })
+        const result = await callWithBindings((root: any) => {
+          return root.example.myString
+        })
+        expect(result).to.equal('my-words')
+      })
+    
+      it('should proxy arrays', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            myArr: [123, 'my-words'],
+          })
+        })
+        const result = await callWithBindings((root: any) => {
+          return root.example.myArr
+        })
+        expect(result).to.deep.equal([123, 'my-words'])
+      })
+    
+      it('should make arrays immutable', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            myArr: [123, 'my-words'],
+          })
+        })
+        const immutable = await callWithBindings((root: any) => {
+          try {
+            root.example.myArr.push(456)
+            return false
+          } catch {
+            return true
+          }
+        })
+        expect(immutable).to.equal(true)
+      })
+    
+      it('should proxy booleans', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            myBool: true,
+          })
+        })
+        const result = await callWithBindings((root: any) => {
+          return root.example.myBool
+        })
+        expect(result).to.equal(true)
+      })
+    
+      it('should proxy promises and resolve with the correct value', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            myPromise: Promise.resolve('i-resolved'),
+          })
+        })
+        const result = await callWithBindings(async (root: any) => {
+          return await root.example.myPromise
+        })
+        expect(result).to.equal('i-resolved')
+      })
+    
+      it('should proxy promises and reject with the correct value', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            myPromise: Promise.reject('i-rejected'),
+          })
+        })
+        const result = await callWithBindings(async (root: any) => {
+          try {
+            await root.example.myPromise
+            return null
+          } catch (err) {
+            return err
+          }
+        })
+        expect(result).to.equal('i-rejected')
+      })
+    
+      it('should proxy promises and resolve with the correct value if it resolves later', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            myPromise: () => new Promise(r => setTimeout(() => r('delayed'), 20)),
+          })
+        })
+        const result = await callWithBindings(async (root: any) => {
+          return await root.example.myPromise()
+        })
+        expect(result).to.equal('delayed')
+      })
+    
+      it('should proxy nested promises correctly', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            myPromise: () => new Promise(r => setTimeout(() => r(Promise.resolve(123)), 20)),
+          })
+        })
+        const result = await callWithBindings(async (root: any) => {
+          return await root.example.myPromise()
+        })
+        expect(result).to.equal(123)
+      })
+    
+      it('should proxy methods', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            getNumber: () => 123,
+            getString: () => 'help',
+            getBoolean: () => false,
+            getPromise: async () => 'promise'
+          })
+        })
+        const result = await callWithBindings(async (root: any) => {
+          return [root.example.getNumber(), root.example.getString(), root.example.getBoolean(), await root.example.getPromise()]
+        })
+        expect(result).to.deep.equal([123, 'help', false, 'promise'])
+      })
+    
+      it('should proxy methods that are callable multiple times', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            doThing: () => 123
+          })
+        })
+        const result = await callWithBindings(async (root: any) => {
+          return [root.example.doThing(), root.example.doThing(), root.example.doThing()]
+        })
+        expect(result).to.deep.equal([123, 123, 123])
+      })
+    
+      it('should proxy methods in the reverse direction', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            callWithNumber: (fn: any) => fn(123),
+          })
+        })
+        const result = await callWithBindings(async (root: any) => {
+          return root.example.callWithNumber((n: number) => n + 1)
+        })
+        expect(result).to.equal(124)
+      })
+    
+      it('should proxy promises in the reverse direction', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            getPromiseValue: async (p: Promise<any>) => await p,
+          })
+        })
+        const result = await callWithBindings(async (root: any) => {
+          return await root.example.getPromiseValue(Promise.resolve('my-proxied-value'))
+        })
+        expect(result).to.equal('my-proxied-value')
+      })
+
+      it('should proxy objects with number keys', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            [1]: 123,
+            [2]: 456,
+            '3': 789
+          })
+        })
+        const result = await callWithBindings(async (root: any) => {
+          return [root.example[1], root.example[2], root.example[3], Array.isArray(root.example)]
+        })
+        expect(result).to.deep.equal([123, 456, 789, false])
+      })
+    
+      it('it should proxy null and undefined correctly', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            values: [null, undefined]
+          })
+        })
+        const result = await callWithBindings((root: any) => {
+          // Convert to strings as although the context bridge keeps the right value
+          // IPC does not
+          return root.example.values.map((val: any) => `${val}`)
+        })
+        expect(result).to.deep.equal(['null', 'undefined'])
+      })
+
+      it('should proxy typed arrays and regexps through the serializer', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            arr: new Uint8Array(100),
+            regexp: /a/g
+          })
+        })
+        const result = await callWithBindings((root: any) => {
+          return [root.example.arr.__proto__ === Uint8Array.prototype, root.example.regexp.__proto__ === RegExp.prototype]
+        })
+        expect(result).to.deep.equal([true, true])
+      })
+    
+      it('it should handle recursive objects', async () => {
+        await makeBindingWindow(() => {
+          const o: any = { value: 135 }
+          o.o = o
+          contextBridge.exposeInMainWorld('example', {
+            o,
+          })
+        })
+        const result = await callWithBindings((root: any) => {
+          return [root.example.o.value, root.example.o.o.value, root.example.o.o.o.value]
+        })
+        expect(result).to.deep.equal([135, 135, 135])
+      })
+    
+      it('it should follow expected simple rules of object identity', async () => {
+        await makeBindingWindow(() => {
+          const o: any = { value: 135 }
+          const sub = { thing: 7 }
+          o.a = sub
+          o.b = sub
+          contextBridge.exposeInMainWorld('example', {
+            o,
+          })
+        })
+        const result = await callWithBindings((root: any) => {
+          return root.example.a === root.example.b
+        })
+        expect(result).to.equal(true)
+      })
+    
+      it('it should follow expected complex rules of object identity', async () => {
+        await makeBindingWindow(() => {
+          let first: any = null
+          contextBridge.exposeInMainWorld('example', {
+            check: (arg: any) => {
+              if (first === null) {
+                first = arg
+              } else {
+                return first === arg
+              }
+            },
+          })
+        })
+        const result = await callWithBindings((root: any) => {
+          const o = { thing: 123 }
+          root.example.check(o)
+          return root.example.check(o)
+        })
+        expect(result).to.equal(true)
+      })
+    
+      // Can only run tests which use the GCRunner in non-sandboxed environments
+      if (!useSandbox) {
+        it('should release the global hold on methods sent across contexts', async () => {
+          await makeBindingWindow(() => {
+            require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', (contextBridge as any).debugGC()))
+            contextBridge.exposeInMainWorld('example', {
+              getFunction: () => () => 123
+            })
+          })
+          expect((await getGCInfo()).functionCount).to.equal(2)
+          await callWithBindings(async (root: any) => {
+            root.x = [root.example.getFunction()]
+          })
+          expect((await getGCInfo()).functionCount).to.equal(3)
+          await callWithBindings(async (root: any) => {
+            root.x = []
+            root.GCRunner.run()
+          })
+          expect((await getGCInfo()).functionCount).to.equal(2)
+        })
+
+        it('should release the global hold on objects sent across contexts when the object proxy is de-reffed', async () => {
+          await makeBindingWindow(() => {
+            require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', (contextBridge as any).debugGC()))
+            let myObj: any
+            contextBridge.exposeInMainWorld('example', {
+              setObj: (o: any) => {
+                myObj = o
+              },
+              getObj: () => myObj
+            })
+          })
+          await callWithBindings(async (root: any) => {
+            root.GCRunner.run()
+          })
+          // Initial Setup
+          let info = await getGCInfo()
+          expect(info.liveFromValues).to.equal(3)
+          expect(info.liveProxyValues).to.equal(3)
+          expect(info.objectCount).to.equal(6)
+
+          // Create Reference
+          await callWithBindings(async (root: any) => {
+            root.x = { value: 123 }
+            root.example.setObj(root.x)
+            root.GCRunner.run()
+          })
+          info = await getGCInfo()
+          expect(info.liveFromValues).to.equal(4)
+          expect(info.liveProxyValues).to.equal(4)
+          expect(info.objectCount).to.equal(8)
+
+          // Release Reference
+          await callWithBindings(async (root: any) => {
+            root.example.setObj(null)
+            root.GCRunner.run()
+          })
+          info = await getGCInfo()
+          expect(info.liveFromValues).to.equal(3)
+          expect(info.liveProxyValues).to.equal(3)
+          expect(info.objectCount).to.equal(6)
+        })
+
+        it('should release the global hold on objects sent across contexts when the object source is de-reffed', async () => {
+          await makeBindingWindow(() => {
+            require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', (contextBridge as any).debugGC()))
+            let myObj: any;
+            contextBridge.exposeInMainWorld('example', {
+              setObj: (o: any) => {
+                myObj = o
+              },
+              getObj: () => myObj
+            })
+          })
+          await callWithBindings(async (root: any) => {
+            root.GCRunner.run()
+          })
+          // Initial Setup
+          let info = await getGCInfo()
+          expect(info.liveFromValues).to.equal(3)
+          expect(info.liveProxyValues).to.equal(3)
+          expect(info.objectCount).to.equal(6)
+
+          // Create Reference
+          await callWithBindings(async (root: any) => {
+            root.x = { value: 123 }
+            root.example.setObj(root.x)
+            root.GCRunner.run()
+          })
+          info = await getGCInfo()
+          expect(info.liveFromValues).to.equal(4)
+          expect(info.liveProxyValues).to.equal(4)
+          expect(info.objectCount).to.equal(8)
+
+          // Release Reference
+          await callWithBindings(async (root: any) => {
+            delete root.x
+            root.GCRunner.run()
+          })
+          info = await getGCInfo()
+          expect(info.liveFromValues).to.equal(3)
+          expect(info.liveProxyValues).to.equal(3)
+          expect(info.objectCount).to.equal(6)
+        })
+
+        it('should not crash when the object source is de-reffed AND the object proxy is de-reffed', async () => {
+          await makeBindingWindow(() => {
+            require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', (contextBridge as any).debugGC()))
+            let myObj: any;
+            contextBridge.exposeInMainWorld('example', {
+              setObj: (o: any) => {
+                myObj = o
+              },
+              getObj: () => myObj
+            })
+          })
+          await callWithBindings(async (root: any) => {
+            root.GCRunner.run()
+          })
+          // Initial Setup
+          let info = await getGCInfo()
+          expect(info.liveFromValues).to.equal(3)
+          expect(info.liveProxyValues).to.equal(3)
+          expect(info.objectCount).to.equal(6)
+
+          // Create Reference
+          await callWithBindings(async (root: any) => {
+            root.x = { value: 123 }
+            root.example.setObj(root.x)
+            root.GCRunner.run()
+          })
+          info = await getGCInfo()
+          expect(info.liveFromValues).to.equal(4)
+          expect(info.liveProxyValues).to.equal(4)
+          expect(info.objectCount).to.equal(8)
+
+          // Release Reference
+          await callWithBindings(async (root: any) => {
+            delete root.x
+            root.example.setObj(null)
+            root.GCRunner.run()
+          })
+          info = await getGCInfo()
+          expect(info.liveFromValues).to.equal(3)
+          expect(info.liveProxyValues).to.equal(3)
+          expect(info.objectCount).to.equal(6)
+        })
+      }
+    
+      it('it should not let you overwrite existing exposed things', async () => {
+        await makeBindingWindow(() => {
+          let threw = false
+          contextBridge.exposeInMainWorld('example', {
+            attempt: 1,
+            getThrew: () => threw
+          })
+          try {
+            contextBridge.exposeInMainWorld('example', {
+              attempt: 2,
+              getThrew: () => threw
+            })
+          } catch {
+            threw = true
+          }
+        })
+        const result = await callWithBindings((root: any) => {
+          return [root.example.attempt, root.example.getThrew()]
+        })
+        expect(result).to.deep.equal([1, true])
+      })
+    
+      it('should work with complex nested methods and promises', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            first: (second: Function) => second(async (fourth: Function) => {
+              return await fourth()
+            })
+          })
+        })
+        const result = await callWithBindings((root: any) => {
+          return root.example.first((third: Function) => {
+            return third(() => Promise.resolve('final value'))
+          })
+        })
+        expect(result).to.equal('final value')
+      })
+
+      it('should throw an error when recursion depth is exceeded', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            doThing: (a: any) => console.log(a)
+          })
+        })
+        let threw = await callWithBindings((root: any) => {
+          try {
+            let a: any = []
+            for (let i = 0; i < 999; i++) {
+              a = [ a ]
+            }
+            root.example.doThing(a)
+            return false
+          } catch {
+            return true
+          }
+        })
+        expect(threw).to.equal(false)
+        threw = await callWithBindings((root: any) => {
+          try {
+            let a: any = []
+            for (let i = 0; i < 1000; i++) {
+              a = [ a ]
+            }
+            root.example.doThing(a)
+            return false
+          } catch {
+            return true
+          }
+        })
+        expect(threw).to.equal(true)
+      })
+    
+      it('should not leak prototypes', async () => {
+        await makeBindingWindow(() => {
+          contextBridge.exposeInMainWorld('example', {
+            number: 123,
+            string: 'string',
+            boolean: true,
+            arr: [123, 'string', true, ['foo']],
+            getNumber: () => 123,
+            getString: () => 'string',
+            getBoolean: () => true,
+            getArr: () => [123, 'string', true, ['foo']],
+            getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']]}),
+            getFunctionFromFunction: async () => () => null,
+            object: {
+              number: 123,
+              string: 'string',
+              boolean: true,
+              arr: [123, 'string', true, ['foo']],
+              getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']]}),
+            },
+            receiveArguments: (fn: any) => fn({ key: 'value' })
+          })
+        })
+        const result = await callWithBindings(async (root: any) => {
+          const { example } = root
+          let arg: any
+          example.receiveArguments((o: any) => { arg = o })
+          const protoChecks = [
+            [example, Object],
+            [example.number, Number],
+            [example.string, String],
+            [example.boolean, Boolean],
+            [example.arr, Array],
+            [example.arr[0], Number],
+            [example.arr[1], String],
+            [example.arr[2], Boolean],
+            [example.arr[3], Array],
+            [example.arr[3][0], String],
+            [example.getNumber, Function],
+            [example.getNumber(), Number],
+            [example.getString(), String],
+            [example.getBoolean(), Boolean],
+            [example.getArr(), Array],
+            [example.getArr()[0], Number],
+            [example.getArr()[1], String],
+            [example.getArr()[2], Boolean],
+            [example.getArr()[3], Array],
+            [example.getArr()[3][0], String],
+            [example.getFunctionFromFunction, Function],
+            [example.getFunctionFromFunction(), Promise],
+            [await example.getFunctionFromFunction(), Function],
+            [example.getPromise(), Promise],
+            [await example.getPromise(), Object],
+            [(await example.getPromise()).number, Number],
+            [(await example.getPromise()).string, String],
+            [(await example.getPromise()).boolean, Boolean],
+            [(await example.getPromise()).fn, Function],
+            [(await example.getPromise()).fn(), String],
+            [(await example.getPromise()).arr, Array],
+            [(await example.getPromise()).arr[0], Number],
+            [(await example.getPromise()).arr[1], String],
+            [(await example.getPromise()).arr[2], Boolean],
+            [(await example.getPromise()).arr[3], Array],
+            [(await example.getPromise()).arr[3][0], String],
+            [example.object, Object],
+            [example.object.number, Number],
+            [example.object.string, String],
+            [example.object.boolean, Boolean],
+            [example.object.arr, Array],
+            [example.object.arr[0], Number],
+            [example.object.arr[1], String],
+            [example.object.arr[2], Boolean],
+            [example.object.arr[3], Array],
+            [example.object.arr[3][0], String],
+            [await example.object.getPromise(), Object],
+            [(await example.object.getPromise()).number, Number],
+            [(await example.object.getPromise()).string, String],
+            [(await example.object.getPromise()).boolean, Boolean],
+            [(await example.object.getPromise()).fn, Function],
+            [(await example.object.getPromise()).fn(), String],
+            [(await example.object.getPromise()).arr, Array],
+            [(await example.object.getPromise()).arr[0], Number],
+            [(await example.object.getPromise()).arr[1], String],
+            [(await example.object.getPromise()).arr[2], Boolean],
+            [(await example.object.getPromise()).arr[3], Array],
+            [(await example.object.getPromise()).arr[3][0], String],
+            [arg, Object],
+            [arg.key, String]
+          ]
+          return {
+            protoMatches: protoChecks.map(([a, Constructor]) => a.__proto__ === Constructor.prototype)
+          }
+        })
+        // Every protomatch should be true
+        expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true))
+      })
+    })
+  }
+
+  generateTests(true)
+  generateTests(false)
+})

+ 13 - 0
spec-main/fixtures/api/context-bridge/can-bind-preload.js

@@ -0,0 +1,13 @@
+const { contextBridge, ipcRenderer } = require('electron')
+
+console.info(contextBridge)
+
+let bound = false
+try {
+  contextBridge.exposeInMainWorld('test', {})
+  bound = true
+} catch {
+  // Ignore
+}
+
+ipcRenderer.send('context-bridge-bound', bound)

+ 1 - 0
spec-main/fixtures/api/context-bridge/empty.html

@@ -0,0 +1 @@
+<html></html>

+ 1 - 1
spec/asar-spec.js

@@ -43,7 +43,7 @@ describe('asar package', function () {
       it('does not leak fd', function () {
         let readCalls = 1
         while (readCalls <= 10000) {
-          fs.readFileSync(path.join(process.resourcesPath, 'default_app.asar', 'index.js'))
+          fs.readFileSync(path.join(process.resourcesPath, 'default_app.asar', 'main.js'))
           readCalls++
         }
       })