Skip to content

id(world_net): WorldClient records paging trusts host pages — no record caps re-checked, no ordering/cursor validation, a non-advancing next_after loops forever #6505

Closed

Description

@dezren39

Summary

WorldClient's records query (pkgs/id/src/world_net.rs: records :1446, records_sequenced :1465, record_pages :1483-1525, origin/main fc073b5) trusts the host's record pages completely:

  • record_pages decodes each page with serde_json::from_value::<Records>(reply["records"].clone()) (:1514) and appends it to an unbounded Vec of pages. It re-checks none of the record caps: no per-key check (empty / > MAX_RECORD_KEY_BYTES 256 / control characters), no per-value check (> MAX_RECORD_VALUE_BYTES 8192 serialized), and no total-count check (MAX_RECORDS 4096) across pages.
  • It performs no ordering or cursor validation: keys within/across pages are never checked to be ascending, duplicates across pages are silently merged by records()' extend, and next_after is never checked to advance (or to equal the last key of the page it came with).
  • The loop terminates only when the host eventually sends next_after: null. Per-frame size is bounded (MAX_FRAME_BYTES in frames.rs), but the aggregate across pages is not.

A world host is not trusted by the client in the rest of this file — the caller joins someone else's world with a join capability — and the two sibling receipt paths in the same crate were hardened for exactly this:

The plain records path — the one the CLI's records read (commands/world.rs, client.records(...)) and records_sequenced use — has none of that.

Verified behavior (laptop, probe ~/git/fleet/probe-records-pages, real id crate + rogue iroh host speaking /id-world/1)

  • A (over-cap page): one page with 5,000 small records + a 9,000-byte value + a key containing U+0001, next_after: null → records() returns Ok with 5,002 records (caps would allow 4,096 records, values ≤ 8,192 bytes, no control chars in keys).
  • B (stuck cursor): every reply is {"a":1} with next_after: "a" → records() never returns (still looping after a 3 s timeout; the rogue host saw 43,639 records requests in that window). Unbounded page accumulation = hang + memory growth against any buggy or malicious host.
  • C (out-of-order / duplicate pages): page 1 {"b":1}, next_after:"b"; page 2 {"a":2,"b":3}, next_after:null → records_sequenced() returns Ok, with a accepted below the cursor and b silently overwritten 1 → 3. The admin path in the same file refuses all three shapes.

Why it matters

Any client that reads records from a world it does not operate (the normal join flow) can be hung indefinitely or memory-flooded by the host, and over-cap records flow on to callers that assume the parse_records bounds hold for every Records value. The fix pattern already exists twice in the codebase; this path was missed.

Acceptance criteria

  • record_pages (or its callers) re-checks every received record against the MAX_RECORD* caps (key 1..=256 bytes, no control characters; value ≤ 8,192 bytes serialized), matching parse_records / read_records semantics, and refuses the read with an error naming the offending key.
  • The total number of records accumulated across pages is capped at MAX_RECORDS (4,096); exceeding it is an error, not silent truncation.
  • Keys must be strictly ascending within and across pages (accounting for the prefix/after the client itself requested); a duplicate or out-of-order key is an error.
  • next_after must equal the last key of the page it accompanies and must strictly advance across iterations; a non-advancing or mismatched cursor is an error (mirroring the admin-history checks in the same file).
  • Regression tests drive WorldClient against stub/rogue replies covering: over-count page, oversized value, control-char key, stuck cursor, out-of-order page, duplicate key across pages — each refused promptly.
  • Conforming hosts are unaffected: the existing world_net / world_session records tests stay green (server pages are MAX_RECORDS_PAGE_BYTES-bounded and ascending by construction).

Activity

  1. developing-today commented on Nov 6, 2026

    @developing-today
    Contributor
  2. 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)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 6, 2026
  3. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

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

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  6. 12 remaining items

  7. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (PR open)

  8. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    Residual filed as #6578 (creator lane A, this run): at origin/main b0c472d the RecordsBudget/cursor checks that landed for this issue do refuse its original shapes (over-cap, stuck cursor, out-of-order), but empty pages with a strictly advancing next_after still loop record_pages forever (probe: 55,615 requests in 3s), and a non-string next_after silently terminates the read as if complete. #6578 scopes only those two remaining page shapes in the same loop; the fix for this issue and #6578 can land together.

  9. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/needs-reviewQueue task finished and is awaiting review
    on Nov 6, 2026
  10. dezren39 commented on Nov 6, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  11. added a commit that references this issue on Nov 7, 2026
  12. dezren39 commented on Nov 7, 2026

    @dezren39
    MemberAuthor

    Agent note - done (all acceptance criteria met; closing)

  13. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 7, 2026
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)queue/claimedQueue worker lifecycle statesource/creatorFiled by the new-issue-creator jobstatus:donetask/triageQueue packet kind: verify, reproduce, and route this itemwork/assignedQueue worker lifecycle state

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions