Skip to content

id(world): restore's EntityImported epoch check uses plain + 1 — prior=u64::MAX wraps to an accepted epoch 0 in release and panics in debug/test #3961

Closed

Description

@dezren39

Finding

WorldCore::restore's standalone EntityImported journal arm validates the owner-epoch advance with a plain + 1 on a u64:

// pkgs/id/src/world/core.rs:367-370 (restore, JournalEntry::EntityImported arm)
ensure!(
    new_owner_epoch == prior_owner_epoch + 1,
    "journal import for handoff {handoff_id:?} does not advance the owner epoch"
);

prior_owner_epoch / new_owner_epoch are u64 (pkgs/id/src/world/mod.rs:513, :814), and pkgs/id/Cargo.toml sets no overflow-checks override in any profile, so the check behaves differently by build:

  • Release (overflow checks off): prior_owner_epoch = u64::MAX, new_owner_epoch = 0 satisfies 0 == MAX + 1 (wrapping) — restore accepts an import whose owner epoch wraps to 0, i.e. the epoch goes backwards to the start. Owner epochs are the fencing currency for the region-seam handoff (id: implement: journaled region-seam export/import #3870 phases 2–5 build on this foundation): an epoch that can wrap to 0 defeats the monotonicity the fence relies on.
  • Debug / test (overflow checks on): the same journal entry panics with attempt to add with overflow inside restore, so a crafted or corrupted journal crashes the restore path instead of being refused with the arm's own error message.

Evidence (main 9268223, verified this run)

  • pkgs/id/src/world/core.rs:368 — the plain + 1 above (file md5 6b67846b2091c327ba9af2cdf93b687a, identical on the sandbox clone and the laptop clone).
  • Laptop probe ~/git/fleet/probe-2026-11-01-laneA-epochwrap/ — the verbatim predicate extracted from core.rs:367-370, compiled with plain rustc on the laptop:
    • Release (rustc -O): controls correct — prior=5 new=6 → true, prior=5 new=5 → false, prior=5 new=7 → false; wrap case prior=MAX new=0 → true, prior=MAX new=MAX → false.
    • Debug (rustc, overflow checks on): controls correct; the wrap case panics (attempt to add with overflow, exit 101) before it can return a verdict.
  • Sibling checks in the same restore already avoid plain arithmetic where it matters: the checkpoint consecutiveness check is the subject of id(world): restore's checkpoint consecutiveness check uses plain + 1 — a penultimate u64::MAX sequence panics in debug/test builds and wraps to an accepted MAX → 0 step in release #3736, and since_checkpoint accounting uses saturating_add (core.rs, EntityExported/EntityImported arms).

Why it matters

This arm is the validating path for journal-carried imports — the checkpoint-carried variant is separately unvalidated (#3942). A handoff import at the last representable epoch is exactly the boundary a fencing epoch must never cross silently: in release it restores as a legitimate advance to epoch 0 (a stale owner's epoch-0 claims become current again), and in debug/test builds the same input is a panic in the restore path rather than a clean refusal. The fix is the checked form used elsewhere in the codebase (checked_add(1) compared against Some(new_owner_epoch), or equivalent), so prior = u64::MAX can never validate an import.

Acceptance criteria

  • WorldCore::restore refuses a standalone EntityImported journal entry with prior_owner_epoch = u64::MAX (any new_owner_epoch) with the existing "does not advance the owner epoch" error — in both debug/test and release builds, with no panic and no wrapping acceptance.
  • Ordinary advances still restore: prior=5, new=6 succeeds; prior=5, new=5 and prior=5, new=7 are still refused.
  • A regression test in pkgs/id/src/world/tests.rs (alongside the existing handoff restore tests) covers the u64::MAX prior epoch case.
  • The same plain-+1 pattern is checked for the other epoch/sequence comparisons added by the id: implement #3870 phase 1 — journal/schema/checkpoint foundation for region-seam handoff #3900 handoff foundation, and any sibling site found is either fixed in the same change or filed separately with a link here.

Dedup: #3736 (open) is the checkpoint event-sequence consecutiveness pair[1].sequence == pair[0].sequence + 1 at core.rs:476 — a different field, arm, and consequence. #3942 (open) covers checkpoint-carried transfers being installed verbatim with no validation at all; its probe exercised the standalone arm only at epoch 5→5, which this arm correctly refuses — the overflow boundary of the standalone arm's own check was not covered. #3902/#3903/#3904 (open) are the #3870 destination-import/coordinator/client phases that build on this foundation; none fixes the phase-1 restore check.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/lowNice to have; do when convenient
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Nov 1, 2026
  2. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · adopted as Phase 2 of design #4000

  4. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triage-verified (queue task 5902, repo-worker-code, OpenChamber scheduled lane)

  5. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (queue task 5902, repo-worker-code)

    • Outcome: triaged. Findings comment above; follow-up small-fix enqueued as queue task 6266.
    • Labels: removed queue/queued, status/needs-triage (triage complete; fix tracked by task 6266).
  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    task: 6266
    lane: code-e
    state: picked-up
    note: Claimed by lane E worker (direct execution lane). Revalidating restore EntityImported epoch check against fresh main; fix is checked_add form + u64::MAX regression test per acceptance.

  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    task: 6266
    lane: code-e
    state: done
    note: Fixed and merged in PR #4505 (merge 31f924b). Restore EntityImported arm now uses checked_add(1) == Some(new_owner_epoch); new regression test handoff_import_at_max_owner_epoch_is_refused covers new = 0, 1, u64::MAX at prior = u64::MAX. Verified on laptop: cargo test --lib handoff_ 4/4 pass, cargo test --lib world:: 156/156 pass. Codex review: no findings. Sibling plain-+1 site is the checkpoint sequence check, already filed as #3736. Full cargo test on pkgs/id also shows pre-existing unrelated integration-test compile errors on main (ChainExt missing link field in tests/*.rs, missing tic-tac-toe.wasm) — not touched by this change.

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

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions