Skip to content

node:http2: reject the RFC 9113/7541 client-side protocol violations node does - #36343

Closed
robobun wants to merge 8 commits into
mainfrom
claude/farm/16729aae/h2-client-frame-validation
Closed

robobun wants to merge 8 commits into
mainfrom
claude/farm/16729aae/h2-client-frame-validation

Conversation

@robobun

@robobun robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

http2.connect() accepts nine server-side protocol violations that RFC 9113 / RFC 7541 require an endpoint to treat as errors and that node (via nghttp2) rejects. Every case is deterministic on bun 1.4.0 and errors in node v26.3.0:

Violation RFC node v26 bun before
server sends SETTINGS_ENABLE_PUSH=1 9113 §8.4 session ERR_HTTP2_ERROR accepted
DATA on an idle stream 9113 §5.1 session ERR_HTTP2_ERROR silent RST(STREAM_CLOSED)
HEADERS on an idle odd stream (client never opened it) 9113 §5.1 session ERR_HTTP2_ERROR opens a phantom stream
HEADERS on an idle even stream (never promised) 9113 §5.1 session ERR_HTTP2_ERROR opens a phantom stream
WINDOW_UPDATE on an idle stream 9113 §5.1 session ERR_HTTP2_ERROR ignored
WINDOW_UPDATE overflows a stream's send window 9113 §6.9.1 session ERR_HTTP2_ERROR ignored (stream not in engine map)
response block without :status 9113 §8.3.2 stream ERR_HTTP2_STREAM_ERROR delivered to 'response'
response block with :method/:path 9113 §8.3.1 stream ERR_HTTP2_STREAM_ERROR delivered to 'response'
HPACK table-size-update after the first field 7541 §4.2 session ERR_HTTP2_ERROR accepted

The last three surface attacker-controlled header shapes to application 'response' listeners.

Standalone repro (raw hostile h2c server vs http2.connect)
$ bun repro.mjs
ACCEPT ENABLE_PUSH=1 from server: NO ERROR
ACCEPT DATA on idle stream 3: NO ERROR
ACCEPT HEADERS on idle stream 3: NO ERROR
ACCEPT HEADERS on idle even stream 2: NO ERROR
ACCEPT WINDOW_UPDATE on idle stream 3: NO ERROR
ACCEPT send-window overflow on stream 1: NO ERROR
ACCEPT response without :status: NO ERROR [got response {"x-only":"hi"}]
ACCEPT response with :method/:path: NO ERROR [got response {":status":200,":method":"GET",":path":"/"}]
ACCEPT HPACK size-update mid-block: NO ERROR [got response {":status":200,"x-a":"b"}]

$ node repro.mjs
ERROR  ENABLE_PUSH=1 from server: session error: ERR_HTTP2_ERROR
ERROR  DATA on idle stream 3: session error: ERR_HTTP2_ERROR
ERROR  HEADERS on idle stream 3: session error: ERR_HTTP2_ERROR
ERROR  HEADERS on idle even stream 2: session error: ERR_HTTP2_ERROR
ERROR  WINDOW_UPDATE on idle stream 3: session error: ERR_HTTP2_ERROR
ERROR  send-window overflow on stream 1: session error: ERR_HTTP2_ERROR
ERROR  response without :status: stream error: ERR_HTTP2_STREAM_ERROR (rstCode=1)
ERROR  response with :method/:path: stream error: ERR_HTTP2_STREAM_ERROR (rstCode=1)
ERROR  HPACK size-update mid-block: session error: ERR_HTTP2_ERROR

Cause

Inbound frame validation in src/runtime/api/bun/h2/connection.rs was thorough for a server receiving requests but largely unchecked for a client receiving responses:

  • validate_unit rejects ENABLE_PUSH > 1 but not ENABLE_PUSH = 1 from a server (the §8.4 role rule).
  • DATA/HEADERS/WINDOW_UPDATE on an unknown stream id took the closed/evicted branch (RST or silent fallthrough) without distinguishing an idle id above the high-water marks.
  • handle_window_update's §6.9.1 overflow check only ran for streams in the engine map; a client's own requests are opened by the legacy outbound encoder and were never inserted there by a WINDOW_UPDATE.
  • finish_header_block enforced the §8.3.1 request pseudo-header rules but had no §8.3.2 response counterpart; wrong_direction was server-only.
  • The RFC 7541 §4.2 rule that a dynamic-table-size-update must be first in a header block is not enforced by lshpack (lshpack_dec_decode accepts one at the start of every call), and the decode loop calls it once per field.

