Skip to content

fix: reject imported style end anchors without their style start - #1261

Draft
1AbdullahButt wants to merge 1 commit into
loro-dev:mainfrom
1AbdullahButt:fix/1254-style-end-anchor
Draft

1AbdullahButt wants to merge 1 commit into
loro-dev:mainfrom
1AbdullahButt:fix/1254-style-end-anchor

Conversation

@1AbdullahButt

Copy link
Copy Markdown

Fixes #1254

Summary

A malformed update can contain a text StyleEnd op whose previous op (id - 1) is not its StyleStart. The update from the issue decodes, in a detached doc, to a mark_end at counter 5 right after the hello insert (counter 4). The richtext diff calculator's style_for_end_anchor assumes id - 1 is a StyleStart. On this input it hits unreachable!() (or as_list().unwrap()) while the doc's locks are held, and the doc is unusable afterwards.

Fix

This is checked at import time, alongside the movable-list Move/Set reference validation. The import is rejected with a DecodeError and rolled back, so the malformed op is not hidden.

  • OpLog::insert_new_change records every Text StyleEnd that is not paired with a StyleStart immediately before it in the same change and container. Honest marks are always paired that way, so the usual case needs no history lookup.
  • The new OpLog::validate_style_end_anchors_in_import_scope checks the recorded ends. id - 1 must be a StyleStart in the same text. If it is not in the stored history, the end is accepted only on a shallow doc whose shallow-root state has the style, the same fallback style_for_end_anchor uses.
  • validate_op_refs_in_import_scope runs both checks. It replaces the three validate_movable_list_elem_refs_in_import_scope call sites: attached import, detached import, and the import_batch reattach.
  • import_op_can_reject(op, detached = true) now also counts Text StyleEnd ops, so detached imports open a rollback scope for them. Attached imports already open one for any Text op.
  • Updated context/import-batch-atomicity.md and added a changeset.

The outdated (RLE) import path, update_oplog_and_apply_delta_to_state_if_needed, doesn't run the movable-list validation either. I left it alone.

Tests

New crates/loro/tests/text_style_end_invalid_ops.rs:

  • the exact base and malformed update from the issue: import fails, the doc is unchanged and still usable
  • forged JSON mark_end after an insert (attached and detached), after a mark on another text, after a map op, and as a peer's first op: all DecodeError, doc unchanged and usable
  • an honest mark whose StyleStart and StyleEnd arrive in separate updates_in_range exports: still accepted, attached and detached

What I could and could not run: this machine has no Rust toolchain, so I have not compiled this or run cargo test. It is a draft until CI runs. What I did check: with loro-crdt@1.17.1 from npm, I imported the issue's update into a detached doc and exported JSON. That confirmed the malformed op is a mark_end at counter 5 after the counter-4 insert, which is the case this check rejects.

Claude Code (AI) wrote this change; I reviewed it before opening.

🤖 Generated with Claude Code

A malformed update could carry a text StyleEnd whose previous op is not
its StyleStart. The richtext diff calculator (`style_for_end_anchor`)
then hit `unreachable!()` while the doc held its locks, leaving the doc
unusable. Imports now check these anchors with the other op references
of the import rollback scope and return a DecodeError, rolling back.

Fixes loro-dev#1254

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

Importing a malformed update panics in style_for_end_anchor and leaves the document unusable

1 participant