Repository navigation
is_deleted() aborts with "Parent is not registered" on deleted-node metadata in a snapshot-loaded doc #1158
Description
is_deleted(), and every local edit, on the metadata of certain deleted tree nodes panics with InternalError: Parent is not registered (arena.rs:462) in a document loaded from a snapshot (or created by fork()). The panic happens while the state lock is held, so the lock is poisoned and the process aborts (sync.rs:34 while unwinding). Importing the same history as updates instead of a snapshot works (is_deleted() == true).
Reproduces on main (1eb14b3); found by the adversarial review of #1152 / #1153 (their dead-container harness aborts on seeds 5 and 116 of 150).
Repro (loro crate, debug or release)
use loro::{ContainerTrait, ExportMode, LoroDoc, TreeParentId};
#[test]
fn snapshot_meta_of_node_created_under_deleted_parent() {
let a = LoroDoc::new();
a.set_peer_id(1).unwrap();
let tree = a.get_tree("tree");
let parent = tree.create(TreeParentId::Root).unwrap();
a.commit();
let b = LoroDoc::new();
b.set_peer_id(2).unwrap();
b.import(&a.export(ExportMode::all_updates()).unwrap()).unwrap();
// b creates a child under `parent` while a concurrently deletes `parent`
let child = b.get_tree("tree").create(parent).unwrap();
b.commit();
tree.delete(parent).unwrap();
a.commit();
a.import(&b.export(ExportMode::all_updates()).unwrap()).unwrap();
// later history, so that the child's Create sits in an older change block
for i in 0..5000 {
a.get_map("m").insert(&format!("k{}", i % 50), i).unwrap();
a.commit();
}
let loaded = LoroDoc::new();
loaded.import(&a.export(ExportMode::Snapshot).unwrap()).unwrap();
let meta = loaded.get_tree("tree").get_meta(child).unwrap();
meta.is_deleted(); // panics: InternalError: Parent is not registered (arena.rs:462) -> abort
// meta.insert("k", 1) aborts the same way (Transaction::apply_local_op checks is_deleted first)
}Without the 5000 filler commits, or when the child is created before the delete by the same peer, it returns true as expected.
Cause (from reading the code)
DocState::is_deleted walks SharedArena::get_parent. The meta container's parent link comes from one of:
- parsing the change that holds the node's
Createop (register_container_and_parent_link,visit_created_childrenregisters the meta with the tree as parent), or - the lazy resolver installed by
InnerStore::decode(inner_store.rs~515), which reads the parent from the child's own KV entry in the snapshot state.
Here neither applies: the snapshot's change blocks are parsed lazily and the Create is in an old block nobody has parsed, and the meta of a node that was created inside an already-deleted subtree was never alive, so the state has no entry for it. get_parent then panics (arena.rs:462).
Possible fixes: let the resolver fall back to the container's creating op (a normal container ID is the ID of the op that created it; parsing that change registers the parent), taking care that the change store may already be locked when get_parent runs and that the op can be below a shallow root; or treat "no resolvable parent" as deleted in is_deleted instead of panicking. Both are larger than the PRs that found it, so they are not included there.
Since this is reachable from loro-crdt too (isDeleted() / edits on such a node's data), it traps the WASM instance.
🤖 Generated with Claude Code
Activity
fix: resolve a container's parent from the change that created it
fix: resolve a container's parent from the change that created it (#1159