Skip to content

fix: use FRAME_SIZE_ERROR and FLOW_CONTROL_ERROR for malformed control frames - #992

Closed
jaeminbird wants to merge 1 commit into
hyperium:masterfrom
jaeminbird:fix-control-frame-error-codes
Closed

jaeminbird wants to merge 1 commit into
hyperium:masterfrom
jaeminbird:fix-control-frame-error-codes

Conversation

@jaeminbird

Copy link
Copy Markdown

On a malformed control frame the server always answered GOAWAY with PROTOCOL_ERROR. RFC 9113 requires FRAME_SIZE_ERROR for wrong-length SETTINGS, PING, WINDOW_UPDATE, RST_STREAM and GOAWAY frames (§4.2, 6.4, 6.5, 6.7, 6.9), and FLOW_CONTROL_ERROR for a SETTINGS_INITIAL_WINDOW_SIZE above 2^31-1 (§6.5.2). decode_frame (src/codec/framed_read.rs) now maps each case through a load_error_reason() helper, with a new internal InvalidInitialWindowSize variant (src/frame/mod.rs, raised in src/frame/settings.rs); frame::Error is public only under the unstable feature, so stable users just see the GOAWAY code change. +24/-6 in src, plus 77 lines of tests.

A raw-socket probe sending one bad frame per case (script below): h2 before the fix 0/6 correct, nginx 1.31.8 6/6, h2 after the fix 6/6. h2spec doesn't catch this — it accepts a plain connection close.

  • 6 new end-to-end tests in tests/h2-tests/tests/server.rs fail before and pass after.
  • cargo test (446 + 40) and cargo test -p h2-tests pass; fmt, check --features unstable and clippy --lib -D warnings clean.
  • Not tested: a dedicated RST_STREAM case (it uses the same mapping); the Go net/http comparison; the workspace fuzz crate can't build on Windows.
h2probe.py
"""Send one malformed HTTP/2 frame over h2c prior knowledge and print the GOAWAY error code.

usage: python -I h2probe.py HOST PORT
"""
import socket
import struct
import sys

PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
NAMES = {0: "NO_ERROR", 1: "PROTOCOL_ERROR", 3: "FLOW_CONTROL_ERROR", 6: "FRAME_SIZE_ERROR"}


def frame(ftype, flags, sid, payload, length=None):
    n = len(payload) if length is None else length
    return struct.pack(">I", n)[1:] + bytes([ftype, flags]) + struct.pack(">I", sid) + payload


CASES = [
    ("SETTINGS ACK with 6-byte payload (RFC 9113 6.5)", "FRAME_SIZE_ERROR",
     frame(4, 1, 0, b"\x00\x03\x00\x00\x00\x64")),
    ("SETTINGS length 5 (6.5)", "FRAME_SIZE_ERROR", frame(4, 0, 0, b"\x00\x03\x00\x00\x00")),
    ("PING length 7 (6.7)", "FRAME_SIZE_ERROR", frame(6, 0, 0, b"\x00" * 7)),
    ("WINDOW_UPDATE stream 0 length 3 (6.9)", "FRAME_SIZE_ERROR", frame(8, 0, 0, b"\x00\x00\x01")),
    ("GOAWAY length 4 (4.2)", "FRAME_SIZE_ERROR", frame(7, 0, 0, b"\x00" * 4)),
    ("SETTINGS_INITIAL_WINDOW_SIZE 2^31 (6.5.2)", "FLOW_CONTROL_ERROR",
     frame(4, 0, 0, b"\x00\x04\x80\x00\x00\x00")),
]


def run(host, port, data):
    s = socket.create_connection((host, port), timeout=3)
    s.sendall(PREFACE + frame(4, 0, 0, b""))
    s.sendall(data)
    buf = b""
    try:
        while True:
            chunk = s.recv(65536)
            if not chunk:
                break
            buf += chunk
    except socket.timeout:
        pass
    s.close()
    i = 0
    while i + 9 <= len(buf):
        n = int.from_bytes(buf[i:i + 3], "big")
        t = buf[i + 3]
        if t == 7:
            code = int.from_bytes(buf[i + 13:i + 17], "big")
            return NAMES.get(code, str(code))
        i += 9 + n
    return "no GOAWAY (closed)"


host, port = sys.argv[1], int(sys.argv[2])
for name, want, data in CASES:
    got = run(host, port, data)
    print(f"{'OK ' if got == want else 'BAD'} {name}: got {got}, RFC wants {want}")

…l frames

RFC 9113 requires FRAME_SIZE_ERROR when a SETTINGS, PING, WINDOW_UPDATE,
RST_STREAM or GOAWAY frame has the wrong length (sections 4.2, 6.4, 6.5,
6.7, 6.9), and FLOW_CONTROL_ERROR when SETTINGS_INITIAL_WINDOW_SIZE is
above 2^31-1 (section 6.5.2). These all went away with PROTOCOL_ERROR.
@seanmonstar

Copy link
Copy Markdown
Member

Note that the spec explicitly allows returning PROTOCOL_ERROR for any of those error codes instead. We have chosen to do so.

@jaeminbird
jaeminbird deleted the fix-control-frame-error-codes branch October 30, 2026 10:52
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