fix(ios): relocate rest timer liveRegion off zero-size AX node (#565) - #567
Conversation
Remove the standalone 0.dp Box with liveRegion=Polite semantics in RestTimerCard and attach liveRegion=Polite + contentDescription= lastAnnouncedText onto the visible timer Text instead. The zero-size liveRegion node increased accessibility-element disposal churn during the 1Hz rest-timer recomposition and raced iOS UIAccessibility's AX-tree poll, triggering EXC_BAD_ACCESS (SIGSEGV) in Compose Multiplatform's AccessibilityElement.contentOffset/bounds path (Accessibility.uikit.kt:725-726) when AXRuntime queried accessibilityFrame on a disposed element. Relocating the semantics onto a stable, visible, non-zero-size element that persists across ticks eliminates the zero-size AX node while preserving the throttled VoiceOver/TalkBack countdown announcements at 5s/10s/0s/paused intervals (the LaunchedEffect/lastAnnouncedText logic is unchanged). Bounded to RestTimerCard.kt only. Complements the CMP 1.10.3 -> 1.11.1 upgrade already on main (upstream JetBrains/compose-multiplatform#5543). RCA: GPT-5.5 xhigh (issuecomment-4734439771). Fixes #565
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Code Review
This pull request refactors the accessibility announcement logic in RestTimerCard by removing a zero-size Box (which was causing iOS crashes) and moving the liveRegion semantics to the visible timer Text. Feedback points out that this relocation causes an accessibility issue where focusing on the timer reads stale announcement text instead of the actual time. The reviewer suggests restoring a separate announcement Box with a safe size of 1.dp to avoid both the iOS crash and the stale focus issue, and cleaning up the associated comments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Code Review Roast 🔥Verdict: No Issues Found | Recommendation: Merge Oh wait, this PR is actually clean. I need to sit down. I had my flamethrower warmed up and everything. 📊 Overall: Like finding a unicorn in production — I didn't think clean PRs existed anymore, but here we are. Files Reviewed (1 file)
Previous Review Summary (commit af7d47c)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit af7d47c)Verdict: No Issues Found | Recommendation: Merge Oh wait, this PR is actually clean. I need to sit down. I had my flamethrower warmed up and everything. 📊 Overall: Like finding a unicorn in production — I didn't think clean PRs existed anymore, but here we are. Files Reviewed (1 file)
Reviewed by step-3.7-flash-20260528 · 156,144 tokens |
…to timer Text (#565) Follow-up addressing Gemini Code Assist review on PR #567: relocating contentDescription = lastAnnouncedText onto the visible timer Text caused a screen-reader focus regression — focusing the timer read stale announcement text (e.g. "20 seconds remaining") instead of the actual displayed time. Restore the separate liveRegion Box but at 1.dp (non-zero) instead of 0.dp. This satisfies the RCA acceptance criteria ("an equivalent stable, non-zero- size composable that persists across ticks") and avoids both the zero-size AX node crash (issue #565) and the stale-focus regression. Safe given CMP 1.11.1 isAlive guard already on main. The visible timer Text no longer carries contentDescription, so it reads its actual time when focused by VoiceOver/TalkBack. Local: :shared:testAndroidHostTest PASS (184 suites, 2058 tests, 0 failures)
Summary
Restores the standalone liveRegion
BoxinRestTimerCard.ktbut changes its size from0.dpto1.dp(non-zero). This eliminates the zero-size AX node that triggered the iOSEXC_BAD_ACCESScrash while preserving the original announcement/focus behavior. Bounded toRestTimerCard.ktonly.Fixes #565
Root cause (from GPT-5.5 xhigh RCA — issuecomment-4734439771)
Use-after-free in the Compose Multiplatform iOS accessibility bridge.
AccessibilityElement.contentOffset()(Accessibility.uikit.kt:726) dereferencesnode.scrollContentOffsetwithout anisAlive/disposed guard; when iOSAXRuntimequeriesaccessibilityFrame→bounds()→contentOffset()on an element disposed during recomposition, thenode.semanticsNodedereference hits freed memory →EXC_BAD_ACCESS (SIGSEGV)at0x8.The rest-between-sets screen triggered it because
RestTimerCard.ktmounted a zero-sizeliveRegion = PoliteBoxwhosecontentDescriptionchurned during the 1 Hz rest-timer recomposition, racing iOS UIAccessibility's AX-tree poll (upstream match: JetBrains/compose-multiplatform#5543, ~83% on iOS 26.x).Fix
Change the announcement
Boxsize from0.dpto1.dp. A non-zero-size element:liveRegion+contentDescription = lastAnnouncedTextstay on the separate hidden node, so the visible timerTextreads its actual displayed time when focused by a screen reader.This is safer than relocating
contentDescriptiononto the visible timerText(the initial approach in commitaf7d47c), which caused a screen-reader focus regression: focusing the timer read stale announcement text instead of the actual time. The1.dpapproach avoids both the crash and that regression.CMP
1.10.3 → 1.11.1upgrade already onmain(commit360b592) adds the upstreamisAlive/hit-test guard (PR #2760), providing an additional layer of protection against the underlying UAF.Follow-up commit (e1b0c35)
Addresses Gemini Code Assist review feedback: the initial commit (
af7d47c) relocatedliveRegion+contentDescription = lastAnnouncedTextonto the visible timerText, which caused a screen-reader focus regression (stale announcement text read on focus instead of actual time). This follow-up reverts that relocation and instead changes the original0.dpBox to1.dp, keeping announcements on a separate non-zero-size node. The outdated comment block was removed as suggested.Non-goals (per RCA)
main).Test plan
:shared:testAndroidHostTestPASS (184 suites, 2058 tests, 0 failures) with-Pskip.supabase.check=true.EXC_BAD_ACCESSin accessibility bounds path.