Fix

All in connection.rs:

  • is_idle_stream_id() centralises the §5.1 test (not in either map, above both last_stream_id marks). DATA, HEADERS (client), WINDOW_UPDATE use it to GOAWAY(PROTOCOL_ERROR).
  • SETTINGS apply-loop rejects ENABLE_PUSH != 0 from a server.
  • WINDOW_UPDATE inserts a locally-opened stream into the engine map before the overflow check (same shim pattern as DATA/RST_STREAM), and a per-stream overflow is now a connection FLOW_CONTROL_ERROR (NGHTTP2_ERR_FLOW_CONTROL) to match node's observable behaviour.
  • finish_header_block computes is_response; request pseudo-headers in a response block are wrong_direction, and a response block missing :status is malformed. Both take the existing malformed path (RST(PROTOCOL_ERROR), no on_headers_complete).
  • The decode loop GOAWAY(COMPRESSION_ERROR)s when the byte at the next offset is a §6.3 size-update opcode after at least one field has been decoded.

Supersedes #32987, which covered the idle-stream subset of this and has drifted against main.

Verification

  • bun bd test test/js/node/http2/h2-conformance.test.ts: 69 pass (58 existing + 11 new). The 10 new violation tests fail under USE_SYSTEM_BUN=1; the well-formed-response control test passes under both.
  • bun bd test test/js/node/http2/node-http2.test.js: 307 pass / 6 skip / 0 fail.
  • All 254 test/js/node/test/parallel/test-http2-*.js pass.

[review] gate passed · iteration 2 · 4 files touched

fails on main (without fix)
ASAN without fix: 12 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/js/node/http2/h2-conformance.test.ts"
bun test v1.4.0 (3723d8ad8)

test/js/node/http2/h2-conformance.test.ts:
(pass) connection preface & SETTINGS handshake (checklist §1) > server sends a SETTINGS frame first (§1.4) [666.37ms]
(pass) connection preface & SETTINGS handshake (checklist §1) > server ACKs the client's SETTINGS frame (§3.5) [241.75ms]
(pass) connection preface & SETTINGS handshake (checklist §1) > a SETTINGS frame with a non-zero stream id is a PROTOCOL_ERROR (§3.5) [209.69ms]
(pass) connection preface & SETTINGS handshake (checklist §1) > a SETTINGS frame whose length is not a multiple of 6 is a FRAME_SIZE_ERROR (§3.5) [154.75ms]
(pass) connection preface & SETTINGS handshake (checklist §1) > a SETTINGS ACK that carries a payload is a FRAME_SIZE_ERROR (§3.5) [113.93ms]
(pass) PING (checklist §3.7) > server replies to PING with a PING ACK echoing the payload [92.36ms]
(pass) PING (checklist §3.7) > a PING with length != 8 is a FRAME_SIZE_ERROR [104.45ms]
(pass) PING (checklist §3.7) > a PING on a non-zer
... (truncated)

release without fix: 2 FAILED
bun test v1.4.0-canary.1 (12108b435)

test/js/node/http2/h2-conformance.test.ts:
(pass) connection preface & SETTINGS handshake (checklist §1) > server sends a SETTINGS frame first (§1.4) [16.40ms]
(pass) connection preface & SETTINGS handshake (checklist §1) > server ACKs the client's SETTINGS frame (§3.5) [3.29ms]
(pass) connection preface & SETTINGS handshake (checklist §1) > a SETTINGS frame with a non-zero stream id is a PROTOCOL_ERROR (§3.5) [2.85ms]
(pass) connection preface & SETTINGS handshake (checklist §1) > a SETTINGS frame whose length is not a multiple of 6 is a FRAME_SIZE_ERROR (§3.5) [1.50ms]
(pass) connection preface & SETTINGS handshake (checklist §1) > a SETTINGS ACK that carries a payload is a FRAME_SIZE_ERROR (§3.5) [1.20ms]
(pass) PING (checklist §3.7) > server replies to PING with a PING ACK echoing the payload [1.08ms]
(pass) PING (checklist §3.7) > a PING with length != 8 is a FRAME_SIZE_ERROR [2.80ms]
(pass) PING (checklist §3.7) > a PING on a non-zero stream id is a PROTOCOL_ERROR [1.24ms]
(pass) WINDOW_UPDATE (checklist §6) > a connection-level WINDOW_UPDATE with a 0 increment is a PROTOCOL_ERROR [1.05ms]
(pass) WINDOW_UPDAT
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/js/node/http2/h2-conformance.test.ts"
bun test v1.4.0 (3723d8ad8)

