Skip to content

Fix crash: clamp model offset in ctrlOffsetAfterMove + findBefore/findAfter#29

Merged
willwade merged 1 commit into
mainfrom
fix/ctrl-offset-crash
Jun 21, 2026
Merged

Fix crash: clamp model offset in ctrlOffsetAfterMove + findBefore/findAfter#29
willwade merged 1 commit into
mainfrom
fix/ctrl-offset-crash

Conversation

@willwade

Copy link
Copy Markdown

Problem

Crash on DasherMac (macOS 26.5 hardened libc++): string::operator[] assertion fires when expanding a control node that tries to calculate a delete offset.

Stack trace

frame #4: string::operator[](this="aaaaaaaaaa.", __pos=75)     ← 11-char string, index 75
frame #5: findBefore(pos=75)                                    at CAPI.cpp:89
frame #6: getRange(buf="aaaaaaaaaa.", ...)                      at CAPI.cpp:134
frame #7: ctrlOffsetAfterMove(offsetBefore=76)                  at CAPI.cpp:400
frame #8: DeleteAction::calculateNewOffset(offsetBefore=75)     at ControlManager.cpp:193
frame #9: NodeTemplate::calculateNewOffset(offsetBefore=75)     at ControlManager.cpp:67
frame #10: CContNode::PopulateChildren()                        at ControlManager.cpp:94
frame #11: CDasherModel::ExpandNode()

Root cause

ctrlOffsetAfterMove uses the model's node offset (75) directly as an index into the edit buffer (11 chars). These diverge because control nodes have offsets in the model tree but produce no edit buffer characters — so the model offset can grow far beyond the buffer length.

Fix

Three layers of defence:

  1. ctrlOffsetAfterMove: clamp offsetBefore to editBuffer.size() before passing to getRange
  2. findBefore: clamp pos to s.size()-1 at entry (the function accesses s[pos])
  3. findAfter: clamp pos to s.size() at entry (same defensive pattern)

8 insertions, 1 deletion. No API changes.

…dAfter

Stack trace from DasherMac (macOS 26.5 hardened libc++):

  string::operator[](this='aaaaaaaaaa.', __pos=75)
    ← findBefore(pos=75) at CAPI.cpp:89
    ← getRange(buf='aaaaaaaaaa.', ...) at CAPI.cpp:134
    ← ctrlOffsetAfterMove(offsetBefore=76) at CAPI.cpp:400
    ← DeleteAction::calculateNewOffset(offsetBefore=75) at ControlManager.cpp:193
    ← CContNode::PopulateChildren() at ControlManager.cpp:94
    ← CDasherModel::ExpandNode()

Root cause: ctrlOffsetAfterMove uses the model's node offset (75)
as an index into the edit buffer (11 chars). Model offset != edit
buffer length — they diverge because control nodes have offsets
in the model tree but produce no edit buffer characters.

Three fixes:

1. ctrlOffsetAfterMove: clamp offsetBefore to editBuffer.size()
   before passing to getRange.

2. findBefore: clamp pos to s.size()-1 at entry. The function
   accesses s[pos] and s[p] which crash if pos >= size.

3. findAfter: clamp pos to s.size() at entry. Same defensive
   pattern for the forward search.

Signed-off-by: will wade <willwade@gmail.com>
@willwade willwade merged commit 32bc29f into main Jun 21, 2026
14 checks passed
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