CVE-2020-26272: Exposure of Resource to Wrong Sphere (CWE-668)
value: The Electron framework lets you write cross-platform desktop applications using JavaScript, HTML and CSS. In affected versions of Electron IPC messages sent from the main process to a subframe in the renderer process, through webContents.sendToFrame, event.reply or when using the remote module, can in some cases be delivered to the wrong frame. If your app uses remote, calls webContents.sendToFrame, or calls event.reply in an IPC message handler then it is impacted by this issue. This has been fixed in versions 9.4.0, 10.2.0, 11.1.0, and 12.0.0-beta.9. There are no workarounds for this issue.
CWE description: The product exposes a resource to the wrong control sphere, providing unintended actors with inappropriate access to the resource.
CVSS v3 base score: 6.5
Published: 2021-01-28T19:15Z
Repository: https://github.com/electron/electron
Language: TypeScript
Files changed: docs/api/structures/ipc-main-event.md, docs/api/structures/ipc-main-invoke-event.md, docs/api/web-contents.md, lib/browser/api/web-contents.ts, lib/browser/remote/server.ts, shell/browser/api/electron_api_web_contents.cc, shell/browser/api/electron_api_web_contents.h, shell/common/gin_helper/event_emitter.cc, spec-main/api-ipc-main-spec.ts, spec-main/fixtures/snapshots/proxy-window-open.snapshot.txt, spec-main/guest-window-manager-spec.ts, typings/internal-electron.d.ts
Commit date: 2020-12-09 12:48:16 -0800
Commit message: fix: restrict sendToFrame to same-process frames by default (#26875)


Diff: diff --git a/docs/api/structures/ipc-main-event.md b/docs/api/structures/ipc-main-event.md
index f222de35b8..ae32dc46a6 100644
--- a/docs/api/structures/ipc-main-event.md
+++ b/docs/api/structures/ipc-main-event.md
@@ -1,7 +1,8 @@
 # IpcMainEvent Object extends `Event`
 
+* `processId` Integer - The internal ID of the renderer process that sent this message
 * `frameId` Integer - The ID of the renderer frame that sent this message
 * `returnValue` any - Set this to the value to be returned in a synchronous message
 * `sender` WebContents - Returns the `webContents` that sent the message
 * `ports` MessagePortMain[] - A list of MessagePorts that were transferred with this message
 * `reply` Function - A function that will send an IPC message to the renderer frame that sent the original message that you are currently handling.  You should use this method to "reply" to the sent message in order to guarantee the reply will go to the correct process and frame.
diff --git a/docs/api/structures/ipc-main-invoke-event.md b/docs/api/structures/ipc-main-invoke-event.md
index 235b219c2d..b765791e82 100644
--- a/docs/api/structures/ipc-main-invoke-event.md
+++ b/docs/api/structures/ipc-main-invoke-event.md
@@ -1,4 +1,5 @@
 # IpcMainInvokeEvent Object extends `Event`
 
+* `processId` Integer - The internal ID of the renderer process that sent this message
 * `frameId` Integer - The ID of the renderer frame that sent this message
 * `sender` WebContents - Returns the `webContents` that sent the message
diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md
index fb25451195..52c582404a 100644
--- a/docs/api/web-contents.md
+++ b/docs/api/web-contents.md
@@ -1694,11 +1694,11 @@ app.whenReady().then(() => {
 </html>
 ```
 
 #### `contents.sendToFrame(frameId, channel, ...args)`
 
-* `frameId` Integer
+* `frameId` Integer | [number, number]
 * `channel` String
 * `...args` any[]
 
 Send an asynchronous message to a specific frame in a renderer process via
 `channel`, along with arguments. Arguments will be serialized with the
diff --git a/lib/browser/api/web-contents.ts b/lib/browser/api/web-contents.ts
index 344e271693..6de83d38f0 100644
--- a/lib/browser/api/web-contents.ts
+++ b/lib/browser/api/web-contents.ts
@@ -146,27 +146,27 @@ WebContents.prototype._sendInternal = function (channel, ...args) {
     throw new Error('Missing required channel argument');
   }
 
   return this._send(true /* internal */, channel, args);
 };
-WebContents.prototype.sendToFrame = function (frameId, channel, ...args) {
+WebContents.prototype.sendToFrame = function (frame, channel, ...args) {
   if (typeof channel !== 'string') {
     throw new Error('Missing required channel argument');
-  } else if (typeof frameId !== 'number') {
-    throw new Error('Missing required frameId argument');
+  } else if (!(typeof frame === 'number' || Array.isArray(frame))) {
+    throw new Error('Missing required frame argument (must be number or array)');
   }
 
-  return this._sendToFrame(false /* internal */, frameId, channel, args);
+  return this._sendToFrame(false /* internal */, frame, channel, args);
 };
-WebContents.prototype._sendToFrameInternal = function (frameId, channel, ...args) {
+WebContents.prototype._sendToFrameInternal = function (frame, channel, ...args) {
   if (typeof channel !== 'string') {
     throw new Error('Missing required channel argument');
-  } else if (typeof frameId !== 'number') {
-    throw new Error('Missing required frameId argument');
+  } else if (!(typeof frame === 'number' || Array.isArray(frame))) {
+    throw new Error('Missing required frame argument (must be number or array)');
   }
 
-  return this._sendToFrame(true /* internal */, frameId, channel, args);
+  return this._sendToFrame(true /* internal */, frame, channel, args);
 };
 
 // Following methods are mapped to webFrame.
 const webFrameMethods = [
   'insertCSS',
@@ -454,12 +454,13 @@ WebContents.prototype._callWindowOpenHandler = function (event: any, url: string
     return null;
   }
 };
 
 const addReplyToEvent = (event: any) => {
+  const { processId, frameId } = event;
   event.reply = (...args: any[]) => {
-    event.sender.sendToFrame(event.frameId, ...args);
+    event.sender.sendToFrame([processId, frameId], ...args);
   };
 };
 
 const addReturnValueToEvent = (event: any) => {
   Object.defineProperty(event, 'returnValue', {
diff --git a/lib/browser/remote/server.ts b/lib/browser/remote/server.ts
index f51fad0c14..e95570c5af 100644
--- a/lib/browser/remote/server.ts
+++ b/lib/browser/remote/server.ts
@@ -18,11 +18,11 @@ if (!features.isRemoteModuleEnabled()) {
 const FUNCTION_PROPERTIES = [
   'length', 'name', 'arguments', 'caller', 'prototype'
 ];
 
 type RendererFunctionId = [string, number] // [contextId, funcId]
-type FinalizerInfo = { id: RendererFunctionId, webContents: electron.WebContents, frameId: number };
+type FinalizerInfo = { id: RendererFunctionId, webContents: electron.WebContents, frameId: [number, number] };
 type CallIntoRenderer = (...args: any[]) => void
 
 // The remote functions in renderer processes.
 const rendererFunctionCache = new Map<string, WeakRef<CallIntoRenderer>>();
 // eslint-disable-next-line no-undef
@@ -41,11 +41,11 @@ function getCachedRendererFunction (id: RendererFunctionId): CallIntoRenderer |
   if (ref !== undefined) {
     const deref = ref.deref();
     if (deref !== undefined) return deref;
   }
 }
-function setCachedRendererFunction (id: RendererFunctionId, wc: electron.WebContents, frameId: number, value: CallIntoRenderer) {
+function setCachedRendererFunction (id: RendererFunctionId, wc: electron.WebContents, frameId: [number, number], value: CallIntoRenderer) {
   // eslint-disable-next-line no-undef
   const wr = new WeakRef<CallIntoRenderer>(value);
   const mapKey = id[0] + '~' + id[1];
   rendererFunctionCache.set(mapKey, wr);
   finalizationRegistry.register(value, {
@@ -218,11 +218,11 @@ const fakeConstructor = (constructor: Function, name: string) =>
       }
     }
   });
 
 // Convert array of meta data from renderer into array of real values.
-const unwrapArgs = function (sender: electron.WebContents, frameId: number, contextId: string, args: any[]) {
+const unwrapArgs = function (sender: electron.WebContents, frameId: [number, number], contextId: string, args: any[]) {
   const metaToValue = function (meta: MetaTypeFromRenderer): any {
     switch (meta.type) {
       case 'nativeimage':
         return deserialize(meta.value);
       case 'value':
@@ -421,22 +421,22 @@ handleRemoteCommand(IPC_MESSAGES.BROWSER_GET_CURRENT_WEB_CONTENTS, function (eve
 
   return valueToMeta(event.sender, contextId, customEvent.returnValue);
 });
 
 handleRemoteCommand(IPC_MESSAGES.BROWSER_CONSTRUCTOR, function (event, contextId, id, args) {
-  args = unwrapArgs(event.sender, event.frameId, contextId, args);
+  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
   const constructor = objectsRegistry.get(id);
 
   if (constructor == null) {
     throwRPCError(`Cannot call constructor on missing remote object ${id}`);
   }
 
   return valueToMeta(event.sender, contextId, new constructor(...args));
 });
 
 handleRemoteCommand(IPC_MESSAGES.BROWSER_FUNCTION_CALL, function (event, contextId, id, args) {
-  args = unwrapArgs(event.sender, event.frameId, contextId, args);
+  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
   const func = objectsRegistry.get(id);
 
   if (func == null) {
     throwRPCError(`Cannot call function on missing remote object ${id}`);
   }
@@ -449,22 +449,22 @@ handleRemoteCommand(IPC_MESSAGES.BROWSER_FUNCTION_CALL, function (event, context
     throw err;
   }
 });
 
 handleRemoteCommand(IPC_MESSAGES.BROWSER_MEMBER_CONSTRUCTOR, function (event, contextId, id, method, args) {
-  args = unwrapArgs(event.sender, event.frameId, contextId, args);
+  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
   const object = objectsRegistry.get(id);
 
   if (object == null) {
     throwRPCError(`Cannot call constructor '${method}' on missing remote object ${id}`);
   }
 
   return valueToMeta(event.sender, contextId, new object[method](...args));
 });
 
 handleRemoteCommand(IPC_MESSAGES.BROWSER_MEMBER_CALL, function (event, contextId, id, method, args) {
-  args = unwrapArgs(event.sender, event.frameId, contextId, args);
+  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
   const object = objectsRegistry.get(id);
 
   if (object == null) {
     throwRPCError(`Cannot call method '${method}' on missing remote object ${id}`);
   }
@@ -477,11 +477,11 @@ handleRemoteCommand(IPC_MESSAGES.BROWSER_MEMBER_CALL, function (event, contextId
     throw err;
   }
 });
 
 handleRemoteCommand(IPC_MESSAGES.BROWSER_MEMBER_SET, function (event, contextId, id, name, args) {
-  args = unwrapArgs(event.sender, event.frameId, contextId, args);
+  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
   const obj = objectsRegistry.get(id);
 
   if (obj == null) {
     throwRPCError(`Cannot set property '${name}' on missing remote object ${id}`);
   }
diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc
index cec721384b..c3ed99c09c 100644
--- a/shell/browser/api/electron_api_web_contents.cc
+++ b/shell/browser/api/electron_api_web_contents.cc
@@ -2706,31 +2706,44 @@ bool WebContents::SendIPCMessageWithSender(bool internal,
   electron_renderer->Message(internal, channel, std::move(args), sender_id);
   return true;
 }
 
 bool WebContents::SendIPCMessageToFrame(bool internal,
-                                        int32_t frame_id,
+                                        v8::Local<v8::Value> frame,
                                         const std::string& channel,
                                         v8::Local<v8::Value> args) {
   v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
   blink::CloneableMessage message;
   if (!gin::ConvertFromV8(isolate, args, &message)) {
     isolate->ThrowException(v8::Exception::Error(
         gin::StringToV8(isolate, "Failed to serialize arguments")));
     return false;
   }
-  auto frames = web_contents()->GetAllFrames();
-  auto iter = std::find_if(frames.begin(), frames.end(), [frame_id](auto* f) {
-    return f->GetRoutingID() == frame_id;
-  });
-  if (iter == frames.end())
-    return false;
-  if (!(*iter)->IsRenderFrameLive())
+  int32_t frame_id;
+  int32_t process_id;
+  if (gin::ConvertFromV8(isolate, frame, &frame_id)) {
+    process_id = web_contents()->GetMainFrame()->GetProcess()->GetID();
+  } else {
+    std::vector<int32_t> id_pair;
+    if (gin::ConvertFromV8(isolate, frame, &id_pair) && id_pair.size() == 2) {
+      process_id = id_pair[0];
+      frame_id = id_pair[1];
+    } else {
+      isolate->ThrowException(v8::Exception::Error(gin::StringToV8(
+          isolate,
+          "frameId must be a number or a pair of [processId, frameId]")));
+      return false;
+    }
+  }
+
+  auto* rfh = content::RenderFrameHost::FromID(process_id, frame_id);
+  if (!rfh || !rfh->IsRenderFrameLive() ||
+      content::WebContents::FromRenderFrameHost(rfh) != web_contents())
     return false;
 
   mojo::AssociatedRemote<mojom::ElectronRenderer> electron_renderer;
-  (*iter)->GetRemoteAssociatedInterfaces()->GetInterface(&electron_renderer);
+  rfh->GetRemoteAssociatedInterfaces()->GetInterface(&electron_renderer);
   electron_renderer->Message(internal, channel, std::move(message),
                              0 /* sender_id */);
   return true;
 }
 
diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h
index 5822558a5a..be4c73aa49 100644
--- a/shell/browser/api/electron_api_web_contents.h
+++ b/shell/browser/api/electron_api_web_contents.h
@@ -256,11 +256,11 @@ class WebContents : public gin::Wrappable<WebContents>,
                                 const std::string& channel,
                                 blink::CloneableMessage args,
                                 int32_t sender_id = 0);
 
   bool SendIPCMessageToFrame(bool internal,
-                             int32_t frame_id,
+                             v8::Local<v8::Value> frame,
                              const std::string& channel,
                              v8::Local<v8::Value> args);
 
   void PostMessage(const std::string& channel,
                    v8::Local<v8::Value> message,
diff --git a/shell/common/gin_helper/event_emitter.cc b/shell/common/gin_helper/event_emitter.cc
index ebb52d2ac9..710147b54f 100644
--- a/shell/common/gin_helper/event_emitter.cc
+++ b/shell/common/gin_helper/event_emitter.cc
@@ -3,10 +3,11 @@
 // found in the LICENSE file.
 
 #include "shell/common/gin_helper/event_emitter.h"
 
 #include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/render_process_host.h"
 #include "shell/browser/api/event.h"
 #include "shell/common/gin_helper/dictionary.h"
 #include "shell/common/gin_helper/object_template_builder.h"
 
 namespace gin_helper {
@@ -65,12 +66,14 @@ v8::Local<v8::Object> CreateNativeEvent(
   }
 
   Dictionary dict(isolate, event);
   dict.Set("sender", sender);
   // Should always set frameId even when callback is null.
-  if (frame)
+  if (frame) {
     dict.Set("frameId", frame->GetRoutingID());
+    dict.Set("processId", frame->GetProcess()->GetID());
+  }
   return event;
 }
 
 }  // namespace internal
 
diff --git a/spec-main/api-ipc-main-spec.ts b/spec-main/api-ipc-main-spec.ts
index 3f54527a5b..b63dd7e8c0 100644
--- a/spec-main/api-ipc-main-spec.ts
+++ b/spec-main/api-ipc-main-spec.ts
@@ -1,10 +1,11 @@
 import { expect } from 'chai';
 import * as path from 'path';
 import * as cp from 'child_process';
 import { closeAllWindows } from './window-helpers';
 import { emittedOnce } from './events-helpers';
+import { defer } from './spec-helpers';
 import { ipcMain, BrowserWindow } from 'electron/main';
 
 describe('ipc main module', () => {
   const fixtures = path.join(__dirname, 'fixtures');
 
@@ -57,7 +58,32 @@ describe('ipc main module', () => {
       await emittedOnce(appProcess.stdout, 'end');
 
       output = JSON.parse(output);
       expect(output).to.deep.equal(['error']);
     });
+
+    it('can be replied to', async () => {
+      ipcMain.on('test-echo', (e, arg) => {
+        e.reply('test-echo', arg);
+      });
+      defer(() => {
+        ipcMain.removeAllListeners('test-echo');
+      });
+
+      const w = new BrowserWindow({
+        show: false,
+        webPreferences: {
+          nodeIntegration: true
+        }
+      });
+      w.loadURL('about:blank');
+      const v = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
+        const { ipcRenderer } = require('electron')
+        ipcRenderer.send('test-echo', 'hello')
+        ipcRenderer.on('test-echo', (e, v) => {
+          resolve(v)
+        })
+      })`);
+      expect(v).to.equal('hello');
+    });
   });
 });
