Skip to content

fix: proxy dos caps — connections, hpack, http2 streams - #446

Merged
kacy merged 3 commits into
mainfrom
hardening/proxy-dos-caps
Jun 23, 2026
Merged

fix: proxy dos caps — connections, hpack, http2 streams#446
kacy merged 3 commits into
mainfrom
hardening/proxy-dos-caps

Conversation

@kacy

@kacy kacy commented Jun 23, 2026

Copy link
Copy Markdown
Owner

summary

second of the four-PR hardening pass. caps that stop a single client from
exhausting threads or memory on the L7 proxy. all reject-the-excess /
bound-the-unbounded — normal traffic is unaffected.

what's in here

1. listener connection cap (listener_runtime.zig)
the accept loop incremented active_connections but never checked it
before std.Thread.spawn — a connection flood spawned detached threads
until the OS thread/fd limit. added max_connections = 1024; at the cap
the accepted fd is closed immediately (load shed) rather than spawning.
mirrors the API server's existing guard. count+admit decision extracted
into admitConnection() with a unit test.

2. hpack decode bounds (hpack.zig)
decodeInteger could accumulate a near-usize value that overflows the
start + len arithmetic in decodeString (panic). capped the running
value at 16 MiB; also capped headers-per-block (256), total decoded
header bytes (1 MiB), and clamped a peer's dynamic-table-size update to
1 MiB. three new tests.

3. http2 stream + buffer caps (http2_connection_router.zig)
a single connection could open unlimited streams (each growing the
stream list and dialing an upstream) or stream bytes without completing
a frame (growing downstream_buf). capped concurrent streams at 128 —
refused past the limit with a 503 before any upstream dial, so no fd
leak — and bounded un-parsed downstream bytes at 1 MiB.

note: the downstream read already had a timeout via the poll loop +
expireTimedOutStreams, so the agent-flagged "no read timeout" was
already mitigated; the real gaps were the unbounded buffers, fixed here.

tests

  • admitConnection sheds load at the connection cap
  • decodeInteger rejects a value above the cap
  • decodeHeaderBlock rejects too many headers
  • updateMaxSize clamps a peer's oversized table-size update
  • full suite green (2239 passed, 0 failed); panic_audit + fmt clean

verification

  • YOQ_SKIP_SLOW_TESTS=1 zig build test — 2239 passed
  • tools/panic_audit.sh clean; zig fmt --check clean
  • existing http2 / hpack / listener tests stay green (normal traffic
    unaffected)

kacy added 3 commits June 23, 2026 01:41
the listener incremented active_connections but never checked it before
std.Thread.spawn — a connection flood would spawn threads until the OS
limit. add a max_connections cap (1024) and shed at the cap by closing
the accepted fd, mirroring the api server's guard. extracted the
count+admit decision into admitConnection() with a unit test.
decodeInteger could accumulate a near-usize value that overflows the
downstream start+len arithmetic in decodeString. cap the running value
(16 MiB). also cap headers-per-block (256), total decoded header bytes
(1 MiB), and clamp a peer's dynamic-table-size update (1 MiB) so a
crafted header block can't exhaust memory. three new tests.
a single http2 connection could open unlimited streams (growing the
streams list + dialing an upstream each) or stream bytes without ever
completing a frame (growing downstream_buf). cap concurrent streams at
128 — refused past the limit with 503 before any upstream dial, so no
fd leaks — and bound un-parsed downstream bytes at 1 MiB.
@kacy
kacy merged commit 59a6bbc into main Jun 23, 2026
9 of 10 checks passed
@kacy
kacy deleted the hardening/proxy-dos-caps branch June 23, 2026 01:41
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