Fix tile-tree corruption from a zero-length emit under a document-wide mark #84

Closed
StoneCypher wants to merge 1 commit from StoneCypher/view:fix-empty-emit-tile-corruption into main
First-time contributor

Summary

TileUpdate.emit() can drop a line from the tile tree when an emit covers a
zero-length range while a mark decoration spans that position, later crashing
in TilePointer.advance (parents.pop() is undefined). This is the
empty-emit tail of the same area addressed in 6.43.3 (dev #1717) and 6.43.4
(dev #1720); both of those only covered the non-empty paths.

Root cause

On a zero-length emit (from == to), RangeSet.spans(from, from, ...) iterates
nothing, so neither the span nor point callback runs and markCount keeps
its 0 initializer -- even though a mark is open (openEnd > 0). So
openWidget = openEnd > markCount is spuriously true, and the guarded
addLineStartIfNotCovered is skipped, leaving the tile tree one line short; a
later edit then overruns it in TilePointer.advance.

6.43.4's markCount = active.length was added inside the span callback,
which a zero-length emit never reaches -- which is why it did not cover this
case.

Fix

Only recompute openWidget when the emit actually covered content; an empty
emit can neither open nor close a widget, so leave the flag untouched:

if (from < to) this.openWidget = openEnd > markCount

Every non-empty path (everything #1717 / #1720 touched) is byte-identical.

Verification

  • cm-runtests: 389/389 unchanged.
  • A trusted-input reproduction -- a real drag-selection + blank-line click
    under a whole-document mark plus highlightActiveLine, driven via Selenium's
    Actions API (synthetic dispatch does not reproduce this) -- asserts the
    tile invariant state.doc.length - docView.tile.length:
    build tile deficit
    6.43.2 0 (clean)
    6.43.3 1 (corrupt)
    6.43.4 1 (corrupt)
    6.43.5 1 (corrupt)
    6.43.5 + this patch 0 (clean)

Because the trigger needs trusted input it can't be an in-page test; I have a
self-contained Node + Selenium reproduction (pure @codemirror/view, no other
deps) I'm glad to share or adapt if it's useful.

Originally surfaced via a whole-document lint diagnostic (an error with no
source position) in StoneCypher/jssm#891.

## Summary `TileUpdate.emit()` can drop a line from the tile tree when an emit covers a zero-length range while a mark decoration spans that position, later crashing in `TilePointer.advance` (`parents.pop()` is `undefined`). This is the empty-emit tail of the same area addressed in 6.43.3 (dev #1717) and 6.43.4 (dev #1720); both of those only covered the non-empty paths. ## Root cause On a zero-length emit (`from == to`), `RangeSet.spans(from, from, ...)` iterates nothing, so neither the `span` nor `point` callback runs and `markCount` keeps its `0` initializer -- even though a mark is open (`openEnd > 0`). So `openWidget = openEnd > markCount` is spuriously `true`, and the guarded `addLineStartIfNotCovered` is skipped, leaving the tile tree one line short; a later edit then overruns it in `TilePointer.advance`. 6.43.4's `markCount = active.length` was added *inside* the `span` callback, which a zero-length emit never reaches -- which is why it did not cover this case. ## Fix Only recompute `openWidget` when the emit actually covered content; an empty emit can neither open nor close a widget, so leave the flag untouched: ```js if (from < to) this.openWidget = openEnd > markCount ``` Every non-empty path (everything #1717 / #1720 touched) is byte-identical. ## Verification - `cm-runtests`: 389/389 unchanged. - A trusted-input reproduction -- a real drag-selection + blank-line click under a whole-document mark plus `highlightActiveLine`, driven via Selenium's Actions API (synthetic `dispatch` does **not** reproduce this) -- asserts the tile invariant `state.doc.length - docView.tile.length`: | build | tile deficit | | --- | --- | | 6.43.2 | 0 (clean) | | 6.43.3 | 1 (corrupt) | | 6.43.4 | 1 (corrupt) | | 6.43.5 | 1 (corrupt) | | 6.43.5 + this patch | 0 (clean) | Because the trigger needs trusted input it can't be an in-page test; I have a self-contained Node + Selenium reproduction (pure `@codemirror/view`, no other deps) I'm glad to share or adapt if it's useful. Originally surfaced via a whole-document lint diagnostic (an error with no source position) in [StoneCypher/jssm#891](https://github.com/stonecypher/jssm/issues/891).
FIX: Fix a tile-tree corruption under a document-spanning mark decoration
where a zero-length emit could drop a line and later crash in
TilePointer.advance.

A zero-length emit (from == to) runs neither the span nor the point
callback in RangeSet.spans, so markCount stays at its 0 initializer even
while a mark is open (openEnd > 0). openWidget = openEnd > markCount was
therefore spuriously true, and the guarded addLineStartIfNotCovered was
skipped, leaving the tile tree one line short; a later edit overran it in
TilePointer.advance (destructuring parents.pop() as undefined). Only
recompute openWidget when the emit actually covered content -- an empty
emit can neither open nor close a widget.

This is the empty-emit tail of the same area touched by 6.43.3 (dev #1717)
and 6.43.4 (dev #1720), whose fixes only covered the non-empty paths;
6.43.4's markCount = active.length lives inside the span callback, which a
zero-length emit never reaches. The trigger in the wild is a
whole-document lint diagnostic (an error with no source position) kept
active by states-first authoring.
Author
First-time contributor

I didn't attach the repro from the original outreach because it looks like it doesn't fit your testing methodology

If I'm wrong about that, tell me and I'll attach it

So the repro won't fit your existing test rig because it needs "trusted input" to trigger, something that only comes from browser interactions, never code interactions, and so the style of your existing stuff as a node script by definition cannot trigger the bug

But selenium can lie about trusted input; you just need to use a different interface

I decided to just attach it as a follow-on PR #85. No need to have a discussion or modify a pull. Just look at the second pull, and decide if you want it as well. (Or either.)

It does work a little differently than your existing stuff. No new installed tools, still in mocha selenium, still running from the same test runner. But the shape is a little different, by necessity. YMMV.

~~I didn't attach the repro from the original outreach because it looks like it doesn't fit your testing methodology~~ ~~If I'm wrong about that, tell me and I'll attach it~~ So the repro won't fit your existing test rig because it needs "[trusted input](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API)" to trigger, something that only comes from browser interactions, never code interactions, and so the style of your existing stuff as a node script by definition cannot trigger the bug But selenium can lie about trusted input; you just need to use a different interface I decided to just attach it as a follow-on PR #85. No need to have a discussion or modify a pull. Just look at the second pull, and decide if you want it as well. (Or either.) It does work a little differently than your existing stuff. No new installed tools, still in mocha selenium, still running from the same test runner. But the shape is a little different, by necessity. YMMV.
marijn closed this pull request 2026-07-06 08:53:04 +02:00
Owner

Thanks for the report. Attached patch should handle this. I was able to create a regular test case for the issue.

Thanks for the report. Attached patch should handle this. I was able to create a regular test case for the issue.
Author
First-time contributor

it isn't clear to me how that's able to generate Trusted Input. did you see it catch actual faults?

it isn't clear to me how that's able to generate Trusted Input. did you see it catch actual faults?
Owner

There is no need for trusted input. And yes, without the patch that test fails.

There is no need for trusted input. And yes, without the patch that test fails.
Author
First-time contributor

Okay, then I was wrong about why that probe wasn't working.

Okay, then I was wrong about why that probe wasn't working.

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
codemirror/view!84
No description provided.