Skip to content

fix(byte-storage): pre-scan envelope bytes before decoding them (LAB-3479) - #80

Merged
27Bslash6 merged 4 commits into
mainfrom
lab-3479-envelope-pre-scan
Sep 29, 2026
Merged

27Bslash6 merged 4 commits into
mainfrom
lab-3479-envelope-pre-scan

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

ByteStorage::retrieve now runs the protocol's Retrieve Flow step-2 structural pre-scan over the envelope bytes before rmp_serde materialises a StorageEnvelope, and this crate executes the protocol's decode-bounds.json vectors 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 marker 0xc1, and truncated input are rejected. str/bin/ext payloads are skipped by offset; the only allocation is one u64 per open collection, and all counts are u64. The opcode table matches cachekit-py's check_msgpack_structure and cachekit-rs's check_structure. One deliberate difference from cachekit-py's walk: an empty collection counts as a nesting level, which is how spec/interop-mode.md → Decode bounds defines depth (cachekit-rs bounds depth in rmp_serde, which counts it the same way).
  • retrieve() and validate() both go through a new private decode_envelope(): pre-scan, then the typed decode. validate() decodes the same untrusted bytes, so it gets the same guard.
  • Error contract unchanged. A pre-scan rejection is ByteStorageError::DeserializationFailed whose message starts with decode 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's InvalidInput.
  • Conformance test. tests/decode_bounds_vectors.rs vendors test-vectors/decode-bounds.json 1.1.0 byte-identical from protocol 1729eb7e, pins its sha256 and the 17/3 vector counts, and drives every vector through retrieve():
    • each reject vector must fail with the decode pre-scan: prefix. A bare is_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;
    • each accept vector must get past the pre-scan (the typed decode still rejects them, since they are not envelopes);
    • a real map-form envelope with an unknown key nested exactly 100 deep decodes successfully on explicit 2 MiB and 1 MiB threads, and 101 deep is rejected by the pre-scan.
  • Docs. SECURITY.md gets 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 warnings and cargo fmt --check: clean. cargo build --target wasm32-unknown-unknown --features wasm: builds.
  • The new tests also pass on i686-unknown-linux-musl (32-bit usize).
  • Mutation check: removing the pre-scan call, counting depth on arrays only, counting depth on non-empty collections only, charging one slot per map pair, checking each header only against the bytes after it, and raising the bound to 1024 each fail at least one test.
  • Cost: for a bin envelope 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.rs so that it describes accurately how this walk's depth accounting differs from the sibling implementations.

Changes

  • src/msgpack_bounds.rs (module docs):
    • The previous comment said that both cachekit-py's check_msgpack_structure and cachekit-rs's check_structure do not count empty collections as nesting levels.
    • The revised comment limits that difference to cachekit-py. It states that this walk counts an empty collection as a nesting level, which matches the spec's definition of depth.
    • It adds that cachekit-rs enforces depth through rmp_serde rather than in its structural walk.

