Skip to content

Add fuzz properties for HTTP/1 and HTTP/2 connections - #520

Merged
ericmj merged 6 commits into
mainfrom
http-fuzz-properties
Sep 27, 2026
Merged

ericmj merged 6 commits into
mainfrom
http-fuzz-properties

Conversation

@ericmj

@ericmj ericmj commented Sep 27, 2026

Copy link
Copy Markdown
Member

Builds on #519, which includes #518. Their commits are included here until they merge, and the HTTP/2 property fails without them. The six fuzz commits are meant to be rebase-merged rather than squashed.

Both properties use StreamData to generate whole connection scenarios. The server's bytes are fed in random chunk sizes through stream/2 (active mode) or recv/3 over the real socket (passive mode), interleaved with client calls.

  • test/mint/http1/fuzz_test.exs pipelines one to three requests against random responses. The responses include interim responses, HTTP/1.0, invalid status lines and headers, content-length, chunked with extensions and trailers, close-delimited bodies, and byte mutations.
    • Client calls: streamed request bodies and new requests.
    • Scenarios vary stream_headers, max_header_list_size, case_sensitive_headers, optional_responses and CONNECT requests.
  • test/mint/http2/fuzz_test.exs sends random server frames: every frame type, padding, CONTINUATION, valid and invalid header lists, PUSH_PROMISE with valid and invalid stream IDs, and raw bytes.
    • Client calls: cancel, ping, new requests, settings changes and streamed bodies.
    • The server keeps its own view of the connection window.

The properties check that nothing raises and that every response belongs to a known request in the order status, headers, data, trailers, done. They also check the following:

  • Nothing follows done or an error.
  • A closed or errored connection has no open requests and isn't writable.
  • open_request_count/1 and the HTTP/2 stream counters match the unfinished requests.
  • A completed body matches its content-length, and HEAD, 204 and 304 responses have no body.
  • The HTTP/2 connection window matches what the server sent and received.

FUZZ_RUNS sets the number of scenarios. The defaults are 100 for HTTP/1 (about 0.3 s) and 10 for HTTP/2 (about 3 s).

Run against v1.10.1, every one of 20 seeds × 300 runs fails for both properties. The failure classes are:

  • HTTP/1:
    • stream_request_body/3 raising FunctionClauseError;
    • pipelined requests left open after a connection-closing response;
    • :done for a request that had no status yet.
  • HTTP/2:
    • DATA before HEADERS;
    • an off-by-one connection window with padded DATA;
    • {:stream_not_found, id} for PUSH_PROMISE on a closed stream;
    • promised refs missing from the ref index;
    • {:done, ref} from RST_STREAM NO_ERROR before any headers.

With the current fixes, both pass 2,000 runs on several seeds.

@coveralls

coveralls commented Sep 27, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 4

Coverage increased (+0.2%) to 90.374%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1953
Covered Lines: 1765
Line Coverage: 90.37%
Coverage Strength: 712.82 hits per line

💛 - Coveralls

@ericmj
ericmj marked this pull request as ready for review September 27, 2026 18:50
The property pipelines one to three requests and feeds stream/2 randomly
generated responses (interim responses, HTTP/1.0 and HTTP/1.1, valid and
invalid status lines and headers, content-length, chunked with
extensions and trailers, and close-delimited bodies, with optional byte
mutations) in random chunk sizes, then checks that stream/2 never
raises, that every response belongs to a known request in the order
status, headers, data, trailers, done, that nothing follows done or an
error, and that a closed connection has no open requests left.

FUZZ_RUNS sets the number of scenarios; the default is 100.
…_headers

Scenarios now pick active or passive mode, feeding bytes through
recv/3 over the real socket in passive mode, randomly enable
stream_headers, stream request bodies with stream_request_body/3 and
open new requests while responses arrive. The harness accepts its own
socket so listen sockets are closed after each scenario.
Scenarios now vary max_header_list_size, case_sensitive_headers and
optional_responses, include CONNECT requests, and check that a completed
response has exactly the body its content-length announced, that HEAD,
204, 304 and CONNECT 2xx responses have no body, and that
open_request_count/1 matches the number of unfinished requests.
The property opens one to three requests against the test server and
feeds stream/2 a random sequence of server frames (HEADERS with valid
and invalid header lists, CONTINUATION, padding, DATA, PUSH_PROMISE with
valid and invalid stream IDs, RST_STREAM, SETTINGS, PING, GOAWAY,
WINDOW_UPDATE, PRIORITY, unknown frame types and raw bytes) in random
chunk sizes, interleaved with cancel_request/2, stream_request_body/3
and ping/2 calls. It checks that stream/2 never raises, that every
response belongs to a known request in the order status, headers, data,
trailers, done, that nothing follows done, an error or a cancel, that
the connection is not writable after an error, and that the stream
counters and the ref-to-stream index match the stream map.

FUZZ_RUNS sets the number of scenarios; the default is 10.
…and passive mode

Server actions are now encoded as the scenario runs, so client actions
can open requests, change client settings and stream request bodies
between server frames, and HEADERS frames can carry random bytes as
their header block. Scenarios also pick active or passive mode, feeding
bytes over the real socket with recv/3 in passive mode.
Scenarios now vary the request method, the client settings, the
connection window and its refill threshold, and the server's handshake
settings. The server keeps its own view of the connection window from
the DATA payloads it sent and the WINDOW_UPDATE frames it received, and
the property checks it against Mint's window after every segment, that
increments are within 1..2^31-1, that a completed response's body size
matches a valid content-length, that HEAD, 204 and 304 responses have no
body, and that open_request_count/1 matches the unfinished requests.
@ericmj
ericmj force-pushed the http-fuzz-properties branch from ea3de22 to 4f337e5 Compare September 27, 2026 20:00
@ericmj
ericmj merged commit bf2455f into main Sep 27, 2026
3 checks passed
@ericmj
ericmj deleted the http-fuzz-properties branch September 27, 2026 20:55
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