Skip to content

fix(ios): relocate rest timer liveRegion off zero-size AX node (#565) - #567

Merged
9thLevelSoftware merged 2 commits into
mainfrom
fix/rest-timer-ax-uaf-565
Jun 17, 2026
Merged

fix(ios): relocate rest timer liveRegion off zero-size AX node (#565)#567
9thLevelSoftware merged 2 commits into
mainfrom
fix/rest-timer-ax-uaf-565

Conversation

@9thLevelSoftware

@9thLevelSoftware 9thLevelSoftware commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

Restores the standalone liveRegion Box in RestTimerCard.kt but changes its size from 0.dp to 1.dp (non-zero). This eliminates the zero-size AX node that triggered the iOS EXC_BAD_ACCESS crash while preserving the original announcement/focus behavior. Bounded to RestTimerCard.kt only.

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) dereferences node.scrollContentOffset without an isAlive/disposed guard; when iOS AXRuntime queries accessibilityFramebounds()contentOffset() on an element disposed during recomposition, the node.semanticsNode dereference hits freed memory → EXC_BAD_ACCESS (SIGSEGV) at 0x8.

The rest-between-sets screen triggered it because RestTimerCard.kt mounted a zero-size liveRegion = Polite Box whose contentDescription churned 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 Box size from 0.dp to 1.dp. A non-zero-size element:

  • Avoids the zero-size AX node disposal churn that raced iOS UIAccessibility.
  • Satisfies the RCA acceptance criteria: "an equivalent stable, non-zero-size composable that persists across ticks."
  • Preserves the original announcement/focus behavior — liveRegion + contentDescription = lastAnnouncedText stay on the separate hidden node, so the visible timer Text reads its actual displayed time when focused by a screen reader.

This is safer than relocating contentDescription onto the visible timer Text (the initial approach in commit af7d47c), which caused a screen-reader focus regression: focusing the timer read stale announcement text instead of the actual time. The 1.dp approach avoids both the crash and that regression.

CMP 1.10.3 → 1.11.1 upgrade already on main (commit 360b592) adds the upstream isAlive/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) relocated liveRegion + contentDescription = lastAnnouncedText onto the visible timer Text, 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 original 0.dp Box to 1.dp, keeping announcements on a separate non-zero-size node. The outdated comment block was removed as suggested.

Non-goals (per RCA)

  • Does not modify upstream CMP source (library dependency).
  • Does not remove accessibility/announcement support — VoiceOver/TalkBack functionality preserved.
  • Does not bundle a CMP version change (the 1.11.1 upgrade already landed separately on main).
  • Does not change rest-timer logic, pause/extend/skip controls, or announcement throttling.

Test plan

  • Local: :shared:testAndroidHostTest PASS (184 suites, 2058 tests, 0 failures) with -Pskip.supabase.check=true.
  • CI: all checks green.
  • Manual: TestFlight build on iOS 26.x with VoiceOver enabled, rest between sets, no EXC_BAD_ACCESS in accessibility bounds path.

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
Copilot AI review requested due to automatic review settings June 17, 2026 19:36
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@kilo-code-bot

kilo-code-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

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)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/RestTimerCard.kt
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)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/RestTimerCard.kt

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)
@9thLevelSoftware
9thLevelSoftware merged commit fa0fb93 into main Jun 17, 2026
10 checks passed
@9thLevelSoftware
9thLevelSoftware deleted the fix/rest-timer-ax-uaf-565 branch June 17, 2026 21:41
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.

[Bug] iOS TestFlight crash during rest between sets in Compose accessibility bounds

2 participants