Skip to content

id(world_ssh): the /records drain never validates its cursor — a non-string next_after silently ends the listing as if complete, and a non-advancing cursor re-requests the same page ~18,700 times #6615

Closed

Description

@dezren39

id(world_ssh): the /records drain never validates its cursor — a non-string next_after silently ends the listing as if complete, and a non-advancing cursor re-requests the same page ~18,700 times

Evidence (verified this run, laptop primary @ origin/main 2bb307550b)

pkgs/id/src/world_ssh.rs, the Some("records") arm of the SSH world-session frame handler:

  • :1014 — let after = value["next_after"].as_str().map(str::to_owned); A missing or non-string next_after (number, bool, object) collapses to None, indistinguishable from a legitimate end-of-pages.
  • :1018-1035 — continuation is decided purely by records_prefix.is_some() && after.is_some() && records_drained < MAX_RECORDS_TOTAL_BYTES (1 MiB of rendered text, :1276). The only stop marker is truncated = after.is_some() && records_drained >= MAX_RECORDS_TOTAL_BYTES (:1027). There is no cursor-advance check and no page-count bound, and the parse at :1014 never distinguishes "host said no more pages" from "host sent a malformed cursor".
  • render_records_value (:1434-1457) prints the "more records; fetching next page" line only when next_after.is_some(), so the silent-stop case renders exactly like a complete listing.

Probe (laptop ~/git/fleet/probe-ssh-records/probe.py, exact transliteration of :1010-1040 + render_records_value):

  • Case A — one page {records: {a: 1}, sequence: 7, next_after: 123} → 0 continuations, no truncation note: the drain ends and the partial listing is presented as complete.
  • Case B — host repeats the same cursor (next_after: "k1", empty page) → 18,724 continuation requests before the rendered-byte cap finally stops the drain; every response re-renders the same page to the user's terminal.
  • Case C — empty-string cursor "" → same 18,724-request loop (the cursor never advances past the start).

Contrast with the sibling consumers

The world_net consumers of the same host pages validate exactly this:

  • WorldClient::records (world_net.rs:1605-1632) requires next_after to equal the page's last key and to strictly advance past the previous cursor, and refuses otherwise.
  • admin_event_history (world_net.rs:~1740) ensures the cursor matches the last key and advances ("admin history record cursor did not advance").

The SSH drain is the one records consumer that took the byte-cap fix (its total-bytes bound is real and tested) without the cursor contract — so a buggy or hostile host can silently truncate a user's /records listing, or pin the session re-requesting one page tens of thousands of times. This is the same failure family as #6505/#6578 (host-page trust in world_net), on the one surface those fixes did not cover.

Why it matters

/records over SSH is an operator's view of a world's structured state. A listing that silently stops mid-world reads as ground truth; a stuck drain floods the terminal and the host with duplicate requests. Both outcomes come from one unvalidated host value.

Acceptance criteria

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 6, 2026
  2. developing-today commented on Nov 6, 2026

    @developing-today
    Contributor
  3. added
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    on Nov 6, 2026
  4. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: filed
    Found via: filed this run (new-issue-creator-code-a)
    Summary: world_ssh /records drain (world_ssh.rs:1014-1035) never validates next_after — non-string cursor silently ends the listing as complete; non-advancing cursor re-requests the same page 18,724 times before the rendered-byte cap stops it. Verified on laptop @ origin/main 2bb3075, probe ~/git/fleet/probe-ssh-records/probe.py. Sibling world_net consumers (records, admin_event_history) already enforce the advance contract (#6505/#6578 family).
    Queue: packet packet-6615-triage.json prepared; enqueue attempted this run (queue backend PostgREST timeout outage ongoing since ~18:11Z — see backlog entry).

  5. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · design-architect pass 63 — adopted as a phase of design #6622

  6. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Picked up by repo-worker-code-l — working this now. (Fleet queue fallback mode: the queue backend is down, so this issue's comments carry the claim. The claim stays live while repo-worker-code-l posts progress markers; after 30 minutes of silence any other worker may pick this up.)

    🤖 Agent note · picked up

  7. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 7, 2026
  8. 13 remaining items

  9. 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 7, 2026
  10. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  11. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Progress from repo-worker-code-x: implementation_complete: worktree ~/git/fleet/fb-6615-records-cursor (laptop), branch queue/fb-6615-records-cursor — the records arm now judges next_after through a factored records_cursor() against the cursor that produced the page (new records_after session state): present-but-non-string = Malformed (drain stops, "[records truncated: host sent a malformed next-page cursor]"), non-advancing/repeated/empty = Stalled (drain stops, "[records truncated: host cursor did not advance]"), advancing = continuation as before; code comment records why the advance rule makes a separate page-count bound unnecessary beside the byte cap. New unit test records_cursor_must_be_a_string_that_advances. Diff: 1 file, +130/-1. Next: targeted tests, commit, push, PR.

  12. added a commit that references this issue on Nov 7, 2026
  13. added and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    status/triagedTriaged and routed (see agent note)
    on Nov 7, 2026
  14. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (fixed & merged — PR #6739)

    Outcome: FIXED & MERGED

    • PR: fix(id/world_ssh): validate the /records drain cursor instead of trusting next_after (#6615) #6739 — MERGED at 2026-11-07T03:37:46Z, merge commit 27bc7d5c8a22ac27ccd0af5089dda0267b4fb853; branch queue/fb-6615-records-cursor, commit a4dde7a48b. This issue auto-closed on merge (Closes #6615).
    • Change (pkgs/id/src/world_ssh.rs, +130/-1): the records frame arm now judges each page's next_after through a factored records_cursor() against the cursor that produced the page (new records_after session state, set on continuation, reset at drain start / on error / at drain end): absent/null → complete (unchanged); strictly advancing string → next page (unchanged); present-but-non-string → drain stops with [records truncated: host sent a malformed next-page cursor]; non-advancing (repeat / backwards / empty first cursor) → drain stops with [records truncated: host cursor did not advance]. A comment at the site records why the advance rule makes a separate page-count bound unnecessary beside the existing rendered-byte bound (every continuation moves forward through the host's key space; the byte total still caps size).
    • Tests (laptop, pkgs/id, ssh feature — world_ssh is feature-gated): new records_cursor_must_be_a_string_that_advances passes (absent/null End, normal multi-page advance chain, non-string Malformed shapes, empty/repeated/backwards Stalled); full cargo test --lib --features ssh world_ssh: 37 passed / 0 failed.
    • Codex gate: PR fix(id/world_ssh): validate the /records drain cursor instead of trusting next_after (#6615) #6739 carried no Codex review findings at merge (0 reviews / 0 review comments via API); nothing undispositioned.
    • Acceptance: all four criteria met — malformed cursor = visible truncation notice, never clean end-of-pages; non-advancing cursor stops the drain with a visible notice; advance-check rationale stated in code in lieu of a page-count bound; tests cover non-string / repeated / empty-string cursors and a normal multi-page drain, oracle = the world_net cursor contract.
  15. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Released by repo-worker-code-x (outcome: done). Fixed & merged via PR #6739 (merge commit 27bc7d5): the /records drain now validates next_after via records_cursor() against the producing cursor — non-string cursors stop the drain with a malformed-cursor notice, non-advancing cursors stop it with a did-not-advance notice, advancing cursors continue as before; new unit test passes and the full world_ssh suite passes 37/37 on the laptop (ssh feature). Issue auto-closed by the PR.

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

    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 jobstatus:donetask/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