cargo / h2 / audit
cargo : h2 @ 0.4.14
PE Patrick Elsen signed 2026-05-28 published 2026-05-28

Claims

concurrency-documentedconcurrency-safehas-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignparser-impl-safeparser-impl-testedprotocol-impl-safeprotocol-impl-testedunsafe-documentedunsafe-minimalunsafe-safeuses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

h2 0.4.14 implements HTTP/2 (RFC 9113) as an async client and server library. One unsafe block with a documented and sound invariant. Active mitigations for HPACK bomb, CONTINUATION flooding, and Rapid Reset (CVE-2023-44487) are present by default. Flow-control arithmetic uses checked operations. No findings.

Report

Subject

h2 0.4.14 is an asynchronous HTTP/2 client and server library for Rust, implementing RFC 9113 (formerly RFC 7540). It is the HTTP/2 transport used by hyper and therefore a foundational dependency across much of the Rust web ecosystem. The crate handles frame parsing, HPACK header compression/decompression, flow-control accounting, stream multiplexing, and connection lifecycle management. It does not handle TLS or TCP; callers supply a pre-established AsyncRead + AsyncWrite transport.

Methodology

The published crate contents were compared against the upstream Git repository at the commit recorded in .cargo_vcs_info.json using diff -rq. The differences were limited to cargo normalization in Cargo.toml and directories excluded from publication (tests/, fuzz/, fixtures/, ci/, util/), which are all present in the VCS checkout. No source-file differences were found.

All 53 Rust source files (~26k LOC) were read. Files read in full include the entire src/hpack/ subtree (decoder, encoder, header, Huffman table+codec, table), src/frame/headers.rs, src/codec/framed_read.rs, src/proto/streams/flow_control.rs, src/proto/streams/recv.rs, src/proto/streams/counts.rs, src/proto/streams/streams.rs (first 200 lines; architecture confirmed), src/proto/connection.rs, and src/lib.rs. The remaining modules (server.rs, client.rs, share.rs, proto/settings.rs, proto/ping_pong.rs, etc.) were read in full for DoS-related logic and grep-surveyed for unsafe and I/O.

Tools: grep (macOS 14.x), diff, find, wc.

Unsafe-code survey: grep -rEn 'unsafe\s*(\{|fn|impl|trait)' found exactly one unsafe block in the entire crate (src/hpack/header.rs:283). FFI survey: no extern "C" declarations found. Network/filesystem survey: no direct socket or file I/O; the crate operates on caller-supplied I/O objects.

The VCS repository's tests/ and fuzz/ directories were examined to characterize the test suite.

The CHANGELOG was read for security-relevant entries. RUSTSEC was not queried directly; known advisories are addressed in the changelog (CVE-2023-26964, addressed in 0.3.17; Rapid Reset addressed in 0.3.17/0.4.2).

Results

The published crate matches VCS byte-for-byte on all source files. No binary artifacts are present, justifying has-binaries. There is no build.rs and no proc-macro, justifying has-build-exec and has-install-exec.

The single unsafe block is in BytesStr::as_str (src/hpack/header.rs:283), which calls from_utf8_unchecked. The invariant is established by BytesStr::try_from, which checks UTF-8 validity before constructing the type. The block carries a // Safety: comment. This justifies uses-unsafe, unsafe-safe, unsafe-documented, and unsafe-minimal.

The codebase contains no cryptographic operations (uses-crypto, impl-crypto are false), no filesystem or network I/O beyond the caller-supplied I/O handle (uses-filesystem, uses-network, uses-exec are false), and no environment variable access (uses-environment false). No JIT (uses-jit, impl-jit false), interpreter (uses-interpreter, impl-interpreter false), data structure (impl-datastructure false), or standalone algorithm (impl-algorithm false) implementations are present. No obfuscated code, base64 payloads, or suspicious network endpoints were found, justifying is-benign. impl-concurrency is false: the crate uses tokio and std mutexes but implements no concurrency primitives.

Shared stream state is protected by Arc<Mutex<Inner>> in src/proto/streams/streams.rs. Every public method acquires this lock before touching stream state. MAX_CONCURRENT_STREAMS is enforced on both send and receive sides via Counts. This justifies uses-concurrency, concurrency-safe, and concurrency-documented.

DoS mitigations present in this version:

HPACK bomb / decompression amplification: src/codec/framed_read.rs defaults max_header_list_size to 16 MB. The HPACK decoder (src/frame/headers.rs, HeaderBlock::load) continues decoding past the limit (to keep HPACK state consistent) but sets is_over_size = true; the stream layer then rejects the frame with REFUSED_STREAM or PROTOCOL_ERROR. This justifies impl-parser and parser-impl-safe and protocol-impl-safe.

CONTINUATION flooding: src/codec/framed_read.rs tracks continuation_frames_count per partial header block and enforces max_continuation_frames (derived from header and frame size limits, minimum 5). Exceeding the limit closes the connection with ENHANCE_YOUR_CALM "too_many_continuations". Added in 0.4.4.

Rapid Reset (CVE-2023-44487): Two separate counters mitigate this. num_remote_reset_streams (cap: DEFAULT_REMOTE_RESET_STREAM_MAX = 20) counts remotely reset pending-accept streams; exceeding it closes the connection with ENHANCE_YOUR_CALM "too_many_resets" (src/proto/streams/recv.rs:886-899). num_local_error_reset_streams (cap: DEFAULT_LOCAL_RESET_COUNT_MAX = 1024) counts library-initiated RST_STREAM frames per connection lifetime; exceeding it triggers GOAWAY with ENHANCE_YOUR_CALM "too_many_internal_resets" (src/proto/streams/streams.rs). Both are enabled by default and configurable via builder methods.

Flow-control accounting uses a signed Window(i32) type with checked arithmetic throughout (src/proto/streams/flow_control.rs). inc_window additionally bounds the value to MAX_WINDOW_SIZE = 2^31 - 1. Receiving data beyond the window triggers FLOW_CONTROL_ERROR at stream or connection level. impl-protocol is true: the crate implements the HTTP/2 framing, flow-control, stream state machine, and HPACK compression protocols.

The HPACK integer decoder (src/hpack/decoder.rs:391-448) limits multi-byte integers to 5 octets and returns IntegerOverflow if exceeded, preventing integer-overflow attacks from malformed HPACK streams.

