MarshallOfSound

#52847: fix: keep remote api::WebContents alive while their content::WebContents lives

Merged
Created: Aug 16, 2026, 5:19:41 AM
Merged: Aug 17, 2026, 6:43:20 AM
4 comments
Target: main

Description of Change

Fixes #52158, the recurring SIGSEGV in WebContents::DevToolsOpened() on the macOS CI shards (same bug as #29890 / #34042 / #47187).

The api::WebContents for the DevTools content::WebContents is created up front in InspectableWebContents::ShowDevTools and the handle is dropped. It's a kRemote WebContents so nothing pins it, and nothing references its wrapper again until the frontend has loaded and DevToolsOpened() stashes it in devtools_web_contents_. If a GC lands in between, the wrapper is collected but the C++ object is still findable via From() until the deferred delete runs, and FromOrCreate() hands DevToolsOpened() an empty handle.

openDevTools()                                                devtools frontend "loadCompleted"
  |                                                                          |
  ShowDevTools: FromOrCreate(devtools wc)  ->  api::WebContents #2           LoadCompleted -> DevToolsOpened
  |             (handle dropped, wrapper only weakly held)                    |  FromOrCreate(devtools wc)
  |                                                                          |
  |            GC: wrapper of #2 collected                                    |
  |            FirstWeakCallback: dead_ = true, wrapper_.Reset()              |
  |            (delete is posted, From() still finds #2)                     |
  |                  |                                                        |
  |                  +--- delete task runs before loadCompleted  ------------>|  From() finds nothing, creates #3
  |                  |                                                        |  (web-contents-created fires twice,
  |                  |                                                        |   devToolsWebContents.id != #2)
  |                  |                                                        |
  |                  +--- loadCompleted arrives before the delete  ---------->|  From() returns zombie #2
                                                                             |  GetWrapper() empty -> handle.get() == nullptr
                                                                             |  handle->owner_window() => SEGV_ACCERR
                                                                             |  (0x1e0 on main, 0x1d0 on 42-x-y = offsetof(owner_window_))
  • Fix: Pin() remote api::WebContents in the constructor and Unpin() in WebContentsDestroyed(), so a remote api::WebContents lives exactly as long as its content::WebContents. Same Pinnable mechanism background pages already use, no new v8::Global.
  • Regression spec forces a major GC between openDevTools() and devtools-opened and asserts exactly one DevTools api::WebContents was created and it's the one on devToolsWebContents. Fails on the pre-fix build (expected [ 2, 3 ] to deeply equal [ 3 ]), passes with the fix.
  • #52166 wasn't the fix for this, it protects the inspected api::WebContents. Here that one is alive and the DevTools one is the zombie; the crash reproduced on 42-x-y with #52511 present.
History of api::WebContents lifetime management (for the curious)
  • Pre gin (mate::TrackableObject): weak wrapper + a per class weak map. Owned WebContents were kept alive by their window / webview, everything else was collectable whenever JS dropped it.
  • #24651 (2020) ginified WebContents, added the UserDataLink WeakPtr so From() can find the api object from a content::WebContents. Still nothing strong for remotes.
  • #23128 (2021-04) removed the RenderProcessReady backstop and started eagerly creating api::WebContents for DevTools / extension hosts / mime guests. This is where "created early, referenced late" starts, #29890 shows up two months later.
  • #30030 (2021-07, ccb16e2921) pinned background page api::WebContents in InitWithExtensionView for exactly this reason ("live until the underlying content::WebContents is destroyed"). kRemote returns early right above that Pin, so it was left out. That's the only Pin WebContents ever had, and there was never an Unpin().
  • #46389 (2025) blamed a dangling owner_window_ WeakPtr, closed. #47243 added the DCHECKs in DevToolsOpened (the DCHECK_EQ(handle->owner_window(), nullptr) is now the line we crash on in CI builds). #49406 / #51420 / #52166 all guard the inspected WebContents being freed, which is a different bug.
  • Why it's timing dependent: V8 only runs second pass phantom callbacks synchronously for forced GCs, for a natural GC they're a posted task, so the zombie window has been at least one task since 2021. #50688 (2026-04) added DeleteSoon on top and widened it by another task, which lines up with #52158 showing up in June.

Checklist

  • I have built and tested this change
  • I have filled out the PR description
  • npm test passes (webContents module, chrome extensions, session module, webFrameMain module, BrowserWindow module devtools, MenuItems: 558 ok / 0 failing locally)
  • tests are changed or added

Release Notes

Notes: Fixed a rare crash in the main process when DevTools were opened and a garbage collection ran before the DevTools frontend finished loading.

Backports

42-x-y
Pending
Waiting for a manual backport
43-x-y
Merged
PR Number
#52903
Merged At
Aug 17, 2026, 8:06:43 AM
Released In
Not yet
Release Date
Not yet
44-x-y
Merged
PR Number
#52904
Merged At
Aug 17, 2026, 8:04:06 AM
Released In
v44.0.0-beta.5
Release Date
Aug 17, 2026, 3:01:12 PM

Semver Impact

Major
Breaking changes
Minor
New features
Patch
Bug fixes
None
Docs, tests, etc.

Semantic Versioning helps users understand the impact of updates:

  • Major (X.y.z): Breaking changes that may require code modifications
  • Minor (x.Y.z): New features that maintain backward compatibility
  • Patch (x.y.Z): Bug fixes that don't change the API
  • None: Changes that don't affect using facing parts of Electron