Skip to content

id(tuple): decode recurses on NESTED with no depth limit — a crafted 100 KB tag key (50k nested tags) aborts the process with a stack overflow #2121

Closed

Description

@dezren39

Problem

tuple::decode recurses on the NESTED tag with no depth limit, so a crafted
key of repeated 0x05 bytes aborts the whole process with a stack overflow —
an error no caller can catch or recover from.

Evidence (main 42796be5)

  • pkgs/id/src/tuple.rs:380 — pub fn decode(bytes: &[u8]) -> Result<Vec<TupleValue>>
    walks the input via decode_one.
  • pkgs/id/src/tuple.rs:393,428 — decode_one's NESTED arm loops by calling
    itself recursively (match decode_one(data, pos, true)?). There is no depth
    counter, no input-length guard, and no cap anywhere in the file (the only
    MAX-style checks in the decoder are the per-field canonicality rules added
    for id(tuple): decode accepts negative ints that wrap to zero/positive and NaN floats — the negative branch has no range check, unlike the positive branch #1610).
  • Contrast in the same crate: pkgs/id/src/chain.rs decodes peer-supplied CBOR
    with an explicit .recursion_limit(64) (RECURSION_LIMIT, chain.rs:39,350).
    The tuple decoder — which parses the same class of untrusted bytes — has no
    equivalent.
  • Reachability: the decoder runs on iroh-docs entry keys, which are written by
    other replicas of a shared namespace: pkgs/id/src/tags.rs:585,609
    (decode_alpha_key / decode_omega_key call decode(raw) on the raw key)
    and tags.rs:1401-1415 (entry_to_tag_alpha / entry_to_tag_omega, called
    from the tag query paths at tags.rs:1240,1281,1383,1395). One crafted key in
    a synced α/Ω doc crashes every node that lists/searches that namespace's tags.

Reproduction (verified this run)

Probe crate on the laptop (~/git/fleet/probe-2026-10-29-tuple-depth/) that
compiles the crate's own tuple.rs at main 42796be5 verbatim
(md5 8fd9b15126d0772d28bb984bd86f83f2, identical in both clones) and calls
tuple::decode on [0x05; N] ++ [0x00; N]:

  • N=1,000 and N=10,000 on a 2 MiB stack: decode OK.
  • N=50,000 (a 100 KB input) on a 2 MiB stack: thread has overflowed its stack / fatal runtime error: stack overflow, aborting, exit 134 (SIGABRT).
  • N=100,000 on an 8 MiB stack: same abort, exit 134.

A stack overflow in Rust is a process abort, not a Result::Err — the
?-based error handling in every caller is irrelevant once it trips.

Why it matters

Tag namespaces sync between peers; the α/Ω key space is not written only by
this node. A single malicious or corrupt entry key turns a routine tag
list/search on any replica into a full process crash (denial of service that
repeats on every query until the entry is removed by other means). The crate
already treats this exact risk as real for CBOR headers (recursion limit 64);
tuple keys deserve the same bound.

Acceptance criteria

  • tuple::decode rejects nesting deeper than a named, documented limit
    (consistent with the crate's CBOR RECURSION_LIMIT of 64, or a justified
    value) with a normal Err, at any input size and on a small (e.g. 2 MiB)
    stack — no abort.
  • Regression tests: [0x05; 100_000] ++ [0x00; 100_000], a just-over-limit
    nested value, and a legitimately nested value at/under the limit (round-trip
    through the encoder) — the first two error, the last decodes.
  • decode_alpha_key / decode_omega_key surface the depth error as an
    ordinary tag error (no panic/abort) on a crafted doc entry key.
  • The encoder side is considered too: if the encoder can still produce
    nesting deeper than the decoder accepts, that asymmetry is either fixed or
    explicitly documented.

Activity

  1. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  2. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    effort/sSmall: expected under 2 hours of work
    on Oct 29, 2026
  3. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-10-29T05:05Z (America/Chicago)
    • Found via: filed this run
    • Task: Filed this issue after verifying the unbounded NESTED recursion in tuple::decode with a laptop probe compiling the crate's own tuple.rs at main 42796be5 (50k nesting aborts on a 2 MiB stack, 100k on 8 MiB, exit 134); labelled and enqueued a triage packet to repo-health-code-pilot to pin the depth limit.
    • Expectation: effort/s — triage pins the limit (chain.rs RECURSION_LIMIT=64 precedent) and routes a small-fix; done when over-limit nested keys return a normal Err instead of aborting the process.
    • Links: evidence hidden_files/issue-code-tuple-nested-depth-body.md; probe ~/git/fleet/probe-2026-10-29-tuple-depth/ (laptop); packet hidden_files/queue-pilot/packet-2121-triage.json
  4. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    Corroborating evidence from new-issue-creator-code-b (2026-10-30 run, main 0281d10): re-verified with a verbatim-copy probe of pkgs/id/src/tuple.rs on the laptop (~/git/fleet/probe-2026-10-30-laneB-tupledepth/).

    • Main thread (8 MiB stack): depth 10,000 nested tags decode Ok; depth 50,000 (100 KB input) → thread 'main' has overflowed its stack, process abort (exit 134).
    • Spawned thread with the default 2 MiB stack (the tokio worker/blocking-thread case, which is where tag query decodes run): depth 10,000 Ok; depth 20,000 (40 KB crafted key) → stack overflow abort.

    So the abort threshold on a realistic worker thread is ~40 KB of crafted key, not 100 KB. Control: a legit depth-3 nested tuple decodes Ok. No depth limit exists in decode/decode_one (tuple.rs:383-448).

  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  6. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  7. 95 remaining items

  8. removed
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    event: picked-up
    task: 5100 (repo-health-code-pilot, OpenChamber scheduled lane)
    state: claimed
    verified: Fresh state confirms #2121 OPEN and assigned to Drew; PR #4083 is OPEN on the task branch and authored by Drew. Prior saved progress explicitly says not to touch Drew-owned PR/worktree. No files or PR changes made.
    next: park 30 minutes; resume only after explicit release/handoff, then revalidate checks and reviews.

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

    @dezren39
    MemberAuthor

    agent-note:v1
    event: parked
    task: 5100 (repo-health-code-pilot, OpenChamber scheduled lane)
    state: parked until 2026-11-02T06:42:30Z
    blocker: #2121 remains assigned to Drew and PR #4083 is open on the task branch with Drew as author. The saved hard-stop forbids touching that PR or worktree. No source or PR changes were made.
    resume: after Drew explicitly releases or hands off the work; then revalidate #2121, PR #4083, checks and reviews. Queue task parked for 30 minutes.

  12. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
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)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator 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