Skip to content

fix: the h2 header-list bound is a limit, so it belongs in the reuse hash - #36

Merged
pigri merged 1 commit into
mainfrom
fix/h2-header-cap-followups
Sep 29, 2026
Merged

pigri merged 1 commit into
mainfrom
fix/h2-header-cap-followups

Conversation

@pigri

@pigri pigri commented Sep 29, 2026

Copy link
Copy Markdown

Follow-up to #35, which merged before these review findings were addressed.

The documented failure mode was wrong

#35 said h2 "allows a multiple of this value before failing the stream
outright". Measured on this branch, with a bound of 8KiB:

header list result pooled connection
2x (inside the 4x abuse multiplier) stream error alive
32x connection error, header_list_way_too_large dead

Past the multiplier it is a connection error, not a stream one. On a
pooled, multiplexed upstream that also fails every other request in flight on
that connection — where before the setting existed the response was simply
accepted.

That blast radius is the actual trade this setting makes: a bounded failure
on a shared connection instead of an unbounded header list. It is now in the
doc comment, and there is a test for the stream-local band that the old
comment described and nothing covered — both earlier tests drove 32x, so only
the connection-error path was ever exercised.

Reuse hash

h2_max_header_list_size now takes part in reuse_hash. #35 excluded it "like
the window sizes"; that was the wrong comparison. Those are tuning
parameters, this is a limit, and sharing a pool across different values
breaks it in both directions:

  • a peer that set a bound gets none, if the pooled connection was established
    by a peer that set nothing;
  • a peer that set nothing inherits a sibling's cap, and its legitimate large
    responses fail — taking the shared connection with them, per the table
    above.

A limit that applies or not depending on which peer connected first is not a
limit. New test: peers differing only in this value do not share a
connection, and identical peers still pool.

Some(0)

Rejected at handshake, as the two adjacent window settings already are. It
does not mean "no bound" — it makes the abuse limit zero too, so the first
response on every connection to that peer is a connection error, with no
diagnostic pointing at the config.

Test hardening

serve_one_fat_response swallowed its send failure. If a future h2 enforced
the peer's advertised SETTINGS_MAX_HEADER_LIST_SIZE on send, the server
would quietly send nothing and the oversize test would still pass on the
resulting error — going vacuous without failing. It now expects the send.

Not addressed here

The knob still has no consumer, so the exposure #35 described is not closed
by either PR. That is the synapse side and is next.

Verification

cargo fmt --check clean; cargo clippy --all-targets --all --deny=warnings
exit 0; cargo test -p pingora-core --lib 623 pass, with only
connectors::l4::tests::test_conn_error_addr_not_avail failing, which needs
host networking. Each new guard was mutated: removing the reuse-hash entry,
removing the zero check, and widening the band test each fail exactly one
test.

Review follow-ups to #35, which merged before they were addressed.

The doc contract was wrong in the case the setting exists for. It said h2
"allows a multiple of this value before failing the stream outright";
measured on this branch with a bound of 8KiB, 2x over drops the headers and
errors the stream with the connection still usable, but 32x over raises a
*connection* error — ENHANCE_YOUR_CALM, `header_list_way_too_large` — and the
connection is finished. On a pooled, multiplexed upstream that fails every
other request in flight on it. That blast radius is the trade the setting
makes and it was neither documented nor tested; now it is both, including a
case for the stream-local band the old comment described and nothing covered.

`h2_max_header_list_size` now takes part in `reuse_hash`. Excluding it
alongside the window sizes was wrong: those are tuning parameters, this is a
limit, and sharing a pool across different values breaks it in both
directions. A peer that asked for a bound gets none when an unbounded peer
established the connection, and a peer that asked for none inherits a
sibling's cap — which, past the abuse multiplier, takes the shared connection
down with it. A limit that applies or not depending on who connected first is
not a limit.

`Some(0)` is rejected at handshake, as the window sizes already are. It does
not mean "no bound": it makes the abuse limit zero too, so the first response
on every connection to that peer is a connection error, with nothing pointing
at the misconfiguration.

The test server now `expect`s its send instead of swallowing the error. If a
future h2 enforced the peer's advertised limit on send, the server would
quietly send nothing and the oversize test would still pass on the resulting
error — testing nothing.
@pigri
pigri merged commit bd9b753 into main Sep 29, 2026
4 checks passed
@pigri
pigri deleted the fix/h2-header-cap-followups branch September 29, 2026 09:27
@linear-code

linear-code Bot commented Sep 29, 2026

Copy link
Copy Markdown

SYN-317

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.

1 participant