* ci: run clang-tidy on macOS and Windows
* ci: copy framework headers for clang-tidy on macOS
* chore: exclude electron_smooth_round_rect.cc in CI
* chore: C-style casts are discouraged; use static_cast [google-readability-casting]
* chore: add extra args on Windows to clear out warnings
* ci: fix for macOS --remote-build none
* fix: ensure corsEnabled: false protocol handlers do not work across protocols
Subresource requests for registered custom protocols are routed to
ElectronURLLoaderFactory via the renderer's per-scheme URLLoaderFactoryBundle
entry, which bypasses the network service's CorsURLLoaderFactory. This meant a
cross-origin page could fetch() a scheme registered with {supportFetchAPI: true}
and read the response body even when {corsEnabled: true} was not set.
Replicate CorsURLLoader::StartRequest's kCorsDisabledScheme gate in
ElectronURLLoaderFactory::CreateLoaderAndStart so cross-origin mode=cors
requests to such schemes fail before the JS handler runs, and tag cross-origin
mode=no-cors responses as opaque so the body is not script-readable while <img>
and similar subresource loads continue to work.
Re-enable the long-disabled "disallows CORS and fetch requests when only
supportFetchAPI is specified" test, add coverage for the opaque/no-cors,
same-origin, handler-not-invoked, corsEnabled-unaffected and net.fetch-unaffected
cases, and migrate spec helpers that were exercising a {supportFetchAPI: true}
scheme cross-origin to a corsEnabled scheme.
* chore: oxfmt
* fix: intermittent CI failure is-not-alwaysOnTop
Ensure that the `always-on-top-changed` event always fires with the
right 'alwaysOnTop' boolean, regardless of interaction between
SetZOrderLevel() and MoveBehindTaskBarIfNeeded(). We know what the
value will be when all of the HWND events settle, so use that value.
* test: temporary commit to torture-test the new change with 1000 iterations
* test: keep eventually-becomes-consistent test but do not loop 1000 times
* feat: add `Notification.getHistory()` static method (macOS)
Add `Notification.getHistory()` which returns a `Promise<Notification[]>`
of all delivered notifications still present in Notification Center.
Each returned Notification is a live object connected to the corresponding
delivered notification — interaction events (click, reply, action, close)
will fire on these objects, enabling apps to re-attach event handlers after
a restart.
Key implementation details:
- Queries UNUserNotificationCenter's getDeliveredNotifications API
- Creates live Notification objects with populated id, groupId, title,
subtitle, and body properties from what macOS provides
- Registers each object with the presenter via Restore() so the
NotificationCenterDelegate routes events correctly
- Restored notifications use is_restored_ flag to prevent removal from
Notification Center when the JS object is garbage collected
- Requires code-signed builds (unsigned builds resolve with empty array)
Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
* test: fix typecheck
* fix: avoid dangling presenter pointer in GetHistory callback
* fix: document show() behavior
Notifications returned by getHistory() now set is_restored_ so that Dismiss() skips removal from Notification Center on GC. Calling show() on a restored notification removes the original from NC and posts a new one.
* fix: address code review feedback
* test: fix oxfmt linting
* docs: update docs/api/notification.md
Co-authored-by: Erick Zhao <erick@hotmail.ca>
---------
Co-authored-by: Claude <svc-devxp-claude@slack-corp.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>
fix: prevent use-after-free when destroying guest WebContents during event emission
Multiple event emission sites in WebContents destroy the underlying C++
object via a JavaScript event handler calling webContents.destroy(), then
continue to dereference the freed `this` pointer. This is exploitable
through <webview> guest WebContents because Destroy() calls `delete this`
synchronously for guests, unlike non-guests which safely defer deletion.
The fix has two layers:
1. A new `is_emitting_event_` flag is checked in Destroy() — when true,
guest deletion is deferred to a posted task instead of executing
synchronously. This is separate from `is_safe_to_delete_` (which
gates LoadURL re-entrancy) to avoid rejecting legitimate loadURL
calls from event handlers.
2. AutoReset<bool> guards on `is_emitting_event_` are added to
CloseContents, RenderViewDeleted, DidFinishNavigation, and
SetContentsBounds, preventing synchronous destruction while their
Emit() calls are on the stack.
Destroy() now requires both `is_safe_to_delete_` (navigation re-entrancy)
and `!is_emitting_event_` (event emission) to allow synchronous guest
deletion. The existing AutoReset guards on `is_safe_to_delete_` in
DidStartNavigation, DidRedirectNavigation, and ReadyToCommitNavigation
are also now effective for guests.
* fix: UAF in api::UtilityProcessWrapper
Detach the wrapper from ServiceProcessHost during termination instead
of waiting for destruction. Add a regression test that forces GC.
This fixes a UAF error reported by ASAN: the wrapper lost its last JS
reference and become collectible after emitting exit *but* before it
had been removed from the global observer list.
UtilityProcessWrapper is now cppgc-managed as of b9e462f397, but its
ServiceProcessHost observer cleanup still depended on destructor-time
teardown.
* fixup! fix: UAF in api::UtilityProcessWrapper
fix: much better cleanup from Deepak code review
The PDF viewer's "save with changes" feature uses
`window.showSaveFilePicker()`, but the PDF extension runs in a
cross-origin iframe (chrome-extension:// inside the app's origin).
Chromium's File System Access API blocks cross-origin subframes from
showing file pickers unless the embedder explicitly allows them via
`ContentClient::IsFilePickerAllowedForCrossOriginSubframe()`.
Chrome overrides this in `ChromeContentClient` to allowlist the PDF
extension origin, but Electron never did — so the picker was always
blocked with a SecurityError.
This adds the same override to `ElectronContentClient`, allowing the
built-in PDF extension origin to bypass the cross-origin check.
* refactor: migrate electron::api::GlobalShortcut to cppgc
* refactor: lazy-create electron::api::GlobalShortcut
copy the lazy-create idom used by electron::api::Screen
* refactor: use gin::WeakCellFactory in GlobalCallbacks
* fix: make a copy of `callback` before running it
safeguard against the callback changing the map, invalidating `cb`
* chore: reduce unnecessary diffs with main
* fixup! refactor: use gin::WeakCellFactory in GlobalCallbacks
fix: must Trace() the weak cell factory
* fix: destruction order
- Setup isolate dispose observer to run destruction sequences
and remove self persistent reference
- Skip NOTREACHED check during destruction, it can happen
as a result of plaform listeners scheduling callbacks when Unregister is invoked.
- Fix the order of unregistration in GlobalShortcut::Unregister
- Add GlobalShortcut::UnregisterAllInternal to avoid any callsites
that can re-enter V8
* fix: crash during gc from incorrect cppgc object headers
* chore: update patches
* chore: cleanup
* chore: fix lint
---------
Co-authored-by: deepak1556 <hop2deep@gmail.com>
refactor: use StartUpdating in desktopCapturer
Replace the one-shot Update() callback model with the continuous
StartUpdating() observer model for NativeDesktopMediaList.
Fixes a macOS DCHECK(can_refresh()) crash in UpdateSourceThumbnail(),
where ScreenCaptureKit's recurrent thumbnail capturer would post
UpdateSourceThumbnail callbacks after the one-shot refresh_callback_
had been consumed. Now, can_refresh() is always true because
refresh_callback_ is repopulated via ScheduleNextRefresh().
Each capturer (window, screen) gets its own ListObserver that tracks
readiness via OnSourceAdded and OnSourceThumbnailChanged events.
Once a list has both sources and thumbnails (or thumbnails aren't
requested), its data is snapshotted and the capturer checks if all
requested types are ready before resolving to JS.
Also remove the "skip_next_refresh_" Chromium patch, which was a
workaround for the timing mismatch between the one-shot Update()
model and ScreenCaptureKit's asynchronous thumbnail delivery.
refactor: simplify state logic in DesktopCapturer
* fix: nodeIntegrationInWorker not working in AudioWorklet
* fix: deadlock on Windows when destroying non-AudioWorklet worker contexts
The previous change kept the WebWorkerObserver alive across
ContextWillDestroy so the worker thread could be reused for the next
context (AudioWorklet thread pooling, Chromium CL:5270028). This is
correct for AudioWorklet but wrong for PaintWorklet and other worker
types, which Blink does not pool — each teardown destroys the thread.
For those worker types, ~NodeBindings was deferred to the thread-exit
TLS callback. By that point set_uv_env(nullptr) had already run, so on
Windows the embed thread was parked in GetQueuedCompletionStatus with a
stale async_sent latch that swallowed the eventual WakeupEmbedThread()
from ~NodeBindings. uv_thread_join then blocked forever, deadlocking
renderer navigation. The worker-multiple-destroy crash case timed out
on win-x64/x86/arm64 as a result. macOS/Linux (epoll/kqueue) don't have
the latch and were unaffected.
Plumb is_audio_worklet from WillDestroyWorkerContextOnWorkerThread into
ContextWillDestroy. For non-AudioWorklet contexts, restore the
pre-existing behavior of calling lazy_tls->Set(nullptr) at the end of
the last-context cleanup so ~NodeBindings runs while the worker thread
is still healthy. AudioWorklet continues to keep the observer alive so
the next pooled context can share NodeBindings.
* chore: address review feedback
* fix: stop embed thread before destroying environments in worker teardown
FreeEnvironment (called via environments_.clear()) runs uv_run to drain
handle close callbacks. On Windows, both that uv_run and the embed
thread's PollEvents call GetQueuedCompletionStatus on the same IOCP
handle. IOCP completions are consumed by exactly one waiter, so the
embed thread can steal completions that FreeEnvironment needs, causing
uv_run to block indefinitely. On Linux/Mac epoll_wait/kevent can wake
multiple waiters for the same event so the race doesn't manifest.
Add NodeBindings::StopPolling() which cleanly joins the embed thread
without destroying handles or the loop, and allows PrepareEmbedThread +
StartPolling to restart it later. Call StopPolling() in
WebWorkerObserver::ContextWillDestroy before environments_.clear() so
FreeEnvironment's uv_run is the only thread touching the IOCP.
Split PrepareEmbedThread's handle initialization (uv_async_init,
uv_sem_init) from thread creation via a new embed_thread_prepared_ flag
so the handles survive across stop/restart cycles for pooled worklets
while the embed thread itself can be recreated.
* chore: address outstanding feedback
fix: simpleFullScreen exits when web content calls requestFullscreen
SetHtmlApiFullscreen only checked IsFullscreen() to detect that the
window was already fullscreen, missing the simple-fullscreen case on
macOS. When web content triggered requestFullscreen the code fell
through to SetFullScreen(true) which toggled simple fullscreen off.
Include IsSimpleFullScreen() in the guard so the HTML-API fullscreen
state is updated without touching the window's fullscreen mode.
* chore: use emplace and use it correctly
* chore: redundant cast to the same type [google-readability-casting]
* chore: do not create objects with +new [google-objc-avoid-nsobject-new]
* chore: default arguments on virtual or override methods are prohibited [google-default-arguments]
* chore: warning: C-style casts are discouraged; use static_cast [google-readability-casting]
CFLocaleGetValue already returns CFTypeRef so that redundant static_cast was removed
* chore: refactor block to avoid use after move warning from clang-tidy
Looks like clang-tidy couldn't tell these were two mutually exclusive
branches so there was no actual issue, but refactoring is cleaner
anyway since it makes it more DRY.
* chore: C-style casts are discouraged; use static_cast [google-readability-casting]
No cast needed here, everything is already the correct type
* chore: C-style casts are discouraged; use static_cast/const_cast/reinterpret_cast [google-readability-casting]
* chore: use '= default' to define a trivial destructor [modernize-use-equals-default]
* chore: use range-based for loop instead [modernize-loop-convert]
* chore: redundant void argument list [modernize-redundant-void-arg]
* chore: address code review feedback
* chore: use auto
Co-authored-by: Charles Kerr <charles@charleskerr.com>
---------
Co-authored-by: Charles Kerr <charles@charleskerr.com>
* feat: capture JS stack trace on renderer OOM
When a renderer process approaches its V8 heap limit, capture the
JavaScript stack trace and write it to both a Crashpad crash key
("js-oom-stack") and stderr.
The stack trace is captured via RequestInterrupt rather than directly
inside the NearHeapLimitCallback because CurrentStackTrace is unsafe
to call during OOM — V8 FATALs on optimized (TurboFan) frames that
have had their deoptimization data garbage-collected. RequestInterrupt
defers the capture to the next V8 safe point, where all frames are
guaranteed to have deopt data available. This matches Node.js's
approach of never capturing JS stacks inside the heap limit callback.
The callback is registered once per isolate via an atomic guard in
RendererClientBase::DidCreateScriptContext, preventing the CHECK
failure V8 raises on duplicate AddNearHeapLimitCallback registrations
(which would otherwise occur on page navigations or multiple contexts).
Refs: #46078
Made-with: Cursor
* Update shell/renderer/oom_stack_trace.cc
Co-authored-by: Niklas Wenzel <dev@nikwen.de>
* Update shell/renderer/oom_stack_trace.cc
Co-authored-by: Niklas Wenzel <dev@nikwen.de>
* test: add crash reporter test for OOM JS stack trace
Add a test that verifies the `electron.v8-oom.stack` crash key contains
the JS stack trace (including function names) when a renderer process
runs out of memory. Also deduplicate the heap info formatting in
oom_stack_trace.cc.
Refs: #46078
Made-with: Cursor
* fix: lint formatting in oom_stack_trace.cc
Made-with: Cursor
* fix: use proper logger API instead of cstdio
* fix: check heap headroom before capturing OOM stack trace
deepak1556: "Should there be check for available heap size [for]
CurrentStackTrace and formatting"
CurrentStackTrace allocates StackTraceInfo + StackFrameInfo on the V8
heap. If the 20 MB bump is partially consumed by the time the interrupt
fires, these allocations trigger a secondary OOM. Guard with a 2 MB
headroom check.
Made-with: Cursor
* fix: handle V8 cage limit when bumping heap for OOM stack capture
deepak1556: "Does this bumping work when we are at the cage limit of
4GB"
V8's pointer compression cage caps the heap at ~4 GB. When
current_heap_limit is already near the ceiling, our 20 MB bump gets
clamped to zero and the interrupt never fires. Detect this and record
heap info as the final crash key instead of waiting for a stack trace
that won't arrive.
Made-with: Cursor
* feat: add V8 heap statistics as OOM crash keys
deepak1556: "V8 seems to capture heap stats as crash keys but it gets
missed today due to the OOM callback override... wonder if we can
include that to get some more heuristics in the dump."
Record heap used/total/limit/available, per-space stats for old_space
and large_object_space, native/detached context counts, and utilization
percentage as crash keys. Also add heap stats in the V8OOMErrorCallback
in node_bindings.cc for the final OOM crash report.
Made-with: Cursor
* feat: support worker thread isolates for OOM stack trace
deepak1556: "You need a separate registration for worker threads via
WorkerScriptReadyForEvaluationOnWorkerThread but that also means the
process global g_registered_isolate would break."
Chromium has one V8 isolate per thread (main + one per web worker), so
thread_local is equivalent to per-isolate storage. Replace the global
atomic + mutex/set with a constinit thread_local OomState* that holds
the isolate pointer and per-isolate is_in_oom flag. The void* data
parameter on AddNearHeapLimitCallback delivers OomState* directly into
callbacks, so the hot path needs no TLS lookup.
Add WorkerScriptReadyForEvaluationOnWorkerThread and
WillDestroyWorkerContextOnWorkerThread overrides to RendererClientBase
so both ElectronRendererClient and ElectronSandboxedRendererClient get
worker OOM registration. Update ElectronRendererClient to call the base
class in both worker lifecycle methods.
Add a web worker OOM test that spawns a dedicated Worker with a memory
leak and verifies the stack trace captures the worker function name.
Made-with: Cursor
* fix: register OOM callback for all script contexts
When context isolation is enabled, ShouldNotifyClient skips
DidCreateScriptContext for the main world, but user JS still runs there
and can OOM. Register in DidInstallConditionalFeatures which fires for
every script context. The TLS dedup guard prevents double-registration
on the same isolate.
Made-with: Cursor
* fix: guard against division by zero and cage size changes in OOM handler
Add a zero-guard on heap_size_limit before computing utilization
percentage — maximizes robustness in an OOM code path.
Add static_assert on kPtrComprCageReservationSize to catch any
upstream V8 change to the cage size at compile time.
Made-with: Cursor
* fix: address review feedback on OOM stack trace PR
- Remove redundant RegisterOomStackTraceCallback from
electron_render_frame_observer.cc; DidCreateScriptContext is sufficient
since main world and isolated world share the same isolate
- Replace thread_local OomState* with base::ThreadLocalOwnedPointer
wrapped in base::NoDestructor per Chromium style for non-trivially
destructible types
- Change heap-headroom and cage-limit logs from ERROR to INFO since
users cannot act on these diagnostics
- Add comment explaining why base class is called last in
WillDestroyWorkerContextOnWorkerThread (OOM deregistration ordering)
Made-with: Cursor
* fix: skip OOM stack trace registration for worklet contexts
Worklets can share a thread and isolate via WorkletThreadHolder's
per-process singleton pattern. With per-thread OOM state, the first
worklet to be destroyed would prematurely remove the callback for
any remaining worklets on the same thread. Skip worklets entirely
to avoid this; can be revisited with ref-counting if needed.
Made-with: Cursor
* fix: prevent dangling raw_ptr<v8::Isolate> in OOM state
The OomState held a raw_ptr<v8::Isolate> that outlived the isolate on
the main thread: gin::IsolateHolder destroyed the isolate during
shutdown, but the OomState (stored in thread-local storage) was only
released later in JavascriptEnvironment::~JavascriptEnvironment. This
triggers a dangling pointer check when building with
enable_dangling_raw_ptr_checks.
Register OomState as a gin::PerIsolateData::DisposeObserver so it
clears the raw_ptr and removes the NearHeapLimitCallback before the
isolate is destroyed, regardless of destructor ordering.
Suggested-by: Deepak Mohan
Made-with: Cursor
* test: verify OOM crash keys end-to-end via crash reporter
Replace stderr-based OOM tests with end-to-end crash dump validation.
Instead of parsing log output, start a crash reporter server, trigger
renderer OOM, and verify the uploaded crash dump contains the expected
`electron.v8-oom.*` annotations — the same code path production crash
reports take.
Consolidate all OOM test scenarios (basic heap leak, JSON.stringify,
web worker) into a single `describe('OOM crash keys')` block inside
api-crash-reporter-spec using the existing crash fixture app with new
renderer-oom-json and renderer-oom-worker crash types.
The web worker test verifies that OOM crash keys are present but does
not assert on the JS function name: the 20 MB heap bump may be
exhausted before V8 reaches a safe point to fire the stack-capture
interrupt, leaving the crash key at "(stack pending)". Increasing the
bump or switching to a synchronous capture strategy would fix this but
is left for a follow-up.
Remove the standalone oom-stack-trace-spec.ts and its fixture app.
Made-with: Cursor
---------
Co-authored-by: Niklas Wenzel <dev@nikwen.de>
chore: address blink gc plugin errors
Key fixes:
- Replace `base::WeakPtrFactory` with `gin::WeakCellFactory` in
MenuMac, MenuViews, and NetLog, since weak pointers to cppgc-managed
objects must go through weak cells
- Replace `v8::Global<v8::Value>` with `cppgc::Persistent<Menu>` for
the menu reference in BaseWindow
- Stop using `gin_helper::Handle<T>` with cppgc types; use raw `T*`
and add a `static_assert` to prevent future misuse
- Add proper `Trace()` overrides for Menu, MenuMac, MenuViews, and
NetLog to ensure cppgc members are visited during garbage collection
- Replace `SelfKeepAlive` prevent-GC mechanism in Menu with a
`cppgc::Persistent` prevent-GC captured in `BindSelfToClosure`
- Introduce `GC_PLUGIN_IGNORE` macro to suppress
known-safe violations: mojo::Remote fields, ObjC bridging pointers,
and intentional persistent self-references
- Mark `ArgumentHolder` as `CPPGC_STACK_ALLOCATED()` in both Electron's
and gin's function_template.h to silence raw-pointer-to-GC-type
warnings
* feat: allow to set id and groupId
* feat: use Id's without hash but check length
* feat: adds visual grouping via groupTitle
* test: tests added for id, groupId and groupTitle
* fix: unused vars on Mac and Linux
* fix: remove redundant parameter
* fix: add doc links for id and group
* fix: throw if groupId is missing
* fix: test
fix: webContents.print() ignoring mediaSize when silent
PR #49523 moved the default media size fallback into OnGetDeviceNameToUse,
but the new code unconditionally writes kSettingMediaSize — clobbering
any mediaSize the caller had already set in WebContents::Print() from
options.mediaSize / pageSize. As a result, silent prints with an
explicit pageSize (e.g. "Letter") fell back to A4 with tiny content.
Only populate the default/printer media size when the caller hasn't
already supplied one, preserving the precedence:
1. user-supplied mediaSize / pageSize
2. printer default (when usePrinterDefaultPageSize is true)
3. A4 fallback
* fix: use proper OOPIF PDF check in `StreamsPrivateAPI`
* fix: add `ShouldEnableSubframeZoom` override to `ElectronBrowserClient` for upstream parity
* fix: add `MaybeOverrideLocalURLCrossOriginEmbedderPolicy` override to `ElectronBrowserClient` for upstream parity
* fix: add `DoesSiteRequireDedicatedProcess` override to `ElectronBrowserClient` for upstream parity
* style: move `DoesSiteRequireDedicatedProcess` to correct override section
* chore: remove window enlargement revert patch
Chromium removed the `window_enlargement_` system from
DesktopWindowTreeHostWin (1771dbae), which was a workaround for an AMD
driver bug from 2013 (crbug.com/286609) where translucent HWNDs smaller
than 64x64 caused graphical glitches. Chromium confirmed this is no
longer needed and shipped the removal.
This removes the revert patch and all Electron-side code that depended
on the `kEnableTransparentHwndEnlargement` feature flag, including the
`GetExpandedWindowSize` helper and max size constraint expansion in
`NativeWindow::GetContentMaximumSize`.
* test: remove obsolete <64x64 transparent window test
The test was added in 2018 (#12904) to verify the AMD driver
workaround that artificially enlarged translucent HWNDs smaller than
64x64 (crbug.com/286609). The workaround set the real HWND to 64x64
and subtracted a stored window_enlargement_ from every client/window
bounds query, so getContentSize() reported the originally-requested
size even though the actual HWND was larger.
With both the Chromium window_enlargement_ system and Electron's
GetExpandedWindowSize gone, setContentSize on a transparent
thickFrame window calls SetWindowPos directly. WS_THICKFRAME windows
are subject to DefWindowProc's MINMAXINFO.ptMinTrackSize clamp on
programmatic resizes (Chromium's OnGetMinMaxInfo ends with
SetMsgHandled(FALSE), so DefWindowProc overwrites the zeroed
min-track with system defaults), which on Windows Server 2025
floors at 32x39 — hence the failing [32, 39] vs [30, 30].
The removed feature_list.cc comment explicitly flagged this test as
the blocker for retiring kEnableTransparentHwndEnlargement, so
delete it alongside the workaround it was validating.
#47171 migrated `std::deque` to `base::circular_deque` in
`shell/common/crash_keys.cc`. However, `CrashKeyString` wraps a
`crashpad::Annotation` that holds self-referential pointers and
registers itself in a process-global linked list. `circular_deque`
relocates elements on growth (via `VectorBuffer::MoveConstructRange`),
leaving those pointers dangling — causing missing crash keys or a hung
crashpad handler (especially on macOS). The `base/containers/README.md`
warns: "Since `base::deque` does not have stable iterators and it will
move the objects it contains, it may not be appropriate for all uses."
Reverts to `std::deque`, whose block-based layout never relocates
existing elements. Adds a regression test that registers 50 dynamic
crash keys and verifies they all survive a renderer crash.
Notes: Fixed crash keys being lost and the crash reporter hanging on
macOS when many dynamic crash keys were registered.
Made-with: Cursor