Repository navigation
Description
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), payload0xFF→ ones-complementabs = 0→ decodes toInt(0)— zero via a negative tag (canonical zero is the single0x14tag). - tag
0x0C(8-byte negative), payload0x00 × 8→abs = u64::MAX→0 - abswraps → decodes toInt(1)— a positive value from a negative tag, at the negative end of the sort order. - tag
0x0C, payload0x7F 0x00…→abs = 9295429630892703743(> 2^63) → decodes toInt(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 == 0orabs > 2^63returns an error (mirroring the positive branch's range check), with unit tests forabs == 0,abs == 2^63(valid,i64::MIN),abs == 2^63 + 1, andabs == 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
developing-today commentedon Oct 28, 2026
CODE-634
dezren39 commentedon Oct 28, 2026
🤖 Agent note · state: filed
dezren39 commentedon Oct 28, 2026
🤖 Agent note · state: filed (addendum)
decode_one's NESTED branch (tuple.rs:428-438) recurses with no depth limit — thenested: boolparameter carries no depth, and there is no counter anywhere indecode/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.rsdecode_alpha_key/decode_omega_key. Contrastchain.rs:39,350: CBOR decoding of received events setsRECURSION_LIMIT: 64for exactly this reason. Suggested addition to the fix scope: thread a depth limit throughdecode_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.55 remaining items
dezren39 commentedon Oct 28, 2026
🤖 Agent note · parked
fix(id/tuple): decode range-checks negative ints, rejects NaN and non…
dezren39 commentedon Oct 28, 2026
🤖 Agent note · done