Skip to content

import with a reused peer id panics and poisons the doc; dropping it can abort #1118

Closed

Description

@lightsofapollo

Hi! This started with a bug on our side: two clients ended up with the same peer id and wrote different ops. We know the docs say not to reuse peer ids. Still, the result was harder to recover from than we expected:

use loro::{ExportMode, LoroDoc};

fn main() {
    let history = LoroDoc::new();
    history.set_peer_id(7).unwrap();
    history.get_map("m").insert("k", 0).unwrap();
    history.get_text("t").insert(0, "a").unwrap();
    history.commit(); // 7@0, 7@1

    let other = LoroDoc::new();
    other.set_peer_id(7).unwrap();
    other.get_text("t").insert(0, "xyz").unwrap();
    other.commit(); // 7@0..=2

    let update = other.export(ExportMode::all_updates()).unwrap();
    let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| history.import(&update)));
    assert!(r.is_err()); // generic-btree lib.rs:618: elem.rle_len=1 but pos.offset=2
    drop(history);       // panics again: poisoned LoroMutex (sync.rs:34)
}

The panic comes from RichtextState::apply_diff (in debug builds a debug_assert! in richtext_state.rs fires first). After that:

  • The doc's mutex is poisoned, so every later call panics, including Drop.
  • If the doc is dropped while that first panic is unwinding (it's owned inside the catch_unwind closure, or there's no catch_unwind), the second panic happens during cleanup and the process aborts.

Sometimes it doesn't panic at all: when the shifted position happens to be valid, import returns Ok and the text comes out scrambled ("hello world" became "helabclo wordefgld" in one run).

Same on 1.13.9, 1.16.0, 1.16.2 and main (ad5b2a6). #1068 looks like the same poisoned-mutex-in-Drop pattern with a different trigger.

Would it be possible for import to return an Err here, or for Drop not to panic on a poisoned lock, so a failed import can be caught?

Runnable repro with all three cases and a version matrix: https://github.com/lightsofapollo/loro-bug-repros (cargo run -p bug2-import-abort).

Activity

  1. added a commit that references this issue on Sep 30, 2026
  2. chapdel commented on Oct 15, 2026

    @chapdel

    Thanks for the fix! A downstream confirmation, with a smaller trigger (the receiver has one map op from peer 1; the update comes from another peer-1 history that inserts and deletes two characters in a text, exported with ExportMode::updates from {1: 1}):

    • loro = "=1.16.2": richtext_state.rs:2277 (pos: 0, len: 1, self.len(): 0), then poisoned LoroMutex, then the process aborts (SIGABRT), in debug and release builds.
    • main at 4f60d92: import returns Err(DecodeError("text diff deletes 1 at 0 but state only has 0")).

    crates.io still has 1.16.2 as the latest loro. Is a release with #1171 planned? It would let Rust users drop the process isolation they need around import today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions