Rust & systems · Deep dive
The request that never finishes.
A Rust HTTP/2 lesson in resource exhaustion: memory can be safe while the CPU is very busy doing nothing useful.
Kinwilder research desk · · AI-assisted analysis
Suppose a server bounds the size of its header buffer. It rejects oversized requests. It never performs an out-of-bounds write. Can an attacker still make it waste work? The 2024 HTTP/2 CONTINUATION disclosures are a useful answer.
The established behavior
RustSec's RUSTSEC-2024-0332 describes repeated CONTINUATION frames causing h2 to keep processing and consume CPU. Its advisory notes that Tokio's task budget helps preserve service, though latency increases. Fixes were released in the 0.3.26 and 0.4.4 branches. This is a historical case study; check maintained versions and current advisories before deploying. RustSec, maintainer explanation.
HTTP/2 permits a field block to span HEADERS and CONTINUATION frames. END_HEADERS indicates completion. The protocol's DATA flow-control mechanism does not supply a general work budget for every control frame. RFC 9113.
Our analysis: every incomplete operation needs a budget
A parser's safety properties should include more than the space required to store a result. Ask how much work it can perform before producing a useful result, and who controls when that result becomes complete.
Four budgets matter: bytes accepted, decoded size, elapsed time, and processing work. One is not automatically a substitute for another. A low-bandwidth connection may still trigger disproportionate CPU work. A wall-clock timeout may not protect the rest of the server if a parser monopolizes a worker before the timer gets a chance to run. Scheduler fairness helps other tasks progress, but it does not make the attack cost disappear.
That is the broader systems lesson here. Memory safety protects certain invariants inside an operation. Availability requires limits across sequences of operations and across concurrent clients.
What to instrument
Monitor time spent in incomplete header processing, connection lifetimes, rejected header blocks, CPU cost per completed request, and latency for a separate legitimate client. Request-per-second counters alone may be misleading when expensive work never reaches a completed application request.
A useful authorized regression test has a clear resource envelope and an independent health probe. Stop the test when the envelope is reached. Record whether the server closes a problematic connection and whether a normal client remains responsive. Avoid judging success only by the absence of a crash.
Where to patch
Inventory every component that terminates HTTP/2: edge proxy, ingress, service mesh, and application server. Determine which hop actually decodes the frames. An updated outer proxy can reduce exposure without proving that a directly reachable inner listener is fixed.
Resolve h2 versions from the deployed dependency graph, including indirect dependencies. Update to maintained releases containing the relevant fixes, then verify the effective configuration and reachable protocols. A lockfile in a development checkout is evidence about that checkout, not the executable running in production.
For a learner, the next question is the right one: “What else can be valid enough to parse, but expensive enough to become a denial of service?” Compression, regular expressions, image decoders, and graph traversals are fruitful review categories. That is a method for investigating systems you are authorized to test, not a claim that any particular implementation is vulnerable.