Repository navigation
Description
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_pagesdecodes each page withserde_json::from_value::<Records>(reply["records"].clone())(:1514) and appends it to an unboundedVecof pages. It re-checks none of the record caps: no per-key check (empty / >MAX_RECORD_KEY_BYTES256 / control characters), no per-value check (>MAX_RECORD_VALUE_BYTES8192 serialized), and no total-count check (MAX_RECORDS4096) 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, andnext_afteris 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_BYTESinframes.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:
world_records::read_records(iroh-docs receipt) re-checks everyMAX_RECORD*cap on receipt (id(world_records): read_records returns replicated records with none of the record caps re-checked — count, key, and value limits are enforced on publish and on admin-history receipt, but not on replica receipt #6483, merged in PR id(world_records): enforce MAX_RECORD* caps on receipt in read_records #6495).- The admin-history client path in this same file (
admin_event_history:1529, records decode :1612) budgets every frame, validates each record throughAdminHistoryBudget::add_record(:567; key length/control, value size, total count, total bytes), checks pages are in order ("admin history record pages are out of order"), checks the cursor matches the last key, and checks "admin history record cursor did not advance". The server side of the plain query (world_session/mod.rsrecords_session:1781) pages aparse_records-validated set in ascending key order underMAX_RECORDS_PAGE_BYTES(768 KiB), so every check proposed below holds for conforming hosts by construction.
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()returnsOkwith 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}withnext_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()returnsOk, withaaccepted below the cursor andbsilently 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 theMAX_RECORD*caps (key 1..=256 bytes, no control characters; value ≤ 8,192 bytes serialized), matchingparse_records/read_recordssemantics, 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/afterthe client itself requested); a duplicate or out-of-order key is an error. -
next_aftermust 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
WorldClientagainst 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
developing-today commentedon Nov 6, 2026
CODE-1965
dezren39 commentedon Nov 6, 2026
🤖 Agent note · filed
WorldClient::record_pages(world_net.rs:1483): no MAX_RECORD* re-checks, no ordering/cursor validation, non-advancing next_after loops forever (43,639 requests in 3s in the probe)dezren39 commentedon Nov 6, 2026
🤖 Agent note · picked up
12 remaining items
dezren39 commentedon Nov 6, 2026
🤖 Agent note · done (PR open)
dezren39 commentedon Nov 6, 2026
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.
test(id): prefix new record_pages tests with test_ per pkgs/id conven…
dezren39 commentedon Nov 6, 2026
🤖 Agent note · parked
Merge pull request #6579 from developing-today/queue/11007-6505-recor…
dezren39 commentedon Nov 7, 2026
Agent note - done (all acceptance criteria met; closing)
RecordsBudgetinpkgs/id/src/world_net.rs(verified fresh at current main this shift) validates every recordrecord_pagesreceives: key 1..=256 bytes with no control characters, serialized value ≤ 8192 bytes with the error naming the key, total across pages capped atMAX_RECORDS(4096), keys strictly ascending within and across pages; the cursor checks (next_afterequals the page's last key and strictly advances) are exercised by the tests below. It reached main unconventionally (keeper snapshote756e67d58+ repairbcf6e5d585), as documented on PR test(id): rogue-host regression tests for record_pages receipt validation (#6505) #6579.eb65c13ea562). Its rogue-host tests drive the realWorldClientover iroh against scripted pages: over-count page, oversized value, control-char key, stuck cursor, duplicate key across pages, out-of-order page, mismatched cursor — all refused — plus a conforming two-page read accepted. Fullworld_netsuite on the merged branch: 36 passed, 0 failed.