test/js/node/http2/h2-conformance.test.ts:
(pass) connection preface & SETTINGS handshake (checklist §1) > server sends a SETTINGS frame first (§1.4) [623.83ms]
(pass) connection preface & SETTINGS handshake (checklist §1) > server ACKs the client's SETTINGS frame (§3.5) [192.05ms]
(pass) connection preface & SETTINGS handshake (checklist §1) > a SETTINGS frame with a non-zero stream id is a PROTOCOL_ERROR (§3.5) [176.30ms]
(pass) connection preface & SETTINGS handshake (checklist §1) > a SETTINGS frame whose length is not a multiple of 6 is a FRAME_SIZE_ERROR (§3.5) [135.02ms]
(pass) connection preface & SETTINGS handshake (checklist §1) > a SETTINGS ACK that carries a payload is a FRAME_SIZE_ERROR (§3.5) [114.76ms]
(pass) PING (checklist §3.7) > server replies to PING with a PING ACK echoing the payload [103.31ms]
(pass) PING (checklist §3.7) > a PING with length != 8 is a FRAME_SIZE_ERROR [125.15ms]
(pass) PING (checklist §3.7) > a PING on a non-ze
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 950ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/26] gen cpp.rs (cppbind)
[2/26] gen JSSink.{cpp,h,lut.h,rs}
generated_jssink.rs: 6 sinks, 72 exported symbols
Generating /workspace/bun/build/release/codegen/JSSink.lut.h from /workspace/bun/build/release/codegen/JSSink.lut.txt
[3/26] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 240 extern-C blocks audited
[4/26] gen JS modules (bundle-modules)
Preprocess modules (14123ms)
Bundle modules (62ms)
Postprocesss modules (202ms)
Bundle Functions (931ms)
Generate Code (35ms)

[15.37s] Bundled "src/js" for production
  2570 kb
  193 internal modules
  13 native modules
  90 internal functions across 19 files
