Skip to content

fix(ext/node): enforce HTTP/2 server header list size limits#33494

Merged
littledivy merged 4 commits into
denoland:mainfrom
divybot:claude/test-http2-too-large-headers
Apr 26, 2026
Merged

fix(ext/node): enforce HTTP/2 server header list size limits#33494
littledivy merged 4 commits into
denoland:mainfrom
divybot:claude/test-http2-too-large-headers

Conversation

@divybot

@divybot divybot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Enables test-http2-too-large-headers in node_compat suite.

Test plan

  • cargo test --test node_compat -- test-http2-too-large-headers

@fibibot fibibot 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.

Approach is right and the test scenario works through cleanly: server's local SETTINGS_MAX_HEADER_LIST_SIZE = 100 propagates via nghttp2_session_get_local_settings, and the cumulative current_length + header_length check in add_header rejects once the running total crosses 100. The pseudo-headers themselves push the running total past 100 before the giant foo header even arrives, but that's still observable as an NGHTTP2_ENHANCE_YOUR_CALM RST to the client, which is what the test asserts. The empty-name short-circuit and the order-of-checks match Node's Http2Stream::AddHeader in src/node_http2.cc.

Two real divergences from Node's AddHeader worth resolving before this lands — both flagged inline. Neither breaks the test this PR enables, but the first one means this PR can't co-exist with #33495 (same Session::max_header_pairs field, contradictory defaulting logic), so the two need to converge. Marking COMMENT rather than approving until the overlap with #33495 is sorted.

Comment thread ext/node/ops/http2/session.rs Outdated
DEFAULT_MAX_HEADER_LIST_PAIRS
} else {
pairs
};

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.

