Skip to content

fix(fuzz): fuzz the codec that ships — core 0.4.0, all 14 targets, fail loudly (LAB-1136) - #251

Merged
27Bslash6 merged 3 commits into
mainfrom
lab-1136-fuzz-ci-real-evidence
Jul 29, 2026
Merged

fix(fuzz): fuzz the codec that ships — core 0.4.0, all 14 targets, fail loudly (LAB-1136)#251
27Bslash6 merged 3 commits into
mainfrom
lab-1136-fuzz-ci-real-evidence

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Closes LAB-1136.

What

The fuzz harness has been a false safety signal for three minor versions of cachekit-core: the fuzz workspace pinned =0.2.1 (own [workspace], so parent bumps never propagate), the smoke job hardcoded 4 of 14 targets, and no failure path existed for a target that fails to build or run. LAB-900 shipped the 0.4.0 msgpack bin envelope citing a green fuzz job that had fuzzed the old array-of-ints codec.

  • rust/fuzz/Cargo.toml: cachekit-core =0.2.1 → =0.4.0 (same feature set, exact pin kept and now documented). Cargo.lock is now committed (was gitignored — with CARGO_HOME=/tmp every CI run freshly resolved all transitives on the self-hosted runner).
  • Compile fixes for 0.4.0: StorageEnvelope::new(Vec<u8>) → new(&[u8]) at four call sites (the two the ticket named plus two in byte_storage_format_injection.rs); two stale error-message assertions updated (0.4.0 reworded its error variants — the invariants themselves held); one tautological assertion deleted.
  • fuzz-smoke.yml: builds all targets once, runs every target from cargo fuzz list (no hand-maintained allow-list to rot), fails on any non-zero build/run exit, guards against a failing/empty fuzz list (command substitution in a for-word does not propagate under set -e), pins cargo-fuzz 0.13.2, timeout 20 → 45 min.
  • security-deep.yml extended fuzz: finding — yes, the feature flags were erroring. --features compression,checksum against a crate with no [features] table makes cargo error before any fuzzing, and || true swallowed it: the nominal 3×1h job was a no-op. Flags dropped, || true removed, timeout 3600-- -max_total_time=3600 (clean hour exits 0, crash/build error fails), crash check retained, if: failure() artifact upload added, dead coverage step deleted, timeout 200 → 240 min with the arithmetic in a comment.

Budget arithmetic (fuzz-smoke)

~2 min rustup + ~10 min cargo-fuzz install (no cross-run cache) + ~10 min one-shot ASAN build + 14 × 60 s fuzzing ≈ 36 min worst case → timeout-minutes: 45.

Target list — cargo fuzz build result (local, nightly-2026-04-27, cargo-fuzz 0.13.2)

All 14 build and each ran clean under a smoke run (none excluded):

target build run
byte_storage_checksum_collision
byte_storage_compress
byte_storage_corrupted_envelope
byte_storage_decompress
byte_storage_empty_data
byte_storage_format_injection
byte_storage_integer_overflow
encryption_aad_injection
encryption_key_derivation
encryption_large_payload
encryption_nonce_reuse
encryption_roundtrip
encryption_truncated_ciphertext
integration_layered_security

Negative proof (the job can fail)

With compile_error!("negative proof: job must go red") appended to encryption_roundtrip.rs, both new steps were executed locally under GitHub Actions shell semantics (bash --noprofile --norc -e -o pipefail):

  • Build all fuzz targets step → exit 1
  • run-loop step → exit 1

The old workflow turned the same condition into a ::warning:: plus a marker file that nothing ever read.

Expert panel