Impact

  • No functional, behavioral, or public API changes.
  • The opcode table, the nesting bound constant, and the decode logic are unchanged.

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::retrieve and ByteStorage::validate before rmp_serde decodes them into a StorageEnvelope. The input can no longer set how deep the decoder recurses (serde's derive skips unknown map keys with a recursive IgnoredAny). The decoder also no longer pre-allocates containers based on lengths the input declares but cannot back.

Changes

Public API behavior

  • ByteStorage::retrieve and ByteStorage::validate: signatures are unchanged. Both now route decoding through a new internal decode_envelope helper, which runs the pre-scan first.
  • A pre-scan rejection returns the existing ByteStorageError::DeserializationFailed variant, with a message prefixed decode pre-scan: . Bindings that map on the error variant see no difference.
  • Trailing bytes after the envelope are still ignored, as before.
  • retrieve now has an # Errors doc section describing this behavior.

New internal module: src/msgpack_bounds.rs

This module is crate-private. It is gated on the compression, checksum and messagepack features. It provides check_msgpack_structure(bytes, max_depth) and MAX_DEPTH = 100.

The walk reads headers only. It skips str/bin/ext payloads by offset and allocates one u64 per open collection. All counts use u64.

It rejects:

  • Nesting deeper than 100 levels. Every array or map header counts as a level, including empty ones.
  • Oversized str/bin/ext lengths: a header declaring more payload bytes than remain in the input.
  • Oversized element counts: pending collection elements, summed across all open collections, exceeding the remaining bytes.
  • The reserved marker 0xc1.
  • Truncated input.

Unit tests cover the following:

  • every scalar marker class
  • truncations
  • inclusive depth bounds for arrays and maps, including empty ones
  • overflow-safe handling of the widest length claims
  • acceptance of real and legacy envelopes, and rejection of every strict prefix of them

Tests

tests/decode_bounds_vectors.rs runs the protocol's vendored tests/vectors/decode-bounds.json (v1.1.0, 17 reject and 3 accept vectors, sha256-pinned) through retrieve.

  • Each reject vector must fail with the decode pre-scan: prefix. Depth-only and overclaim-only vectors must also give the specific matching reason.
  • validate must return false for every reject vector.
  • Accept vectors must not be rejected by the pre-scan.
  • An envelope nested exactly MAX_DEPTH deep 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 deserialize StorageEnvelope directly 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

  • Security
    • retrieve() and validate() now check MessagePack structure before decoding, rejecting truncated input, impossible length claims, reserved markers and nesting deeper than 100 levels. Trailing bytes remain accepted.
    • Pre-scan failures return a deserialization error; validation returns false when decoding or extraction fails.
  • Documentation
    • Added details on decode limits, error handling and protocol-vector coverage.
  • Tests
    • Added reference vectors covering accepted inputs, boundary cases and rejected inputs.

…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.
@kodus-27b

kodus-27b Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ✅

Access your configuration settings here.

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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 configuration

Configuration used: Repository: cachekit-io/cachekit-core/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: b7176bc1-f65a-462b-a0fe-6101b3b4266a

📥 Commits

Reviewing files that changed from the base of the PR and between 0ea290b and 1b5a92b.

📒 Files selected for processing (4)
  • README.md
  • SECURITY.md
  • src/msgpack_bounds.rs
  • tests/decode_bounds_vectors.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

A structural MessagePack pre-scan now runs before typed envelope decoding in ByteStorage::retrieve and ByteStorage::validate. It checks nesting depth, framing, and declared sizes. The change adds versioned decode-bounds vectors, tests, and documentation.

Changes

Envelope decode bounds

Layer / File(s) Summary
Structural scan and bounds checks
src/msgpack_bounds.rs, src/lib.rs
A structural checker enforces depth and input-backed size limits before typed decoding. Tests cover marker classes, truncation, depth limits, oversized declarations, trailing bytes, and envelope prefixes.
Envelope decoding integration
src/byte_storage.rs
retrieve and validate use a shared decode path. Pre-scan failures return DeserializationFailed with the decode pre-scan: prefix.
Protocol vectors and decode contract
tests/decode_bounds_vectors.rs, tests/vectors/decode-bounds.json, .gitattributes, README.md, SECURITY.md
Versioned vectors and feature-gated tests cover accepted and rejected inputs. Documentation describes the pre-scan checks and error contract. .gitattributes identifies both vector files as byte-exact.

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
Loading

Merge Risk: ⚪ Minimal · up to 1b5a9

No concrete issue remains that would prevent merging after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 1b5a9

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
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The inspected exposure is a forged or corrupted envelope supplied to existing ByteStorage operations, with potential process-resource impact during decoding. The supplied evidence does not establish a tenant, deployment, or binding-level exposure scope.

Security Findings and Attack Paths

  • inferred — For the inspected retrieval and validation paths, attacker-supplied structural overclaims and excessive nesting now encounter the checker before typed deserialization. No introduced or worsened attack path was established in those paths.

Trust Boundaries and Controls

  • observed — The existing serialized-input size checks precede the new structural gate; successful scans proceed to typed decoding and then the existing extraction checks. The pre-scan does not authenticate an envelope or replace decompression bounds.

Resilience and Maintainability Implications

  • observed — The documented serialized and decompressed size ceilings can still permit substantial per-call memory use in constrained runtimes. Those ceilings predate this pre-scan; its structural checks do not lower them.

Hardening Proposals

  • proposed — Callers accepting untrusted envelopes outside ByteStorage could use a guarded decode path and set size limits appropriate to their runtime, rather than assuming direct StorageEnvelope deserialization receives this protection.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes a functional pre-scan change, but the stated PR objectives describe comment-only documentation clarifications. The title does not match the reported main change. Retitle the pull request to describe the documentation-only clarification, including the depth-counting behaviour and the enforcement location.
Docstring Coverage ⚠️ Warning 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: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

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.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

kodus-27b[bot]
kodus-27b Bot previously approved these changes Sep 29, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Sep 29, 2026
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.
@kodus-27b

kodus-27b Bot commented Sep 29, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ✅

Access your configuration settings here.

kodus-27b[bot]
kodus-27b Bot previously approved these changes Sep 29, 2026
cachekit-rs bounds depth in rmp_serde, not in check_structure; only cachekit-py's walk skips empty collections. Comment-only.
@kodus-27b

kodus-27b Bot commented Sep 29, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ✅

Access your configuration settings here.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

@kody review --force

@kodus-27b

kodus-27b Bot commented Sep 29, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ✅

Access your configuration settings here.

@27Bslash6
27Bslash6 merged commit a043e13 into main Sep 29, 2026
34 checks passed
@27Bslash6
27Bslash6 deleted the lab-3479-envelope-pre-scan branch September 29, 2026 14:59
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