diff --git a/spec-main/fixtures/snapshots/proxy-window-open.snapshot.txt b/spec-main/fixtures/snapshots/proxy-window-open.snapshot.txt
index 84a69d2164..5e91374a1b 100644
--- a/spec-main/fixtures/snapshots/proxy-window-open.snapshot.txt
+++ b/spec-main/fixtures/snapshots/proxy-window-open.snapshot.txt
@@ -1,11 +1,12 @@
 [
   [
     "top=5,left=10,resizable=no",
     {
       "sender": "[WebContents]",
-      "frameId": 1
+      "frameId": 1,
+      "processId": "placeholder-process-id"
     },
     "about:blank",
     "frame name",
     "new-window",
     {
@@ -35,11 +36,12 @@
   ],
   [
     "zoomFactor=2,resizable=0,x=0,y=10",
     {
       "sender": "[WebContents]",
-      "frameId": 1
+      "frameId": 1,
+      "processId": "placeholder-process-id"
     },
     "about:blank",
     "frame name",
     "new-window",
     {
@@ -68,11 +70,12 @@
   ],
   [
     "backgroundColor=gray,webPreferences=0,x=100,y=100",
     {
       "sender": "[WebContents]",
-      "frameId": 1
+      "frameId": 1,
+      "processId": "placeholder-process-id"
     },
     "about:blank",
     "frame name",
     "new-window",
     {
@@ -101,11 +104,12 @@
   ],
   [
     "x=50,y=20,title=sup",
     {
       "sender": "[WebContents]",
-      "frameId": 1
+      "frameId": 1,
+      "processId": "placeholder-process-id"
     },
     "about:blank",
     "frame name",
     "new-window",
     {
@@ -132,11 +136,12 @@
   ],
   [
     "show=false,top=1,left=1",
     {
       "sender": "[WebContents]",
-      "frameId": 1
+      "frameId": 1,
+      "processId": "placeholder-process-id"
     },
     "about:blank",
     "frame name",
     "new-window",
     {
diff --git a/spec-main/guest-window-manager-spec.ts b/spec-main/guest-window-manager-spec.ts
index 41e91d84b3..3c687916b3 100644
--- a/spec-main/guest-window-manager-spec.ts
+++ b/spec-main/guest-window-manager-spec.ts
@@ -187,10 +187,13 @@ function stringifySnapshots (snapshots: any, pretty = false) {
       return '[WebContents]';
     }
     if (key === 'openerId' && typeof value === 'number') {
       return 'placeholder-opener-id';
     }
+    if (key === 'processId' && typeof value === 'number') {
+      return 'placeholder-process-id';
+    }
     if (key === 'returnValue') {
       return 'placeholder-guest-contents-id';
     }
     return value;
   }, pretty ? 2 : undefined);
diff --git a/typings/internal-electron.d.ts b/typings/internal-electron.d.ts
index 94b36dd993..49c64a3c1e 100644
--- a/typings/internal-electron.d.ts
+++ b/typings/internal-electron.d.ts
@@ -66,12 +66,12 @@ declare namespace Electron {
     browserWindowOptions: BrowserWindowConstructorOptions;
     _windowOpenHandler: ((opts: {url: string, frameName: string, features: string}) => any) | null;
     _callWindowOpenHandler(event: any, url: string, frameName: string, rawFeatures: string): Electron.BrowserWindowConstructorOptions | null;
     _setNextChildWebPreferences(prefs: Partial<Electron.BrowserWindowConstructorOptions['webPreferences']> & Pick<Electron.BrowserWindowConstructorOptions, 'backgroundColor'>): void;
     _send(internal: boolean, channel: string, args: any): boolean;
-    _sendToFrame(internal: boolean, frameId: number, channel: string, args: any): boolean;
-    _sendToFrameInternal(frameId: number, channel: string, ...args: any[]): boolean;
+    _sendToFrame(internal: boolean, frameId: number | [number, number], channel: string, args: any): boolean;
+    _sendToFrameInternal(frameId: number | [number, number], channel: string, ...args: any[]): boolean;
     _postMessage(channel: string, message: any, transfer?: any[]): void;
     _sendInternal(channel: string, ...args: any[]): void;
     _printToPDF(options: any): Promise<Buffer>;
     _print(options: any, callback?: (success: boolean, failureReason: string) => void): void;
     _getPrinters(): Electron.PrinterInfo[];

Vulnerable code: * `frameId` Integer
WebContents.prototype.sendToFrame = function (frameId, channel, ...args) {
  } else if (typeof frameId !== 'number') {
    throw new Error('Missing required frameId argument');
  return this._sendToFrame(false /* internal */, frameId, channel, args);
WebContents.prototype._sendToFrameInternal = function (frameId, channel, ...args) {
  } else if (typeof frameId !== 'number') {
    throw new Error('Missing required frameId argument');
  return this._sendToFrame(true /* internal */, frameId, channel, args);
    event.sender.sendToFrame(event.frameId, ...args);
type FinalizerInfo = { id: RendererFunctionId, webContents: electron.WebContents, frameId: number };
function setCachedRendererFunction (id: RendererFunctionId, wc: electron.WebContents, frameId: number, value: CallIntoRenderer) {
const unwrapArgs = function (sender: electron.WebContents, frameId: number, contextId: string, args: any[]) {
  args = unwrapArgs(event.sender, event.frameId, contextId, args);
  args = unwrapArgs(event.sender, event.frameId, contextId, args);
  args = unwrapArgs(event.sender, event.frameId, contextId, args);
  args = unwrapArgs(event.sender, event.frameId, contextId, args);
  args = unwrapArgs(event.sender, event.frameId, contextId, args);
                                        int32_t frame_id,
  auto frames = web_contents()->GetAllFrames();
  auto iter = std::find_if(frames.begin(), frames.end(), [frame_id](auto* f) {
    return f->GetRoutingID() == frame_id;
  });
  if (iter == frames.end())
    return false;
  if (!(*iter)->IsRenderFrameLive())
  (*iter)->GetRemoteAssociatedInterfaces()->GetInterface(&electron_renderer);
                             int32_t frame_id,
  if (frame)
      "frameId": 1
      "frameId": 1
      "frameId": 1
      "frameId": 1
      "frameId": 1
    _sendToFrame(internal: boolean, frameId: number, channel: string, args: any): boolean;
    _sendToFrameInternal(frameId: number, channel: string, ...args: any[]): boolean;
Fixed code: * `processId` Integer - The internal ID of the renderer process that sent this message
* `processId` Integer - The internal ID of the renderer process that sent this message
* `frameId` Integer | [number, number]
WebContents.prototype.sendToFrame = function (frame, channel, ...args) {
  } else if (!(typeof frame === 'number' || Array.isArray(frame))) {
    throw new Error('Missing required frame argument (must be number or array)');
  return this._sendToFrame(false /* internal */, frame, channel, args);
WebContents.prototype._sendToFrameInternal = function (frame, channel, ...args) {
  } else if (!(typeof frame === 'number' || Array.isArray(frame))) {
    throw new Error('Missing required frame argument (must be number or array)');
  return this._sendToFrame(true /* internal */, frame, channel, args);
  const { processId, frameId } = event;
    event.sender.sendToFrame([processId, frameId], ...args);
type FinalizerInfo = { id: RendererFunctionId, webContents: electron.WebContents, frameId: [number, number] };
function setCachedRendererFunction (id: RendererFunctionId, wc: electron.WebContents, frameId: [number, number], value: CallIntoRenderer) {
const unwrapArgs = function (sender: electron.WebContents, frameId: [number, number], contextId: string, args: any[]) {
  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
                                        v8::Local<v8::Value> frame,
  int32_t frame_id;
  int32_t process_id;
  if (gin::ConvertFromV8(isolate, frame, &frame_id)) {
    process_id = web_contents()->GetMainFrame()->GetProcess()->GetID();
  } else {
    std::vector<int32_t> id_pair;
    if (gin::ConvertFromV8(isolate, frame, &id_pair) && id_pair.size() == 2) {
      process_id = id_pair[0];
      frame_id = id_pair[1];
    } else {
      isolate->ThrowException(v8::Exception::Error(gin::StringToV8(
          isolate,
          "frameId must be a number or a pair of [processId, frameId]")));
      return false;
    }
  }

  auto* rfh = content::RenderFrameHost::FromID(process_id, frame_id);
  if (!rfh || !rfh->IsRenderFrameLive() ||
      content::WebContents::FromRenderFrameHost(rfh) != web_contents())
  rfh->GetRemoteAssociatedInterfaces()->GetInterface(&electron_renderer);
                             v8::Local<v8::Value> frame,
#include "content/public/browser/render_process_host.h"
  if (frame) {
    dict.Set("processId", frame->GetProcess()->GetID());
  }
import { defer } from './spec-helpers';

    it('can be replied to', async () => {
      ipcMain.on('test-echo', (e, arg) => {
        e.reply('test-echo', arg);
      });
      defer(() => {
        ipcMain.removeAllListeners('test-echo');
      });

      const w = new BrowserWindow({
        show: false,
        webPreferences: {
          nodeIntegration: true
        }
      });
      w.loadURL('about:blank');
      const v = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
        const { ipcRenderer } = require('electron')
        ipcRenderer.send('test-echo', 'hello')
        ipcRenderer.on('test-echo', (e, v) => {
          resolve(v)
        })
      })`);
      expect(v).to.equal('hello');
    });
      "frameId": 1,
      "processId": "placeholder-process-id"
      "frameId": 1,
      "processId": "placeholder-process-id"
      "frameId": 1,
      "processId": "placeholder-process-id"
      "frameId": 1,
      "processId": "placeholder-process-id"
      "frameId": 1,
      "processId": "placeholder-process-id"
    if (key === 'processId' && typeof value === 'number') {
      return 'placeholder-process-id';
    }
    _sendToFrame(internal: boolean, frameId: number | [number, number], channel: string, args: any): boolean;
    _sendToFrameInternal(frameId: number | [number, number], channel: string, ...args: any[]): boolean;