Skip to content

Validate range in NSMutableAttributedString.add(link:for:color:) - #1446

Open
nortfiz wants to merge 1 commit into
overtake:masterfrom
nortfiz:fix/add-link-range-bounds
Open

Validate range in NSMutableAttributedString.add(link:for:color:)#1446
nortfiz wants to merge 1 commit into
overtake:masterfrom
nortfiz:fix/add-link-range-bounds

Conversation

@nortfiz

@nortfiz nortfiz commented Aug 14, 2026

Copy link
Copy Markdown

Opening one specific chat kills the app immediately, every time. I collected 16 identical crash reports in 22 minutes. The client restores the last chat on launch, so it crashes again on every relaunch and there's no way out from the UI.

add(link:for:color:) in packages/TGUIKit/Sources/Extensions.swift passes its range straight to addAttribute with no validation:

func add(link:Any, for range:NSRange, color: NSColor = presentation.colors.link)  {
    self.addAttribute(NSAttributedString.Key.link, value: link, range: range)
    self.addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: range)
}

A range with non-zero length running past the end of the string raises NSRangeException. Message layout runs on messagesViewQueue, nothing catches it there, and NSApplicationUncaughtExceptionHandler takes the process down.

Crash is on Telegram 12.9 (282526), macOS 26.4, arm64:

EXC_BREAKPOINT (SIGTRAP), queue: messagesViewQueue

Foundation   -[NSRLEArray objectAtIndex:effectiveRange:]
Foundation   -[NSConcreteMutableAttributedString addAttribute:value:range:]
Telegram     <frame A>
Telegram     <frame B>
libdispatch  _dispatch_lane_serial_drain

The App Store build is stripped, so I identified <frame A> by disassembly. The report's arm64 UUID matches the shipping binary, so the offsets are usable directly. <frame A> is reached by a direct bl from <frame B> with no Foundation frame between them, which rules out anything invoked through a Foundation block. Its body puts the receiver in x20 (swiftself), loads arg 1 as an existential and bridges it with _bridgeAnythingToObjectiveC, moves args 2 and 3 into x4/x5 with no arithmetic, and loads a second attribute key immediately after the recorded return address. That is add(link:for:color:).

Behaviour of addAttribute against an 11-character string:

range result
{0,5} {3,0} {11,0} {100,0} {NSNotFound,0} ok
{100,3} {8,50} {-4,6} {NSNotFound,5} NSMutableRLEArray objectAtIndex:effectiveRange:: Out of bounds

Two things follow. Empty ranges are harmless at any location, so the existing if range.location != NSNotFound guards around this helper aren't the protection that's missing: range(of:) returns {NSNotFound, 0}, which never throws. And the offending range must have non-zero length.

The helper has 61 call sites. 20 of them pass a range with no bounds check at all, several on the message layout path (ChatRowItem, ChatServiceItem, ChatContactRowItem).

The fix clamps inside the helper with trimRange from the same file. The NSNotFound and negative checks have to come first: trimRange only clamps from above, and computing location + length on {NSNotFound, n} traps on Int overflow before any range logic runs. Valid ranges pass through unchanged, and the worst case becomes a link that loses its styling.

I could not narrow down which of the 20 call sites produces the bad range. The shipping build is stripped and the crash report doesn't reach past the helper. If it'd be useful I can add an assertionFailure on the rejected path so debug builds surface the caller instead of silently skipping.

Related: #919, same queue and same Foundation frame, open since January 2023.

The range argument was handed to addAttribute with no bounds check. A
range with non-zero length running past the end of the string raises
NSRangeException, and since message layout runs on messagesViewQueue
there is no handler above it, so the process is terminated.

Reject NSNotFound and negative values before anything else, since
trimRange only clamps from above and location + length would trap on
Int overflow. Then clamp with trimRange and skip empty ranges.
@CLAassistant

CLAassistant commented Aug 14, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@nortfiz nortfiz mentioned this pull request Aug 14, 2026
@nortfiz

nortfiz commented Aug 14, 2026

Copy link
Copy Markdown
Author

Full write-up with the disassembly and the range table: https://nortfiz.github.io/telegram-macos-crash/

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.

2 participants