Runestone icon indicating copy to clipboard operation
Runestone copied to clipboard

Reproducible "Fatal error: 2 is out of bounds. Valid range is 0 - 0" with specific content

Open Timac opened this issue 4 months ago • 3 comments

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?

  1. 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 {
    
  2. Compile and run the Example app

  3. Type in the UI "X-X\n". Note that you need to type or paste this text.

  4. 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.

Timac avatar Nov 13 '25 20:11 Timac

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

Timac avatar Nov 13 '25 20:11 Timac

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
					}

Timac avatar Nov 13 '25 20:11 Timac

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)
    }

Timac avatar Nov 13 '25 20:11 Timac