Adversarial fuzz tests written from RFC 9113 turned up eight live
conformance gaps. Each fix is pinned by the test that found it.
Four are about GOAWAY, and they compound. A peer could revoke its own
GOAWAY simply by sending a SETTINGS frame: mark_preface_received moved
the connection to Open unconditionally, and the client reads every
SETTINGS as the peer's preface, so new streams were admitted on a
connection the peer had already disowned -- and a GOAWAY we sent for a
protocol error was undone the same way. A GOAWAY smuggled onto a
non-zero stream id was honoured as a shutdown, so whoever can place one
frame on any stream got to dictate the last-stream-id, which is what
decides whether an in-flight request may be retried. A second GOAWAY
could raise that id, which the RFC forbids precisely because the streams
above it may already have been retried elsewhere. And streams the peer
opened after our own GOAWAY were registered rather than discarded,
pinning attacker-controlled state on a connection already declared dead.
The rest: an overflowing WINDOW_UPDATE was dropped silently rather than
terminating the stream or connection (sec 6.9.1), so the two ends
disagreed about the window; SETTINGS_MAX_CONCURRENT_STREAMS was
advertised and never enforced, leaving the number decoration and the
stream-state bound imaginary (sec 5.1.2); DATA on a stream that never
existed was accepted (sec 5.1); and a trailer block without END_STREAM
was accepted as "unusual but valid" when sec 8.1 makes it malformed,
leaving a message that never ends.
Those last three end in a stream error, so the connection survives --
which is exactly why each one still HPACK-decodes the block it rejects.
HPACK is a running context shared by every stream (sec 4.3), so skipping
a block would strand our decoder behind the peer's encoder and corrupt
every later stream. The same reasoning applies to the discarded
post-GOAWAY streams, where sec 6.8 spells it out.
Two primitives fell out. Connection.is_idle_stream tells an id that was
never opened from one that was opened and has closed, which sec 5.1
treats differently: only the idle case is an error, since the closed
case is the benign race with an RST_STREAM still in flight.
ROLE.message_opened marks the header block that opens a message, which
also fixed the client recording an interim 1xx block as the response and
then filing the real response under trailers.