This pairs == 0 → DEFAULT_MAX_HEADER_LIST_PAIRS mapping diverges from Node in two ways:

  1. Node distinguishes "user explicitly set 0" from "option not provided" by checking the FLAGS bit (see src/node_http2.cc and the parallel logic in PR fix(ext/node): rebind http2 state buffers post-snapshot, enforce maxHeaderListPairs #33495). Here, both cases collapse to 128.
  2. When the user does set a low value (or 0), Node clamps to a per-session-type minimum: 4 for servers, 1 for clients (node_http_common-inl.h: GetServerMaxHeaderPairs / GetClientMaxHeaderPairs). This PR has no clamp, so maxHeaderListPairs: 0 produces an effective limit of 128 instead of Node's 4 (server) / 1 (client).

Concrete fallout: parallel/test-http2-too-many-headers.js (which #33495 enables) constructs a server with maxHeaderListPairs: 0 and expects rejection at the 5th header. Under this PR's logic that test would fail because max_header_pairs resolves to 128, not 4.

Since #33495 already implements the flag-aware read + min-clamp on this same field, the two PRs need to converge — please coordinate and pick one defaulting strategy (the #33495 one matches Node).

// Http2Stream::AddHeader behaviour.
if current_pairs >= max_header_pairs
|| current_length.saturating_add(header_length as u64) > max_header_length
{

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.

Node's Http2Stream::AddHeader also gates on session-wide memory budget:

if (!session_->has_available_session_memory(length) ||
    current_headers_.size() == max_header_pairs_ ||
    current_headers_length_ + length > max_header_length_) {
  return false;
}

This PR drops the has_available_session_memory arm. Without it a single session could accept headers that fit per-stream but exhaust the per-session memory budget. Not exercised by the test you're enabling, but worth flagging since the comment claims this "mirrors Node's Http2Stream::AddHeader behaviour" — it doesn't quite.

divybot added a commit to divybot/deno that referenced this pull request Apr 26, 2026
- Read OptionsIndex::MaxHeaderListPairs only when its FLAGS bit is set,
  so an unset option no longer collapses to the same path as an
  explicit zero.
- Apply Node's per-session-type minimum (server: 4, client: 1) after
  defaulting, matching GetServerMaxHeaderPairs / GetClientMaxHeaderPairs
  in node_http_common-inl.h.
- Document the Http2Stream::AddHeader divergence around session memory
  accounting; Deno does not yet track per-session memory, so the
  has_available_session_memory arm is intentionally omitted.

Addresses review feedback on denoland#33494.

@fibibot fibibot 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.

LGTM after 959a5c7a. Both inline concerns from the prior review are addressed precisely:

  • Http2Options::new now gates the maxHeaderListPairs read on the FLAGS bit (flags & (1 << OptionsIndex::MaxHeaderListPairs as u32)), so an unset slot doesn't masquerade as a user-supplied 0, and applies the per-session-type minimum (Server => 4, Client => 1) afterwards. With this, parallel/test-http2-too-many-headers.js's maxHeaderListPairs: 0 → effective 4 semantics now hold, which means this PR can co-exist with #33495's enforcement-side change without contradiction.
  • The add_header comment is updated to acknowledge the missing has_available_session_memory arm and points at src/node_http2.cc so the next person to wire up per-session memory accounting in Deno knows where to plug it in.

The two PRs (#33494 and #33495) implement the same flag-aware-read + min-clamp logic on the same Http2Options struct in slightly different styles (#33495 uses a mut binding outside the with_options closure; this PR uses a tuple destructure with mut outside). They'll trivially merge-conflict; either resolution works.

divybot added a commit to divybot/deno that referenced this pull request Apr 26, 2026
- Read OptionsIndex::MaxHeaderListPairs only when its FLAGS bit is set,
  so an unset option no longer collapses to the same path as an
  explicit zero.
- Apply Node's per-session-type minimum (server: 4, client: 1) after
  defaulting, matching GetServerMaxHeaderPairs / GetClientMaxHeaderPairs
  in node_http_common-inl.h.
- Document the Http2Stream::AddHeader divergence around session memory
  accounting; Deno does not yet track per-session memory, so the
  has_available_session_memory arm is intentionally omitted.

Addresses review feedback on denoland#33494.

Co-authored-by: Divy Srivastava <[email protected]>
@divybot
divybot force-pushed the claude/test-http2-too-large-headers branch from 959a5c7 to 93c3685 Compare April 26, 2026 02:37
@littledivy

Copy link
Copy Markdown
Member

@divybot merge main

divybot added a commit to divybot/deno that referenced this pull request Apr 26, 2026
- Read OptionsIndex::MaxHeaderListPairs only when its FLAGS bit is set,
  so an unset option no longer collapses to the same path as an
  explicit zero.
- Apply Node's per-session-type minimum (server: 4, client: 1) after
  defaulting, matching GetServerMaxHeaderPairs / GetClientMaxHeaderPairs
  in node_http_common-inl.h.
- Document the Http2Stream::AddHeader divergence around session memory
  accounting; Deno does not yet track per-session memory, so the
  has_available_session_memory arm is intentionally omitted.

Addresses review feedback on denoland#33494.

Co-authored-by: Divy Srivastava <[email protected]>
@divybot
divybot force-pushed the claude/test-http2-too-large-headers branch from 93c3685 to 2f9edbf Compare April 26, 2026 12:06
divybot and others added 2 commits April 26, 2026 18:28
Enables tests/node_compat/runner/suite/test/parallel/test-http2-too-large-headers.js

Co-authored-by: Divy Srivastava <[email protected]>
- Read OptionsIndex::MaxHeaderListPairs only when its FLAGS bit is set,
  so an unset option no longer collapses to the same path as an
  explicit zero.
- Apply Node's per-session-type minimum (server: 4, client: 1) after
  defaulting, matching GetServerMaxHeaderPairs / GetClientMaxHeaderPairs
  in node_http_common-inl.h.
- Document the Http2Stream::AddHeader divergence around session memory
  accounting; Deno does not yet track per-session memory, so the
  has_available_session_memory arm is intentionally omitted.

Addresses review feedback on denoland#33494.

Co-authored-by: Divy Srivastava <[email protected]>
@divybot
divybot force-pushed the claude/test-http2-too-large-headers branch from 34646f7 to ac3cad3 Compare April 26, 2026 13:04

@fibibot fibibot 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.

LGTM after the rebase. The conflict with #33495 is gone — that PR merged the Http2Options::max_header_pairs field, flag-aware read, and per-session-type minimum clamp on its own. This PR's net change against base is now the slim 34/-4 piece I expected:

  • add_header short-circuits for empty header names (matches Node's Http2Stream::AddHeader IsZeroLength(name) early return).
  • The reject condition now combines both current_pairs >= max_header_pairs AND current_length + header_length > local SETTINGS_MAX_HEADER_LIST_SIZE, returning false to trip the existing nghttp2_submit_rst_stream(NGHTTP2_ENHANCE_YOUR_CALM) path.
  • The comment honestly notes the has_available_session_memory arm is still missing and points at where to wire it up later if Deno gains per-session memory accounting.
  • Test config enables test-http2-too-large-headers.js; the count-limit test (test-http2-too-many-headers.js) is already enabled via #33495's merge.

CI is still building (33 pending) but no failures yet.

divybot and others added 2 commits April 26, 2026 19:29
Enables tests/node_compat/runner/suite/test/parallel/test-http2-too-large-headers.js

Co-authored-by: Divy Srivastava <[email protected]>

@littledivy littledivy 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.

LGTM

@littledivy
littledivy merged commit 79d3405 into denoland:main Apr 26, 2026
112 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.

3 participants