Skip to content

fix: reject imported changes with invalid deps on their own peer - #1237

Open
Frando wants to merge 1 commit into
loro-dev:mainfrom
Frando:pr/own-peer-deps
Open

Frando wants to merge 1 commit into
loro-dev:mainfrom
Frando:pr/own-peer-deps

Conversation

@Frando

@Frando Frando commented Oct 28, 2026

Copy link
Copy Markdown

A local commit depends on the doc's frontiers. The frontiers hold at most the latest op of the change's own peer. So a change c@p (counter c of peer p) can depend on p only through (c-1)@p. An imported update can claim any deps, though.

If a change depends on an older op of its own peer, import panics. The import computes the change's lamport from that old op. The DAG then merges the change into the peer's last node, and AppDag asserts "lamport is not continuous" while it holds the op log lock. The doc is poisoned after that.

Binary blocks store a dep on the own peer as one bit that means (c-1)@p. So only JSON updates or crafted bytes can carry such a dep.

Repro: peer 1 commits three changes. Export them with export_json_updates_without_peer_compression. Set the deps of 2@1 to ["0@1"] and import the JSON into a new doc:

panicked at crates/loro-internal/src/oplog/loro_dag.rs:341:21:
assertion `left == right` failed: lamport is not continuous

Fix

OpLog::check_own_peer_deps returns DecodeError for any dep on the change's own peer other than (c-1)@p. The error names both ids, for example "Invalid change 2@1: it depends on 0@1". The check runs in check_and_trim_known_part_in_arena, next to the skipped-counter check, before anything is changed. It covers these imports:

  • binary updates, also each blob of import_batch
  • JSON updates
  • snapshots imported into a non-empty doc

A snapshot imported into an empty doc takes the fast path and skips the check. That does no harm: the DAG nodes come from the block metadata, so the merge that panics does not run.

Valid changes never have such a dep. The check is one pass over the deps of the new changes, with no allocation.

Tests

crates/loro/tests/import_own_peer_deps.rs has three tests:

  • a dep on an older op, attached and detached, alone and next to a dep on another peer
  • deps on the change's own counter or a later one, and a first change that depends on its own peer
  • a rejected change rejects the whole update, also the valid changes before it

Each test checks that the doc keeps its version and value and stays usable. On main, the first test panics and the other two fail because the import succeeds.

This PR includes a patch changeset for loro-crdt, because JSON import in JS hits the same panic. cargo test -p loro -p loro-internal passes: 1548 passed, 0 failed, 27 ignored.

A change of peer p at counter c can depend on p only through (c-1)@p.
A crafted dep on an older op of p made the import panic with "lamport
is not continuous" and poisoned the document. See
context/import-peer-id-reuse.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Frando
Frando marked this pull request as ready for review October 28, 2026 08:13

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.

1 participant