Skip to content

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

Closed

Description

@dezren39

Problem

pkgs/id/src/tuple.rs decode / decode_one (380, 393) accepts integer and float encodings that the encoder can never produce and that the positive-int branch explicitly refuses — so a crafted or corrupted tuple key decodes to a different value than its tag claims, silently.

Verified this run by replicating the decoder's exact arithmetic (no code compiled on the sandbox):

1. Negative ints have no range check — they wrap to zero or positive

Positive branch (tuple.rs:446-461) checks the payload:

ensure!(i64::try_from(v).is_ok(), "positive integer exceeds i64::MAX");

Negative branch (tuple.rs:464-478) has no equivalent check on abs before 0u64.wrapping_sub(abs) as i64:

  • tag 0x13 (1-byte negative), payload 0xFF → ones-complement abs = 0 → decodes to Int(0) — zero via a negative tag (canonical zero is the single 0x14 tag).
  • tag 0x0C (8-byte negative), payload 0x00 × 8 → abs = u64::MAX → 0 - abs wraps → decodes to Int(1) — a positive value from a negative tag, at the negative end of the sort order.
  • tag 0x0C, payload 0x7F 0x00… → abs = 9295429630892703743 (> 2^63) → decodes to Int(9151314442816847873), again positive.

The encoder (push_int, tuple.rs:~335-350) can only emit abs in 1..=2^63 for values i64::MIN..=-1.

2. Floats decode to NaN, which the encoder refuses

Encoder float() (tuple.rs:260-264) rejects NaN ("NaN cannot be encoded in tuple keys"; TupleValue::Float is documented "NaN excluded"). The FLOAT64 decode branch (tuple.rs:482-499) reverses the sort transform and returns f64::from_be_bytes(b) with no is_nan check — e.g. payload 21 FF F8 00 00 00 00 00 00 decodes to Float(NaN).

3. Non-minimal int encodings are accepted

Both int branches accept leading-zero payloads the encoder's byte_len minimal encoding never emits — e.g. tag 0x16 (2-byte positive) payload 00 01 decodes to Int(1), whose canonical encoding is 0x15 01. Two distinct keys therefore decode to the same value.

Why it matters

Tuple keys are the sort-preserving keys for the tag store: tags.rs decode_alpha_key / decode_omega_key (tags.rs:585, 609) run decode over keys read back from the namespace documents and trust the decoded subject/key/value. A key that decodes to a value outside its tag's range breaks the encoding's core invariant — byte order preserves semantic order (module doc, tuple.rs:1-25) — and does so silently: the positive branch proves the authors intended out-of-range payloads to be an error, the negative branch just misses it. This is the same asymmetry class as the store's other canonical-decoding rules (e.g. chain.rs refuses non-canonical encodings outright).

Acceptance criteria

  • Decoding a negative-int payload with abs == 0 or abs > 2^63 returns an error (mirroring the positive branch's range check), with unit tests for abs == 0, abs == 2^63 (valid, i64::MIN), abs == 2^63 + 1, and abs == u64::MAX.
  • Decoding a float payload that reverses to NaN returns an error, with a unit test using a crafted NaN payload.
  • Non-minimal int encodings (a payload whose first byte makes a shorter encoding possible) are either refused with tests, or explicitly documented as accepted — one behaviour, pinned.
  • All existing tuple round-trip and sort-order tests still pass; no encoder change (encodings on disk/in documents are unchanged).

Activity

  1. developing-today commented on Oct 28, 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 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: filed

    • Agent: new-issue-creator (new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b · 2026-10-28 01:20 CDT run
    • Found via: filed this run
    • Task: Filed tuple decoder canonicality bug: negative-int branch lacks the positive branch's range check (wraps to 0/positive), NaN floats decode despite encoder refusal, non-minimal int encodings accepted. Enqueued for triage.
    • Expectation: effort/s — triage confirms the fix scope (decoder range/NaN checks + tests) in pkgs/id/src/tuple.rs; no encoder change.
    • Links: packet enqueued to repo-health-code-pilot (see backlog entry)
  4. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: filed (addendum)

    • Agent: new-issue-creator (new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b · 2026-10-28 01:20 CDT run
    • Found via: filed this run
    • Task: Addendum to this issue, same decoder: decode_one's NESTED branch (tuple.rs:428-438) recurses with no depth limit — the nested: bool parameter carries no depth, and there is no counter anywhere in decode/decode_one. A crafted key of N repeats of the 0x05 nested tag (plus terminators) recurses N deep and overflows the stack (a process abort, not a catchable error) in anything that decodes stored keys, e.g. tags.rs decode_alpha_key/decode_omega_key. Contrast chain.rs:39,350: CBOR decoding of received events sets RECURSION_LIMIT: 64 for exactly this reason. Suggested addition to the fix scope: thread a depth limit through decode_one (a small constant in the spirit of chain's 64) and refuse deeper nesting with an error; add a test with a deeply-nested payload.
    • Expectation: effort/s — folds into the same triage/fix as the int/float canonicality items; no separate issue filed (same decoder, same hardening change).
    • Links: 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
  5. added
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 28, 2026
  6. 55 remaining items

  7. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

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

    @dezren39
    MemberAuthor

    🤖 Agent note · done

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