Reproducible "Fatal error: 2 is out of bounds. Valid range is 0 - 0" with specific content
What happened?
When you replace the specific content "X-X\n" with "XX\n" using "self.contentView.textView.text", you can always reproduce the error:
Runestone/RedBlackTree.swift:39: Fatal error: 2 is out of bounds. Valid range is 0 - 0. This issue is under investigation. Please open an issue at https://github.com/simonbs/Runestone/issues and include this stack trace and a sample text file if possible. This fatal error is only thrown in debug builds.
Reproducible with
- iPhone 15 Pro with iOS 26.1
- Xcode 26.1 (17B55)
Please see attached video
What are the steps to reproduce?
-
Add code to execute
self.contentView.textView.text = "XX\n"For example:
extension MainViewController: MenuSelectionHandler { // swiftlint:disable:next cyclomatic_complexity func handleSelection(of menuItem: MenuItem) { switch menuItem {to
extension MainViewController: MenuSelectionHandler { // swiftlint:disable:next cyclomatic_complexity func handleSelection(of menuItem: MenuItem) { debugPrint("**** WILL CHANGE!") self.contentView.textView.text = "XX\n" debugPrint("**** DID CHANGE!") switch menuItem { -
Compile and run the Example app
-
Type in the UI "X-X\n". Note that you need to type or paste this text.
-
Tap on More > Find to trigger the code
self.contentView.textView.text = "XX\n"
Result:
"**** WILL CHANGE!"
Runestone/RedBlackTree.swift:39: Fatal error: 2 is out of bounds. Valid range is 0 - 0. This issue is under investigation. Please open an issue at https://github.com/simonbs/Runestone/issues and include this stack trace and a sample text file if possible. This fatal error is only thrown in debug builds.
https://github.com/user-attachments/assets/c82255c9-a762-4555-918e-797f0e7564ab
What is the expected behavior?
Such an error should not occur.
Here is the backtrace:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = Fatal error: 2 is out of bounds. Valid range is 0 - 0. This issue is under investigation. Please open an issue at https://github.com/simonbs/Runestone/issues and include this stack trace and a sample text file if possible. This fatal error is only thrown in debug builds.
frame #0: 0x000000018d0f7dec libswiftCore.dylib`_swift_runtime_on_report
frame #1: 0x000000018d1dd360 libswiftCore.dylib`_swift_stdlib_reportFatalErrorInFile + 208
frame #2: 0x000000018d20e754 libswiftCore.dylib`closure #1 in _assertionFailure(_:_:file:line:flags:) + 512
frame #3: 0x000000018d20d8c0 libswiftCore.dylib`_assertionFailure(_:_:file:line:flags:) + 172
* frame #4: 0x00000001038dda24 Example.debug.dylib`RedBlackTree.node(location=2) at RedBlackTree.swift:39:13
frame #5: 0x0000000103961814 Example.debug.dylib`LineController.lineFragmentNode(location=2) at LineController.swift:166:26
frame #6: 0x00000001039101e0 Example.debug.dylib`TextInputStringTokenizer.position(position=0x00000001172b6e60, direction=1) at TextInputStringTokenizer.swift:92:53
frame #7: 0x000000010390febc Example.debug.dylib`TextInputStringTokenizer.position(position=0x00000001172b6e60, granularity=.line, direction=1) at TextInputStringTokenizer.swift:39:25
frame #9: 0x00000001973032a4 UIKitCore`-[UIResponder(WritingToolsSupport) _shouldShowWritingToolsInCandidateBar] + 740
frame #10: 0x0000000195c4a190 UIKitCore`-[UIResponder(WritingToolsSupport) _shouldDisplayWritingToolsCandidateOptions] + 56
frame #11: 0x00000001963eebcc UIKitCore`-[_UIKeyboardStateManager generateCandidatesWithOptions:] + 332
frame #12: 0x00000001963c9b18 UIKitCore`__73-[_UIKeyboardStateManager updateForChangedSelectionWithExecutionContext:]_block_invoke_4 + 196
frame #13: 0x0000000196b4b118 UIKitCore`-[UIKeyboardTaskExecutionContext returnExecutionToParentWithInfo:] + 184
frame #14: 0x00000001963caf80 UIKitCore`__79-[_UIKeyboardStateManager syncInputManagerToKeyboardStateWithExecutionContext:]_block_invoke_3 + 136
frame #15: 0x0000000196b4c330 UIKitCore`-[UIKeyboardTaskEntry execute:] + 208
frame #16: 0x0000000195c3e440 UIKitCore`-[UIKeyboardTaskQueue continueExecutionOnMainThread] + 424
frame #17: 0x0000000196b4bb14 UIKitCore`-[UIKeyboardTaskQueue waitUntilTaskIsFinished:] + 140
frame #18: 0x0000000196b4bc4c UIKitCore`-[UIKeyboardTaskQueue performSingleTask:breadcrumb:] + 132
frame #19: 0x00000001963c9740 UIKitCore`-[_UIKeyboardStateManager updateForChangedSelection] + 144
frame #20: 0x00000001963c9f00 UIKitCore`-[_UIKeyboardStateManager selectionDidChange:] + 552
frame #21: 0x0000000197029604 UIKitCore`-[UITextInteractionInputDelegate selectionDidChange:] + 128
frame #22: 0x000000010391e440 Example.debug.dylib`TextInputView.string.setter(newValue="XX
") at TextInputView.swift:446:36
frame #23: 0x0000000103935aa0 Example.debug.dylib`TextView.text.setter(newValue="XX\n") at TextView.swift:30:34
frame #24: 0x000000010389f488 Example.debug.dylib`MainViewController.handleSelection(menuItem=presentFind) at MainViewController.swift:167:34
frame #26: 0x00000001038b4c90 Example.debug.dylib`closure #1 in MenuButton.makeFeaturesMenuElements(_0=0x0000000116f5fc00) at MenuButton.swift:30:49
frame #27: 0x0000000195b072b8 UIKitCore`___lldb_unnamed_symbol298135 + 56
frame #28: 0x0000000196c95bec UIKitCore`-[UIAction performWithSender:target:] + 112
frame #29: 0x000000019735ee60 UIKitCore`-[UIContextMenuInteraction contextMenuPresentation:didSelectMenuLeaf:] + 272
frame #30: 0x0000000196f0aeb4 UIKitCore`-[_UIContextMenuPresentation contextMenuUIController:didSelectMenuLeaf:] + 60
frame #31: 0x0000000196ce499c UIKitCore`__83-[_UIClickPresentationInteraction _handleDidTransitionToPossibleFromState:context:]_block_invoke + 44
frame #32: 0x00000001961cafcc UIKitCore`__84-[_UIRapidClickPresentationAssistant dismissWithReason:alongsideActions:completion:]_block_invoke + 136
frame #33: 0x00000001961cb270 UIKitCore`-[_UIRapidClickPresentationAssistant _animateDismissalWithReason:actions:completion:] + 496
frame #34: 0x00000001961caefc UIKitCore`-[_UIRapidClickPresentationAssistant dismissWithReason:alongsideActions:completion:] + 252
frame #35: 0x0000000195ce0054 UIKitCore`stateMachineSpec_block_invoke_4 + 784
frame #36: 0x000000019593cca4 UIKitCore`handleEvent + 256
frame #37: 0x0000000196ce4d24 UIKitCore`-[_UIClickPresentationInteraction _cancelWithReason:alongsideActions:completion:] + 120
frame #38: 0x000000019735e3a8 UIKitCore`-[UIContextMenuInteraction contextMenuPresentation:didRequestDismissalWithReason:alongsideActions:completion:] + 528
frame #39: 0x0000000196f0af34 UIKitCore`-[_UIContextMenuPresentation contextMenuUIController:didRequestDismissalWithReason:alongsideActions:completion:] + 88
frame #40: 0x0000000197016314 UIKitCore`-[_UIContextMenuUIController contextMenuView:didSelectElement:] + 248
frame #41: 0x0000000196199750 UIKitCore`-[_UIContextMenuView _performActionForElement:] + 80
frame #42: 0x00000001961977a0 UIKitCore`-[_UIContextMenuView _handleSelectionForElement:] + 256
frame #43: 0x0000000196197c6c UIKitCore`-[_UIContextMenuView _handleSelectionGesture:] + 960
frame #44: 0x000000019684fe54 UIKitCore`-[UIGestureRecognizerTarget _sendActionWithGestureRecognizer:] + 128
frame #45: 0x0000000195c192fc UIKitCore`_UIGestureRecognizerSendTargetActions + 92
frame #46: 0x0000000195c190bc UIKitCore`_UIGestureRecognizerSendActions + 268
frame #47: 0x00000001959aa604 UIKitCore`-[UIGestureRecognizer _updateGestureForActiveEvents] + 308
frame #48: 0x0000000196857f7c UIKitCore`-[UIGestureRecognizer gestureNode:didUpdatePhase:] + 300
frame #49: 0x000000019b58dacc Gestures`___lldb_unnamed_symbol1016 + 1004
frame #50: 0x000000019b5cc8d0 Gestures`___lldb_unnamed_symbol2265 + 488
frame #51: 0x000000019b591784 Gestures`___lldb_unnamed_symbol1037 + 4332
frame #52: 0x000000019b59cc28 Gestures`___lldb_unnamed_symbol1215 + 176
frame #53: 0x00000001968495c0 UIKitCore`-[UIGestureEnvironment _updateForEvent:window:] + 528
frame #54: 0x0000000196d7c2dc UIKitCore`-[UIWindow sendEvent:] + 2924
frame #55: 0x0000000196d5ed28 UIKitCore`-[UIApplication sendEvent:] + 396
frame #56: 0x000000019599e950 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 1076
frame #57: 0x00000001959ada40 UIKitCore`__processEventQueue + 4812
frame #58: 0x00000001959a0868 UIKitCore`updateCycleEntry + 172
frame #59: 0x00000001959aeafc UIKitCore`_UIUpdateSequenceRunNext + 128
frame #60: 0x00000001959adf8c UIKitCore`schedulerStepScheduledMainSectionContinue + 60
frame #61: 0x000000027d6db560 UpdateCycle`UC::DriverCore::continueProcessing() + 84
frame #62: 0x00000001900044cc CoreFoundation`__CFMachPortPerform + 168
frame #63: 0x00000001900340b0 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 60
frame #64: 0x0000000190033fd8 CoreFoundation`__CFRunLoopDoSource1 + 508
frame #65: 0x000000019000bc1c CoreFoundation`__CFRunLoopRun + 2168
frame #66: 0x000000019000aa6c CoreFoundation`_CFRunLoopRunSpecificWithOptions + 532
frame #67: 0x0000000230c34498 GraphicsServices`GSEventRunModal + 120
frame #68: 0x00000001959ceba4 UIKitCore`-[UIApplication _run] + 792
frame #69: 0x0000000195977a78 UIKitCore`UIApplicationMain + 336
frame #70: 0x0000000195aa368c UIKitCore`___lldb_unnamed_symbol297395 + 104
frame #73: 0x00000001038a6148 Example.debug.dylib`main at <compiler-generated>:0
frame #74: 0x000000018d022e28 dyld`start + 7116
From the backtrace, this seems to be related to Apple's WritingTools. A possible workaround (but might not be the best solution) would be to check oldSelectedRange.length > 0:
var string: NSString {
get {
stringView.string
}
set {
if newValue != stringView.string {
stringView.string = newValue
languageMode.parse(newValue)
lineManager.rebuild()
if let oldSelectedRange = selectedRange {
inputDelegate?.selectionWillChange(self)
if oldSelectedRange.length > 0 {
selectedRange = safeSelectionRange(from: oldSelectedRange)
} else {
selectedRange = nil
}
Maybe a better solution is to add safety checks in safeSelectionRange, as a similar issue might occur in other places where safeSelectionRange is called:
private func safeSelectionRange(from range: NSRange) -> NSRange? {
guard range.length > 0 else {
return nil
}
let stringLength = stringView.string.length
let cappedLocation = min(max(range.location, 0), stringLength)
let cappedLength = min(max(range.length, 0), stringLength - cappedLocation)
return NSRange(location: cappedLocation, length: cappedLength)
}