Skip to content

Testing & CI hardening phase 2: fuzz the HTTP/1.1 parser, even out malformed-server coverage (#48) - #54

Merged
aaylward merged 1 commit into
mainfrom
claude/issue-48-ndwegf
Jul 8, 2026
Merged

aaylward merged 1 commit into
mainfrom
claude/issue-48-ndwegf

Conversation

@aaylward

@aaylward aaylward commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

What

Phase 2 of #48 (after #53), taking the two hostile-input items:

HTTP-parser fuzzing. The hand-rolled HTTP/1.1 message reader was the one network-facing parser with no fuzz coverage. It moves out of socket_transport.cc into a pure, callback-fed function (smithy/http/http1.h) behind the same transports — byte-for-byte behavior-preserving (all existing socket/hostile-framing tests pass unchanged). On top of it:

  • //fuzz:http1_fuzz: a libFuzzer harness that replays wire bytes in varying chunk sizes so the incremental header/body accumulation paths get exercised, wired into the CI fuzz smoke loop and make fuzz-smoke (plus the deterministic //fuzz:http1_fuzz_smoke cc_test that runs in every bazel matrix job).
  • http1_hostile_test.cc: a hostile bank at the pure-parser level — request-smuggling framing (duplicate/conflicting Content-Length, transfer-encoding, TE+CL), hostile content-lengths, malformed header blocks, an every-strict-prefix truncation property, header floods, the accept side (padding/case-insensitivity/body-until-EOF), and the start-line helpers. No sockets, so unlike socket_transport_hostile_test.cc it also runs on Windows.
  • Hardening found while banking: an empty Content-Length and a +4-style signed one previously parsed as valid lengths (strtoull laxity); the parser now requires digits-only per RFC 9110.

Malformed-server evenness. Only jsonrpc2 had a malformed-server suite (its model carries httpMalformedRequestTests; the alloy and official rpcv2Cbor suites carry none). New hand-written suites — outside the golden generated/ trees, so the byte-identical regeneration check is untouched — pin the generated servers' reject paths before the handler runs:

  • simpleRestJson (PizzaAdminService): 404/405 routing, unparseable JSON body → 400 + x-error-type: SerializationException, wrong content-type → 415, and the suite-exact enum-violation ValidationException message + fieldList path.
  • The simpleRestJson @pattern-violation wire message Tracking: testing & CI hardening #48 explicitly called out as unasserted, via the roundtrip REST fixture's pattern-constrained SinkId — plus the exact length-violation message.
  • rpcv2Cbor (RpcV2Protocol): missing/wrong smithy-protocol header → 400 SerializationException, wrong content-type → 415 UnsupportedMediaTypeException, truncated and non-map CBOR bodies → 400, 404/405 routing — asserting the CBOR __type and protocol headers on every error response.

Also: CI's clang-format check (and make lint/format) now covers protocol-tests outside generated/.

Remaining on #48: golden self-ratification, generator-class unit tests, union×{cbor, jsonRpc2} conformance cells, coverage tooling, benchmark gating, cross-platform sanitizers, executing-rule Bazel tests beyond the compile harness.

Testing

  • bazel test //... green locally — 72 tests (up from 67: the http1 hostile bank, the http1 fuzz smoke, and the three malformed-server suites), with the Boost/Beast targets and //benchmarks/... excluded per docs/development.md's proxy note; CI covers those.
  • A 15-second real-libFuzzer + ASan run of the new harness: 160k executions, no findings.
  • The parser extraction is verified behavior-preserving by the pre-existing socket_transport_test and socket_transport_hostile_test (9 raw-socket framing attacks) passing unchanged.
  • No Java changes; goldens untouched.

Checklist

  • Tests added/updated for the change
  • bazel test //... passes locally (Boost/benchmark targets excluded per docs; CI covers them); codegen untouched, gradle job unaffected
  • Formatting clean (clang-format, buildifier; spotless N/A — no Java changes)
  • Architectural decisions recorded as an ADR (not applicable — the parser extraction is an internal refactor with no consumer-facing surface change)

🤖 Generated with Claude Code

https://claude.ai/code/session_012VFUppN1idpmU1UrbHkknZ


Generated by Claude Code

…lformed-server coverage

Continues issue #48 after #53, taking the two hostile-input items:

- The hand-rolled HTTP/1.1 message reader — the one network-facing parser
  with no fuzz coverage — moves out of socket_transport.cc into a pure,
  callback-fed function (smithy/http/http1.h) behind the same transports,
  byte-for-byte behavior-preserving. On top of it:
  * //fuzz:http1_fuzz, a libFuzzer harness that replays the wire bytes in
    varying chunk sizes so the incremental header/body accumulation paths
    are exercised, wired into the CI fuzz smoke loop and `make fuzz-smoke`
    (and the deterministic-driver //fuzz:http1_fuzz_smoke variant that runs
    in every bazel matrix job).
  * http1_hostile_test.cc, a platform-independent hostile bank at the pure
    parser level: request-smuggling framing (duplicate/conflicting CL,
    transfer-encoding, TE+CL), hostile content-lengths, malformed header
    blocks, every-strict-prefix truncation, header floods, plus the
    accept-side (padding, case-insensitivity, body-until-EOF) and the
    start-line helpers. Unlike socket_transport_hostile_test.cc it needs no
    sockets, so it also runs on Windows.
  Hardening found while banking: an empty Content-Length and a "+4"-style
  signed one previously parsed as valid lengths (strtoull laxity); the
  parser now requires digits-only per RFC 9110.

- Malformed-server coverage was jsonrpc2-only (its model carries
  httpMalformedRequestTests; the alloy and official rpcv2Cbor suites carry
  none). New hand-written suites — outside the golden generated/ trees —
  pin the generated servers' reject paths before the handler runs:
  * simpleRestJson (PizzaAdminService): 404/405 routing, unparseable JSON
    body -> 400 + x-error-type SerializationException, wrong content-type
    -> 415, and the suite-exact enum-violation ValidationException message.
  * simpleRestJson @pattern-violation wire message (issue #48's explicit
    gap) via the roundtrip REST fixture's pattern-constrained SinkId,
    plus the exact length-violation message.
  * rpcv2Cbor (RpcV2Protocol): missing/wrong smithy-protocol header -> 400
    SerializationException, wrong content-type -> 415
    UnsupportedMediaTypeException, truncated and non-map CBOR bodies,
    404/405 routing — asserting the CBOR __type and protocol headers on
    every error response.

CI's clang-format check (and make lint/format) now covers protocol-tests
outside generated/.

Verified locally: bazel test //... green (72 tests; Boost/benchmark targets
excluded per docs/development.md's proxy note, CI covers them), plus a 15s
real-libFuzzer ASan run of the new harness (160k execs, no findings).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012VFUppN1idpmU1UrbHkknZ
@aaylward
aaylward merged commit 477a52c into main Jul 8, 2026
12 checks passed
aaylward pushed a commit that referenced this pull request Jul 8, 2026
…overage tooling

Continues issue #48 after #53 and #54:

- Union x {rpcv2Cbor, jsonRpc2} conformance (protocol-tests/unions/): union
  round-tripping was only pinned for simpleRestJson; the other two cells
  relied on the seeded random integration tests, which flip a coin on
  whether the union appears at all and can only prove serde
  self-consistency — a symmetric encode/decode bug passes them. The new
  suites pin the wire subdocument for every SinkChoice variant
  deterministically, in all four directions per protocol (client encode,
  client decode, server decode, server echo), plus the reject cells
  (empty, multi-member, unknown-member, null-member, non-map unions), the
  exactly-one-member wire invariant, and the __type discriminator
  tolerance. Both suites were mutation-checked: one expectation flipped
  per suite fails the suite, so a first-run pass is not vacuous.

- Code-coverage tooling (issue #48's "module gaps only findable by manual
  srcs<->tests correspondence"): a coverage CI job runs bazel coverage
  with a combined lcov report over the runtime, prints the per-module
  summary in the log, and uploads the rendered HTML as a build artifact;
  `make coverage` runs the same locally. Measurement only, no threshold
  gate yet — same posture as the benchmarks job.

Verified locally: bazel test //... green (74 tests; Boost/benchmark targets
excluded per docs/development.md's proxy note, CI covers them), and
bazel coverage --combined_report=lcov produces a 21-file runtime report.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012VFUppN1idpmU1UrbHkknZ
aaylward pushed a commit that referenced this pull request Jul 8, 2026
… pinning, macOS sanitizers

Continues issue #48 (after #53, #54, #55) and works through issue #56:

- Union member-type gauntlet (protocol-tests/unions/): SinkChoice only
  covers string/int/struct members, so a generated-in-graph UnionGauntlet
  service (same Bazel path as codegen/compile-tests) extends the union x
  {rpcv2Cbor, jsonRpc2} cells to blob, timestamp, list, map, enum, intEnum,
  and recursive-struct members. Each member kind is pinned with a
  hand-built typed value on one side and a hand-built wire document on the
  other — never both ends produced by the code under test — through client
  encode, the full client<->server loopback loop, and the wire subdocument.
  Protocol-specific representations are pinned where they differ (blobs as
  base64 text and timestamps as epoch-seconds numbers in JSON; blob and
  tag-1 timestamp nodes in CBOR).
- Byte-exact request vectors: the CBOR body pinned as hex derived by hand
  from RFC 8949's deterministic encoding rules, and the full JSON-RPC
  envelope pinned as its exact compact sorted-key text — closing the seam
  Document-level equality leaves between "codec is deterministic" and
  "serde feeds the codec what we think" (issue #56).
- Error-shape union cell: a modeled error whose union member rides next to
  its __type discriminator, inspected on the wire (the payload keeps both)
  and round-tripped into the typed error detail on the client — the case
  the serde's exactly-one-member arithmetic exists to tolerate. That
  contract is now documented in docs/generated-types.md, giving the
  tolerance test an authority beyond the implementation (issue #56).
- Reject-cell diagnosis pinning (issue #56): the phase-3 union reject cells
  now assert which rule was violated ("expected exactly one union member",
  "unknown or missing union member", "expected a map on the wire") on both
  client and server, not merely that a rejection happened.
- Sanitizers x platforms (issue #48): the asan+ubsan CI job is now a
  linux/clang + macos/apple-clang matrix, putting the transport layer's
  Apple-specific paths (SO_NOSIGPIPE, libc++) under a sanitizer for the
  first time. MSVC ASan under Bazel remains future work.

Also confirmed while scoping: the Gradle toolchain works in this
environment (gradle build + the Java unit suite pass), so the
generator-class unit-test item is unblocked for a next phase.

Model note: recursion *through* a union (Node carrying a BigUnion) is
rejected by the generator with a clear diagnostic (std::variant needs
complete alternatives); the gauntlet documents that boundary and covers a
self-recursive struct as a union member instead.

Verified locally: bazel test //... green (76 tests; Boost/benchmark targets
excluded per docs/development.md's proxy note, CI covers them).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012VFUppN1idpmU1UrbHkknZ
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