Skip to content

fix: reject GOAWAY frames with a non-zero stream identifier - #951

Merged
seanmonstar merged 1 commit into
hyperium:masterfrom
jaideeppyne:goaway-stream-id
Aug 31, 2026
Merged

fix: reject GOAWAY frames with a non-zero stream identifier#951
seanmonstar merged 1 commit into
hyperium:masterfrom
jaideeppyne:goaway-stream-id

Conversation

@jaideeppyne

Copy link
Copy Markdown
Contributor

RFC 9113 section 6.8:

The GOAWAY frame applies to the connection, not a specific stream. An endpoint MUST treat a GOAWAY frame with a stream identifier other than 0x00 as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.

GoAway::load is the only frame loader that never got the frame head, so the stream id was never looked at. A GOAWAY sent on stream 1 was accepted as a normal connection shutdown.

I found this probing the connection-level frames against the spec. SETTINGS, PING and RST_STREAM all already reject a bad stream id; GOAWAY was the odd one out. Against examples/server, GOAWAY on stream 1 got GOAWAY(NO_ERROR) back, byte for byte the same as the legal stream-0 case. With this change it gets GOAWAY(PROTOCOL_ERROR), and stream 0 still gets NO_ERROR.

The fix passes head into load and checks is_zero(), matching what Ping::load does. The existing generic error mapping in framed_read already turns that into a connection-level PROTOCOL_ERROR.

Oracle and counts:

  • new codec test read_goaway_stream_id_not_zero fails before, passes after (I reverted only the two source files and kept the test to confirm: expected error; actual=Some(Ok(GoAway { error_code: NO_ERROR, .. })))
  • h2spec 2.6 --strict against examples/server is unchanged: 147 tests, 146 passed, 1 skipped, 0 failed. h2spec's 6.8 test passes either way because it accepts a closed connection as evidence of a connection error, and h2 was closing anyway by honouring the GOAWAY
  • full cargo test --all-features is unchanged

Unrelated, but proto::streams::recv::tests::clear_recv_buffer_caps_capacity_before_overflow already fails on a pristine master for me (assertion failed: self.slab.is_empty() at store.rs:231). Not touched here, happy to file it separately.

AI disclosure: I used Claude Code to help with this change.

Problem

RFC 9113 section 6.8 says an endpoint MUST treat a GOAWAY frame with a
stream identifier other than 0x00 as a connection error of type
PROTOCOL_ERROR. `GoAway::load` never saw the frame head, so the stream
identifier was not checked and such a frame was accepted as an ordinary
connection shutdown.

Solution

Pass the head into `GoAway::load` and reject a non-zero stream id, the
same way SETTINGS and PING already do.

Validation

New codec test `read_goaway_stream_id_not_zero` fails before the change
and passes after. Against the example server, a GOAWAY on stream 1 used
to get GOAWAY(NO_ERROR) back and now gets GOAWAY(PROTOCOL_ERROR); a
GOAWAY on stream 0 is unchanged. h2spec 2.6 --strict is unchanged at 146
passed / 1 skipped / 0 failed.

@seanmonstar seanmonstar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice improvement, thanks!

@seanmonstar
seanmonstar merged commit d4a37fc into hyperium:master Aug 31, 2026
6 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