Skip to content

[Bug]: webContents.debugger use-after-free: cppgc-managed Debugger never detaches its DevToolsAgentHostClient, crashing the browser process #53376

Closed as not planned

Description

@sethwebster

Preflight Checklist

Electron Version

41.5.0 (also reproduced on 43.4.1)

What operating system(s) are you using?

macOS

Operating System Version

macOS 26.6.2

What arch are you using?

arm64 (including Apple Silicon)

Last Known Working Electron version

Unknown — the affected code path was introduced with the cppgc/Oilpan migration of electron::api::Debugger

Expected Behavior

Attaching webContents.debugger to a WebContents (in our case, <webview> guest contents), receiving CDP notifications, and later letting the WebContents be destroyed should never crash the browser process, regardless of GC timing.

Actual Behavior

The browser process segfaults (EXC_BAD_ACCESS / KERN_INVALID_ADDRESS at 0x10/0x18 — a null/dangling pointer plus member offset) on CrBrowserMain. We collected three crashes in one day across two Electron versions (41.5.0 and 43.4.1), in two distinct but related signatures.

Signature A (2 of 3 crashes) — inbound CDP notification dispatched to a freed client (symbolicated from macOS .ips via breakpad symbols, Electron 41.5.0):

0  content::DevToolsSession::DispatchProtocolNotification(...)
1  blink::mojom::DevToolsSessionHostStubDispatch::Accept(...)
2  mojo::InterfaceEndpointClient::HandleIncomingMessageThunk::Accept(mojo::Message*)
3  mojo::MessageDispatcher::Accept(mojo::Message*)
4  mojo::InterfaceEndpointClient::HandleIncomingMessage(mojo::Message*)
5  IPC::ChannelAssociatedGroupController::AcceptOnEndpointThread(...)
6  base::TaskAnnotator::RunTaskImpl(base::PendingTask&)
7  base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork()
8  base::MessagePumpCFRunLoopBase::RunWorkSource(void*)
   ... CFRunLoop / NSApplication run loop ...

On 43.4.1 the same crash appears with frame 0 in content::DevToolsSession::DispatchProtocolResponseOrNotification(content::DevToolsAgentHostClient*, ...).

Signature B (1 of 3) — WebContents teardown force-detaches sessions into a freed client (Electron 41.5.0):

0  content::DevToolsAgentHostImpl::ForceDetachAllSessionsImpl()
1  content::RenderFrameDevToolsAgentHost::DestroyOnRenderFrameGone()
2  content::RenderFrameDevToolsAgentHost::FrameDeleted(...)
3  content::WebContentsImpl::WebContentsObserverList::NotifyObservers(...)
4  content::WebContentsImpl::OnFrameTreeNodeDestroyed(content::FrameTreeNode*)
5  content::FrameTree::Shutdown()
6  content::WebContentsImpl::~WebContentsImpl()
7  content::WebContentsImpl::WebContentsTreeNode::DestroyOwnedInnerWebContents(...)
8  content::WebContentsImpl::WebContentsTreeNode::DetachInnerWebContents(...)
9  content::WebContentsImpl::WebContentsTreeNode::OnFrameTreeNodeDestroyed(...)
10 content::FrameTreeNode::~FrameTreeNode()
11 content::RenderFrameHostImpl::ResetChildren()
12 content::FrameTree::Shutdown()
13 content::WebContentsImpl::~WebContentsImpl()
14 electron::InspectableWebContents::~InspectableWebContents()
15 electron::api::WebContents::~WebContents()
16 electron::api::WebContents::DeleteThisIfAlive()
17 base::TaskAnnotator::RunTaskImpl(base::PendingTask&)
   ... message pump ...

Testcase Gist URL

No response

Additional Information

Root-cause analysis (from reading the v41.5.0 / main sources; we do not have a minimal repro yet — the crash depends on GC timing, which matches its "random, tens of minutes apart" occurrence in production):

electron::api::Debugger is now a cppgc/Oilpan-managed gin::Wrappable, but it registers itself with content as a raw content::DevToolsAgentHostClient* via agent_host_->AttachClient(this):
https://github.com/electron/electron/blob/v41.5.0/shell/browser/api/electron_api_debugger.cc#L119

Its destructor never detaches that client:
https://github.com/electron/electron/blob/v41.5.0/shell/browser/api/electron_api_debugger.cc#L35

Debugger::~Debugger() = default;

DetachClient is only called from the explicit JS-facing Debugger::Detach():
https://github.com/electron/electron/blob/v41.5.0/shell/browser/api/electron_api_debugger.cc#L126-L131

The only strong root keeping an attached Debugger alive is the owning api::WebContents's member:
https://github.com/electron/electron/blob/v41.5.0/shell/browser/api/electron_api_web_contents.h#L783

cppgc::Persistent<api::Debugger> debugger_;

So once the api::WebContents wrapper goes away (e.g. <webview> guest destruction via the posted DeleteThisIfAlive() task, as in Signature B), nothing on the content side owns or pins the still-attached client. When cppgc sweeps the Debugger while DevToolsSession/DevToolsAgentHost still hold the raw client pointer:

  • the next CDP notification from the renderer dispatches into freed memory (Signature A — easy to hit with chatty domains like Network.enable / Log.enable / Page.startScreencast), or
  • a later WebContents-tree teardown reaches ForceDetachAllSessionsImpl(), which invokes client->AgentHostClosed() on freed memory (Signature B). Sweep order between the api::WebContents and its Debugger in the same GC cycle is unspecified, so the Debugger can be gone by the time the WebContents teardown runs.

The bug is still present on main — Debugger::~Debugger() = default;:
https://github.com/electron/electron/blob/main/shell/browser/api/electron_api_debugger.cc#L36

Before the cppgc migration this was masked because the wrappable's deterministic destruction paths differed; with Oilpan, destruction is decoupled from the last use, so the raw client pointer in content outlives the object.

Suggested fix: ensure the client is detached before the object can be reclaimed — e.g. call agent_host_->DetachClient(this) in ~Debugger() (mirroring Detach()), and/or detach in a WebContentsDestroyed() observer hook so the session never survives its client.

Context in which we hit this: our app attaches webContents.debugger.attach("1.3") to <webview> guest WebContents and enables Runtime/Network/Log/Accessibility (constant notification stream). Guests are routinely destroyed and recreated. Crashes appear minutes to tens of minutes later, aligned with GC pressure rather than any user action. Happy to provide the full .ips reports or symbolicated dumps on request.

Activity

  1. github-actions commented on Sep 1, 2026

    @github-actions
    Contributor

    Hello @sethwebster. Thanks for taking the time to open an issue and helping to make Electron better!

    This issue was automatically closed because its body does not follow any of this repository's issue templates. Maintainers rely on the sections in those templates to triage issues, and issues that are missing them usually cannot be acted upon.

    To get this issue triaged, please either:

  2. added theissue type on Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions