Skip to content

docs(security): record the bounded LZ4 path and the unbounded Arrow one (LAB-2504) - #275

Open
27Bslash6 wants to merge 2 commits into
mainfrom
lab-2504-document-decompression-bound
Open

docs(security): record the bounded LZ4 path and the unbounded Arrow one (LAB-2504)#275
27Bslash6 wants to merge 2 commits into
mainfrom
lab-2504-document-decompression-bound

Conversation

@27Bslash6

Copy link
Copy Markdown
Contributor

Docs-only. Part of the LAB-2504 cross-SDK decompression-bomb audit. Depends on cachekit-core#75 — the new [core-decompress] link targets an anchor that PR adds, so merge core first.

Two findings, opposite verdicts

The default read path is sound. ByteStorage.retrieve in rust/src/python_bindings.rs is a pass-through to cachekit-core's extract(), which caps output at min(512 MiB, 1000 × compressed_len) before decompressing. Worth stating because the MessagePack size caps sit on already-decompressed bytes — a reader can easily assume those caps are the protection when they are in fact downstream of it.

ArrowSerializer is not. deserialize() hands the body to pa.ipc.open_file(...).read_all(), which decompresses zstd with no size or ratio limit and never touches extract(). Reproduced end-to-end through the real serializer:

stored envelope :        2,570 bytes  ->  67,108,864 bytes  (26,112:1)
stored envelope :        8,714 bytes  -> 268,435,456 bytes  (30,805:1)

cachekit-core rejects at 1000:1. Neither existing control covers it: the [8-byte xxh3][Arrow IPC] prefix is unkeyed, and max_value_size is enforced on the write path only (cache_handler.py), making it a producer-side quota rather than a check on bytes coming back off the wire.

Documented rather than patched — the fix is a wire-size/L1-footprint decision, filed as LAB-2730 with the PoC and the ruled-out approaches. Mitigations that work today are named in the doc.

What this PR corrects about its own first draft

The expert panel caught three errors in the initial commit:

  • The listed mitigation "use compression=None" was false and actively harmful. deserialize() never reads self.compression; the reader decompresses according to the stored stream's own BodyCompression metadata, so an attacker's forged envelope declares zstd regardless of the reader's setting. Anyone who followed that advice would have believed they were protected. Now stated explicitly as a non-mitigation.
  • Exposure was wrong in both directions. Narrower: @cache.io is the CachekitIO backend preset and does not select Arrow — that needs an explicit serializer="arrow" plus the [data] extra. Wider: deserialize() also accepts raw ARROW1 bodies with no checksum at all via the legacy integrity-off branch, so an attacker need not even recompute the unkeyed prefix.
  • "A sound bound requires a Flatbuffers walk" was wrong. Uncompressed Arrow IPC allocates in proportion to its own length (measured ratio 1.000), so refusing bodies that declare BodyCompression makes len(body) a real pre-decompression bound in three lines. It costs the compression feature, which is why it is still an owner call — but the doc now says a bound exists and what it costs, rather than implying none does. The Flatbuffers walk is only needed to keep compression.

Scope

No code. The Arrow fix is LAB-2730; core's constants are LAB-2505.

Closes LAB-2504 for this repo.

…ne (LAB-2504)

Two findings from the LAB-2504 cross-SDK audit of the decompression paths.

The default read path is sound and now says so: ByteStorage.retrieve is a
pass-through to cachekit-core's extract(), which bounds output at
min(512 MiB, 1000 x compressed_len) before decompressing. Worth stating
explicitly because the msgpack caps sit on already-decompressed bytes, so a
reader can easily assume those caps are the protection when they are in fact
downstream of it.

ArrowSerializer is the exception and gets a warning. Its read path never
touches extract(): deserialize() hands the body to
pa.ipc.open_file(...).read_all(), which decompresses zstd with no size or
ratio limit. Measured 2,570 bytes -> 64 MiB (26,112:1), a ratio core rejects
at 1000:1. Neither existing control covers it: the xxHash3-64 prefix is
unkeyed so a backend-write attacker recomputes it, and max_value_size is
enforced on serialize only, making it a producer-side quota rather than a
check on what comes back off the wire.

Documented rather than patched because pyarrow exposes no sound bound --
no read-side size cap, no allocation-limiting pool, and write_table emits a
single record batch so per-batch accumulation misses the common case, while a
post-read_all() check runs after the allocation it should prevent. The real
fix reads each buffer's uncompressed-length prefix from the record-batch
Flatbuffers metadata, which is a new parser on untrusted input and wants its
own review: LAB-2730. Until then the warning names the mitigations that do
work today.
… (LAB-2504)

Expert-panel findings on the previous commit.

The listed mitigation 'use compression=None' was FALSE and actively harmful --
deserialize() never reads self.compression; pa.ipc.open_file decompresses
according to the stored stream's own BodyCompression metadata, so an attacker's
forged envelope declares zstd regardless of the reader's setting. Anyone who
followed that advice would have believed they were protected. Now stated as a
non-mitigation, explicitly.

Exposure was wrong in both directions. Narrower: @cache.io is the CachekitIO
BACKEND preset and does not select Arrow -- that needs an explicit
serializer='arrow' plus the [data] extra. Wider: deserialize() also accepts raw
ARROW1 bodies with no checksum at all via the legacy integrity-off branch, so
an attacker need not recompute the unkeyed xxh3 prefix the old text said they
would.

'A sound bound requires a Flatbuffers walk' was wrong. Uncompressed Arrow IPC
allocates in proportion to its own length (measured ratio 1.000), so refusing
bodies that declare BodyCompression makes len(body) a real pre-decompression
bound in three lines. It costs the compression feature, which is a wire-size
and L1-footprint call for the owner rather than a drive-by fix -- so the
section now says a bound exists and what it costs, instead of implying none
does. The Flatbuffers walk is only needed to KEEP compression.

Also records the second measured data point (8,714 -> 256 MiB, 30,805:1) and
trims the call-chain symbol names, which would rot on any refactor.
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 56 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 96 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Team

Run ID: 02ba3978-1366-48d8-84cb-8f18e55d467f

📥 Commits

Reviewing files that changed from the base of the PR and between 8e48846 and 328b078.

📒 Files selected for processing (1)
  • SECURITY.md

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

@kodus-27b

kodus-27b Bot commented Sep 2, 2026

Copy link
Copy Markdown

Kody Review Complete

Great news! 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

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.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

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

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.

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