feat: bound the size of an incoming HTTP/2 frame - #1264
Open
pjfanning wants to merge 2 commits into
Open
Conversation
Motivation: `Http2FrameParsing` read a frame's 24-bit length field and then took that many bytes with no upper bound, so a peer could declare a length up to the field's maximum of 16 MiB - 1 and make the frame parser buffer that much for a single frame. That happens before the HPACK and entity limits apply, so `max-header-list-size`, `incoming-stream-level-buffer-size` and `incoming-connection-level- buffer-size` do not bound it, and it is multiplied by the number of connections. Modification: Add a `max-frame-size` setting to the HTTP/2 server and client settings, defaulting to 512kB, and reject a larger frame with a FRAME_SIZE_ERROR on its frame header, before the payload is buffered. The value is validated against the bounds RFC 9113, section 4.2 sets for SETTINGS_MAX_FRAME_SIZE, 16 KiB to 16 MiB - 1. The setting is a limit on what is accepted, not an advertisement. An earlier revision did advertise it as SETTINGS_MAX_FRAME_SIZE, which h2spec's "4.2 Frame Size" case showed to be actively harmful: DATA frames are flow controlled and the initial window is 64 KiB, so a peer that sizes its first frame to a larger advertised value trips a FLOW_CONTROL_ERROR before the window has grown. Advertising nothing keeps a well-behaved peer at the 16 KiB default it already assumes, and the configured value is then pure leniency for peers that exceed it - always accepting at least as much as any peer is told it may send. Result: A single frame can no longer make the parser hold up to 16 MiB; the bound is 512kB by default and configurable. Frames from a spec compliant peer are unaffected, and pekko's own existing behaviour of accepting frames well above 16 KiB is preserved. Tests: - sbt "http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerSpec org.apache.pekko.http.impl.engine.http2.Http2ClientSpec org.apache.pekko.http.impl.engine.http2.H2SpecIntegrationSpec" - pass (236 tests), including the h2spec conformance suite. A new test sends a frame one byte over a configured 16 KiB limit and expects GOAWAY(FRAME_SIZE_ERROR); verified it fails with the size check disabled. The existing "fail if more data is received than stream-level window allows" test deliberately sends a single 512001 byte frame to exceed the stream buffer, so it now raises max-frame-size to 1 MiB to reach the flow-control check it is about. - sbt http-core/mimaReportBinaryIssues - pass - sbt "+http-core/compile" - pass on 2.13.18 and 3.3.8 References: None - bounds how much a single HTTP/2 frame can buffer
The new abstract members on the HTTP/2 settings traits are only flagged on Scala 3; the 2.13 check filters them under a broader rule, so a scoped run on the default Scala version alone did not surface them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Http2FrameParsingread a frame's 24-bit length field and then took that many bytes with no upper bound:So a peer could declare a length up to the field's maximum of 16 MiB - 1 and make the frame parser buffer that much for a single frame. This happens before the HPACK and entity limits apply, so
max-header-list-size,incoming-stream-level-buffer-sizeandincoming-connection-level-buffer-sizedo not bound it — and it is multiplied by the number of connections.Modification
Add a
max-frame-sizesetting to the HTTP/2 server and client settings, defaulting to512kB, and reject a larger frame with aFRAME_SIZE_ERRORon its frame header, before the payload is buffered. The value is validated against the bounds RFC 9113 §4.2 sets forSETTINGS_MAX_FRAME_SIZE(16 KiB … 16 MiB - 1).The setting is a limit on what is accepted, not an advertisement, and that is deliberate. An earlier revision of this branch did advertise it as
SETTINGS_MAX_FRAME_SIZE, and h2spec's "4.2 Frame Size" case showed that to be actively harmful: DATA frames are flow controlled and the initial window is 64 KiB, so a peer that sizes its first frame to a larger advertised value trips aFLOW_CONTROL_ERRORbefore the window has grown. h2spec did exactly that and the conformance test went red.Advertising nothing keeps a well-behaved peer at the 16 KiB default it already assumes, and the configured value is then pure leniency for peers that exceed it — we always accept at least as much as any peer is told it may send. That also preserves pekko's existing behaviour of accepting frames well above 16 KiB, which its own tests rely on.
Why 512 kB and not the RFC ceiling: defaulting to 16 MiB would leave the default posture exactly as it is today and fix nothing unless an operator tuned it. 512 kB matches
incoming-stream-level-buffer-size, so the parser's transient buffering stays the same order as the stream buffer instead of 32× larger, while still allowing 32× the protocol default.Result
A single frame can no longer make the parser hold up to 16 MiB; the bound is 512 kB by default and configurable. Frames from a spec-compliant peer are unaffected.
Tests
sbt "http2-tests/testOnly ...Http2ServerSpec ...Http2ClientSpec ...H2SpecIntegrationSpec"— pass (236 tests), including the h2spec conformance suite. A new test sends a frame one byte over a configured 16 KiB limit and expectsGOAWAY(FRAME_SIZE_ERROR); verified it fails with the size check disabled.max-frame-sizeto 1 MiB in order to reach the flow-control check it is actually about. It was the only test in the suite that needed adjusting.sbt http-core/mimaReportBinaryIssues— pass.sbt "+http-core/compile"— pass on 2.13.18 and 3.3.8.scalafmton the changed files — clean.References
None - bounds how much a single HTTP/2 frame can buffer
🤖 Generated with Claude Code