Skip to content

Harden Compose Desktop crash workarounds (macOS a11y + context-menu NPE)#228

Merged
sproctor merged 2 commits into
masterfrom
disable-mac-a11y-workaround
Jul 7, 2026
Merged

Harden Compose Desktop crash workarounds (macOS a11y + context-menu NPE)#228
sproctor merged 2 commits into
masterfrom
disable-mac-a11y-workaround

Conversation

@sproctor

@sproctor sproctor commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Two independent, unrecoverable upstream Compose Desktop crashes that reach production Sentry as fatal uncaught exceptions on the AWT event thread. Both are framework bugs with nothing for us to fix; the app should not die over them.

1. macOS a11y focus-target NPE (CMP-10170)

Reported on macOS (macbookair):

addFirst in unknown file
defaultAccessibilityFocusTarget in ComposeSceneAccessibility.kt [Line 169]
onAccessibleReceivedFocus in ComposeSceneAccessibility.kt [Line 86]
...
syncNodes in SemanticsOwnerAccessibility.kt

While retargeting a11y focus after a node is removed, defaultAccessibilityFocusTarget() walks the Accessible tree into an ArrayDeque, but getAccessibleChild() can return null and ArrayDeque rejects nulls (ComposeSceneAccessibility.kt:168-169). Still unguarded in compose-multiplatform 1.11.x.

We already swallow this in installUncaughtExceptionWorkaround, but the swallow is reactive: it only prevents process death. The crash happens mid syncNodes, so the a11y tree is left inconsistent and re-throws on the next focus change. It fires in practice only on macOS, whose a11y system aggressively queries AccessibleContext.

Fix: disable the Compose Desktop accessibility bridge on macOS by setting -Dcompose.accessibility.enable=false before the first window opens (Compose reads that system property lazily when it constructs the first ComposeSceneAccessibility). With the bridge off, accessibleContextProvider is null and the crashing focus-retarget path never runs.

  • macOS only; Windows/Linux keep working screen-reader support and don't hit this in practice.
  • An explicit -Dcompose.accessibility.enable=... on the command line is left untouched as an escape hatch.
  • The existing uncaught-exception swallow stays as the cross-platform safety net.

Trade-off: no VoiceOver support on macOS until the upstream fix lands, which beats a crash loop.

2. Context-menu arrow-key NPE

Reported separately (not macOS-specific):

invoke-ZmokQxo in BasicContextMenuRepresentation.skiko.kt [Line 89]
invoke in BasicContextMenuRepresentation.skiko.kt [Line 80]
onKeyEvent-ZmokQxo in CanvasLayersComposeScene.skiko.kt [Line 586]
...
keyPressed in ComposeSceneMediator.desktop.kt

Pressing an arrow key while the text context menu (right-click Copy/Paste popup) is open runs DefaultOpenContextMenu's onKeyEvent handler, which calls FocusManager.moveFocus to step between menu items; that traversal NPEs inside Compose's focus machinery. The crashing frame is BasicContextMenuRepresentation_skikoKt$DefaultOpenContextMenu$2$1 (the Function1<KeyEvent, Boolean> key handler). Not accessibility-related; still unguarded in 1.11.x.

Fix: extend isKnownComposeBug to also swallow NullPointerExceptions whose stack contains a BasicContextMenuRepresentation frame, alongside the existing a11y focus-target guard. (Frame-name matching is reliable because the desktop build never runs ProGuard.)

Testing

  • ./gradlew :desktopApp:ktlintCheck :desktopApp:compileKotlin pass.

🤖 Generated with Claude Code

sproctor and others added 2 commits July 7, 2026 17:06
The ComposeSceneAccessibility.defaultAccessibilityFocusTarget NPE
(CMP-10170) fires in practice only on macOS, whose a11y system keeps
querying AccessibleContext and so keeps driving the crashing
node-sync/focus-retarget path. The existing uncaught-exception swallow
only prevents process death; the a11y tree is left mid-sync and
re-throws on the next focus change.

Set -Dcompose.accessibility.enable=false on macOS before the first
window opens (Compose reads the flag lazily when it builds the first
ComposeSceneAccessibility) so the crash path never runs. An explicit
-Dcompose.accessibility.enable=... on the command line is left untouched
as an escape hatch. The swallow stays as the cross-platform safety net.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pressing an arrow key while the text context menu (the right-click
Copy/Paste popup) is open runs DefaultOpenContextMenu's onKeyEvent
handler, which calls FocusManager.moveFocus to step between menu items;
that traversal NPEs inside Compose's focus machinery, surfacing as an
uncaught exception on the AWT event thread with a
BasicContextMenuRepresentation frame. It is not accessibility-related
and not macOS-specific, and is still unguarded in
compose-multiplatform 1.11.x.

Extend isKnownComposeBug to also swallow NullPointerExceptions whose
stack contains a BasicContextMenuRepresentation frame, alongside the
existing a11y focus-target guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sproctor sproctor changed the title Disable Compose a11y bridge on macOS to dodge focus-target NPE Harden Compose Desktop crash workarounds (macOS a11y + context-menu NPE) Jul 7, 2026
@sproctor
sproctor merged commit e5a5640 into master Jul 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant