Skip to content

Fix TextBox IME replacement range crash#5346

Open
lawrencecchen wants to merge 3 commits into
mainfrom
task-sentry-textbox-nsrangeexception
Open

Fix TextBox IME replacement range crash#5346
lawrencecchen wants to merge 3 commits into
mainfrom
task-sentry-textbox-nsrangeexception

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Clamp TextBox NSTextInput replacement ranges to current UTF-16 storage bounds before calling NSTextView.insertText/setMarkedText.
  • Add a regression test for a stale IME replacement range at the Japanese text UTF-16 end boundary.

Testing

  • scripts/lint-pbxproj-test-wiring.sh: pass
  • Remote AWS cmux-unit red/green attempt: invalid, runner ran out of disk during app compilation before executing the focused test.

Issues


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Low Risk
Localized defensive bounds checks on text input paths; regression test added; no auth or data-layer changes.

Overview
Fixes a crash when IME (e.g. Japanese input) passes replacement or marked-text ranges that extend past the current UTF-16 text storage—such as a stale range at the end of 日本語.

TextBoxInputTextView now clamps replacementRange and selectedRange to valid bounds before calling insertText / setMarkedText, using the attributed string length (or the incoming marked string length for selection). Attachment cleanup and marked-text callbacks use the same clamped ranges.

A unit test covers inserting with an out-of-range replacement at the UTF-16 end boundary.

Reviewed by Cursor Bugbot for commit 1c81399. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Clamp and normalize IME replacement and selection ranges in TextBoxInputTextView to valid UTF-16 bounds to prevent NSRange crashes and ensure correct caret placement. Fixes Sentry CMUXTERM-MACOS-VD0.

  • Bug Fixes
    • Clamp insertText/setMarkedText replacementRange to the current text length; sanitize selectedRange within marked text, defaulting to the end when NSNotFound.
    • Add tests for clamping a stale IME range at the Japanese end boundary and for normalizing a missing selectedRange.

Written for commit 57e092f. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Prevented out-of-bounds ranges during input-method (IME) interactions by sanitizing and clamping replacement/selection ranges, improving stability for international input.
    • Ensured marked-text handling no longer propagates invalid ranges, fixing cursor/selection anomalies.
  • Tests

    • Added tests covering insertion and marked-text scenarios with international characters and IME edge cases.

@vercel

vercel Bot commented Jun 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 4, 2026 9:31am
cmux-staging Ready Ready Preview, Comment Jun 4, 2026 9:31am

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

TextBoxInputTextView now clamps/sanitizes NSRange inputs in insertText and setMarkedText using new private helpers to avoid out-of-bounds ranges during IME/marked-text interactions. Tests validate UTF-16-boundary insertion and normalization of NSNotFound ranges for marked text.

Changes

Range Sanitization for IME Text Input

Layer / File(s) Summary
Range sanitization helper utilities
Sources/TextBoxInput.swift
Private helpers introduced (sanitizedRange/sanitizedTextStorageReplacementRange/textInputStringLength) to clamp NSRange values and compute marked-text lengths from Any inputs.
Sanitized insertText and setMarkedText
Sources/TextBoxInput.swift
insertText sanitizes replacementRange before queuing attachment cleanup and delegating to super; setMarkedText computes marked-text length, sanitizes selectedRange and clamps replacementRange to text storage length before calling super.
IME boundary and marked-text tests
cmuxTests/TextBoxMentionCompletionTests.swift
Two tests: one asserts insertText(_:replacementRange:) clamps a stale IME replacement range at a UTF-16 end and advances selectedRange; the other asserts setMarkedText handles NSNotFound for selectedRange/replacementRange by normalizing to the marked-text end.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#5340: Related IME/marked-text changes touching TextBoxInputTextView insertion/marked-text handling.

Poem