Panel ran (bug-hunter-supreme, security-specialist, code-craftsman, catchphrase-agent) on the first commit; surviving findings applied in the second commit — including a CRIT (the for t in $(cargo fuzz list) empty-list silent-pass), the uncommitted fuzz lockfile (CWE-829), cargo-fuzz version pinning, the security-deep timeout under-budget, and the missing crash-artifact upload. One out-of-scope finding (Atheris job's || true swallow + crash artifacts landing outside the checked corpus dir) is filed separately rather than expanding this PR.

Docs

No docs needed: rust/fuzz/README.md and rust/fuzz/Makefile already document/enumerate all 14 targets; DEVELOPMENT.md fuzzing instructions are generic. The only stale text (core 0.1.1 / closed #114 comments) lived in the workflows and is deleted here. Repo-wide sweep for =0.2.1 / core 0.1.1 / #114 is clean.

Summary by CodeRabbit

  • Tests

    • Expanded fuzz testing to discover and run all available targets automatically.
    • Fuzz failures now stop the workflow immediately, with crash and timeout artefacts retained for investigation.
    • Updated tests to remain reliable across revised error messages and extreme input sizes.
  • Chores

    • Standardised fuzzing tools and aligned fuzz tests with the current core release.
    • Improved fuzzing workflow time limits and build-artifact handling.

…l loudly (LAB-1136)

The fuzz workspace resolved deps independently of the parent crate
(own [workspace]), so the core 0.4.0 bump never reached it: every fuzz
iteration since has exercised the old 0.2.1 array-of-ints codec, and the
msgpack bin envelope decode path had zero fuzz coverage.

- rust/fuzz/Cargo.toml: cachekit-core =0.2.1 -> =0.4.0 (same features)
- Fix 0.3.0 StorageEnvelope::new(Vec<u8>) -> new(&[u8]) at four call
  sites (checksum_collision, format_injection x2, layered_security)
- Update two stale error-message assertions to 0.4.0's reworded
  variants ('integrity check failed', 'input exceeds maximum size');
  the invariants themselves (corruption rejected, size limits enforced)
  still hold
- fuzz-smoke.yml: build all targets once, run every target from
  'cargo fuzz list' (no hand-maintained allow-list to rot), and let any
  non-zero build/run exit fail the job - the old .fuzz_failures marker
  was written but never read, so a broken target could not turn the job
  red. Timeout 20 -> 45 min (14 targets x 60s = 14 min fuzzing plus
  ~20 min toolchain install + ASAN builds on an uncached runner).
- security-deep.yml: the extended-fuzz steps passed --features
  compression,checksum to a crate with no [features] table, so cargo
  errored before fuzzing and '|| true' swallowed it - the nominal
  3x1h job was a no-op. Drop the bogus flags, replace 'timeout 3600
  ... || true' with '-- -max_total_time=3600' so a clean hour exits 0
  and a build error or crash fails the job.
- Delete stale comments citing core 0.1.1 and closed #114
…des (LAB-1136)

Panel (bug-hunter, security, craftsman, catchphrase) on the first commit:

- fuzz-smoke.yml: resolve 'cargo fuzz list' in a standalone assignment
  and fail on an empty list — 'for t in $(cmd)' does not propagate a
  failing $(cmd) under set -e, so a broken list would loop zero times
  and go green having fuzzed nothing [CRIT]
- Commit rust/fuzz/Cargo.lock (was gitignored): with CARGO_HOME=/tmp
  every CI run freshly resolved all fuzz-workspace transitives to
  newest release on a persistent self-hosted runner [MAJ, CWE-829]
- Pin cargo-fuzz --version 0.13.2 in both workflows (floating install
  on self-hosted runner; same pattern as kani-verifier) [MAJ]
- security-deep.yml: timeout 200 -> 240 min with budget arithmetic
  (-max_total_time is pure fuzz time; old 'timeout 3600' capped wall
  time and the job never ran honestly anyway) [MAJ]
- security-deep.yml: add if:failure() crash-artifact upload — a crash
  now fails the fuzz step itself, and without upload the reproducer
  died with the ephemeral runner [MAJ]
- security-deep.yml: delete dead 'Generate coverage report' step
  (output never uploaded or read; reintroduced the || true swallow)
- security-deep.yml: state the real reason for the 3-of-14 subset
  (time budget) now the stale #114 justification is gone
- checksum_collision.rs:85: dead 'Checksum' arm -> 'integrity check
  failed' (no 0.4.0 message contains capital-C Checksum)
- integer_overflow.rs: delete tautological message assert (every 0.4.0
  error matched it); the invariant is no-panic on extreme sizes
- fuzz/Cargo.toml: document why the exact =0.4.0 pin exists and that it
  must move with every parent-crate core bump
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 35 minutes

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

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 97591b40-c5d2-4201-9811-37d0c6bcfb72

📥 Commits

Reviewing files that changed from the base of the PR and between 6df13f6 and cdb146c.

📒 Files selected for processing (4)
  • .github/workflows/fuzz-smoke.yml
  • .github/workflows/security-deep.yml
  • rust/fuzz/Makefile
  • rust/fuzz/fuzz_targets/byte_storage_checksum_collision.rs

Walkthrough

Fuzzing workflows now use pinned tooling, dynamic or explicit target execution, stricter failure handling, and crash-artifact uploads. The fuzz crate pins cachekit-core 0.4.0, while fuzz targets borrow inputs and accept updated error behaviour.

Changes

Fuzzing alignment

Layer / File(s) Summary
Pin fuzzing dependencies and build outputs
.github/workflows/..., rust/fuzz/Cargo.toml, rust/fuzz/.gitignore
The fuzz crate pins cachekit-core to =0.4.0; build output is ignored and Cargo.lock is tracked.
Update fuzz target inputs and assertions
rust/fuzz/fuzz_targets/*
Envelope construction borrows input buffers, integrity-error expectations use updated messages, and oversized-input checks accept any extraction error without requiring specific wording.
Enforce fuzzing workflow execution
.github/workflows/fuzz-smoke.yml, .github/workflows/security-deep.yml
Both workflows pin cargo-fuzz 0.13.2; smoke fuzzing discovers targets dynamically, deep fuzzing runs selected targets with explicit budgets, failures stop jobs, and failing runs upload crash artifacts. Coverage generation is removed.

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

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions as GitHub Actions
  participant CargoFuzz as cargo fuzz
  participant Targets as fuzz targets
  participant Artifacts as crash artifacts

  GitHubActions->>CargoFuzz: install pinned cargo-fuzz and build targets
  GitHubActions->>CargoFuzz: list or select targets
  CargoFuzz->>Targets: run with time budget
  Targets-->>GitHubActions: return success or failure
  GitHubActions->>Artifacts: upload artifacts on failure
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed but doesn't follow the required template and omits several mandatory sections like Type of Change and the checklists. Rewrite it using the repo template: add Description, Motivation, Type of Change, Security Checklist, Testing, Backward Compatibility, and Additional Notes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarises the main change: fuzzing updates for cachekit-core 0.4.0 across all 14 targets with stricter failure handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lab-1136-fuzz-ci-real-evidence

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

@kodus-27b

This comment has been minimized.

@27Bslash6 27Bslash6 changed the title LAB-1136: fuzz the codec that ships — core 0.4.0, all 14 targets, fail loudly fix(fuzz): fuzz the codec that ships — core 0.4.0, all 14 targets, fail loudly (LAB-1136) Jul 29, 2026
kodus-27b[bot]
kodus-27b Bot previously approved these changes Jul 29, 2026
@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 29, 2026
…LAB-1136)

Second expert-panel pass (crypto/protocol gate). Each item below is a way the
hardened gate could still assert less than it claims.

Workflows:
- Enforce the committed fuzz lockfile. `cargo fuzz build/run` have no --locked
  passthrough, so the lockfile added here was advisory to the only commands that
  consume it: on any manifest/lock drift cargo would silently re-resolve every
  transitive to newest-on-crates.io, on a self-hosted runner with an empty
  CARGO_HOME. A `cargo fetch --locked` step now fails loudly on drift. Without
  it the CWE-829 fix was a lockfile nobody checked.
- Guard fuzz_targets/ vs [[bin]] parity. `cargo fuzz list` enumerates Cargo.toml
  stanzas, NOT files, so a new fuzz_targets/*.rs without its stanza is never
  built and never run and the job still goes green -- the same silent-dark mode
  the old hardcoded array caused. Nothing in cargo enforces agreement; assert it.
  The old comment claimed the derived list made new targets impossible to lose,
  which overstated what deriving from Cargo.toml actually buys.
- Build before fuzzing in security-deep.yml, so a compile error surfaces in
  ~10 min as a build failure rather than three hours in as a fuzz-step error.
  Its budget comment already itemised a build step that did not exist.
- Delete the "Check for crashes" step. With `|| true` gone a crash fails its own
  fuzz step and kills the job, so this step could only run in the no-crash case
  and print a reassuring green -- a named step structurally incapable of
  failing, which is precisely the manufactured evidence this PR set out to
  remove. It also ran `find artifacts` without creating the dir, swallowing the
  error. The fuzz steps' exit codes are the signal.
- Cut PR crash-artifact retention 30d -> 3d. This repo is public and the job runs
  on pull_request, so the artifact is a working reproducer for an unfixed defect
  in the shipped compression/AES-GCM path, downloadable by anyone for a month.
- Correct the nightly-pin comments: they justified the pin with "cargo-fuzz
  0.13.1" while both jobs install 0.13.2 -- stale on arrival, in the PR that
  exists to kill stale comments.

byte_storage_checksum_collision.rs:
- Both Ok(_) arms asserted nothing, so the one bug this target exists to find --
  a corrupted payload extracting successfully as *different* data -- passed
  silently. The real invariant is reject-or-return-original; assert it, and for
  the forged all-0xFF checksum assert the genuine checksum really was 0xFF.
- Match error variants instead of Display text. Asserting on err.to_string()
  means a #[error(...)] reword in a core release fails this gate on a false
  crash; the sibling integer_overflow target already dropped that pattern.
- Widen flip_byte_idx u8 -> u32: payloads run to 4096 bytes, so a u8 index
  confined every bit flip this target could ever generate to the first 256.
- Comments said "Blake3"; core 0.4.0 uses xxHash3-64 ("19x faster than Blake3",
  byte_storage.rs:6) and checksum is [u8; 8]. Naming a cryptographic hash where
  a 64-bit non-cryptographic one lives invites an assumption of collision
  resistance this code does not have.

rust/fuzz/Makefile:
- `make quick`/`make deep` still ran every target with `|| true`, so the local
  counterpart of this gate swallowed build and run failures exactly as CI did;
  only crash artifacts could fail it. Now `|| exit 1` (pipefail is already set
  at the top of the file, so it propagates through the tee).
- FUZZ_TARGETS was a hand-maintained 14-target list -- the same drift hazard
  removed from the workflow, three directories away. Derived from the [[bin]]
  stanzas now, recursively expanded so no cargo/grep runs for `make help`.

Verified: cargo check --locked --all-targets clean on all 14 targets;
cargo fetch --locked succeeds against the committed lock (so the new
enforcement step passes rather than spuriously failing); both workflows parse
and pass actionlint/shellcheck; Makefile derives the same 14 target names.
@kodus-27b

kodus-27b Bot commented Jul 29, 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.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@27Bslash6
27Bslash6 merged commit 511b1e5 into main Jul 29, 2026
35 checks passed
@27Bslash6
27Bslash6 deleted the lab-1136-fuzz-ci-real-evidence branch July 29, 2026 13:26
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.

chore: update 9 stale fuzz targets for cachekit-core 0.1.1 API

1 participant