Skip to content

std.cbor: decode limits and duplicate-key refusal by default, deterministic encoding - #3469

Merged
paul-hammant merged 5 commits into
mainfrom
feat/cbor-decode-limits
Oct 30, 2026
Merged

paul-hammant merged 5 commits into
mainfrom
feat/cbor-decode-limits

Conversation

@paul-hammant

@paul-hammant paul-hammant commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

std.cbor gets ready to decode bytes from an untrusted peer, such as the planned sandboxed-child IPC, where the parent decodes whatever the child sends.

  • CborDecodeOptions / default_decode_options() / parse_with(data, opts), following std/tar's ExtractOptions. Defaults: depth 256 (unchanged), 131072 array elements and 131072 map pairs (fxamacker/cbor's defaults), duplicate keys refused, indefinite lengths allowed.
  • parse refuses duplicate map keys (breaking, on purpose): {"grant":"none","grant":"all"} is an error, not a guess about which value wins. Keys are duplicates when they are the same value however encoded, so a definite and an indefinite "a" collide. parse_with with allow_duplicate_keys = 1 gives the old behaviour.
  • A declared string length, array count or map count that can't fit in the bytes left is refused before anything is allocated, including counts near 2^64.
  • encode_deterministic: RFC 8949 §4.2.1 core deterministic encoding (shortest heads and floats, definite lengths, map keys in bytewise order of their encodings, duplicate keys an error). encode still writes a parsed value back byte for byte (cbor/msgpack: trailing bytes accepted, cbor not-well-formed input accepted, cbor diagnose lossy #2470).

Drafted by Jules (paul-hammant#44): options, limits, deterministic encoder, and test_cbor_hardening.ae with vectors cited from fxamacker/cbor. Its PR lists a disable-the-protection check for each limit.

Shaped here (second commit):

  • The duplicate-key check and the key sort were quadratic. A map at the 131072-pair limit, about 400 KB, cost billions of comparisons, turning the limit meant to make hostile input cheap to refuse into a way to burn CPU. Duplicates are now found by merge-sorting the keys' deterministic encodings and comparing neighbours (a std.map set would collide with cbor's own map_get), and encode_deterministic merge-sorts keys too. The new test_large_maps parses and sorts 131072 keys; the whole hardening test takes 0.84 s, and against the first commit's code it was still running when killed at 90 s.
  • Restored comments the first commit removed (why lengths are compared in long, why counts are checked against the bytes left, parse's cbor/msgpack: trailing bytes accepted, cbor not-well-formed input accepted, cbor diagnose lossy #2470 note).
  • Changelog fragment rewritten; docs/api regenerated.

Checked: test_cbor (all RFC 8949 Appendix A vectors), test_cbor_hardening and tests/regression/test_cbor_roundtrip_no_leak.ae pass; both std tests are valgrind-clean (0 errors); ae fmt, make check-docs, check-changelog and check-tests pass.

🤖 Generated with Claude Code

google-labs-jules Bot and others added 5 commits October 29, 2026 18:43
…coder

- Add CborDecodeOptions with safe default limits and parse_with
- Refuse duplicate map keys by default comparing decoded key values
- Enforce bounds on declared string lengths and container counts before looping
- Implement encode_deterministic following RFC 8949 Core Deterministic Encoding
- Add test_cbor_hardening.ae and update docs and changelog

Co-authored-by: paul-hammant <82182+paul-hammant@users.noreply.github.com>
The duplicate-key check compared each key with every earlier one, and
encode_deterministic sorted keys with an insertion sort. Both are quadratic,
so a map at the default 131072-pair limit (about 400 KB) cost billions of
comparisons: the input parse_with exists to refuse cheaply became a way to
spend minutes of CPU. Duplicates are now found through a set keyed by the
hex of each key's deterministic encoding (equal values encode equally,
however they were written), and keys are merge-sorted. test_large_maps
parses and sorts 131072 keys in under a second; on the quadratic version it
was still running when killed at 90 s.

Comments the first commit removed are back: why lengths are compared in
long, why counts are checked against the bytes left, parse's #2470 note.
Changelog fragment rewritten; docs/api regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
check-raw-calls refuses a std module calling another module's raw C
binding (map_put_raw).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Importing std.map into std.cbor fails check-warnings: std.map's get
mangles to map_get, the symbol of cbor's own map_get. Once a map is
parsed, its keys' deterministic encodings are now merge-sorted and
neighbours compared, which is still n log n. test_large_maps still parses
and sorts 131072 keys in about a second; with the check disabled, the
four duplicate-key cases fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@paul-hammant
paul-hammant merged commit 5e15f16 into main Oct 30, 2026
45 checks passed
@paul-hammant
paul-hammant deleted the feat/cbor-decode-limits branch October 30, 2026 01:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant