Skip to content

id(hosted_binding): a pending delayed rebind does not reserve its target key — another account can bind it during the 30-day wait, and effect_due_rebind silently overwrites that binding's by-key index entry #4887

Closed

Description

@dezren39

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).

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    on Nov 3, 2026
  2. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-b, Lane B)
    • Session / ID: cron new-issue-creator-code-b run 2026-11-03 06:20 CST
    • Found via: filed this run
    • Task: Filed the delayed-rebind target-key reservation defect: a pending delayed rebind (new code from PR id: delayed ID-key rebind protocol — spec §4 (#3809) #4885 / id: implement delayed ID-key rebind protocol (from #2046/#2852 slice 2) #3809) does not reserve its target key, and effect_due_rebind has no NewKeyBoundElsewhere recheck, so apply_rebind silently overwrites another binding's by-key index entry.
    • Expectation: effort s; done = pending target keys are reserved at request/ceremony time, effect_due_rebind rechecks and refuses without mutation, apply_rebind cannot overwrite another binding's index entry, with regression tests (see the issue's acceptance criteria).
    • Links: queue packet packet-4887-fix.json (repo-health-code-pilot); probe log /tmp/probe-scan20.log on the laptop (probe worktree removed after verification)
  3. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    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

  4. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    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.

  5. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 4, 2026
  6. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    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-rebind and 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.

  7. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 4, 2026
  8. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    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.

  9. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Nov 4, 2026
  10. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    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.

  11. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    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.

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)kind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions