mirror of
https://github.com/electron/electron.git
synced 2026-10-29 04:04:54 +08:00
main
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cba5248898 |
docs: document preload code cache threat model (#54654)
Explain why SetFromRenderer stores the renderer-produced blob unverified and that the (BrowserContext, ProcessLock, id) scope is the security boundary. Claude-Session: https://claude.ai/code/session_018A4Mt59Nr4h4WSFjyNNkxb Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
05f60b5068 |
refactor: remove Node.js shims from sandboxed preload scripts (#53891)
* refactor: remove Node.js shims from sandboxed preload scripts BREAKING CHANGE. Sandboxed preload scripts and service worker preload scripts no longer get the `events`, `timers` and `url` module shims from `require`, and are no longer run with `Buffer`, `setImmediate` and `clearImmediate` in scope. `require` only loads `electron`; `process` and `global` are still provided. These were npm browser polyfills compiled into the bundle that every sandboxed renderer evaluates, and made up about two thirds of it. With them gone the preload wrapper's parameter list is fixed again, so the code cache no longer needs to be keyed on the wrapper shape. * docs: tabulate the removed preload shims and their replacements |
||
|
|
0ef9950c1c |
perf: make the sandboxed preload Buffer global lazy (#53855)
With context isolation the preload realm's global is private to preloads, so Buffer is defined there as a property that loads the polyfill on first use instead of evaluating it for every frame before any preload runs. Without context isolation it stays a preload-scoped parameter as before. The persisted preload code cache is now also keyed on that wrapper shape, because V8's cached-data check does not notice a changed parameter list. Also drops the `Buffer` hidden value set on the sandboxed global; its native consumer was removed in #34794. With the wrapper shape now depending on where the bundle runs, the renderer decides it from the actual script context (main world vs isolated world) rather than the contextIsolation preference, and names the parameter list in the compiled source so a persisted code cache produced for one shape is rejected rather than run against the other. |
||
|
|
c38d6fd687 |
fix: key sandboxed preload code cache entries by consuming site (#52480)
* fix: bind preload code cache entries to the serving frame and site Record the sha256 of the preload bytes the browser serves to each frame and only accept SetPreloadCodeCache writes whose id and hash match that record, persisting entries under the browser-recorded hash rather than the renderer-supplied one. Entries also record the producing frame's site and are only served back to same-site documents on later loads. Old-format cache files fail the magic check and are re-produced. * refactor: key preload code cache by (id, site) and tie served records to frame lifetime - Key the in-memory and on-disk cache by (id, site) so different-site consumers of the same preload no longer evict each other's entries; the disk payload stays magic + sha256(source) + blob. - Clean up served-preload records via a WebContentsObserver owned by the WebContents itself, so records cannot outlive frames in contents that are never adopted by an api::WebContents wrapper. - Build frame startup data and record served preloads in one shared renderer_startup_data::BuildForFrame() used by both the navigation push and the window.open reply path, hashing each preload source only once. - Drop the api.mojom include from preload_code_cache.h and update the preload code cache spec for the new cache filename scheme. * test: apply oxfmt to preload code cache probe spec No-Verification-Needed: formatting-only change * fix: key preload code cache entries by browser context and process lock Cache entries were keyed by (preload id, deprecated site URL) under a single userData directory, so principals that share a site URL but not a renderer process - sessions/partitions, guests, sandboxed or origin-keyed processes - shared entries. Key the in-memory tier by (BrowserContext, ProcessLock, id), store the disk tier under the BrowserContext's own path, and skip disk persistence for off-the-record contexts. Adds a spec covering separate entries for the same preload consumed from two different sites, and makes the probe cleanup use the retrying remove helper. |
||
|
|
0383e49301 |
fix: invalidate sandboxed preload code cache when source changes without changing length (#51930)
V8's CachedData source check hashes only the source length, so a preload edit that preserves byte length consumed the stale blob and ran the old bytecode. Bind cache entries to sha256 of the source: the renderer ships the hash of what it compiled, the browser stores it in the cache entry and only serves a blob whose hash matches the contents it just read. Reject cache writes for preload ids not served to the sending frame. |
||
|
|
07f25eaedc |
perf: push sandboxed renderer startup data via mojo and cache preload bytecode (#51602)
* perf: push sandboxed renderer startup data via mojo instead of sync IPC
Replaces the BROWSER_SANDBOX_LOAD sync IPC pull with a mojo push
(ElectronFrameStartup.SetStartupData) sent from the browser at
ReadyToCommitNavigation, ahead of CommitNavigation. The push carries
preload script contents (as BigBuffer, shared-memory backed for large
bundles), the browser's environment snapshot, and process.helperExecPath.
The sync IPC parked the renderer main thread on a browser UI-thread
roundtrip during every navigation. Its handler was async with
await fs.promises.readFile() per preload, so under UI-thread contention
each await yielded a turn to other startup work and the stall amplified
~5-8x with two preloads — measured at ~97ms with 20ms busy chunks. The
push eliminates the roundtrip entirely; the renderer reads the data from
a per-frame cache that is guaranteed to be populated before
DidCreateScriptContext fires, because the message is associated with the
navigation channel and ordered before CommitNavigation.
The legacy sync IPC remains as a fallback for the cases the push
does not yet cover (devtools extension subframes, service worker
preload realms).
process.arch/platform/version/versions are no longer shipped over the
wire — they are identical in the renderer (same Electron binary) and are
populated from node::per_process::metadata locally.
* perf: remove BROWSER_SANDBOX_LOAD sync IPC entirely
The previous commit kept the sync IPC as a fallback for paths that don't
get a per-frame ElectronFrameStartup push: service worker preload realms
(no RenderFrame, worker thread) and window.open() child windows whose
synchronous about:blank document is created before the browser knows the
window exists. Both now have a dedicated push and the IPC is removed:
Service workers — a per-process associated mojom::ElectronWorkerStartup
interface is registered via a RenderThreadObserver and the browser pushes
from RenderProcessHostObserver::RenderProcessReady(), strictly before any
StartWorker can spawn a worker thread. Cached behind a lock + WaitableEvent
so the worker thread can block on it. Re-pushed when SW preload scripts are
registered or unregistered while a renderer is alive.
window.open() child windows — the popup's RenderFrame, its synchronous
about:blank document, and its preload all happen inside the renderer's
window.open() call stack, after the [Sync] CreateNewWindow IPC returns and
before control returns to page script. No async browser-to-renderer mojo
message can land in time. The browser does, however, know everything about
the popup during CreateNewWindow (it just ran setWindowOpenHandler and may
have overridden the preload, partition, etc.), so the only correct
zero-extra-IPC transport is to attach the data to the CreateNewWindowReply
itself. A new Chromium patch adds an opaque mojo_base.BigBuffer? field to
CreateNewWindowReply, plus ContentBrowserClient::GetExtraCreateNewWindowReplyData()
and ContentRendererClient::SetPendingCreateNewWindowStartupData() hooks; the
renderer stashes the deserialized data process-globally for the next
ElectronApiServiceImpl constructor on the same call stack.
The browser-side data is built by a new renderer_startup_data::Build()
helper shared by the navigation push, the per-process SW push, and the
CreateNewWindowReply path. process.env is captured fresh per push via
uv_os_environ() — same per-navigation timing as the legacy { ...process.env }
spread. process.arch/platform/version/versions are filled from
node::per_process::metadata in the renderer (same Electron binary) instead
of being shipped over the wire; cldr/tz are runtime ICU state and computed
directly from the renderer's ICU.
BROWSER_NONSANDBOX_LOAD remains for non-sandboxed renderers — it returns
preload paths only (no contents, no env spread), so the contention
amplification doesn't apply.
Verified against stock Electron 42.0.1 that window.open() popups with
setWindowOpenHandler-overridden preloads behave identically.
* perf: V8 code cache for sandboxed preload scripts
Adds an in-memory + on-disk V8 code cache for preload scripts so the
parse + top-level codegen cost is paid once per preload per Electron
version instead of per navigation, and moves preload script compilation
fully into native code so the contents and cache never bounce through
the V8 heap.
createPreloadScript() now takes (scriptId, paramNames) instead of a
wrapped source string. It looks up the preload contents and code cache
directly from the per-frame ElectronApiServiceImpl's mojo-cached
startup data — the contents stay in the BigBuffer until a single
NewFromUtf8() copy for the compile (rather than BigBuffer → V8 string
→ template-literal concat → flatten, ~150 KB of allocation and 2-3
copies per preload eliminated), and the cache bytes never enter V8 at
all (BufferNotOwned for consume, direct BigBuffer construction from the
produced CachedData for ship-back over
ElectronWebContentsUtility.SetPreloadCodeCache). BuildStartupData()
exposes hasContents instead of contents to JS.
The compile uses ScriptCompiler::CompileFunction() with the parameter
names directly instead of a string-templated (function(p0, p1, ...){…})
wrapper — no concat, no flatten, and preload stack traces get correct
line numbers (the wrapper offsets every line by 1 with no
ScriptOrigin::lineOffset compensation).
The browser keys the cache by sha256(id) and persists it to
userData/Code Cache/electron-preload/. V8's CachedData validation
handles invalidation: a blob from a different V8 version, flag set, or
source is rejected at consume time, the renderer compiles from source
and ships a fresh blob, and the stale one is overwritten. Producing the
cache during the cold compile adds ~25% to the first navigation per
launch; warm navigations skip the parse (~5 ms saved per nav in a
debug build, ~22% reduction in pre-parse blocking time).
Service-worker preload realms look up contents from a clone of the
worker startup data captured on ServiceWorkerData when the preload
realm is created. They have no per-frame ship-back channel so they
consume but never produce caches; the experimental
--service-worker-preload flow is otherwise unchanged.
* test: add spec coverage for preload startup data push and code cache
- preload code cache: produces and persists a blob to
userData/Code Cache/electron-preload/, runs the preload identically
when consuming the cache, rejects and re-produces a corrupt disk
cache (V8 CachedData validation rejects garbage and version-mismatched
blobs and the renderer self-heals).
- preload script stack traces: errors report correct line numbers and
the preload's real file path instead of <anonymous>, sandboxed and
not. CompileFunction() with an explicit ScriptOrigin gets both right;
the legacy template-string wrapper had neither.
- service worker preload realm: process.env, process.execPath,
process.arch/platform/version, and process.versions.electron/chrome/node
are populated from the per-process ElectronWorkerStartup push.
- window.open() popup preload: the popup's synchronous about:blank
document runs the preload from setWindowOpenHandler's
overrideBrowserWindowOptions when set (delivered via
CreateNewWindowReply), no preload when only security prefs are
inherited (matching legacy Electron), and the opener's preload when
explicitly carried through the override.
* Delete .claude/scheduled_tasks.lock
* refactor: address PR review feedback
Review fixups independent of the service-worker delivery change:
- preload_code_cache: DCHECK Get/Set are UI-thread only; SKIP_ON_SHUTDOWN
for the disk write so an in-flight write isn't abandoned mid-write.
Did NOT take the suggested first-writer-wins guard — Get() memoizes a
corrupt on-disk blob (only V8 can validate it), so a guard keying on
'entry exists' would pin a bad cache and break the self-heal path; the
duplicate-ship case is rare, identical bytes, and Set() is UI-thread
serialized so it isn't a data race.
- WebContentsPreferences::ShouldUseSandbox(): extract the duplicated
'null prefs => default sandboxed, --enable-sandbox forces sandbox'
check used by the per-frame push.
- preload_utils: LookupPreloadScript returns const PreloadScriptData*
not const unique_ptr<>*; param name V8 conversion moved below the
null-check so a missing script bails without paying for it.
- electron_api_service_impl: drop unused TakeStartupData(); DCHECK the
window.open pending-data slot is touched only on the renderer main
thread; tighten the stale startup_data() comment.
- Stop assigning the unused PreloadScriptData.type field (the field
itself is removed in the following commit with its last setters).
* perf: deliver service worker preload data via EmbeddedWorkerStartParams
Replaces the per-process ElectronWorkerStartup push + the renderer-side
cross-thread global (ElectronRenderThreadObserver: a process-global
WaitableEvent + lock the SW worker thread could block on) with delivery
through the service worker's own EmbeddedWorkerStartParams.
A new ContentBrowserClient::GetServiceWorkerStartupData() hook populates
an opaque BigBuffer on the start params; it rides the marshalling
StartWorker already performs (GlobalScopeCreationParams moved onto the
worker thread by WorkerThread::Start), so the data arrives on the worker
thread ordered with worker creation — no global, no lock, no blocking
wait, and no 'worker raced the push' edge case to handle. The renderer
reads it off WebServiceWorkerContextProxy::ElectronPreloadData() (backed
by WorkerGlobalScope) when it builds the preload realm.
GetServiceWorkerStartupData() early-returns when the session has no
service-worker preloads, so the asar reads + serialization no longer run
on every renderer at RenderProcessReady() (addresses the SendToProcess
cost raised in review). Registry changes are picked up automatically —
the data is rebuilt from SessionPreferences on every StartWorker — so
Session::BroadcastWorkerStartupData and its re-push on register/
unregister are removed.
Also folds, since they share these files: removal of the now-unused
PreloadScriptData.type field and its last setters, the
CreateNewWindowReply target_url gate so the embedder blob isn't built
for popups that navigate (review feedback), and the corrected
CreateNewWindowReply patch commit message.
|