Skip to content

fix(serializers): bound forged-entry error echoes; retire columnar dead code (LAB-3131) - #289

Merged
27Bslash6 merged 2 commits into
mainfrom
lab-3131-bound-forged-echoes-retire-columnar-preamble
Sep 14, 2026
Merged

27Bslash6 merged 2 commits into
mainfrom
lab-3131-bound-forged-echoes-retire-columnar-preamble

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR addresses a log-injection/denial-of-service vulnerability (LAB-3131) where forged cache entries could produce unbounded error log output, and removes dead code from the columnar serialization paths.

Problem

When a poisoned or corrupted cache entry is read, the resulting exception text is influenced by attacker-controlled bytes. Prior mitigations (#276) capped some per-field echoes (marker, column name), but _dtype_from_untrusted still echoed the full forged dtype string. As a result, a forged entry carrying a large dtype (or column name folded up through nested {e} wraps) could inflate a single log line to megabytes — measured at up to a 4.19 MB log line (505x amplification) from an 8.3 KB envelope. Attacker-controlled text could also inject newlines or ANSI/terminal control sequences into log records.

Changes

New bounded_error() helper (serializers/base.py)

  • Introduces ERROR_ECHO_MAX (512 chars) and a bounded_error() function that:
    • Clips over-length error text to a fixed size, appending the true character count for forensics.
    • Escapes all line/terminal-control characters (C0 controls, DEL, C1 range including NEL, and Unicode U+2028/U+2029 separators) so a poisoned read always produces exactly one terminal-safe log line.
  • Clipping happens before escaping, keeping output O(1) regardless of payload size.

Applied at trust-boundary wrap sites

  • cache_handler.py: decrypt/integrity failure warnings, authentication failure errors, and deserialization/interop error messages now route exception text through bounded_error().
  • decorators/wrapper.py: L1 deserialization failure warnings (both sync and async paths) now use bounded_error().

Retire columnar dead code (auto_serializer.py)

  • Introduces _column_trio() as a single shared writer for column/Series markers, used by both _serialize_dataframe and _serialize_series, keeping the marker set and key order in one place (on-wire byte compatibility preserved).
  • _deserialize_dataframe and _deserialize_series now accept an already-decoded document instead of raw bytes, removing the dead bytes-preamble branch that duplicated msgpack decoding. This makes the _expect shape gate reachable in production, so a forged non-dict body is properly refused.

Tests

  • New TestForgedEntryErrorEchoIsBounded verifying clipping, control-char neutralization, and bounded log lines end-to-end (through handle_decrypt_failure and a full L2 read path).
  • New test confirming forged non-dict document bodies are refused by the shape gate.
  • Updated existing serializer tests to call _decode_columnar (the new decoded-document contract).

Mark S added 2 commits September 13, 2026 22:56
…ad code (LAB-3131)

Three trust-boundary hygiene fixes on the DataFrame/Series columnar decode
path, all failing closed already — no change to what is accepted or rejected,
and wire bytes are unchanged (byte-verified against the interop fixtures).

1. Bound untrusted error echoes at the read-path wrap sites. #276 capped the
   per-field marker/column-name echoes, but _dtype_from_untrusted still echoed
   the full forged dtype (numpy's own message + str(dtype)), so a poisoned entry
   drove log volume with payload size (measured pre-#276: a 1 MiB column name
   produced a 4.19 MB log line). Add base.bounded_error() and apply it once at
   each site that logs or re-wraps an untrusted-decode failure
   (cache_handler.handle_decrypt_failure, deserialize_data, _deserialize_interop;
   decorators/wrapper L1 guards) — a global O(1) bound, not per field.

2. Retire the dead bytes preamble in _deserialize_dataframe/_deserialize_series.
   Every production caller decodes under unpackb_bounded first, so the decoders
   now take a decoded document only; the "document is <type>, expected dict"
   shape gate is now reachable in production and covered.

3. Fold the writer trio into one _column_trio(series) beside its _column_values
   decoder mirror, so a third marker cannot be added to one side only.

Tests: bounded-log assertion with a 1 MiB column name + 4 KB forged dtype;
forged non-dict body cases; four direct-call tests updated to the decoded-doc
contract. Full unit suite + interop wire vectors green.
- bounded_error now escapes every C0/C1 control char, DEL and U+2028/U+2029,
  not just \n/\r — the helper enforces its own "one terminal-safe line"
  invariant instead of relying on each caller's text being pre-escaped
  (panel MAJ: ANSI/other separators could still split a log line).
- _column_trio docstring: state the object marker is 2 keys ({type, data}),
  numeric is 3 ({type, data, dtype}); "trio" names the maximal numeric form.
- Tests: assert control-char neutralisation; tighten the bounded-log asserts
  to ERROR_ECHO_MAX; add an end-to-end L2-read test (get_cached_value ->
  _handle_l2_read_error -> handle_decrypt_failure) so a future unbounded log
  added ahead of the bounded site is caught.

Rejected (stated for the record): bound-at-construction instead of at the wrap
sites — contradicts the ticket's explicit "global bound at the {e} wrap sites,
not per field" design; the re-raised messages are already born bounded as
belt-and-suspenders.
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Team

Run ID: 7239dde0-c65e-48e1-9f8c-c3bc934eff29

📥 Commits

Reviewing files that changed from the base of the PR and between 6b89577 and 51f3b65.

📒 Files selected for processing (7)
  • .secrets.baseline
  • src/cachekit/cache_handler.py
  • src/cachekit/decorators/wrapper.py
  • src/cachekit/serializers/auto_serializer.py
  • src/cachekit/serializers/base.py
  • tests/unit/test_auto_serializer_mutation_and_corruption.py
  • tests/unit/test_auto_serializer_new_types.py

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.


Walkthrough

The change bounds exception text in cache and wrapper logs, centralises columnar marker encoding, and requires decoded documents for DataFrame and Series deserialisation. Tests cover corrupted payloads, safe error output, and updated decoding paths. The secret baseline metadata is refreshed.

Changes

Serialization hardening

Layer / File(s) Summary
Bounded error handling
src/cachekit/serializers/base.py, src/cachekit/cache_handler.py, src/cachekit/decorators/wrapper.py, tests/unit/test_auto_serializer_mutation_and_corruption.py
bounded_error limits exception text to 512 characters, records truncation length, and escapes control characters. Cache and L1 warning paths use the bounded output. Tests cover bounded error and integrity-failure logging.
Columnar encoding and decoding contract
src/cachekit/serializers/auto_serializer.py, tests/unit/test_auto_serializer_mutation_and_corruption.py, tests/unit/test_auto_serializer_new_types.py
DataFrame and Series serialisation share _column_trio. Their deserialisers validate already decoded dictionaries. Tests cover forged documents and the decoded-document path.

Secret baseline maintenance

Layer / File(s) Summary
Baseline reference update
.secrets.baseline
The recorded source line reference and baseline generation timestamp are updated.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 51f3b

The bounded error handling remains within its documented contract, with no actionable merge-blocking issue identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 6 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the two main changes: bounded forged-entry error echoes and removal of obsolete columnar code.
Description check ✅ Passed The description clearly explains the motivation, scope, acceptance criteria, testing, compatibility impact, and documentation status. It does not use all template headings or checklist boxes, but it p…
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 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 6 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lab-3131-bound-forged-echoes-retire-columnar-preamble

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

@kodus-27b

kodus-27b Bot commented Sep 13, 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.

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

Comment thread tests/unit/test_auto_serializer_mutation_and_corruption.py
@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.61538% with 4 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/cachekit/cache_handler.py 60.00% 2 Missing ⚠️
src/cachekit/decorators/wrapper.py 33.33% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@27Bslash6
27Bslash6 merged commit 10a1049 into main Sep 14, 2026
37 checks passed
@27Bslash6
27Bslash6 deleted the lab-3131-bound-forged-echoes-retire-columnar-preamble branch September 14, 2026 06:55
27Bslash6 pushed a commit that referenced this pull request Sep 14, 2026
Resolves conflicts against main@284fa7e:
- cache_handler.py / wrapper.py: main (#289) wraps echoed exception text
  in bounded_error() at the read-path log and re-raise sites; this branch
  renders exceptions at every log sink via redact_error_for_log(), which
  emits no exception text at all. Log lines keep redact_error_for_log
  (strictly tighter than the bound, and it also redacts the key); the two
  SerializationError re-raise sites take main's bounded_error(), which the
  branch had left as raw {e}. wrapper.py no longer needs the bounded_error
  import; bounded_error's docstring updated to match.
- pyproject.toml: both sides bumped pip>=26.2; main's file is the superset.
- .secrets.baseline: generated; took main's side, hook regenerated it.

Merge (not rebase) so history stays append-only.
27Bslash6 pushed a commit that referenced this pull request Sep 14, 2026
Resolve the LAB-3131 (#289) overlap in the columnar decode path:

- src/cachekit/serializers/auto_serializer.py: both sides retired the dead isinstance(data, dict) preamble in _deserialize_dataframe/_deserialize_series; keep main's parameter naming and docstrings. The PR's deserialize() collapse and EnvelopeIntegrityError handling are unchanged.

- tests/unit/test_auto_serializer_new_types.py: both sides adapted the four TestColumnarFallbackExtensionDtypes tests to the decoded-document contract; keep the PR's public serialize()/deserialize() round-trip, which routes through main's _decode_columnar.
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