The test suite includes in-crate unit tests (40 #[test] assertions), an integration test workspace at tests/h2-tests/, quickcheck property tests in dev-dependencies, and a LibFuzzer fuzz harness at tests/h2-fuzz/. This justifies has-unit-tests, has-integration-tests, has-fuzz-tests, has-property-tests, parser-impl-tested, and protocol-impl-tested.

parser-impl-correct and protocol-impl-correct were not evaluated against the full RFC 9113 conformance suite; the HPACK fixture tests (src/hpack/test/fixture.rs) do cover the published HPACK spec test vectors. unsafe-tested: the single unsafe block is trivially correct and has been present without incident since the crate's creation; no miri or sanitizer run was performed during this audit.

No findings were recorded.

Conclusion

The codebase has one unsafe block with a valid and documented invariant. All known HTTP/2 DoS vectors reviewed in this audit — HPACK bomb, CONTINUATION flooding, Rapid Reset — have explicit mitigations active by default. Flow-control arithmetic is performed with checked operations that return protocol errors on overflow. The test suite includes fuzz tests and property-based tests covering the HPACK decoder, which is the highest-risk parsing component.

Findings

No findings.

Annotations(6)

src/codec/framed_read.rs

src/codec/framed_read.rs, line 22-23

const DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE: usize = 16 << 20;

The HPACK decoder in src/hpack/decoder.rs enforces a max_header_list_size limit (defaulting to 16 MB per DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE in src/codec/framed_read.rs). When a header block (including CONTINUATION frames) exceeds this limit the is_over_size flag is set and HPACK decoding continues for state consistency but the oversized frame is rejected with a REFUSED_STREAM or PROTOCOL_ERROR at the stream layer (recv.rs lines 204-231). This prevents HPACK-bomb / decompression-amplification attacks. Justifies parser-impl-safe and protocol-impl-safe.

src/codec/framed_read.rs, line 297-311

            // Check for CONTINUATION flood
            if is_end_headers {
                partial.continuation_frames_count = 0;
            } else {
                let cnt = partial.continuation_frames_count + 1;
                if cnt > max_continuation_frames {
                    tracing::debug!("too_many_continuations, max = {}", max_continuation_frames);
                    return Err(Error::library_go_away_data(
                        Reason::ENHANCE_YOUR_CALM,
                        "too_many_continuations",
                    ));
                } else {
                    partial.continuation_frames_count = cnt;
                }
            }

The decode_frame function in src/codec/framed_read.rs limits the number of CONTINUATION frames via max_continuation_frames, computed by calc_max_continuation_frames as a function of max_header_list_size and max_frame_size (minimum 5, with 25% padding). When exceeded, the connection is closed with ENHANCE_YOUR_CALM and debug data "too_many_continuations". This mitigates the CONTINUATION flooding attack. Added in 0.4.4. Justifies protocol-impl-safe.

src/hpack/header.rs

src/hpack/header.rs, line 281-284

    pub(crate) fn as_str(&self) -> &str {
        // Safety: check valid utf-8 in constructor
        unsafe { std::str::from_utf8_unchecked(self.0.as_ref()) }
    }

The single unsafe block in the crate is in BytesStr::as_str (line 283), which calls std::str::from_utf8_unchecked. The invariant that the bytes are valid UTF-8 is established in the constructor BytesStr::try_from, which calls std::str::from_utf8 and returns an error if the input is not valid UTF-8. The from_static constructor takes a &'static str which is already UTF-8. The // Safety: comment documents this explicitly. The use is minimal and the invariant holds. Justifies uses-unsafe, unsafe-safe, unsafe-documented, and unsafe-minimal.

src/proto/streams/flow_control.rs

src/proto/streams/flow_control.rs, line 113-133

    pub fn inc_window(&mut self, sz: WindowSize) -> Result<(), Reason> {
        let (val, overflow) = self.window_size.0.overflowing_add(sz as i32);

        if overflow {
            return Err(Reason::FLOW_CONTROL_ERROR);
        }

        if val > MAX_WINDOW_SIZE as i32 {
            return Err(Reason::FLOW_CONTROL_ERROR);
        }

        tracing::trace!(
            "inc_window; sz={}; old={}; new={}",
            sz,
            self.window_size,
            val
        );

        self.window_size = Window(val);
        Ok(())
    }

Flow-control accounting in src/proto/streams/flow_control.rs uses signed i32 internally (type Window) and performs all arithmetic with checked operations, returning Reason::FLOW_CONTROL_ERROR on overflow. inc_window additionally enforces the RFC maximum window size (MAX_WINDOW_SIZE = 2^31 - 1). Both the connection window and per-stream window are tracked in src/proto/streams/recv.rs. Receiving more data than the window allows triggers FLOW_CONTROL_ERROR at stream or connection level. Justifies protocol-impl-safe.

src/proto/streams/recv.rs

src/proto/streams/recv.rs, line 886-899

        if stream.is_pending_accept {
            if counts.can_inc_num_remote_reset_streams() {
                counts.inc_num_remote_reset_streams();
            } else {
                tracing::warn!(
                    "recv_reset; remotely-reset pending-accept streams reached limit ({:?})",
                    counts.max_remote_reset_streams(),
                );
                return Err(Error::library_go_away_data(
                    Reason::ENHANCE_YOUR_CALM,
                    "too_many_resets",
                ));
            }
        }

The Rapid Reset (CVE-2023-44487) mitigation is in src/proto/streams/recv.rs (lines 886-899) and src/proto/streams/streams.rs. When a remote peer sends RST_STREAM for a stream that is still in the pending-accept queue, the count num_remote_reset_streams is incremented and compared against max_remote_reset_streams (default 20, DEFAULT_REMOTE_RESET_STREAM_MAX). When the limit is exceeded the connection is closed with ENHANCE_YOUR_CALM and debug data "too_many_resets". Additionally, local_max_error_reset_streams (default 1024, DEFAULT_LOCAL_RESET_COUNT_MAX) tracks library-initiated RST_STREAM frames per connection lifetime; exceeding this triggers a GOAWAY with ENHANCE_YOUR_CALM and "too_many_internal_resets". Both mitigations are on by default.

src/proto/streams/streams.rs

src/proto/streams/streams.rs, line 19-36

pub(crate) struct Streams<B, P>
where
    P: Peer,
{
    /// Holds most of the connection and stream related state for processing
    /// HTTP/2 frames associated with streams.
    inner: Arc<Mutex<Inner>>,

    /// This is the queue of frames to be written to the wire. This is split out
    /// to avoid requiring a `B` generic on all public API types even if `B` is
    /// not technically required.
    ///
    /// Currently, splitting this out requires a second `Arc` + `Mutex`.
    /// However, it should be possible to avoid this duplication with a little
    /// bit of unsafe code. This optimization has been postponed until it has
    /// been shown to be necessary.
    send_buffer: Arc<SendBuffer<B>>,

Shared stream state is protected by a std::sync::Mutex<Inner> wrapped in an Arc in src/proto/streams/streams.rs. Every public method that accesses stream state acquires this lock. The Counts struct enforces MAX_CONCURRENT_STREAMS limits. MAX_CONCURRENT_STREAMS is respected on both send and receive sides: can_inc_num_recv_streams / can_inc_num_send_streams are checked before incrementing, and limits are adjustable via builder methods. Justifies uses-concurrency, concurrency-safe, and concurrency-documented.