Repository navigation
Description
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):
- Account A binds key21, then
request_delayed_rebindto key22 → requestPending, bindingrebind_pending. - A third account completes an ordinary
complete_browser_ceremonywith key22 → succeeds. Nothing consulted the pending request:request_delayed_rebindchecks onlybinding_by_irohat request time (hosted_binding.rs:1269), the pending target key is inserted into no index, andsubmit_proof's conflict check (hosted_binding.rs:~997) checks onlybinding_by_iroh. - Time advances past
effective_at;effect_due_rebindwith a valid new-key countersignature → succeeds. Unlike both other paths, it has noNewKeyBoundElsewhererecheck:rebind_with_old_key_authorizationchecksbinding_by_iroh.contains_key(&new_iroh_id)at effect time (hosted_binding.rs:1414) andrequest_delayed_rebindchecks it at request time (:1269);effect_due_rebind(hosted_binding.rs:1462) checks neither that nor pending requests. apply_rebind(hosted_binding.rs:1538-1540) unconditionally doesbinding_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.IrohIdBoundElsewhereor a dedicated pending-rebind conflict), and a secondrequest_delayed_rebindfrom another account naming the same target key fails withNewKeyBoundElsewhere(or equivalent). -
effect_due_rebindre-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 withNewKeyBoundElsewhereand leaves the binding, the request, and both indexes unchanged (theRebindErrorno-mutation contract). -
apply_rebindcannot silently overwrite abinding_by_irohentry 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).
Activity
dezren39 commentedon Nov 3, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 3, 2026
agent-note:v1
state: reviewed (coverage loop-back)
Coverage pass 2026-11-03, loop-back review: this issue was re-reviewed (drift / overlap / already-handled / stale / blockers / ordering) so every open issue in the four repos is reviewed and in the system. Verdict: still live; in the system as task 7589 (small_fix, queued, priority 50) in repo-health-code-pilot. Labels audited and synced. If it is already handled at current main, its triage closes it with evidence; if oversized, the triage files the split; blockers belong on this issue.
Found via: coverage loop-back 2026-11-03
dezren39 commentedon Nov 3, 2026
agent-note: design-architect pass 45 (2026-11-03) ADOPTED this issue as Phase 3 of design #5047 ("A name is claimed once — the hosted binding lifecycle contract"; design doc files/designs/code-id-hosted-binding-lifecycle.md). Linked as a sub-issue of #5047. Triage: do NOT re-file, re-scope or split — record the phase structure instead.
Recorded rulings (detail on #5047): reservation is a predicate over Pending requests — NOT a second index and NOT a binding_by_iroh entry pointing at the pending binding (both alternatives considered and lost in the design doc); the fast path (rebind_with_old_key_authorization) gains the same reservation check so it cannot bypass a reservation; effect_due_rebind's recheck leaves the request Pending on refusal; apply_rebind itself refuses a foreign binding_by_iroh entry as the last-line guard.
dezren39 commentedon Nov 4, 2026
agent-note:v1
state: picked-up
queue: repo-health-code-pilot
task: 7589 (small_fix, worker repo-worker-code, lane bf419306-c4ef-470f-a553-beabf93239de)
by: openchamber-scheduled-lane
at: 2026-11-04T02:08Z
Claimed for revalidation. Issue #4887 is open; the related implementation PR #5150 is already open on branch
queue/8784-4887-delayed-rebindand appears to cover this task's requested scope. I will not create a competing PR; parking for 30 minutes and rechecking that PR on resume.dezren39 commentedon Nov 4, 2026
agent-note:v1
state: parked
queue: repo-health-code-pilot
task: 7589 (parked, worker repo-worker-code, lane bf419306-c4ef-470f-a553-beabf93239de)
by: openchamber-scheduled-lane
at: 2026-11-04T02:08Z
Parked for 30 minutes because open PR #5150 (
queue/8784-4887-delayed-rebind) appears to implement this issue's requested fix; no parallel branch or PR was created. Resume by re-fetching #4887 and #5150, verifying the current diff against the acceptance criteria and ownership; if the PR merged and covers the defect, complete task 7589 as already_done/superseded with fresh GitHub evidence. Otherwise, continue only after the conflict is resolved.dezren39 commentedon Nov 4, 2026
Picked up queue task 7589 and resumed from its prior verified checkpoint. Revalidating issue #4887 and existing implementation PR #5150 before any work; will not create a competing PR.
dezren39 commentedon Nov 4, 2026
agent-note:v1
event: already_done
task: 7589 (repo-health-code-pilot; repo-worker-code; OpenChamber scheduled lane 9aca43b4-5439-4939-9f71-323f5d32521b)
state: issue #4887 is CLOSED by merged PR #5150 (merge commit d45ab1a), now contained in origin/main. Fresh diff inspection covers all acceptance items; the PR reports 27 hosted_binding tests passing and the production-line-limit CI check passed. No files or PR were changed by this lane.