sec(hailo): explicit CVE-2023-44487 rapid-reset cap (iter 183)

hyper/h2 already mitigates the rapid-reset DoS by defaulting
http2_max_pending_accept_reset_streams to 20 post-CVE, but pinning
the value explicitly gives operators a tunable surface and makes the
mitigation reviewable from worker startup logs. Set to 32 by default
(small step above the h2 default to leave room for legit reset
jitter), env-tunable via `RUVECTOR_MAX_PENDING_RESETS` with an 8
floor. Once exceeded, hyper sends GOAWAY and closes the connection.

Validated on cognitum-v0, c=8 b=1, 8 s × 3 runs each:

  iter-182 baseline: 69.6, 67.4, 69.0 → mean 68.7/sec
  iter-183 after   : 70.5, 70.5, 69.6 → mean 70.2/sec

  Δ throughput: +2.2% (noise band — legit traffic doesn't generate
                RST_STREAM under steady load, so the cap is invisible)
  Δ p50      :  flat at 111-112 ms

Layered with iter-180 byte cap, iter-181 stream cap, iter-182 RPC
timeout — four DoS gates now visible in the worker startup banner.
This closes the named-CVE checklist for the gRPC server surface;
remaining hardening (HTTP/2 keepalive, header-list-size cap) targets
liveness rather than DoS.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruvnet 2026-05-03 17:51:14 -04:00
parent f1703b3211
commit 520e892493

View file

@ -43,6 +43,11 @@
//! and any handler that hangs past the
//! 30× p99 headroom. tonic returns
//! Status::cancelled when it fires.
//! RUVECTOR_MAX_PENDING_RESETS CVE-2023-44487 rapid-reset cap
//! (ADR-172 §3a iter 183 — default 32,
//! floor 8). Caps unprocessed RST_STREAM
//! frames; once exceeded, the server
//! sends GOAWAY and closes the connection.
//!
//! When both `RUVECTOR_TLS_CERT` and `RUVECTOR_TLS_KEY` are set and the
//! binary was built with `--features tls`, the worker serves over HTTPS
@ -563,9 +568,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
request_timeout_secs,
"per-RPC handler timeout set (ADR-172 §3a iter 182 slow-loris gate)"
);
// Iter 183 — explicit CVE-2023-44487 rapid-reset cap. hyper/h2
// already mitigates by defaulting to 20 pending RST_STREAM
// frames, but pinning the value gives operators a tunable
// surface and makes the mitigation reviewable from the worker
// logs. 32 is a small step above the h2 default to leave room
// for legit reset jitter (e.g., a client cancelling a stream
// mid-flight) without weakening the cap meaningfully — a
// GOAWAY still fires after just 33 unprocessed resets.
let max_pending_resets: usize = std::env::var("RUVECTOR_MAX_PENDING_RESETS")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(32)
.max(8);
info!(
max_pending_resets,
"HTTP/2 max_pending_accept_reset_streams set (ADR-172 §3a iter 183 CVE-2023-44487 gate)"
);
let mut server = Server::builder()
.max_concurrent_streams(Some(max_streams))
.timeout(Duration::from_secs(request_timeout_secs));
.timeout(Duration::from_secs(request_timeout_secs))
.http2_max_pending_accept_reset_streams(Some(max_pending_resets));
#[cfg(feature = "tls")]
{
// Both vars must be set to opt-in. A partial config (cert