10000 Refactor sandbox preload initialization. by tarruda · Pull Request #12877 · electron/electron · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Refactor sandbox preload initialization. #12877

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

tarruda
Copy link
Contributor
@tarruda tarruda commented May 10, 2018

Use a single synchronous IPC call to retrieve data required by early
sandbox scripts. This has two purposes:

@tarruda tarruda requested a review from a team May 10, 2018 16:22
@tarruda tarruda force-pushed the refactor-sandbox-initialization-to-prevent-race-condition branch from 85ff0a9 to 92adee0 Compare May 10, 2018 16:37
@miniak
Copy link
Contributor
miniak commented May 10, 2018

@tarruda This PR is in conflict with mine #12832. But it could actually be a better solution to the issue I was trying to solve.

@@ -449,3 +450,13 @@ ipcMain.on('ELECTRON_BROWSER_WINDOW_CLOSE', function (event) {
}
event.returnValue = null
})

ipcMain.on('ELECTRON_BROWSER_SANDBOX_LOAD', function (event, preloadPath) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not pass the preloadPath as an argument. We know which webContents sent the IPC so it should be possible to get the preload script path from the webPreferences.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to assume that the renderer process is compromised and can send arbitrary IPC messages. We cannot allow it to load random files from disk pretending it is a preload script.

Copy link
Contributor
@miniak miniak May 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from this issue I like this change, batching multiple synchronous IPC calls into one to make loading faster is definitely welcomed 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to assume that the renderer process is compromised

Electron is not designed to run untrusted code, so we can never assume that. Untrusted code with access to ipcRenderer can easily control the browser process since it is possible to access any node.js module remotely and AFAIK there's no IPC filtering yet.

In any case, I agree that it would be cleaner to take preloadPath from webPreferences directly as it would also remove the need to pass the path as a renderer argument.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are actually planning to add some filtering for remote.require, remote.getGlobal, etc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also if you just let fs.readFileSync throw and not assign the returnValue, sendSync in the renderer process will get stuck waiting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @miniak this is basically a one-liner "read any file plz" API, I'd rather we (in this PR) used event.sender.getLastWebPreferences()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tarruda this is the last remaining issue

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @miniak this is basically a one-liner "read any file plz" API, I'd rather we (in this PR) used event.sender.getLastWebPreferences()

We currently give access to electron.remote.fs which can not only read any file, but also invoke any FS function. There's no regression in terms of security here by allowing the renderer to specify the preloadPath.

These are my arguments for keeping the current behavior and addressing this issue in a separate PR:

  • If we start using event.sender.getLastWebPreferences(), there's no reason to pass the preload path as argument to the renderer command line, so it would become necessary to refactor more code in C++ both in browser/renderer. (Unless we are OK with leaving dead code to be handled in a separate PR)
  • There's no guarantee that this will work as expected without introducing new regressions. Maybe it will, but I will have to investigate if preload will be present in every event.sender.webPreferences (in windows created by window.open, for example, which follow a different webContents creation path).
  • This is a completely separate change, not related to the goal of this PR which is to fix Error: Cannot get property 'id' on missing remote object #12316. I'd rather push refactoring changes in different PRs.

If you guys still insist that this should be done in the current PR, I will do it, but not right now since it will require more testing/verification which I can't do this week.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MarshallOfSound? I guess I am ok if this gets addressed in a separate PR.

process.on('exit', () => preloadProcess.emit('exit'))

// This is the `require` function that will be visible to the preload script
function preloadRequire (module) {
if (preloadModules.has(module)) {
return preloadModules.get(module)
return require(module)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work with browserify?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the preload script is a browserify bundle, then it will have access to the preloadRequire function. This require is private to the sandbox initialization script (which is also a browserify bundle, but independent from what the user creates).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the docs

Browserify parses the AST for require() calls to traverse the entire dependency graph of your project.

doesn't that mean that all the require calls have to have the module name as literal string?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. I will split the remotely-loaded modules which should be lazy loaded (and have -r flags) from the browserify modules which must have static requires. Thanks for the catch

@miniak
Copy link
Contributor
miniak commented May 10, 2018

@tarruda off-topic question, why is clearImmediate not exposed to the preload script, only setImmediate?

const preloadModules = new Set([
'child_process',
'electron',
'fs',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you remove events?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was added recently in #12828

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a merge error, I took this patch from an older electron version we use in our app. Will add it back 👍

@tarruda
Copy link
Contributor Author
tarruda commented May 11, 2018

@tarruda off-topic question, why is clearImmediate not exposed to the preload script, only setImmediate?

I forgot to add it when this file was created. Will add it now 👍

@tarruda
Copy link
Contributor Author
tarruda commented May 11, 2018

@miniak about this change:

In any case, I agree that it would be cleaner to take preloadPath from webPreferences directly as it would also remove the need to pass the path as a renderer argument.

Since it involves some C++ changes I will do it in a separate PR ok?

@tarruda tarruda force-pushed the refactor-sandbox-initialization-to-prevent-race-condition branch from d5f6630 to 27bed69 Compare May 11, 2018 11:07
return loadedModules.get(module)
}
if (remoteModules.has(module)) {
return require(module)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more. < 9E81 /p>

shouldn't this be electron.remote.require instead?

Copy link
Contributor
@miniak miniak May 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in which case do we still need to keep these files?
lib/sandboxed_renderer/api/exports/child_process.js
lib/sandboxed_renderer/api/exports/fs.js
lib/sandboxed_renderer/api/exports/os.js
lib/sandboxed_renderer/api/exports/path.js

Copy link
Contributor
@miniak miniak May 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and remove these from electron.gyp

'./lib/sandboxed_renderer/api/exports/fs.js:fs',
'-r',
'./lib/sandboxed_renderer/api/exports/os.js:os',
'-r',
'./lib/sandboxed_renderer/api/exports/path.js:path',
'-r',
'./lib/sandboxed_renderer/api/exports/child_process.js:child_process'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we still require these files to emulate a real node.js require for electron renderer modules that are reused in sandbox. Those are bundled in the executable and don't know they are running in a browserify environment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean is, electron modules such as crashReporter won't use this require function, so we need to tell browserify that when crashReporter requires fs, it is requiring /lib/sandboxed_renderer/api/exports/fs.js and not browserify shim.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

'path'
])

const loadedModules = new Map([
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe bundledModules could be a better name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, all modules are bundled. The ones that delegate to electron.remote are loaded lazily, but are still bundled.

@tarruda
Copy link
Contributor Author
tarruda commented May 11, 2018

@miniak With exception of taking preload path from webPreferences (which I want to do in a separate PR), I believe I've addressed all your comments. Let me know when you've finished your review so I can squash the fixup commits.

${preloadSrc}
})`

// eval in window scope:
// http://www.ecma-international.org/ecma-262/5.1/#sec-10.4.2
const geval = eval
const preloadFn = geval(preloadWrapperSrc)
const {setImmediate} = require('timers')
preloadFn(preloadRequire, preloadProcess, Buffer, global, setImmediate)
const {setImmediate, clearImmediate} = require('timers')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if (preloadPath) {
const preloadSrc = fs.readFileSync(preloadPath).toString()
const preloadWrapperSrc = `(function(require, process, Buffer, global, setImmediate) {
if (preloadSrc) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to check here if preloadSrc is an Error

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are not logging the error here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've used preloadError for the error

try {
preloadSrc = fs.readFileSync(preloadPath).toString()
} catch (err) {
preloadSrc = err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a separate variable? preloadError?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we need to be aware the Error objects to not JSON stringify very well by default, you'll need to manually pull out the stack and message attributes into a new object

@miniak miniak requested a review from MarshallOfSound May 11, 2018 19:09
preloadSrc, webContentsId, platform, execPath, env
} = electron.ipcRenderer.sendSync('ELECTRON_BROWSER_SANDBOX_LOAD', preloadPath)

process.webContentsId = webContentsId
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For safety

Object.defineProperty(process, 'webContentsId', {
  configurable: false,
  value: webContentsId
})

This way nothing can ever override that value

Copy link
Member
@MarshallOfSound MarshallOfSound left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left some comments

@@ -103,4 +106,6 @@ if (preloadSrc) {
const preloadFn = geval(preloadWrapperSrc)
const {setImmediate, clearImmediate} = require('timers')
preloadFn(preloadRequire, preloadProcess, Buffer, global, setImmediate, clearImmediate)
} else if (preloadError) {
console.error(preloadError.stack)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@miniak is this how you wanted to handle preload error? I can't think of a better way right now, logging via console.error seems like a sane way to do it, since it would be available in the terminal if electron is started with --enable-logging

try {
preloadSrc = fs.readFileSync(preloadPath).toString()
} catch (err) {
preloadError = {stack: err.stack}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't you include name and message as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stack property already contains the message and Error class name. Example from the repl:

> console.log(new Error('msg').stack)
Error: msg
    at repl:1:13
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:468:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7)
    at REPLServer.Interface._onLine (readline.js:282:10)
    at REPLServer.Interface._line (readline.js:631:8)

try {
preloadSrc = fs.readFileSync(preloadPath).toString()
} catch (err) {
preloadError = {stack: err.stack}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hate to be that person 😆 But welcome to javascript where you can throw null

This should check if we can get to .stack before actually trying or our error handling will error 😆

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@tarruda tarruda force-pushed the refactor-sandbox-initialization-to-prevent-race-condition branch 2 times, most recently from be27e54 to ebf6bf4 Compare May 16, 2018 08:47
@tarruda
Copy link
Contributor Author
tarruda commented May 16, 2018

Rebased/squashed commits

@tarruda tarruda force-pushed the refactor-sandbox-initialization-to-prevent-race-condition branch 3 times, most recently from 9f0e12a to c8640ef Compare May 16, 2018 10:57
} = electron.ipcRenderer.sendSync('ELECTRON_BROWSER_SANDBOX_LOAD', preloadPath)

Object.defineProperty(process, 'webContentsId', {
configurable: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

false is a default value of the configurable attribute, no reasons to explicitly set it.
If you want to be explicit then you should also set writable: false (which is also a default value).

@alexeykuzmin
Copy link
Contributor

@tarruda the PR got conflicts, can you please rebase it on the latest master? Thanks!

Use a single synchronous IPC call to retrieve data required by early
sandbox scripts. This has two purposes:

- Optimize preload script initialization by:
  - Using one synchronous IPC call to retrieve preload script,
  webContentsId (more on that later), process.{platform,execPath,env}
  - Lazy loading as many modules as possible.
- Fix #12316 for sandbox. @MarshallOfSound addressed the issue in
  #12342, but it was still present in sandbox mode. By loading
  webContentsId very early and skipping remote module at early
  startup, we fix it for sandbox.
@tarruda tarruda force-pushed the refactor-sandbox-initialization-to-prevent-race-condition branch from c8640ef to 37f42e5 Compare May 21, 2018 12:24
@tarruda
Copy link
Contributor Author
tarruda commented May 21, 2018

@alexeykuzmin Rebased and added writable: false

@MarshallOfSound MarshallOfSound merged commit 6f076f7 into master May 21, 2018
@MarshallOfSound MarshallOfSound deleted the refactor-sandbox-initialization-to-prevent-race-condition branch May 21, 2018 12:56
@trop
Copy link
Contributor
trop bot commented May 21, 2018

An error occurred while attempting to backport this PR to "2-0-x", you will need to perform this backport manually

@alexeykuzmin
Copy link
Contributor

@tarruda Can you please backport the changes to 2-0-x?

@MarshallOfSound
Copy link
Member
MarshallOfSound commented May 21, 2018

cc @tarruda ^^

I swear I commented first 😆

@tarruda
Copy link
Contributor Author
tarruda commented May 21, 2018

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Error: Cannot get property 'id' on missing remote object
4 participants
0