🐰 I hop through UTF‑16 and tidy each range,
Clamp wandering indices, restore every change.
Marked text settles, IME hums a tune,
Small fixes by moonlight, landed by noon.
🥕


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Full Internationalization ❌ Error PR adds 8 new user-facing strings using copied English as placeholders for 18 of 20 locales, violating the rule prohibiting "placeholder, copied English" translations. Replace copied English placeholder translations in Localizable.xcstrings with proper translations for all 20 supported locales before merging.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (16 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PR adds helper methods to TextBoxInputTextView (NSTextView subclass inheriting MainActor) and tests with @MainActor. No shared mutable Sendable types or implicit MainActor models introduced.
Cmux Swift Blocking Runtime ✅ Passed PR changes to insertText/setMarkedText contain only synchronous range sanitization with no blocking/timing synchronization patterns, and tests are deterministic.
Cmux No Hacky Sleeps ✅ Passed Check not applicable: PR only modifies Swift files. Rule scope is non-Swift (TypeScript, JavaScript, shell); Swift blocking covered by "swift-blocking-runtime" check.
Cmux Algorithmic Complexity ✅ Passed New code uses only O(1) operations: range arithmetic, property access, type checks. No loops or collection scans over scalable data. IME callbacks execute once per keystroke, not in batch loops.
Cmux Swift Concurrency ✅ Passed Changes introduce only synchronous NSRange sanitization helpers for AppKit IME handling—no legacy async patterns introduced.
Cmux Swift @Concurrent ✅ Passed PR changes are synchronous NSTextView method overrides for range clamping during IME input, with no async/await, @concurrent, or problematic actor isolation patterns.
Cmux Swift File And Package Boundaries ✅ Passed Focused bug fix adding 9 net lines to existing 5924-line TextBoxInputTextView file for IME range sanitization; preserves extraction path and is appropriate AppKit-glue code for app target.
Cmux Swift Logging ✅ Passed No forbidden logging (print, debugPrint, dump, NSLog) in production code or tests. All changes are range sanitization helpers and regression tests with no logging violations.
Cmux User-Facing Error Privacy ✅ Passed PR adds internal range-clamping logic with no user-facing error messages, alerts, or privacy violations disclosed.
Cmux Swiftui State Layout ✅ Passed Changes confined to TextBoxInputTextView (NSTextView subclass). No new @State/@Published/@observable. Private NSRange utility functions only. Complies with AppKit bridge view exception.
Cmux Architecture Rethink ✅ Passed PR adds pure range-sanitization helpers and applies them to NSTextView overrides without timing delays, locks, observers, or split lifecycle ownership. Clear owner, named invariant, test-driven fix.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed TextBoxMentionCompletionPanel is a non-key popover with cmux.* identifier, explicitly documented in lint script ignore list with rationale.
Title check ✅ Passed The title directly and concisely summarizes the main fix: clamping IME replacement ranges to prevent crashes in TextBox input handling.
Description check ✅ Passed The PR description addresses the template's Summary and Testing sections with specifics, but lacks Demo Video section, Review Trigger block, and incomplete Checklist.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-sentry-textbox-nsrangeexception

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds defensive UTF-16 bounds clamping to TextBoxInputTextView's insertText and setMarkedText overrides, fixing a crash (Sentry CMUXTERM-MACOS-VD0) where a stale IME replacement range with a nonzero length at the end of the text storage triggered an NSRange out-of-bounds trap.

  • sanitizedTextStorageReplacementRange clamps any non-NSNotFound replacementRange to [0, attributedString().length] before it reaches super.insertText or super.setMarkedText, preventing the crash.
  • sanitizedMarkedTextSelectionRange clamps selectedRange against the incoming marked-string length (correct per the NSTextInputClient spec), defaulting to end-of-marked-text when NSNotFound is received.
  • Two regression tests are added: one covering stale insertText ranges at the UTF-16 end boundary, and a second covering setMarkedText with an NSNotFound selectedRange.

Confidence Score: 5/5

Safe to merge — the change is a narrow defensive clamp on two well-understood NSTextView override sites with no effect on non-IME input paths.

The fix is a tightly scoped bounds guard: three private helpers with no external surface, applied only at the two NSTextInputClient entry points that the Sentry crash implicated. The sanitization logic is straightforward and correct — NSNotFound ranges are passed through or defaulted per the NSTextInputClient spec, and all arithmetic correctly constrains both location and length. Two regression tests verify the primary crash scenario and the NSNotFound selectedRange fallback. No concurrency, state, or lifecycle changes are involved.

No files require special attention.

Important Files Changed

Filename Overview
Sources/TextBoxInput.swift Adds three private helpers to clamp IME replacement and selection ranges; insertText and setMarkedText overrides now sanitize ranges before forwarding to super, preventing NSRange crashes.
cmuxTests/TextBoxMentionCompletionTests.swift Adds two regression tests: one for stale insertText ranges at the UTF-16 end of "日本語", and one for setMarkedText with an NSNotFound selectedRange; both verify correct caret placement after sanitization.

Sequence Diagram

sequenceDiagram
    participant IME as IME (e.g. Japanese)
    participant TV as TextBoxInputTextView
    participant SH as sanitizedTextStorageReplacementRange
    participant SM as sanitizedMarkedTextSelectionRange
    participant NS as NSTextView (super)

    IME->>TV: insertText(string, staleRange)
    TV->>SH: clamp(staleRange, upperBound: attributedString().length)
    SH-->>TV: safeRange
    TV->>TV: queueAutomaticAttachmentFileCleanup(safeRange)
    TV->>NS: super.insertText(string, safeRange)
    NS-->>TV: text inserted
    TV->>TV: flushAutomaticAttachmentFileCleanup()
    TV->>TV: onMarkedTextStateChanged()

    IME->>TV: setMarkedText(string, selectedRange, replacementRange)
    TV->>TV: textInputStringLength(string) → markedLen
    TV->>SM: clamp(selectedRange, markedTextLength: markedLen)
    SM-->>TV: safeSelectedRange
    TV->>SH: clamp(replacementRange, upperBound: attributedString().length)
    SH-->>TV: safeReplacementRange
    TV->>NS: super.setMarkedText(string, safeSelectedRange, safeReplacementRange)
    NS-->>TV: marked text set
    TV->>TV: onMarkedTextStateChanged()
Loading

Reviews (2): Last reviewed commit: "Normalize TextBox marked selection range" | Re-trigger Greptile

Comment thread Sources/TextBoxInput.swift Outdated
super.setMarkedText(string, selectedRange: selectedRange, replacementRange: replacementRange)
super.setMarkedText(
string,
selectedRange: Self.sanitizedRange(selectedRange, upperBound: Self.textInputStringLength(string)),

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.

P2 selectedRange in setMarkedText is not covered by a regression test

selectedRange here is sanitized against the incoming marked-text string length (textInputStringLength(string)), which is the correct upper bound per the NSTextInputClient spec (the selection lives within the new marked string, not the text storage). However, there is no test that drives a stale or out-of-bounds selectedRange through setMarkedText to prove this branch works. The existing test only exercises insertText. A stale selectedRange from a multi-step IME composition sequence (e.g. selecting a different candidate after the replacement range was already consumed) would silently fall into this path and a future refactor could break it without a test to catch it.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +3678 to +3684
private static func textInputStringLength(_ string: Any) -> Int {
if let attributed = string as? NSAttributedString {
return attributed.length
}
let plain = (string as? String) ?? String(describing: string)
return (plain as NSString).length
}

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.

P2 For an unrecognised input type, String(describing:) returns a debug description (e.g. "<NSFoo: 0x…>"), so the resulting length is the character count of that label rather than any meaningful text length. Since this function is the defensive guard against malformed IME input, returning 0 for the unexpected branch is safer — it collapses the selection to an empty range rather than clamping against an arbitrary number.

Suggested change
private static func textInputStringLength(_ string: Any) -> Int {
if let attributed = string as? NSAttributedString {
return attributed.length
}
let plain = (string as? String) ?? String(describing: string)
return (plain as NSString).length
}
private static func textInputStringLength(_ string: Any) -> Int {
if let attributed = string as? NSAttributedString {
return attributed.length
}
if let plain = string as? String {
return (plain as NSString).length
}
return 0
}

coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 4, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/TextBoxInput.swift`:
- Around line 3647-3650: The call to super.setMarkedText is passing NSNotFound
through as the selectedRange which can produce an invalid marked-selection;
update the selectedRange passed into Self.sanitizedRange in the setMarkedText
path so that if selectedRange == NSNotFound it is normalized to the caret-at-end
index (use Self.textInputStringLength(string) as the upperBound) when the marked
buffer is non-empty (mirroring the logic in GhosttyNSView+IMEComposition.swift);
apply the same normalization to the second occurrence of setMarkedText around
the 3670–3675 region so both call sites use the sanitized/normalized
selectedRange before calling super.setMarkedText (refer to methods:
setMarkedText, Self.sanitizedRange, Self.textInputStringLength,
sanitizedTextStorageReplacementRange).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 581f600f-de5d-40e2-a075-392152f421f3

📥 Commits

Reviewing files that changed from the base of the PR and between ff172b0 and 1c81399.

📒 Files selected for processing (2)
  • Sources/TextBoxInput.swift
  • cmuxTests/TextBoxMentionCompletionTests.swift

Comment on lines +3647 to +3650
super.setMarkedText(
string,
selectedRange: Self.sanitizedRange(selectedRange, upperBound: Self.textInputStringLength(string)),
replacementRange: sanitizedTextStorageReplacementRange(replacementRange)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize selectedRange == NSNotFound in the marked-text path.

Line 3649 still lets NSNotFound pass through unchanged. In Sources/GhosttyNSView+IMEComposition.swift:11-23, this repo already treats that case as “caret at end of marked text” when the marked buffer is non-empty; keeping NSNotFound here leaves super.setMarkedText with an invalid marked-selection range on the same IME path this patch is hardening.

Suggested fix
-        super.setMarkedText(
-            string,
-            selectedRange: Self.sanitizedRange(selectedRange, upperBound: Self.textInputStringLength(string)),
-            replacementRange: sanitizedTextStorageReplacementRange(replacementRange)
-        )
+        super.setMarkedText(
+            string,
+            selectedRange: Self.sanitizedMarkedSelectionRange(
+                selectedRange,
+                upperBound: Self.textInputStringLength(string)
+            ),
+            replacementRange: sanitizedTextStorageReplacementRange(replacementRange)
+        )
         onMarkedTextStateChanged(hasMarkedText())
     }

@@
-    private static func sanitizedRange(_ range: NSRange, upperBound: Int) -> NSRange {
-        guard range.location != NSNotFound else { return range }
+    private static func sanitizedMarkedSelectionRange(_ range: NSRange, upperBound: Int) -> NSRange {
         let upperBound = max(0, upperBound)
+        guard range.location != NSNotFound else {
+            return upperBound > 0
+                ? NSRange(location: upperBound, length: 0)
+                : NSRange(location: NSNotFound, length: 0)
+        }
         let location = min(max(0, range.location), upperBound)
         let length = min(max(0, range.length), upperBound - location)
         return NSRange(location: location, length: length)
     }

Also applies to: 3670-3675

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/TextBoxInput.swift` around lines 3647 - 3650, The call to
super.setMarkedText is passing NSNotFound through as the selectedRange which can
produce an invalid marked-selection; update the selectedRange passed into
Self.sanitizedRange in the setMarkedText path so that if selectedRange ==
NSNotFound it is normalized to the caret-at-end index (use
Self.textInputStringLength(string) as the upperBound) when the marked buffer is
non-empty (mirroring the logic in GhosttyNSView+IMEComposition.swift); apply the
same normalization to the second occurrence of setMarkedText around the
3670–3675 region so both call sites use the sanitized/normalized selectedRange
before calling super.setMarkedText (refer to methods: setMarkedText,
Self.sanitizedRange, Self.textInputStringLength,
sanitizedTextStorageReplacementRange).

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Sources/TextBoxInput.swift
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@lawrencecchen lawrencecchen dismissed coderabbitai[bot]’s stale review June 4, 2026 09:41

Dismissed stale CodeRabbit changes-requested review from commit 1c81399; current head 57e092f normalizes TextBox setMarkedText selectedRange == NSNotFound and has clean canonical autoreview plus fresh passing CodeRabbit check.

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