fix(byte-storage): pre-scan envelope bytes before decoding them (LAB-3479) - #80
Conversation
…3479) The protocol's Retrieve Flow step 2 requires a structural pre-scan of the envelope bytes before StorageEnvelope is materialised. retrieve() and validate() now share decode_envelope(), which runs a header-only MessagePack walk first: nesting deeper than 100 levels, headers declaring more elements or bytes than the input can back, the reserved 0xc1 marker and truncated input are rejected without allocating. The walk matches the cachekit-py and cachekit-rs opcode tables, and also counts an empty collection as a level, as the spec defines depth. A rejection stays ByteStorageError::DeserializationFailed, with the message prefix "decode pre-scan: ", so no binding sees a new variant. tests/decode_bounds_vectors.rs vendors protocol decode-bounds.json 1.1.0 (sha256-pinned, provenance 1729eb7e) and drives all 17 reject and 3 accept vectors through retrieve(), asserting the pre-scan prefix rather than bare failure. It also decodes a real map-form envelope nested exactly at the bound on 2 MiB and 1 MiB threads. SECURITY.md and README document the bound.
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Providing Context (Files & MCPs)Add these hints in your PR description (or a comment) to unlock deeper checks:
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: cachekit-io/cachekit-core/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (4)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. WalkthroughA structural MessagePack pre-scan now runs before typed envelope decoding in ChangesEnvelope decode bounds
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant ByteStorage
participant decode_envelope
participant check_msgpack_structure
participant MessagePackDecoder
ByteStorage->>decode_envelope: Decode envelope bytes
decode_envelope->>check_msgpack_structure: Check structure with MAX_DEPTH
check_msgpack_structure-->>decode_envelope: Return structural check result
decode_envelope->>MessagePackDecoder: Deserialize checked bytes
Merge Risk: ⚪ Minimal · up to No concrete issue remains that would prevent merging after normal checks. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change adds a protective check to existing retrieval and validation paths without exposing a new public entrypoint. No introduced security concern was established, although direct envelope deserialization remains outside this protection. Retained concerns Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 76.92% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 26 functions across 4 files. (2 skipped: 2 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The walk counts an empty collection as a level, which the py/rs walks do not, so it does not reject exactly the same documents. retrieve is reached by cachekit-py and cachekit-ts, not cachekit-rs. The README no longer says the pre-scan allocates nothing. Comment-only; no behaviour change.
40ed93a
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Providing Context (Files & MCPs)Add these hints in your PR description (or a comment) to unlock deeper checks:
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
cachekit-rs bounds depth in rmp_serde, not in check_structure; only cachekit-py's walk skips empty collections. Comment-only.
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Providing Context (Files & MCPs)Add these hints in your PR description (or a comment) to unlock deeper checks:
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
|
@kody review --force |
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Providing Context (Files & MCPs)Add these hints in your PR description (or a comment) to unlock deeper checks:
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
ByteStorage::retrievenow runs the protocol's Retrieve Flow step-2 structural pre-scan over the envelope bytes beforermp_serdematerialises aStorageEnvelope, and this crate executes the protocol'sdecode-bounds.jsonvectors through it.What changed
src/msgpack_bounds.rs(crate-private). A header-only MessagePack walk: nesting deeper than 100 levels, headers declaring more elements or bytes than the input can back, the reserved marker0xc1, and truncated input are rejected. str/bin/ext payloads are skipped by offset; the only allocation is oneu64per open collection, and all counts areu64. The opcode table matches cachekit-py'scheck_msgpack_structureand cachekit-rs'scheck_structure. One deliberate difference from cachekit-py's walk: an empty collection counts as a nesting level, which is howspec/interop-mode.md→ Decode bounds defines depth (cachekit-rs bounds depth inrmp_serde, which counts it the same way).retrieve()andvalidate()both go through a new privatedecode_envelope(): pre-scan, then the typed decode.validate()decodes the same untrusted bytes, so it gets the same guard.ByteStorageError::DeserializationFailedwhose message starts withdecode pre-scan:. No variant is added (the enum is not#[non_exhaustive]), and bindings that map on the variant behave exactly as before, including the FFI'sInvalidInput.tests/decode_bounds_vectors.rsvendorstest-vectors/decode-bounds.json1.1.0byte-identical from protocol1729eb7e, pins its sha256 and the 17/3 vector counts, and drives every vector throughretrieve():decode pre-scan:prefix. A bareis_err()would pass without any guard, because the decoder fails on these inputs too. Depth-only vectors must hit the depth bound, and overclaim-only vectors the slot budget;SECURITY.mdgets an "Envelope decode bounds" section next to "Decompression limits". The README gets a matching Security entry, a note on the vector test, and the new module in the architecture tree.Out of scope: exposing the walk as public API,
deny_unknown_fields, and trailing-byte handling (still ignored, as before).Verification
cargo test,cargo test --all-features,cargo test --features ffi: green.cargo +1.85 test --all-features(MSRV): green.cargo clippy --all-features --all-targets -- -D warningsandcargo fmt --check: clean.cargo build --target wasm32-unknown-unknown --features wasm: builds.i686-unknown-linux-musl(32-bitusize).binenvelope the walk reads a handful of headers whatever the payload size. A legacy array-of-ints envelope pays one extra linear pass over its int elements, comparable to the decode itself.Summary
This change is documentation-only. It corrects the module-level doc comment in
src/msgpack_bounds.rsso that it describes accurately how this walk's depth accounting differs from the sibling implementations.Changes
src/msgpack_bounds.rs(module docs):check_msgpack_structureand cachekit-rs'scheck_structuredo not count empty collections as nesting levels.rmp_serderather than in its structural walk.Impact
Note
The PR title refers to pre-scanning envelope bytes before decoding. The diff provided contains only this documentation correction and no code changes that implement or alter a pre-scan.
Summary
Adds a structural pre-scan over untrusted envelope bytes. It runs in
ByteStorage::retrieveandByteStorage::validatebeforermp_serdedecodes them into aStorageEnvelope. The input can no longer set how deep the decoder recurses (serde's derive skips unknown map keys with a recursiveIgnoredAny). The decoder also no longer pre-allocates containers based on lengths the input declares but cannot back.Changes
Public API behavior
ByteStorage::retrieveandByteStorage::validate: signatures are unchanged. Both now route decoding through a new internaldecode_envelopehelper, which runs the pre-scan first.ByteStorageError::DeserializationFailedvariant, with a message prefixeddecode pre-scan:. Bindings that map on the error variant see no difference.retrievenow has an# Errorsdoc section describing this behavior.New internal module:
src/msgpack_bounds.rsThis module is crate-private. It is gated on the
compression,checksumandmessagepackfeatures. It providescheck_msgpack_structure(bytes, max_depth)andMAX_DEPTH = 100.The walk reads headers only. It skips str/bin/ext payloads by offset and allocates one
u64per open collection. All counts useu64.It rejects:
0xc1.Unit tests cover the following:
Tests
tests/decode_bounds_vectors.rsruns the protocol's vendoredtests/vectors/decode-bounds.json(v1.1.0, 17 reject and 3 accept vectors, sha256-pinned) throughretrieve.decode pre-scan:prefix. Depth-only and overclaim-only vectors must also give the specific matching reason.validatemust returnfalsefor every reject vector.MAX_DEPTHdeep must decode successfully on 2 MiB and 1 MiB thread stacks. One level deeper must be rejected by the pre-scan.Documentation
SECURITY.md: new "Envelope decode bounds" section. It notes that callers who deserializeStorageEnvelopedirectly bypass the pre-scan.README.md: new "Envelope Decode Bounds" section, the new module added to the architecture tree, and a description of the new test..gitattributes: the comment now also names the new test that pins the vendored vectors.Summary by CodeRabbit
retrieve()andvalidate()now check MessagePack structure before decoding, rejecting truncated input, impossible length claims, reserved markers and nesting deeper than 100 levels. Trailing bytes remain accepted.falsewhen decoding or extraction fails.