nox-h2: let a 1xx be interim, not the answer
on_headers took the first header block on a stream to be the response
whatever its status, so an interim 1xx filled the response slot and the
real response that followed was filed as trailers. The Eio adapter
resolves a call on the first Response event, so a server sending 103
Early Hints -- an ordinary deployed thing, not a hostile peer -- answered
the call with 103 and dropped the 200 carrying the result.
RFC 9113 sec 8.1 lets a server send any number of interim responses
before the final one, so the two are different events, not one event with
a status to squint at. Client.event gains Informational; interim blocks
arrive as that and leave the response slot empty, so Response now means
the one final response and a caller awaiting it cannot latch an interim
block. The adapter ignores Informational and leaves the promise pending.
That also closes the other half of the same sentence: a HEADERS bearing
END_STREAM with an informational status is malformed, which sec 8.1.1
makes a PROTOCOL_ERROR stream error. An interim response promises a
final one, so ending the stream promises a response that can never
arrive, and a caller would wait on a stream nobody will write to again.
The check runs through a new ROLE.malformed hook, applied after the
block is decoded rather than instead of it, so the HPACK context stays
in step on a connection the stream error leaves alive.
nox-h2: let a 1xx be interim, not the answer
on_headers took the first header block on a stream to be the response
whatever its status, so an interim 1xx filled the response slot and the
real response that followed was filed as trailers. The Eio adapter
resolves a call on the first Response event, so a server sending 103
Early Hints -- an ordinary deployed thing, not a hostile peer -- answered
the call with 103 and dropped the 200 carrying the result.
RFC 9113 sec 8.1 lets a server send any number of interim responses
before the final one, so the two are different events, not one event with
a status to squint at. Client.event gains Informational; interim blocks
arrive as that and leave the response slot empty, so Response now means
the one final response and a caller awaiting it cannot latch an interim
block. The adapter ignores Informational and leaves the promise pending.
That also closes the other half of the same sentence: a HEADERS bearing
END_STREAM with an informational status is malformed, which sec 8.1.1
makes a PROTOCOL_ERROR stream error. An interim response promises a
final one, so ending the stream promises a response that can never
arrive, and a caller would wait on a stream nobody will write to again.
The check runs through a new ROLE.malformed hook, applied after the
block is decoded rather than instead of it, so the HPACK context stays
in step on a connection the stream error leaves alive.
nox-h2: let a 1xx be interim, not the answer
on_headers took the first header block on a stream to be the response
whatever its status, so an interim 1xx filled the response slot and the
real response that followed was filed as trailers. The Eio adapter
resolves a call on the first Response event, so a server sending 103
Early Hints -- an ordinary deployed thing, not a hostile peer -- answered
the call with 103 and dropped the 200 carrying the result.
RFC 9113 sec 8.1 lets a server send any number of interim responses
before the final one, so the two are different events, not one event with
a status to squint at. Client.event gains Informational; interim blocks
arrive as that and leave the response slot empty, so Response now means
the one final response and a caller awaiting it cannot latch an interim
block. The adapter ignores Informational and leaves the promise pending.
That also closes the other half of the same sentence: a HEADERS bearing
END_STREAM with an informational status is malformed, which sec 8.1.1
makes a PROTOCOL_ERROR stream error. An interim response promises a
final one, so ending the stream promises a response that can never
arrive, and a caller would wait on a stream nobody will write to again.
The check runs through a new ROLE.malformed hook, applied after the
block is decoded rather than instead of it, so the HPACK context stays
in step on a connection the stream error leaves alive.
nox-h2: let a 1xx be interim, not the answer
on_headers took the first header block on a stream to be the response
whatever its status, so an interim 1xx filled the response slot and the
real response that followed was filed as trailers. The Eio adapter
resolves a call on the first Response event, so a server sending 103
Early Hints -- an ordinary deployed thing, not a hostile peer -- answered
the call with 103 and dropped the 200 carrying the result.
RFC 9113 sec 8.1 lets a server send any number of interim responses
before the final one, so the two are different events, not one event with
a status to squint at. Client.event gains Informational; interim blocks
arrive as that and leave the response slot empty, so Response now means
the one final response and a caller awaiting it cannot latch an interim
block. The adapter ignores Informational and leaves the promise pending.
That also closes the other half of the same sentence: a HEADERS bearing
END_STREAM with an informational status is malformed, which sec 8.1.1
makes a PROTOCOL_ERROR stream error. An interim response promises a
final one, so ending the stream promises a response that can never
arrive, and a caller would wait on a stream nobody will write to again.
The check runs through a new ROLE.malformed hook, applied after the
block is decoded rather than instead of it, so the HPACK context stays
in step on a connection the stream error leaves alive.
nox-h2: hold the RFC 9113 rules a hostile peer leans on
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.
nox-h2: hold the RFC 9113 rules a hostile peer leans on
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.
nox-h2: let a 1xx be interim, not the answer
on_headers took the first header block on a stream to be the response
whatever its status, so an interim 1xx filled the response slot and the
real response that followed was filed as trailers. The Eio adapter
resolves a call on the first Response event, so a server sending 103
Early Hints -- an ordinary deployed thing, not a hostile peer -- answered
the call with 103 and dropped the 200 carrying the result.
RFC 9113 sec 8.1 lets a server send any number of interim responses
before the final one, so the two are different events, not one event with
a status to squint at. Client.event gains Informational; interim blocks
arrive as that and leave the response slot empty, so Response now means
the one final response and a caller awaiting it cannot latch an interim
block. The adapter ignores Informational and leaves the promise pending.
That also closes the other half of the same sentence: a HEADERS bearing
END_STREAM with an informational status is malformed, which sec 8.1.1
makes a PROTOCOL_ERROR stream error. An interim response promises a
final one, so ending the stream promises a response that can never
arrive, and a caller would wait on a stream nobody will write to again.
The check runs through a new ROLE.malformed hook, applied after the
block is decoded rather than instead of it, so the HPACK context stays
in step on a connection the stream error leaves alive.
nox-h2: let a 1xx be interim, not the answer
on_headers took the first header block on a stream to be the response
whatever its status, so an interim 1xx filled the response slot and the
real response that followed was filed as trailers. The Eio adapter
resolves a call on the first Response event, so a server sending 103
Early Hints -- an ordinary deployed thing, not a hostile peer -- answered
the call with 103 and dropped the 200 carrying the result.
RFC 9113 sec 8.1 lets a server send any number of interim responses
before the final one, so the two are different events, not one event with
a status to squint at. Client.event gains Informational; interim blocks
arrive as that and leave the response slot empty, so Response now means
the one final response and a caller awaiting it cannot latch an interim
block. The adapter ignores Informational and leaves the promise pending.
That also closes the other half of the same sentence: a HEADERS bearing
END_STREAM with an informational status is malformed, which sec 8.1.1
makes a PROTOCOL_ERROR stream error. An interim response promises a
final one, so ending the stream promises a response that can never
arrive, and a caller would wait on a stream nobody will write to again.
The check runs through a new ROLE.malformed hook, applied after the
block is decoded rather than instead of it, so the HPACK context stays
in step on a connection the stream error leaves alive.
nox-h2: let a 1xx be interim, not the answer
on_headers took the first header block on a stream to be the response
whatever its status, so an interim 1xx filled the response slot and the
real response that followed was filed as trailers. The Eio adapter
resolves a call on the first Response event, so a server sending 103
Early Hints -- an ordinary deployed thing, not a hostile peer -- answered
the call with 103 and dropped the 200 carrying the result.
RFC 9113 sec 8.1 lets a server send any number of interim responses
before the final one, so the two are different events, not one event with
a status to squint at. Client.event gains Informational; interim blocks
arrive as that and leave the response slot empty, so Response now means
the one final response and a caller awaiting it cannot latch an interim
block. The adapter ignores Informational and leaves the promise pending.
That also closes the other half of the same sentence: a HEADERS bearing
END_STREAM with an informational status is malformed, which sec 8.1.1
makes a PROTOCOL_ERROR stream error. An interim response promises a
final one, so ending the stream promises a response that can never
arrive, and a caller would wait on a stream nobody will write to again.
The check runs through a new ROLE.malformed hook, applied after the
block is decoded rather than instead of it, so the HPACK context stays
in step on a connection the stream error leaves alive.