[4/12] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_runtime v0.0.0 (/workspace/bun/src/runtime)
�[1m�[92m   Compiling�[
... (truncated)
diff hotspot
src/runtime/api/bun/h2/connection.rs      | 154 ++++++++++++++++----
 src/runtime/api/bun/h2/wire.rs            |   2 +
 src/runtime/api/bun/h2_frame_parser.rs    |  33 ++++-
 test/js/node/http2/h2-conformance.test.ts | 230 +++++++++++++++++++++++++++++-
 4 files changed, 381 insertions(+), 38 deletions(-)

gate history · 2 passed · 0 rejected · iteration 2

evidence per changed file
file                                       reads  edits  tests
src/runtime/api/bun/h2/connection.rs          12     32      0
src/runtime/api/bun/h2/wire.rs                 1      1      0
src/runtime/api/bun/h2_frame_parser.rs         7      4      0
test/js/node/http2/h2-conformance.test.ts      8     12      0

…node does

An http2.connect() client accepted nine server-side protocol violations
that RFC 9113 / RFC 7541 require an endpoint to treat as errors and that
node (via nghttp2) rejects:

Connection-level (GOAWAY, session error ERR_HTTP2_ERROR):
  * SETTINGS_ENABLE_PUSH with a value of 1 from a server (RFC 9113 8.4)
  * DATA / HEADERS / WINDOW_UPDATE on an idle stream (5.1)
  * a WINDOW_UPDATE that pushes a stream send window past 2^31-1 (6.9.1;
    the check existed but never fired because locally-opened streams
    were not in the engine map)
  * an HPACK dynamic-table-size-update after the first field in a
    header block (RFC 7541 4.2; lshpack accepts one at the start of
    every decode() call so the position rule is enforced by the caller)

Stream-level (RST_STREAM(PROTOCOL_ERROR), ERR_HTTP2_STREAM_ERROR):
  * a response block without :status or carrying a request
    pseudo-header (8.3.2)

The idle-stream detection uses the existing highest_started_stream_id
Sink hook so streams evicted after full close are still read as closed,
not idle.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 817caeb9-e7d1-46d7-bc02-de2a2b40b764

📥 Commits

Reviewing files that changed from the base of the PR and between 41caa75 and 5027c7e.

📒 Files selected for processing (3)
  • src/runtime/api/bun/h2/connection.rs
  • src/runtime/api/bun/h2_frame_parser.rs
  • test/js/node/http2/h2-conformance.test.ts

Walkthrough

Changes

The HTTP/2 engine now enforces additional RFC 9113 rules for idle streams, SETTINGS, flow-control overflow, HPACK updates, pseudo-headers, and response :status. New conformance tests inject invalid server frames and verify GOAWAY, stream resets, and valid responses.

HTTP/2 protocol validation

Layer / File(s) Summary
Frame and flow-control validation
src/runtime/api/bun/h2/connection.rs, src/runtime/api/bun/h2/wire.rs
Idle-stream frames and invalid SETTINGS_ENABLE_PUSH values now cause connection errors; flow-control overflow uses the new FLOW_CONTROL error constant.
Header-block correctness
src/runtime/api/bun/h2/connection.rs
Response classification validates HPACK update ordering and pseudo-header direction, and requires response blocks to contain :status.
Protocol violation conformance tests
test/js/node/http2/h2-conformance.test.ts
A raw HTTP/2 server probe tests connection-level violations, malformed response headers, and successful response delivery.

Possibly related PRs

  • oven-sh/bun#36230: Both changes modify adjacent finish_header_block validation paths.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly states the main change: rejecting RFC 9113/7541 client-side protocol violations.
Description check ✅ Passed It covers the problem, fix, and verification, but uses different headings than the template.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:16 AM PT - Jul 29th, 2026

❌ @robobun, your commit 5027c7e has 1 failures in Build #85240 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36343

That installs a local version of the PR into your bun-36343 executable, so you can run:

bun-36343 --bun

Comment thread src/runtime/api/bun/h2/connection.rs Outdated
Comment thread src/runtime/api/bun/h2/connection.rs Outdated
Comment thread src/runtime/api/bun/h2/connection.rs Outdated
Comment thread src/runtime/api/bun/h2/connection.rs Outdated
Comment thread src/runtime/api/bun/h2/connection.rs Outdated
Comment thread src/runtime/api/bun/h2/connection.rs Outdated
Comment thread src/runtime/api/bun/h2/connection.rs Outdated
Comment thread src/runtime/api/bun/h2/connection.rs Outdated
Comment thread src/runtime/api/bun/h2/connection.rs Outdated
Comment thread src/runtime/api/bun/h2/connection.rs Outdated
Comment thread src/runtime/api/bun/h2/connection.rs
Comment thread src/runtime/api/bun/h2/connection.rs
Comment thread src/runtime/api/bun/h2/connection.rs
Comment thread src/runtime/api/bun/h2/connection.rs
@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. node:http2: treat frames on idle streams as connection errors (RFC 9113 5.1) #32987 - Explicitly superseded; covers the same idle-stream GOAWAY logic (DATA/HEADERS/WINDOW_UPDATE on idle streams) in connection.rs
  2. node:http2: enforce RFC 9113 message framing on received responses #33004 - Overlaps with response pseudo-header validation (§8.3.2 — reject request pseudo-headers in responses, require :status) in the same connection.rs file

🤖 Generated with Claude Code

@robobun

robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

On the duplicate-PR report: this PR supersedes both #32987 (idle-stream subset) and #33004 (response pseudo-header subset). Both are a month old with merge conflicts against current connection.rs, and neither covers the ENABLE_PUSH, send-window overflow, or HPACK table-size-update-position cases addressed here.

On comment-cop: the flagged lines are two-line RFC-section citations in the same style this file already uses throughout (see the surrounding handle_settings/handle_headers bodies). They were tightened in 41caa75; shortening further would drop the clause references that make the checks auditable.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/api/bun/h2/connection.rs`:
- Around line 1112-1116: Update the :status parsing logic in the HTTP/2 header
parsing flow near is_response and informational so a 100 informational response
with self.header_end_stream set is marked malformed and rejected instead of
completing the stream. Add the corresponding conformance test covering
END_STREAM on an informational response.

In `@test/js/node/http2/h2-conformance.test.ts`:
- Around line 1063-1077: Update the close handler in the request setup around
reqClosed so streamRstCode is assigned whenever req!.rstCode is present,
including the legitimate value 0, rather than using a truthiness check. Preserve
the existing assignment and close-resolution behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8cc360cc-3227-4720-852b-413ebf233d14

📥 Commits

Reviewing files that changed from the base of the PR and between 59242d6 and 41caa75.

📒 Files selected for processing (3)
  • src/runtime/api/bun/h2/connection.rs
  • src/runtime/api/bun/h2/wire.rs
  • test/js/node/http2/h2-conformance.test.ts

Comment thread src/runtime/api/bun/h2/connection.rs
Comment thread test/js/node/http2/h2-conformance.test.ts
Gate on rstCode presence (not truthiness) in the conformance probe.
Comment thread src/runtime/api/bun/h2/connection.rs
Comment thread src/runtime/api/bun/h2/connection.rs Outdated
Comment thread test/js/node/http2/h2-conformance.test.ts
Comment thread src/runtime/api/bun/h2/connection.rs
… already-sent bytes

The idle-stream test compared against the mixed-parity high-water mark so
an idle even id numerically below a higher odd id slipped through.
is_idle_stream_id now compares against the parity-matching mark: the
engine-tracked last_peer_stream_id for peer-parity ids and a new
Sink::highest_local_stream_id() for own-parity ids.

The WINDOW_UPDATE shim that inserts a locally-opened stream into the
engine map seeded its send window at the initial size without the bytes
the legacy encoder has already written (still parked in
pending_stream_send_consumed). track_local_stream now subtracts those via
Sink::take_pending_stream_send_consumed so the overflow check sees the
same window the peer does; the DATA and HEADERS shims use the same
helper.

Also: surface connection-level WINDOW_UPDATE overflow as
NGHTTP2_ERR_FLOW_CONTROL for node-message parity with the per-stream
path; drop the duplicate settingsPayload helper in the test.
Comment thread src/runtime/api/bun/h2/connection.rs
Comment thread src/runtime/api/bun/h2/connection.rs
Comment thread src/runtime/api/bun/h2/connection.rs
Comment thread src/runtime/api/bun/h2/connection.rs
Comment thread src/runtime/api/bun/h2/connection.rs
Comment thread src/runtime/api/bun/h2_frame_parser.rs
@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Builds #85164 (3723d8a) and #85240 (5027c7e): the new conformance tests pass on every lane. Remaining reds on both runs are unrelated flakes (proxy-stress-protocol, spawn-streaming-stdout, setInterval, fastutf8stream-reopen, svelte, napi, jsonc, etc.) plus the pre-existing worker-transfer-terminate-stress JSC assertion. The one recurring h2-conformance.test.ts entry is the pre-existing releases server stream objects GC-WeakRef timing check (exists on main at what was line 1045, untouched by this diff), marked [flaky] with a passing retry on both builds.

All review threads addressed; ready for review.

Comment thread src/runtime/api/bun/h2/connection.rs
Comment thread src/runtime/api/bun/h2/connection.rs
… shim

handle_rst_stream still used a parity-unaware high-water-mark check and a
hand-rolled local-stream shim, so RST_STREAM on an idle even stream below
a higher odd id was tolerated. It now uses is_idle_stream_id and
track_local_stream like DATA/HEADERS/WINDOW_UPDATE.

Also: key the request-side wrong_direction test on is_request instead of
is_server so a PUSH_PROMISE block carrying :status is malformed too.
Comment thread src/runtime/api/bun/h2/connection.rs
@robobun

robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

The diff is ready; the internal gate is bouncing on build infra rather than the change itself.

Both gate ASAN builds (with and without the fix) fail at UnifiedSource-src_jsc_bindings-*.cpp because src/jsc/bindings/JSCFFIBridge.cpp et al. from #35246 are landing in the gate's working tree and need JavaScriptCore/BunFFI.h from a newer WebKit than the container cache has. This PR touches only four Rust/TS files (git diff --name-only 59242d6..HEAD: connection.rs, wire.rs, h2_frame_parser.rs, h2-conformance.test.ts) and no C++.

Local proof with a clean tree:

  • fail-before (stock bun 1.3.14): 2 pass / 13 fail in the new conformance describe (the 2 passes are the positive-control tests)
  • pass-after (bun bd test): 15 pass / 0 fail
  • full file: 73 pass / 0 fail
  • node-http2.test.js: 307 pass / 6 skip / 0 fail
  • all 254 test/js/node/test/parallel/test-http2-*.js pass

Buildkite #85164 (at 3723d8a) ran all 14 then-new conformance tests green on every lane.

Comment thread src/runtime/api/bun/h2/connection.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No new issues found after 5027c7e — every prior finding (parity-aware idle test, RST_STREAM routing, connection-level FLOW_CONTROL lib_error, dead highest_started_stream_id, PUSH_PROMISE :status, duplicate settingsPayload) has been addressed and all threads are resolved. Deferring to a human because this is ~150 lines of new inbound-frame validation in the h2 protocol state machine with a deliberate behavioral escalation (per-stream §6.9.1 overflow: stream error → connection error) and a new send-window seeding shim; worth a maintainer's eyes on the state-machine interactions.

What was reviewed:

  • is_idle_stream_id parity logic vs nghttp2's session_detect_idle_stream; all five §5.1 call sites (both DATA paths, HEADERS, WINDOW_UPDATE, RST_STREAM) now route through it and track_local_stream.
  • take_pending_stream_send_consumed vs the pre-existing rewrite_read drain at h2_frame_parser.rs:5549 — both retain-remove matched entries, so no double-consume.
  • handle_rst_stream rewrite preserves the evicted-after-close tolerance (None => {}) and still only calls on_stream_reset for tracked streams.
  • Response pseudo-header enforcement (is_response gating, missing :status, 1xx+END_STREAM) and the HPACK §4.2 mid-block size-update guard.
Extended reasoning...

Overview

This PR hardens node:http2 client-side inbound frame validation to reject nine RFC 9113/7541 violations that node (via nghttp2) rejects but Bun previously accepted. It touches four files: h2/connection.rs (the protocol state machine — new is_idle_stream_id, track_local_stream, last_peer_stream_id; rewired DATA/HEADERS/WINDOW_UPDATE/RST_STREAM idle checks; response pseudo-header validation; HPACK §4.2 mid-block size-update guard; §6.9.1 overflow escalation), h2/wire.rs (adds lib_error::FLOW_CONTROL), h2_frame_parser.rs (new last_local_stream_id cell, highest_local_stream_id/take_pending_stream_send_consumed Sink impls, drops the dead highest_started_stream_id impl), and 230 lines of new conformance tests.

Security risks

The change is security-positive: it closes cases where a hostile server could deliver malformed response header blocks (missing :status, request pseudo-headers in a response) directly to application 'response' listeners, and where idle-stream frames could open phantom streams. The risk to review is the opposite direction — over-strict validation breaking legitimate servers. The positive-control tests (well-formed response still delivered; WINDOW_UPDATE replenishing already-sent bytes to the 2^31-1 cap accepted) and the 307-pass node-http2.test.js / 254-pass test-http2-* parallel suite give reasonable confidence, but the per-stream §6.9.1 overflow escalation from RST_STREAM to GOAWAY is a deliberate strictness increase for node parity that changes observable behavior.

Level of scrutiny

High. This is production protocol-state-machine code on a hot path (http2.connect), with new state (last_peer_stream_id, last_local_stream_id), new Sink trait methods, and a shim (track_local_stream) that inserts engine stream entries mid-receive with a seeded send window. The interactions between the two pending_stream_send_consumed consumers (the pre-existing rewrite_read drain and the new take_pending_stream_send_consumed) are subtle enough that a maintainer familiar with the legacy-encoder transition should confirm the invariant.

Other factors

The PR has been through five rounds of my own review; every finding was addressed (parity-aware idle test rewritten in 3723d8a, RST_STREAM rewired and PUSH_PROMISE :status fixed in 532dcc8, dead trait method removed in 5027c7e). The current bug-hunt pass found nothing. Test coverage is thorough: 15 new wire-level conformance cases including positive controls and the parity edge case (idle even stream 2 with two open odd requests). CI at #85164 was green on the conformance suite across all lanes. All comment-cop and CodeRabbit threads are resolved. Given the scope and the state-machine subtlety, a human sign-off is still appropriate.

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-29, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants