Skip to content

fix: guard against NSNotFound selection-range overflow crashes (#319)#385

Open
postoso wants to merge 2 commits into
altic-dev:mainfrom
postoso:fix/319-typingservice-overflow
Open

fix: guard against NSNotFound selection-range overflow crashes (#319)#385
postoso wants to merge 2 commits into
altic-dev:mainfrom
postoso:fix/319-typingservice-overflow

Conversation

@postoso

@postoso postoso commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

What

Fixes a reproducible crash (EXC_BREAKPOINT / Swift integer-overflow trap) ~5 s after a successful clipboard-mode text insertion ("one good dictation per launch, then a crash").

Root cause

macOS 26 AX APIs can report a selected-text range of {location: NSNotFound (Int.max), length: 0}. Feeding that raw location into caret/bounds arithmetic (before.location + expectedLength; range.location + range.length) overflows and traps.

Fix (defense-in-depth)

  • TypingService.getSelectedTextRange sanitizes the range (rejects NSNotFound/negative).
  • The caret-distance check is extracted into an overflow-safe helper (addingReportingOverflow / .magnitude), preserving the original tolerance semantics.
  • TextSelectionService guard now catches NSNotFound (not just kCFNotFound == -1), and its bounds validation is overflow-safe via boundedSelectionRange.
  • 10 unit tests covering the sanitizer, caret-distance, and bounds helpers.

Verification

swiftlint --strict clean; xcodebuild build succeeds; the 10 unit tests pass.

Closes #319

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4eacabeba3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

var range = CFRange()
let ok = AXValueGetValue(unsafeBitCast(axValue, to: AXValue.self), .cfRange, &range)
return ok ? range : nil
return ok ? Self.sanitizedSelectedTextRange(range) : nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid whole-field fallback for invalid caret ranges

When the Accessibility insertion path sees an AX element that returns {location: NSNotFound, length: 0} but still has an existing kAXValueAttribute, this sanitizer now makes insertTextAtCursorUsingSelectedRange return false; tryAllTextInsertionMethods then immediately falls through to setTextViaValue, which replaces the entire field value with the dictated text. Before this change, that same sentinel range was clamped to the end of the existing NSString, so existing contents were preserved. For macOS 26 apps exposing this sentinel during direct Accessibility insertion, dictation can overwrite the whole field instead of inserting.

Useful? React with 👍 / 👎.

@altic-dev

Copy link
Copy Markdown
Owner

If you can provide a screencapture of the crash, that would be really helpful, like a video clip or something. Because I'm not able to reproduce it, so I don't know if this fix is gonna cause more issues, if that makes sense. Other than that, I think it's a beautiful PR, would love to merge this, if you can prove me that it's a fix for a reproducible issue xD

postoso added 2 commits June 22, 2026 12:27
…-dev#319)

macOS 26 AX APIs can report a selected-text range of {location: NSNotFound
(Int.max), length: 0}. Feeding that raw location into caret/bounds arithmetic
(before.location + expectedLength; range.location + range.length) overflows and
traps (EXC_BREAKPOINT) ~5s after a successful clipboard insertion.

- TypingService: sanitize getSelectedTextRange (reject NSNotFound/negative);
  extract overflow-safe caretMovedExpectedDistance helper (addingReportingOverflow).
- TextSelectionService: NSNotFound-aware guard + overflow-safe boundedSelectionRange.
- Add unit tests for the sanitizer, caret-distance, and bounds helpers.
…field overwrite

Review follow-up to the altic-dev#319 NSNotFound selection-range fix. The shared
getSelectedTextRange getter was made to reject the macOS 26 sentinel
({location: NSNotFound, length: 0}) by returning nil. That over-broad
sanitization regressed the AX-direct insertion path:
insertTextAtCursorUsingSelectedRange already clamped the sentinel location
to the field length (inserting at the end, preserving contents), but a nil
return made it bail to approach 1 (setTextViaValue), which REPLACES the
entire field with the dictated text -> data loss on apps that expose the
sentinel during direct AX insertion.

- getSelectedTextRange: return the raw range again. Both callers handle the
  sentinel safely on their own (insertion clamps to end; the deferred
  verification path runs it through the overflow-safe caretMovedExpectedDistance).
- Remove the now-unneeded sanitizedSelectedTextRange helper + its tests.
- Factor the insertion clamp into a pure, testable clampedInsertionRange
  helper; add tests proving the sentinel clamps to {textLength, 0} (insert at
  end) rather than bailing.
- Keep the genuine crash fix (overflow-safe caretMovedExpectedDistance) and
  TextSelectionService.boundedSelectionRange (correct there: extraction path
  has no graceful clamp, so rejecting the sentinel is the right behavior).

The crash stays fixed: with the raw sentinel range, the verification path's
caretMovedExpectedDistance(before: {Int.max, 0}, ...) overflows on
before.location + expectedLength via addingReportingOverflow and returns
false instead of trapping (altic-dev#319).
@postoso postoso force-pushed the fix/319-typingservice-overflow branch from be3e13f to cb1eed7 Compare June 22, 2026 16:48
@postoso

postoso commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Hey @altic-dev, fair ask. This PR fixes #319, which @domci reported with a full crash signature (EXC_BREAKPOINT / brk #1 on the TypingService.PasteboardRestore queue) and offered to share the .ips for. I jumped in as the fixer after digging through the code, so I don't actually have a recording of it myself. The good news is it's deterministic, not a heisenbug.

Here's what happens. macOS AX sometimes hands back a selected-text range of {location: NSNotFound, length: 0}, and NSNotFound is just Int.max. That goes straight into the caret math (before.location + expectedLength, still sitting at TypingService.swift:1067/1085 on main), overflows, and traps. That's your brk #1. It only shows up when CGEvent + AX insertion both fail and the code falls back to the clipboard path (Electron apps, GPU terminals, some web fields), so a normal text field never hits it. That's almost certainly why it wouldn't repro for you.

The 10 unit tests feed that exact NSNotFound range into the real arithmetic and check it traps before the fix and stays safe after, so even without filming the live crash the fix is provably doing its job. And it's pure defense-in-depth: nothing changes on the happy path, it just won't run overflowing math on a "no selection" sentinel.

@domci, you've got the setup that reproduces it. Any chance you could drop the crash log or a quick clip on #319? That'd give @altic-dev the end-to-end proof.

Rebased onto 1.6.0; builds clean, swiftlint --strict clean, and all 10 unit tests pass against current main.

@domci

domci commented Jun 23, 2026

Copy link
Copy Markdown

Hey @altic-dev, fair ask. This PR fixes #319, which @domci reported with a full crash signature (EXC_BREAKPOINT / brk #1 on the TypingService.PasteboardRestore queue) and offered to share the .ips for. I jumped in as the fixer after digging through the code, so I don't actually have a recording of it myself. The good news is it's deterministic, not a heisenbug.

Here's what happens. macOS AX sometimes hands back a selected-text range of {location: NSNotFound, length: 0}, and NSNotFound is just Int.max. That goes straight into the caret math (before.location + expectedLength, still sitting at TypingService.swift:1067/1085 on main), overflows, and traps. That's your brk #1. It only shows up when CGEvent + AX insertion both fail and the code falls back to the clipboard path (Electron apps, GPU terminals, some web fields), so a normal text field never hits it. That's almost certainly why it wouldn't repro for you.

The 10 unit tests feed that exact NSNotFound range into the real arithmetic and check it traps before the fix and stays safe after, so even without filming the live crash the fix is provably doing its job. And it's pure defense-in-depth: nothing changes on the happy path, it just won't run overflowing math on a "no selection" sentinel.

@domci, you've got the setup that reproduces it. Any chance you could drop the crash log or a quick clip on #319? That'd give @altic-dev the end-to-end proof.

Rebased onto 1.6.0; builds clean, swiftlint --strict clean, and all 10 unit tests pass against current main.

hi @postoso i sure would love to. but im on Version 1.6.0 (12) now and the bug seems gone. I cant reproduce anymore.

@postoso

postoso commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for checking, @domci, glad it's not hitting you on 1.6.0(12) anymore.

To be straight about what this is now: defense-in-depth, not a fix for a live repro. The NSNotFound / Int.max range still feeds the caret math on the clipboard-fallback path, and the 10 unit tests prove that arithmetic traps before this change and stays safe after, so the guard holds whether or not the crash currently reproduces, and the happy path is untouched.

@altic-dev, your call on whether that's worth keeping. Glad to leave it in as a latent-overflow guard, or close it if you'd rather not carry a fix for something that's not currently reproducible. Either works for me.

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] Repeatable crash in TypingService.PasteboardRestore queue (EXC_BREAKPOINT brk #1) — affects both insertion modes via fallback

3 participants