[Filed by Copilot on behalf of @bghgary]
The Copilot desktop app's main-window WebView2 renderer terminates itself with STATUS_BREAKPOINT (0x80000003). The window goes blank and stays blank until manually refreshed, losing any open canvas panel. 22 confirmed occurrences on one machine between 2026-08-13 and 2026-08-20, currently running at roughly one death per 3.9 hours of working-hours app uptime.
Root cause: a CHECK in Blink's accessibility tree serializer
The renderer serializes its accessibility tree during the post-layout phase of every main frame, and this CHECK fails — third_party/blink/renderer/modules/accessibility/ax_block_flow_iterator.cc:184, in AXBlockFlowData::ComputeNeighborOnLine:
case FragmentItem::kText:
case FragmentItem::kGeneratedText:
if (!it->GetLayoutObject()) [[unlikely]] {
// A generated text fragment item may not have a backing LayoutObject.
// For regular text, we expect them to always have one.
CHECK_EQ(it->Type(), FragmentItem::kGeneratedText); // <-- fires
}
A fragment item of type kText is reached with a null LayoutObject. The code assumes only generated text can lack one. (Chromium's own TODO three lines below notes this area is not fully understood: "Investigate when an item can be text, but its LayoutObject is not marked as such.")
Identified from disassembly, not guessed: the faulting int3 has exactly one predecessor, cmp r15d,2 / jne; r15d is loaded as mov r15d,[r9+34h] / and r15d,7 and drives a 5-entry jump table, i.e. switch (it->Type()); and kGeneratedText == 2 per fragment_item.h. The other three fatal sites in the function are reached by different jumps to different traps.
The path is always backward (PreviousOnLineAsIndex, forward=false) in all 22 occurrences.
Symbolized with the matched private PDB from msdl (msedge.dll.pdb/938FFC9CD8D63BA84C4C44205044422E1); displacements on the load-bearing frames are small, so these are real names:
0a <int3> <- the CHECK
0b blink::AXBlockFlowIterator::PreviousOnLineAsIndex+0x25
0c blink::AXInlineTextBox::NeighboringOnLineWithAXBlockFlowIterator+0xec
0d blink::AXObject::SerializeLineAttributes+0xba
0e blink::AXObject::SerializeInlineTextBox+0x31
0f blink::AXObject::Serialize+0x279
10 ui::AXTreeSerializer<...>::SerializeChangedNodes+0x2ba
11..18 ui::AXTreeSerializer<...>::SerializeChangedNodes+0x5a9 (recursion x8)
19 ui::AXTreeSerializer<...>::SerializeChanges+0xec
1a blink::AXObjectCacheImpl::GetUpdatesAndEventsForSerialization+0x17a
1b blink::AXObjectCacheImpl::SerializeUpdatesAndEvents+0x14c
1c blink::AXObjectCacheImpl::SerializeAXUpdatesIfNeeded+0x21c
1d blink::LocalFrameView::ForAllNonThrottledLocalFrameViews+0x10c
1e blink::LocalFrameView::RunAccessibilitySteps+0x193
1f blink::LocalFrameView::RunPostLifecycleSteps+0x4e
20 blink::LocalFrameView::DidBeginMainFrame+0x19
21 blink::WebFrameWidgetImpl::DidBeginMainFrame+0x40
22 blink::WidgetBase::DidBeginMainFrame+0x58
23 cc::LayerTreeHost::DidBeginMainFrame+0x23
24 cc::ProxyMain::BeginMainFrame+0x548
... base::RunLoop::Run / content::RendererMain
This stack is identical across all 9 dumps on runtime .93 — 49 of 53 frames match exactly, recursion depth is exactly 8 every time, and the only variation is one frame differing by 256 bytes in 4 dumps and frames 10-12 shifting to adjacent call sites in 1 dump.
Recursion depth being fixed at 8 rather than varying suggests a fixed-shape AX subtree — the same element each time, not an arbitrarily deep DOM.
No assistive technology is running on this machine (no Narrator/NVDA/JAWS), and the renderer carries neither --force-renderer-accessibility nor --disable-renderer-accessibility. The app makes its own accessibility announcements (announced EVENT_OBJECT_FOCUS on web-content HWND after activation), which is the UIA/MSAA client interaction that switches renderer accessibility on. So an app that only announces focus is enough to enter this path.
The failing instruction
Read out of the minidumps, no private symbols required.
The faulting instruction is int3, and it is one of a run of ten int3; ud2 pairs:
E8 <rel32> CC 0F 0B CC 0F 0B CC 0F 0B CC 0F 0B CC 0F 0B
CC 0F 0B CC 0F 0B CC 0F 0B CC 0F 0B CC 0F 0B 41 83 FF 02
^^ faulting address = the 7th pair
int3; ud2 is IMMEDIATE_CRASH(). Ten contiguous pairs means ten distinct CHECK/NOTREACHED failure paths in one function. The single E8 in the window is __security_check_cookie (ordinary epilogue), not a check-failure handler — so nothing in this block logs anything. That is why --enable-logging --v=1, armed across five deaths, produced zero FATAL / Check failed lines, and why no dump carries that text in any encoding. Please don't ask for logs; there is nothing to capture.
The byte context is identical across all three runtime builds we have dumps for, with only the call displacement differing.
The crashing thread is CrRendererMain in all 13 dumps checked, across every build — never Compositor, the IO thread, or a pool thread.
Crash keys
| death (PDT) |
ModuleVersion |
ModuleOffset |
app |
| 08-13 12:17 |
151.0.4129.78 |
82219836 |
1.1.8 |
| 08-13 13:37 |
151.0.4129.78 |
82219836 |
1.1.8 |
| 08-14 08:42 |
151.0.4129.86 |
82310033 |
1.1.9 |
| 08-14 09:52 |
151.0.4129.86 |
82310033 |
1.1.9 |
| 08-14 14:22 |
151.0.4129.86 |
82310033 |
1.1.9 |
| 08-17 12:03 |
151.0.4129.86 |
82310033 |
1.1.10 |
| 08-17 17:09 |
151.0.4129.86 |
82310033 |
1.1.10 |
| 08-19 13:08 |
151.0.4129.93 |
82317919 |
1.1.10 |
| 08-20 09:26 |
151.0.4129.93 |
82317919 |
1.1.10 |
| 08-20 10:35 |
151.0.4129.93 |
82317919 |
1.1.10 |
| 08-20 12:27 |
151.0.4129.93 |
82317919 |
1.1.11 |
| 08-20 12:38 |
151.0.4129.93 |
82317919 |
1.1.11 |
| 08-20 14:26 |
151.0.4129.93 |
82317919 |
1.1.11 |
| 08-20 16:02 |
151.0.4129.93 |
82317919 |
1.1.11 |
ApplicationName=msedgewebview2.exe; ProcessType=renderer; SubCode=0x80000003; StackHash=0
Offsets are not comparable across module builds, but each build repeats its own offset exactly, and the exception address in the dump equals ModuleOffset precisely (0x4E8125F = 82317919). The failure has survived four app versions (1.1.8 → 1.1.11) and three runtime builds (.78 → .86 → .93); the offset tracks the runtime and is invariant to the app version.
What we ruled out
- Not time-driven. Renderer lifetime at death: 4.5 to 1278 minutes. Inter-arrival coefficient of variation is 0.85–0.90 against an exponential's predicted 1.0, so the hazard is constant — nothing accumulates and nothing ages.
- Not memory. Footprint at death 266–534 MB, while a healthy renderer peaked at 1435 MB and did not die; ~33 GB free.
- Not GPU/compositing. The crashing thread is always
CrRendererMain.
- Not back/forward cache. Tested directly: with
--disable-features=BackForwardCache verified active in the renderer's command line, two deaths occurred in 126 minutes at the same offset.
- Activity-dependent. The app ran continuously across a full weekend, machine never sleeping, and recorded zero deaths in 20 working-hours against 7.1 expected (P = 0.0008). All 22 confirmed deaths fall between 08:00 and 17:59 local. A renderer that is running but not being driven does not hit this check.
Two app-side defects that stand regardless of the root cause
1. No detection and no recovery. The app logs nothing at the failure — zero matches for ProcessFailed|RenderProcessGone|renderer.*(crash|gone|exit) across a full app log. WebView2's own Breadcrumbs records Tab1 RenderProcessGone and a following #reload, but the window stays blank for up to 29m44s. Observed recovery times span 0.4 s to 29m44s; we have not established whether the fast recoveries are automatic or simply the user refreshing promptly.
2. A session can be created without ever receiving its permission grants, and is then degraded for its entire life. Measured across 186 sessions: the sessions receiving zero session.permissions_changed events are exactly the sessions that raise permission prompts — 6 of 6, against 180 of 180 that never prompt — and each burns the full 600 s permission timeout per prompt. In healthy sessions grants arrive at a median of 0.02 s, so this is not slow delivery; it never happens, and it is never retried. A renderer death is one cause but not the only one — one such session followed a resume from sleep with no death nearby. A retry or repair of grant delivery for an already-created session would fix it, and capping the permission timeout for a session that has never received any grants would bound the damage.
What would help
This is a Blink bug, so the fix belongs in Chromium; no public Chromium issue appears to exist for it yet. Two things are in this app's hands regardless:
- Do not enable renderer accessibility merely because the embedder announces focus events. That is what puts this machine on the failing path, with no assistive technology running.
- Recover the view when
RenderProcessGone fires, instead of leaving a blank window until a human refreshes.
Minidumps for 14 captured deaths are retained and available on request.
Environment
- Copilot desktop app
github.exe 1.1.8 – 1.1.11; Copilot CLI 1.0.79
- WebView2 runtime 151.0.4129.78 → .86 → .93
- Windows 11 build 26310, 63.6 GB RAM
[Filed by Copilot on behalf of @bghgary]
The Copilot desktop app's main-window WebView2 renderer terminates itself with
STATUS_BREAKPOINT(0x80000003). The window goes blank and stays blank until manually refreshed, losing any open canvas panel. 22 confirmed occurrences on one machine between 2026-08-13 and 2026-08-20, currently running at roughly one death per 3.9 hours of working-hours app uptime.Root cause: a
CHECKin Blink's accessibility tree serializerThe renderer serializes its accessibility tree during the post-layout phase of every main frame, and this
CHECKfails —third_party/blink/renderer/modules/accessibility/ax_block_flow_iterator.cc:184, inAXBlockFlowData::ComputeNeighborOnLine:A fragment item of type
kTextis reached with a nullLayoutObject. The code assumes only generated text can lack one. (Chromium's ownTODOthree lines below notes this area is not fully understood: "Investigate when an item can be text, but its LayoutObject is not marked as such.")Identified from disassembly, not guessed: the faulting
int3has exactly one predecessor,cmp r15d,2 / jne;r15dis loaded asmov r15d,[r9+34h] / and r15d,7and drives a 5-entry jump table, i.e.switch (it->Type()); andkGeneratedText == 2perfragment_item.h. The other three fatal sites in the function are reached by different jumps to different traps.The path is always backward (
PreviousOnLineAsIndex,forward=false) in all 22 occurrences.Symbolized with the matched private PDB from msdl (
msedge.dll.pdb/938FFC9CD8D63BA84C4C44205044422E1); displacements on the load-bearing frames are small, so these are real names:This stack is identical across all 9 dumps on runtime
.93— 49 of 53 frames match exactly, recursion depth is exactly 8 every time, and the only variation is one frame differing by 256 bytes in 4 dumps and frames 10-12 shifting to adjacent call sites in 1 dump.Recursion depth being fixed at 8 rather than varying suggests a fixed-shape AX subtree — the same element each time, not an arbitrarily deep DOM.
No assistive technology is running on this machine (no Narrator/NVDA/JAWS), and the renderer carries neither
--force-renderer-accessibilitynor--disable-renderer-accessibility. The app makes its own accessibility announcements (announced EVENT_OBJECT_FOCUS on web-content HWND after activation), which is the UIA/MSAA client interaction that switches renderer accessibility on. So an app that only announces focus is enough to enter this path.The failing instruction
Read out of the minidumps, no private symbols required.
The faulting instruction is
int3, and it is one of a run of tenint3; ud2pairs:int3; ud2isIMMEDIATE_CRASH(). Ten contiguous pairs means ten distinctCHECK/NOTREACHEDfailure paths in one function. The singleE8in the window is__security_check_cookie(ordinary epilogue), not a check-failure handler — so nothing in this block logs anything. That is why--enable-logging --v=1, armed across five deaths, produced zeroFATAL/Check failedlines, and why no dump carries that text in any encoding. Please don't ask for logs; there is nothing to capture.The byte context is identical across all three runtime builds we have dumps for, with only the
calldisplacement differing.The crashing thread is
CrRendererMainin all 13 dumps checked, across every build — neverCompositor, the IO thread, or a pool thread.Crash keys
ApplicationName=msedgewebview2.exe; ProcessType=renderer; SubCode=0x80000003; StackHash=0Offsets are not comparable across module builds, but each build repeats its own offset exactly, and the exception address in the dump equals
ModuleOffsetprecisely (0x4E8125F= 82317919). The failure has survived four app versions (1.1.8 → 1.1.11) and three runtime builds (.78 → .86 → .93); the offset tracks the runtime and is invariant to the app version.What we ruled out
CrRendererMain.--disable-features=BackForwardCacheverified active in the renderer's command line, two deaths occurred in 126 minutes at the same offset.Two app-side defects that stand regardless of the root cause
1. No detection and no recovery. The app logs nothing at the failure — zero matches for
ProcessFailed|RenderProcessGone|renderer.*(crash|gone|exit)across a full app log. WebView2's ownBreadcrumbsrecordsTab1 RenderProcessGoneand a following#reload, but the window stays blank for up to 29m44s. Observed recovery times span 0.4 s to 29m44s; we have not established whether the fast recoveries are automatic or simply the user refreshing promptly.2. A session can be created without ever receiving its permission grants, and is then degraded for its entire life. Measured across 186 sessions: the sessions receiving zero
session.permissions_changedevents are exactly the sessions that raise permission prompts — 6 of 6, against 180 of 180 that never prompt — and each burns the full 600 s permission timeout per prompt. In healthy sessions grants arrive at a median of 0.02 s, so this is not slow delivery; it never happens, and it is never retried. A renderer death is one cause but not the only one — one such session followed a resume from sleep with no death nearby. A retry or repair of grant delivery for an already-created session would fix it, and capping the permission timeout for a session that has never received any grants would bound the damage.What would help
This is a Blink bug, so the fix belongs in Chromium; no public Chromium issue appears to exist for it yet. Two things are in this app's hands regardless:
RenderProcessGonefires, instead of leaving a blank window until a human refreshes.Minidumps for 14 captured deaths are retained and available on request.
Environment
github.exe1.1.8 – 1.1.11; Copilot CLI 1.0.79