Skip to content

Fix #4887: id(hosted_binding): a pending delayed rebind does not reserv #5103

Closed as not planned

Description

@developing-today

Implement GitHub issue #4887 in developing-today/code: id(hosted_binding): a pending delayed rebind does not reserve its target key

Summary

The delayed-rebind protocol added in #4885 (slice 2 of #3809, pkgs/id/src/hosted_binding.rs at 9d1464c05) never reserves the target key of a pending delayed rebind, and effect_due_rebind never re-checks it. During the 30-day wait, another account can bind that exact key by an ordinary ceremony; when the wait elapses, the delayed rebind still effects and apply_rebind silently overwrites the other binding's binding_by_iroh entry. Two active binding records then claim the same iroh_id, and the by-key index points at only one of them — breaking the §1.2 rule the ceremony itself enforces ("one sub per iroh_id at a time", submit_proof conflict check).

Evidence (verified this run, laptop probe at origin/main 9d1464c05)

Probe test appended in a throwaway laptop worktree (probe-2026-11-03-laneB-scan20, removed after the run; log /tmp/probe-scan20.log on the laptop):

  1. Account A binds key21, then request_delayed_rebind to key22 → request Pending, binding rebind_pending.
  2. A third account completes an ordinary complete_browser_ceremony with key22 → succeeds. Nothing consulted the pending request: request_delayed_rebind checks only binding_by_iroh at request time (hosted_binding.rs:1269), the pending target key is inserted into no index, and submit_proof's conflict check (hosted_binding.rs:~997) checks only binding_by_iroh.
  3. Time advances past effective_at; effect_due_rebind with a valid new-key countersignature → succeeds. Unlike both other paths, it has no NewKeyBoundElsewhere recheck: rebind_with_old_key_authorization checks binding_by_iroh.contains_key(&new_iroh_id) at effect time (hosted_binding.rs:1414) and request_delayed_rebind checks it at request time (:1269); effect_due_rebind (hosted_binding.rs:1462) checks neither that nor pending requests.
  4. apply_rebind (hosted_binding.rs:1538-1540) unconditionally does binding_by_iroh.remove(old) + binding_by_iroh.insert(new, this binding), overwriting the third account's index entry.

Probe outcome (asserted in the probe, all true on current main): both A's and the third account's records are Active with iroh_id = key22; binding_by_iroh[key22] names A's binding only; the third account's record is silently unindexed for its own key.

The symmetric hole also exists at request time: two different accounts can both open pending delayed rebinds to the same target key, and whichever effects second overwrites the first's index entry the same way.

Why it matters

The delayed path is the account-recovery path for a lost root key — exactly the path whose target-key ownership must be unambiguous. The 30-day wait is the security property (§4.2), but it also creates a 30-day window in which the target key is claimable by anyone: a squatter who binds the target key during the wait is not stopped at effect time, is not notified, and loses their by-key index entry without any error, while their binding record still says active. Any consumer resolving a binding by iroh_id (device authorization, grant references — §1) then reaches the wrong account's binding.

Acceptance criteria

  • A pending delayed rebind reserves its target key: an ordinary ceremony (submit_proof) naming a key that is the target of a pending rebind fails with a conflict (e.g. IrohIdBoundElsewhere or a dedicated pending-rebind conflict), and a second request_delayed_rebind from another account naming the same target key fails with NewKeyBoundElsewhere (or equivalent).
  • effect_due_rebind re-checks at effect time, before any mutation: if the target key is now bound to a different binding (or is the target of another pending request that was created earlier), it fails with NewKeyBoundElsewhere and leaves the binding, the request, and both indexes unchanged (the RebindError no-mutation contract).
  • apply_rebind cannot silently overwrite a binding_by_iroh entry owned by a different binding: it refuses (or its callers guarantee the check) when the existing entry names another binding.
  • Regression tests cover: ceremony-bind of a pending target key during the wait; two pending rebinds to one target key; effect after the target key was bound elsewhere (state unchanged on refusal); the existing 21 hosted_binding tests still pass (cargo test -p id --lib hosted_binding, laptop or CI).

Distinct from: #4868 / #4871 (the §3 ceremony's consumed-challenge and pairing-code findings in the same file — different functions and defects), #3809 (the delayed-rebind implementation epic this landed under; this is a defect in that new code, found by reading it against the §1.2 conflict rules).

Context: a Jules session began this issue on 2026-11-03 and was stopped mid-flight when the owner retired the Jules service (decommission, #5098). Before starting, check for any pull request or branch that attempt may have produced (search PRs referencing #4887) and treat the issue text above as the source of truth.

House rules from the repo owner: (1) Deliver the finished work as a pull request in this run; put "Fixes #4887" in the PR description so the issue closes on merge. (2) Cover the whole set in the one PR: every instance of the same problem, and any directly related dependency updates batched in — never one PR per item or per dependency. (3) Run the relevant tests and checks before opening the PR. (4) Merging is permitted only when you are reasonably certain the PR is correct and complete (checks green, tests run, low-risk); you never have to merge — when in doubt, leave the PR open and state your confidence and what a human should check. (5) If you hit a genuine blocker (missing credentials, a decision only the owner can make), stop and say exactly what is needed in your final summary.


Launched by pm-dispatch for developing-today/code. Queue task and OpenCode session ids follow in a comment below.

Activity

  1. developing-today commented on Nov 3, 2026

    @developing-today
    ContributorAuthor

    Dispatch launch FAILED at enqueue before a queue task was created, so this dispatch never became real. Closing this tracking issue as not planned so it does not linger as an orphan (#4917).

    Error: getaddrinfo ENOTFOUND nltgxorqwxhgfzseiirt.supabase.co

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions