Skip to content

Phase 7d: fuzz harnesses + fix fuzzer-found CBOR length overflow - #19

Merged
aaylward merged 4 commits into
mainfrom
claude/smithy-cpp-generator-plan-fpeqzt
Jul 7, 2026
Merged

aaylward merged 4 commits into
mainfrom
claude/smithy-cpp-generator-plan-fpeqzt

Conversation

@aaylward

@aaylward aaylward commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

First Phase 7d slice: libFuzzer harnesses for the parsers that face untrusted bytes — and a real memory-safety bug the CBOR harness found on its first run.

The bug (fixed here)

Decoder::DecodeChunkedString bounds-checked with pos_ + length > size_, but length is a CBOR-supplied argument up to 2^64−1. On a 64-bit host the addition overflows size_t and wraps to a small value, sailing past the guard — then std::string::assign(ptr, length) throws std::length_error (an unhandled-exception crash) or, for lengths that don't happen to throw, reads far out of bounds. Any peer sending a malformed CBOR string header could crash an rpcv2Cbor server.

Fixed to the overflow-safe length > size_ - pos_ (the pos_ <= size_ invariant is maintained by every read path) on both the definite-length and indefinite-chunk branches, with a direct cbor_test regression (7bffffffffffffffff and friends) so it stays caught without the fuzzer.

Harnesses

fuzz/ covers JSON decode, CBOR decode, URI percent-decode/encode, and full generated-server request dispatch. Each is written once and built two ways:

  • Deterministic driver (*_fuzz_smoke cc_tests): ~20k seeded pseudo-random inputs through the same entry point, running in every CI job including the ASan/UBSan matrix — reproducible, no fuzzer runtime.
  • Real libFuzzer (*_fuzz binaries, --config=fuzz, clang -fsanitize=fuzzer,address): the new fuzz CI job runs each 30s per PR.

Invariants enforced: decoders never crash/throw and round-trip whatever they accept; PercentDecode round-trips and the encoders accept arbitrary bytes; server dispatch always returns a valid HTTP status for any request. Local soak runs (35s each) after the fix are clean across all four (380k–610k execs).

Docs

docs/fuzzing.md — the harness pattern, how to soak/reproduce locally, and the invariants. Seed corpus + OSS-Fuzz remain post-0.1.0.

Validation

  • bazel test //... — 51/51 green, plus ASan+UBSan on the CBOR + fuzz targets
  • regeneration unaffected; clang-format/buildifier clean

🤖 Generated with Claude Code

https://claude.ai/code/session_01WjaNFwBZxoHdqagvq8ycQf


Generated by Claude Code

claude added 4 commits July 7, 2026 14:21
libFuzzer harnesses for the untrusted-byte parsers — JSON decode, CBOR
decode, URI percent-decode/encode, and full generated-server request
dispatch. Each builds two ways: a deterministic-driver cc_test that
runs in every CI job (including ASan/UBSan, no fuzzer runtime needed)
and a real libFuzzer binary under --config=fuzz that the new `fuzz` CI
job runs 30s per harness on every PR.

The CBOR harness immediately earned its keep: DecodeChunkedString
checked bounds with `pos_ + length > size_`, but length is an
attacker-controlled CBOR argument up to 2^64-1, so the addition
overflowed size_t and wrapped past the guard — then
string::assign(ptr, length) threw std::length_error (an unhandled
crash, and an OOB read on inputs that don't happen to throw). Fixed to
the overflow-safe `length > size_ - pos_` (pos_ <= size_ is invariant)
on both the definite and indefinite-chunk paths, with a direct
cbor_test regression pinning huge text/byte/indefinite lengths.

docs/fuzzing.md documents the harness pattern and invariants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjaNFwBZxoHdqagvq8ycQf
--config=fuzz //fuzz:all also builds the *_fuzz_smoke cc_tests, which
link the deterministic driver's main(); --config=fuzz injects
libFuzzer's own main(), so they collide with a duplicate-symbol link
error. Build each //fuzz:<name>_fuzz binary explicitly instead (the
smoke tests keep running in the plain bazel matrix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjaNFwBZxoHdqagvq8ycQf
__builtin_trap() is a GCC/Clang builtin MSVC doesn't provide, so the
server_dispatch and uri smoke-test builds failed on windows-msvc.
std::abort() (<cstdlib>) is standard and does the same job: fail loud
on an invariant violation for libFuzzer/ASan to catch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjaNFwBZxoHdqagvq8ycQf
…UTF-8

The server_dispatch fuzzer found that smithy::json::Encode terminated
the process on any string containing invalid UTF-8. nlohmann's dump()
defaults to error_handler_t::strict, which throws type_error.316 on
malformed UTF-8; nothing catches it, so std::terminate aborts the
server. Raw bytes reach Encode routinely — @httpLabel segments
(especially greedy {path+}), header values, and blobs get echoed into
response fields — so a single crafted request (e.g. GET /reports/...
with a 0xe1 byte) is a remote denial of service against any restJson1
server.

Encode now dumps with error_handler_t::replace: invalid sequences
become U+FFFD, output stays valid JSON, and the process survives.
Valid UTF-8 (including multibyte) is unaffected. Direct json_test
regression plus the fuzz corpus confirm it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjaNFwBZxoHdqagvq8ycQf
@aaylward
aaylward merged commit 9e2a95f into main Jul 7, 2026
11 checks passed
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.

2 participants