cargo / hyper / audit
cargo : hyper @ 1.9.0
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-benignnetwork-safeparser-impl-safeparser-impl-testedprotocol-impl-safeprotocol-impl-testedunsafe-documentedunsafe-minimalunsafe-safeunsafe-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

hyper 1.9.0, the foundational Rust HTTP/1 and HTTP/2 library. The audit reviewed the HTTP/1 framing and chunked-decoding paths (Transfer-Encoding-over-Content-Length precedence, overflow-checked lengths, bounded extensions and trailers), the HTTP/2 limits delegated to h2, and all 61 unsafe sites; source is byte-identical to VCS. No findings; no RustSec advisory affects the 1.x line.

Report

Subject

hyper 1.9.0 is a low-level HTTP/1 and HTTP/2 implementation for Rust, providing client and server connection types over a caller-supplied transport. Its public API exposes client::conn and server::conn connection drivers, the body::Incoming streaming body, the service traits, the rt::Read/rt::Write I/O traits that abstract the transport, and connection-upgrade support. The crate does not open sockets itself; it reads and writes through the rt traits, leaving TLS and socket management to the caller. It is the foundation under reqwest, axum, and most of the Rust HTTP ecosystem. The build is feature gated (http1, http2, client, server, plus the unstable ffi and tracing); default = [].

Methodology

Tools: openvet 0.6.0, ripgrep, diff, git, and WebFetch/WebSearch against the RustSec database. I read the published contents/ tree (~21.6k LOC across 63 .rs files) and compared it byte-for-byte against the bundled vcs/ git checkout: diff -rq reported only Cargo.toml differing, which is expected cargo normalisation, with all of src/ identical. I read the security-critical HTTP/1 paths in full (proto/h1/role.rs, proto/h1/decode.rs, headers.rs, body/length.rs, the read-buffer code in proto/h1/io.rs and rt/io.rs), every unsafe site (61 occurrences across 16 files), the HTTP/2 server configuration in proto/h2/server.rs, the connection-upgrade downcast in upgrade.rs, and the ffi module surface. I surveyed the remaining modules. I checked RustSec for advisories affecting this version.

Results

hyper's src/ is byte-identical to the VCS checkout. The crate implements the HTTP/1 and HTTP/2 protocols and a hand-written HTTP/1 framing layer, so impl-protocol and impl-parser hold; it does not implement crypto (impl-crypto), interpreters (impl-interpreter), JIT (impl-jit), standalone data structures (impl-datastructure), or general algorithms (impl-algorithm), and it uses but does not implement concurrency primitives (impl-concurrency is false, uses-concurrency true), relying on tokio::sync and std atomics. It uses HTTP and is network code (uses-network) but performs no filesystem, process, environment, or cryptographic operations (uses-filesystem, uses-exec, uses-environment, uses-crypto, uses-jit, uses-interpreter all false). There is no build.rs (build = false), no install hook, and no bundled binaries, so has-build-exec, has-install-exec, and has-binaries are false.

Framing is the security-critical surface. On the server parse path (proto/h1/role.rs), Transfer-Encoding takes precedence over Content-Length: a present Transfer-Encoding whose final coding is not chunked is rejected, a Content-Length is ignored once Transfer-Encoding is seen, Transfer-Encoding on HTTP/1.0 is rejected, and multiple Content-Length headers are accepted only if they agree. headers::from_digits rejects a signed prefix and uses checked arithmetic. Together these are the modern defenses against request smuggling and the conditions behind the historical 0.x advisories (RUSTSEC-2020-0008, RUSTSEC-2021-0020, RUSTSEC-2021-0078, RUSTSEC-2021-0079); no RustSec advisory affects hyper 1.x. The chunked decoder (proto/h1/decode.rs) accumulates chunk-size with checked_mul/checked_add, bounds extensions, trailer count, and trailer bytes, and rejects bare LF in extensions. DecodedLength (body/length.rs) reserves two sentinel values above a MAX_LEN cap so a content-length can never alias the chunked/close-delimited markers. These support parser-impl-safe, protocol-impl-safe, and network-safe. HTTP/2 frame handling is delegated to the h2 crate; proto/h2/server.rs wires hyper's limits (max concurrent streams 200, local-error reset cap 1024, 16 KiB header-list size, optional pending-accept-reset cap) into the h2 builder, where the Rapid Reset (CVE-2023-44487) and CONTINUATION-flood mitigations reside. network-secure is not asserted: hyper is transport agnostic and does not itself provide TLS, and parser-impl-correct / protocol-impl-correct are left unasserted because RFC conformance was not exhaustively checked against a reference suite.

uses-unsafe is true. Excluding the ffi module, the unsafe is confined to a few patterns: reading MaybeUninit header-index storage bounded by the count httparse filled (role.rs), the uninitialized read-buffer fill in proto/h1/io.rs and the ReadBuf/ReadBufCursor abstraction in rt/io.rs (the standard tokio idiom, with advance using checked_add), a TypeId-guarded box downcast in upgrade.rs, a no-op Waker in common/task.rs, and set_len/set_init calls inside #[cfg(nightly)] benchmarks. Each carries a SAFETY comment and its invariant holds, supporting unsafe-safe, unsafe-documented, and unsafe-minimal. The remaining unsafe and all extern "C" live in the ffi module, which compiles only under both the ffi feature and the hyper_unstable_ffi cfg (a compile_error! fires otherwise); there, pointer validity is the documented responsibility of the C caller. unsafe-tested rests on the 124 in-tree tests plus integration suites, with many tests written to also run under cargo +nightly miri (the source gates heavy cases with #[cfg(not(miri))] and CI runs miri). The parser and protocol tests cover overflow, limit enforcement, early EOF, obs-fold, and round-trips, supporting parser-impl-tested and protocol-impl-tested; has-unit-tests and has-integration-tests hold, while has-fuzz-tests and has-property-tests are false (no in-tree fuzz or proptest harness).

Thread-safety is expressed through the type system and documented per type; shared state uses tokio::sync and atomics, and the ffi executor explicitly documents its non-reentrancy and its Send/Sync rationale for the opaque user pointer, supporting concurrency-safe and concurrency-documented. I found no obfuscated code, embedded blobs, telemetry, or suspicious endpoints; the crate's behavior matches its documentation, so is-benign holds.

No findings were recorded.

Conclusion

hyper 1.9.0's published source is byte-identical to its VCS checkout. The HTTP/1 framing layer implements the RFC 9112 Transfer-Encoding-over-Content-Length precedence, rejects ambiguous and overflowing length headers, and bounds chunk extensions and trailers; HTTP/2 framing and its Rapid Reset and CONTINUATION mitigations are delegated to the h2 crate with hyper supplying bounded limits. The 61 unsafe occurrences are concentrated in MaybeUninit header parsing, the uninitialized read-buffer abstraction, and the unstable ffi C API; each non-FFI site carries a SAFETY comment whose invariant holds, and the FFI surface is gated behind a cfg flag and documents its caller obligations. No RustSec advisory affects the 1.x line. The audit recorded no findings.

Findings

No findings.

Annotations(9)

src/body/length.rs

src/body/length.rs, line 20-87

impl DecodedLength {
    pub(crate) const CLOSE_DELIMITED: DecodedLength = DecodedLength(u64::MAX);
    pub(crate) const CHUNKED: DecodedLength = DecodedLength(u64::MAX - 1);
    pub(crate) const ZERO: DecodedLength = DecodedLength(0);

    #[cfg(test)]
    pub(crate) fn new(len: u64) -> Self {
        debug_assert!(len <= MAX_LEN);
        DecodedLength(len)
    }

    /// Takes the length as a content-length without other checks.
    ///
    /// Should only be called if previously confirmed this isn't
    /// CLOSE_DELIMITED or CHUNKED.
    #[inline]
    #[cfg(all(any(feature = "client", feature = "server"), feature = "http1"))]
    pub(crate) fn danger_len(self) -> u64 {
        debug_assert!(self.0 < Self::CHUNKED.0);
        self.0
    }

    /// Converts to an Option<u64> representing a Known or Unknown length.
    #[cfg(all(
        any(feature = "http1", feature = "http2"),
        any(feature = "client", feature = "server")
    ))]
    pub(crate) fn into_opt(self) -> Option<u64> {
        match self {
            DecodedLength::CHUNKED | DecodedLength::CLOSE_DELIMITED => None,
            DecodedLength(known) => Some(known),
        }
    }

    /// Checks the `u64` is within the maximum allowed for content-length.
    #[cfg(any(feature = "http1", feature = "http2"))]
    pub(crate) fn checked_new(len: u64) -> Result<Self, crate::error::Parse> {
        if len <= MAX_LEN {
            Ok(DecodedLength(len))
        } else {
            warn!("content-length bigger than maximum: {} > {}", len, MAX_LEN);
            Err(crate::error::Parse::TooLarge)
        }
    }

    #[cfg(all(
        any(feature = "http1", feature = "http2"),
        any(feature = "client", feature = "server")
    ))]
    pub(crate) fn sub_if(&mut self, amt: u64) {
        match *self {
            DecodedLength::CHUNKED | DecodedLength::CLOSE_DELIMITED => (),
            DecodedLength(ref mut known) => {
                *known -= amt;
            }
        }
    }

    /// Returns whether this represents an exact length.
    ///
    /// This includes 0, which of course is an exact known length.
    ///
    /// It would return false if "chunked" or otherwise size-unknown.
    #[cfg(all(any(feature = "client", feature = "server"), feature = "http2"))]
    pub(crate) fn is_exact(&self) -> bool {
        self.0 <= MAX_LEN
    }
}

DecodedLength encodes an HTTP message body length as a u64 with two reserved sentinels: u64::MAX - 1 for chunked and u64::MAX for close-delimited. checked_new rejects any content-length above MAX_LEN = u64::MAX - 2, so a parsed Content-Length can never alias a sentinel. This keeps the framing decision (known length vs chunked vs read-to-EOF) unambiguous.

Justifies protocol-impl-safe.

src/ffi/mod.rs

src/ffi/mod.rs, line 1-85

// We have a lot of c-types in here, stop warning about their names!
#![allow(non_camel_case_types)]
// fmt::Debug isn't helpful on FFI types
#![allow(missing_debug_implementations)]
// unreachable_pub warns `#[no_mangle] pub extern fn` in private mod.
#![allow(unreachable_pub)]

//! # hyper C API
//!
//! This part of the documentation describes the C API for hyper. That is, how
//! to *use* the hyper library in C code. This is **not** a regular Rust
//! module, and thus it is not accessible in Rust.
//!
//! ## Unstable
//!
//! The C API of hyper is currently **unstable**, which means it's not part of
//! the semver contract as the rest of the Rust API is. Because of that, it's
//! only accessible if `--cfg hyper_unstable_ffi` is passed to `rustc` when
//! compiling. The easiest way to do that is setting the `RUSTFLAGS`
//! environment variable.
//!
//! ## Building
//!
//! The C API is part of the Rust library, but isn't compiled by default. Using
//! `cargo`, staring with `1.64.0`, it can be compiled with the following command:
//!
//! ```notrust
//! RUSTFLAGS="--cfg hyper_unstable_ffi" cargo rustc --crate-type cdylib --features client,http1,http2,ffi
//! ```

// We may eventually allow the FFI to be enabled without `client` or `http1`,
// that is why we don't auto enable them as `ffi = ["client", "http1"]` in
// the `Cargo.toml`.
//
// But for now, give a clear message that this compile error is expected.
#[cfg(not(all(feature = "client", feature = "http1")))]
compile_error!("The `ffi` feature currently requires the `client` and `http1` features.");

#[cfg(not(hyper_unstable_ffi))]
compile_error!(
    "\
    The `ffi` feature is unstable, and requires the \
    `RUSTFLAGS='--cfg hyper_unstable_ffi'` environment variable to be set.\
"
);

#[macro_use]
mod macros;

mod body;
mod client;
mod error;
mod http_types;
mod io;
mod task;

pub use self::body::*;
pub use self::client::*;
pub use self::error::*;
pub use self::http_types::*;
pub use self::io::*;
pub use self::task::*;

/// Return in iter functions to continue iterating.
pub const HYPER_ITER_CONTINUE: std::ffi::c_int = 0;
/// Return in iter functions to stop iterating.
#[allow(unused)]
pub const HYPER_ITER_BREAK: std::ffi::c_int = 1;

/// An HTTP Version that is unspecified.
pub const HYPER_HTTP_VERSION_NONE: std::ffi::c_int = 0;
/// The HTTP/1.0 version.
pub const HYPER_HTTP_VERSION_1_0: std::ffi::c_int = 10;
/// The HTTP/1.1 version.
pub const HYPER_HTTP_VERSION_1_1: std::ffi::c_int = 11;
/// The HTTP/2 version.
pub const HYPER_HTTP_VERSION_2: std::ffi::c_int = 20;

#[derive(Clone)]
struct UserDataPointer(*mut std::ffi::c_void);

// We don't actually know anything about this pointer, it's up to the user
// to do the right thing.
unsafe impl Send for UserDataPointer {}
unsafe impl Sync for UserDataPointer {}

The C API. This module is compiled only when the ffi Cargo feature and the hyper_unstable_ffi cfg are both set; otherwise a compile_error! fires (lines 36-45). It concentrates most of hyper's unsafe and all extern "C" surface. Pointer validity, lifetime, and ownership are the C caller's responsibility, as documented throughout the module (for example the hyper_executor re-entrancy note at lines 43-60). The UserDataPointer Send/Sync impls are explicit acknowledgements that hyper cannot reason about the opaque user pointer. None of this is reachable from the default Rust build.

Justifies uses-unsafe, unsafe-minimal.

src/headers.rs

src/headers.rs, line 35-97

pub(super) fn content_length_parse(value: &HeaderValue) -> Option<u64> {
    from_digits(value.as_bytes())
}

#[cfg(any(feature = "client", all(feature = "server", feature = "http2")))]
pub(super) fn content_length_parse_all(headers: &HeaderMap) -> Option<u64> {
    content_length_parse_all_values(headers.get_all(CONTENT_LENGTH).into_iter())
}

#[cfg(any(feature = "client", all(feature = "server", feature = "http2")))]
pub(super) fn content_length_parse_all_values(values: ValueIter<'_, HeaderValue>) -> Option<u64> {
    // If multiple Content-Length headers were sent, everything can still
    // be alright if they all contain the same value, and all parse
    // correctly. If not, then it's an error.

    let mut content_length: Option<u64> = None;
    for h in values {
        if let Ok(line) = h.to_str() {
            for v in line.split(',') {
                if let Some(n) = from_digits(v.trim().as_bytes()) {
                    if content_length.is_none() {
                        content_length = Some(n)
                    } else if content_length != Some(n) {
                        return None;
                    }
                } else {
                    return None;
                }
            }
        } else {
            return None;
        }
    }

    content_length
}

fn from_digits(bytes: &[u8]) -> Option<u64> {
    // cannot use FromStr for u64, since it allows a signed prefix
    let mut result = 0u64;
    const RADIX: u64 = 10;

    if bytes.is_empty() {
        return None;
    }

    for &b in bytes {
        // can't use char::to_digit, since we haven't verified these bytes
        // are utf-8.
        match b {
            b'0'..=b'9' => {
                result = result.checked_mul(RADIX)?;
                result = result.checked_add((b - b'0') as u64)?;
            }
            _ => {
                // not a DIGIT, get outta here!
                return None;
            }
        }
    }

    Some(result)
}

Content-Length parsing. from_digits accepts only ASCII digits, rejecting a signed (+/-) prefix and any non-digit byte, and uses checked_mul/ checked_add so an overflowing length yields None rather than wrapping. This rejects the +-prefixed Content-Length that caused RUSTSEC-2021-0078. content_length_parse_all_values requires every comma-separated token and every duplicate header to parse to the same value, returning None (an error upstream) otherwise.

Justifies parser-impl-safe.

src/proto/h1/decode.rs

src/proto/h1/decode.rs, line 306-648

impl ChunkedState {
    fn new() -> ChunkedState {
        ChunkedState::Start
    }
    fn step<R: MemRead>(
        &self,
        cx: &mut Context<'_>,
        body: &mut R,
        StepArgs {
            chunk_size,
            chunk_buf,
            extensions_cnt,
            trailers_buf,
            trailers_cnt,
            max_headers_cnt,
            max_headers_bytes,
        }: StepArgs<'_>,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        use self::ChunkedState::*;
        match *self {
            Start => ChunkedState::read_start(cx, body, chunk_size),
            Size => ChunkedState::read_size(cx, body, chunk_size),
            SizeLws => ChunkedState::read_size_lws(cx, body),
            Extension => ChunkedState::read_extension(cx, body, extensions_cnt),
            SizeLf => ChunkedState::read_size_lf(cx, body, *chunk_size),
            Body => ChunkedState::read_body(cx, body, chunk_size, chunk_buf),
            BodyCr => ChunkedState::read_body_cr(cx, body),
            BodyLf => ChunkedState::read_body_lf(cx, body),
            Trailer => ChunkedState::read_trailer(cx, body, trailers_buf, max_headers_bytes),
            TrailerLf => ChunkedState::read_trailer_lf(
                cx,
                body,
                trailers_buf,
                trailers_cnt,
                max_headers_cnt,
                max_headers_bytes,
            ),
            EndCr => ChunkedState::read_end_cr(cx, body, trailers_buf, max_headers_bytes),
            EndLf => ChunkedState::read_end_lf(cx, body, trailers_buf, max_headers_bytes),
            End => Poll::Ready(Ok(ChunkedState::End)),
        }
    }

    fn read_start<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
        size: &mut u64,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        trace!("Read chunk start");

        let radix = 16;
        match byte!(rdr, cx) {
            b @ b'0'..=b'9' => {
                *size = or_overflow!(size.checked_mul(radix));
                *size = or_overflow!(size.checked_add((b - b'0') as u64));
            }
            b @ b'a'..=b'f' => {
                *size = or_overflow!(size.checked_mul(radix));
                *size = or_overflow!(size.checked_add((b + 10 - b'a') as u64));
            }
            b @ b'A'..=b'F' => {
                *size = or_overflow!(size.checked_mul(radix));
                *size = or_overflow!(size.checked_add((b + 10 - b'A') as u64));
            }
            _ => {
                return Poll::Ready(Err(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "Invalid chunk size line: missing size digit",
                )));
            }
        }

        Poll::Ready(Ok(ChunkedState::Size))
    }

    fn read_size<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
        size: &mut u64,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        trace!("Read chunk hex size");

        let radix = 16;
        match byte!(rdr, cx) {
            b @ b'0'..=b'9' => {
                *size = or_overflow!(size.checked_mul(radix));
                *size = or_overflow!(size.checked_add((b - b'0') as u64));
            }
            b @ b'a'..=b'f' => {
                *size = or_overflow!(size.checked_mul(radix));
                *size = or_overflow!(size.checked_add((b + 10 - b'a') as u64));
            }
            b @ b'A'..=b'F' => {
                *size = or_overflow!(size.checked_mul(radix));
                *size = or_overflow!(size.checked_add((b + 10 - b'A') as u64));
            }
            b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)),
            b';' => return Poll::Ready(Ok(ChunkedState::Extension)),
            b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)),
            _ => {
                return Poll::Ready(Err(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "Invalid chunk size line: Invalid Size",
                )));
            }
        }
        Poll::Ready(Ok(ChunkedState::Size))
    }
    fn read_size_lws<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        trace!("read_size_lws");
        match byte!(rdr, cx) {
            // LWS can follow the chunk size, but no more digits can come
            b'\t' | b' ' => Poll::Ready(Ok(ChunkedState::SizeLws)),
            b';' => Poll::Ready(Ok(ChunkedState::Extension)),
            b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
            _ => Poll::Ready(Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "Invalid chunk size linear white space",
            ))),
        }
    }
    fn read_extension<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
        extensions_cnt: &mut u64,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        trace!("read_extension");
        // We don't care about extensions really at all. Just ignore them.
        // They "end" at the next CRLF.
        //
        // However, some implementations may not check for the CR, so to save
        // them from themselves, we reject extensions containing plain LF as
        // well.
        match byte!(rdr, cx) {
            b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
            b'\n' => Poll::Ready(Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "invalid chunk extension contains newline",
            ))),
            _ => {
                *extensions_cnt += 1;
                if *extensions_cnt >= CHUNKED_EXTENSIONS_LIMIT {
                    Poll::Ready(Err(io::Error::new(
                        io::ErrorKind::InvalidData,
                        "chunk extensions over limit",
                    )))
                } else {
                    Poll::Ready(Ok(ChunkedState::Extension))
                }
            } // no supported extensions
        }
    }
    fn read_size_lf<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
        size: u64,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        trace!("Chunk size is {:?}", size);
        match byte!(rdr, cx) {
            b'\n' => {
                if size == 0 {
                    Poll::Ready(Ok(ChunkedState::EndCr))
                } else {
                    debug!("incoming chunked header: {0:#X} ({0} bytes)", size);
                    Poll::Ready(Ok(ChunkedState::Body))
                }
            }
            _ => Poll::Ready(Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "Invalid chunk size LF",
            ))),
        }
    }

    fn read_body<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
        rem: &mut u64,
        buf: &mut Option<Bytes>,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        trace!("Chunked read, remaining={:?}", rem);

        // cap remaining bytes at the max capacity of usize
        let rem_cap = match *rem {
            r if r > usize::MAX as u64 => usize::MAX,
            r => r as usize,
        };

        let to_read = rem_cap;
        let slice = ready!(rdr.read_mem(cx, to_read))?;
        let count = slice.len();

        if count == 0 {
            *rem = 0;
            return Poll::Ready(Err(io::Error::new(
                io::ErrorKind::UnexpectedEof,
                IncompleteBody,
            )));
        }
        *buf = Some(slice);
        *rem -= count as u64;

        if *rem > 0 {
            Poll::Ready(Ok(ChunkedState::Body))
        } else {
            Poll::Ready(Ok(ChunkedState::BodyCr))
        }
    }
    fn read_body_cr<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        match byte!(rdr, cx) {
            b'\r' => Poll::Ready(Ok(ChunkedState::BodyLf)),
            _ => Poll::Ready(Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "Invalid chunk body CR",
            ))),
        }
    }
    fn read_body_lf<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        match byte!(rdr, cx) {
            b'\n' => Poll::Ready(Ok(ChunkedState::Start)),
            _ => Poll::Ready(Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "Invalid chunk body LF",
            ))),
        }
    }

    fn read_trailer<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
        trailers_buf: &mut Option<BytesMut>,
        h1_max_header_size: usize,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        trace!("read_trailer");
        let byte = byte!(rdr, cx);

        put_u8!(
            trailers_buf.as_mut().expect("trailers_buf is None"),
            byte,
            h1_max_header_size
        );

        match byte {
            b'\r' => Poll::Ready(Ok(ChunkedState::TrailerLf)),
            _ => Poll::Ready(Ok(ChunkedState::Trailer)),
        }
    }

    fn read_trailer_lf<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
        trailers_buf: &mut Option<BytesMut>,
        trailers_cnt: &mut usize,
        h1_max_headers: usize,
        h1_max_header_size: usize,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        let byte = byte!(rdr, cx);
        match byte {
            b'\n' => {
                if *trailers_cnt >= h1_max_headers {
                    return Poll::Ready(Err(io::Error::new(
                        io::ErrorKind::InvalidData,
                        "chunk trailers count overflow",
                    )));
                }
                *trailers_cnt += 1;

                put_u8!(
                    trailers_buf.as_mut().expect("trailers_buf is None"),
                    byte,
                    h1_max_header_size
                );

                Poll::Ready(Ok(ChunkedState::EndCr))
            }
            _ => Poll::Ready(Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "Invalid trailer end LF",
            ))),
        }
    }

    fn read_end_cr<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
        trailers_buf: &mut Option<BytesMut>,
        h1_max_header_size: usize,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        let byte = byte!(rdr, cx);
        match byte {
            b'\r' => {
                if let Some(trailers_buf) = trailers_buf {
                    put_u8!(trailers_buf, byte, h1_max_header_size);
                }
                Poll::Ready(Ok(ChunkedState::EndLf))
            }
            byte => {
                match trailers_buf {
                    None => {
                        // 64 will fit a single Expires header without reallocating
                        let mut buf = BytesMut::with_capacity(64);
                        buf.put_u8(byte);
                        *trailers_buf = Some(buf);
                    }
                    Some(ref mut trailers_buf) => {
                        put_u8!(trailers_buf, byte, h1_max_header_size);
                    }
                }

                Poll::Ready(Ok(ChunkedState::Trailer))
            }
        }
    }
    fn read_end_lf<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
        trailers_buf: &mut Option<BytesMut>,
        h1_max_header_size: usize,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        let byte = byte!(rdr, cx);
        match byte {
            b'\n' => {
                if let Some(trailers_buf) = trailers_buf {
                    put_u8!(trailers_buf, byte, h1_max_header_size);
                }
                Poll::Ready(Ok(ChunkedState::End))
            }
            _ => Poll::Ready(Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "Invalid chunk end LF",
            ))),
        }
    }
}

Chunked transfer-decoding state machine. Chunk-size accumulation uses checked_mul/checked_add via the or_overflow! macro (lines 359-368, 391-401), so an oversized chunk-size line errors with "invalid chunk size: overflow" rather than wrapping. Chunk extensions are bounded by CHUNKED_EXTENSIONS_LIMIT (16 KiB, applied across the whole body per the comment at line 17-19), and a bare LF inside an extension is rejected (line 444-447). Trailer bytes are bounded by h1_max_header_size and trailer count by h1_max_headers (put_u8! macro and read_trailer_lf), both enforced before the buffer grows. Trailers are then parsed with httparse and validated through http::HeaderName/HeaderValue constructors. The state machine and limits are covered by the unit tests in this file (overflow, extension-over-limit, trailer-count and trailer-size limits, early EOF).

Justifies parser-impl-safe, parser-impl-tested, protocol-impl-tested.

src/proto/h1/io.rs

src/proto/h1/io.rs, line 223-253

    pub(crate) fn poll_read_from_io(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<usize>> {
        self.read_blocked = false;
        let next = self.read_buf_strategy.next();
        if self.read_buf_remaining_mut() < next {
            self.read_buf.reserve(next);
        }

        // SAFETY: ReadBuf and poll_read promise not to set any uninitialized
        // bytes onto `dst`.
        let dst = unsafe { self.read_buf.chunk_mut().as_uninit_slice_mut() };
        let mut buf = ReadBuf::uninit(dst);
        match Pin::new(&mut self.io).poll_read(cx, buf.unfilled()) {
            Poll::Ready(Ok(_)) => {
                let n = buf.filled().len();
                trace!("received {} bytes", n);
                unsafe {
                    // Safety: we just read that many bytes into the
                    // uninitialized part of the buffer, so this is okay.
                    // @tokio pls give me back `poll_read_buf` thanks
                    self.read_buf.advance_mut(n);
                }
                self.read_buf_strategy.record(n);
                Poll::Ready(Ok(n))
            }
            Poll::Pending => {
                self.read_blocked = true;
                Poll::Pending
            }
            Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
        }
    }

Reading from the transport into the read buffer. The unsafe block exposes the spare capacity of the BytesMut as an uninitialized slice, wraps it in a ReadBuf, and after a successful poll_read advances the buffer by exactly the number of bytes the reader reported as filled (buf.filled().len()). The Read/ReadBuf contract forbids the reader from de-initializing previously written bytes or claiming more filled bytes than it wrote, so advance_mut(n) only commits initialized bytes. The SAFETY comments at lines 230 and 238-241 state these invariants.

Justifies unsafe-safe, unsafe-documented.

src/proto/h1/role.rs

src/proto/h1/role.rs, line 218-334

        // According to https://tools.ietf.org/html/rfc7230#section-3.3.3
        // 1. (irrelevant to Request)
        // 2. (irrelevant to Request)
        // 3. Transfer-Encoding: chunked has a chunked body.
        // 4. If multiple differing Content-Length headers or invalid, close connection.
        // 5. Content-Length header has a sized body.
        // 6. Length 0.
        // 7. (irrelevant to Request)

        let mut decoder = DecodedLength::ZERO;
        let mut expect_continue = false;
        let mut con_len = None;
        let mut is_te = false;
        let mut is_te_chunked = false;
        let mut wants_upgrade = subject.0 == Method::CONNECT;

        let mut header_case_map = if ctx.preserve_header_case {
            Some(HeaderCaseMap::default())
        } else {
            None
        };

        #[cfg(feature = "ffi")]
        let mut header_order = if ctx.preserve_header_order {
            Some(OriginalHeaderOrder::default())
        } else {
            None
        };

        let mut headers = ctx.cached_headers.take().unwrap_or_default();

        headers.reserve(headers_len);

        for header in &headers_indices[..headers_len] {
            // SAFETY: array is valid up to `headers_len`
            let header = unsafe { header.assume_init_ref() };
            let name = header_name!(&slice[header.name.0..header.name.1]);
            let value = header_value!(slice.slice(header.value.0..header.value.1));

            match name {
                header::TRANSFER_ENCODING => {
                    // https://tools.ietf.org/html/rfc7230#section-3.3.3
                    // If Transfer-Encoding header is present, and 'chunked' is
                    // not the final encoding, and this is a Request, then it is
                    // malformed. A server should respond with 400 Bad Request.
                    if !is_http_11 {
                        debug!("HTTP/1.0 cannot have Transfer-Encoding header");
                        return Err(Parse::transfer_encoding_unexpected());
                    }
                    is_te = true;
                    if headers::is_chunked_(&value) {
                        is_te_chunked = true;
                        decoder = DecodedLength::CHUNKED;
                    } else {
                        is_te_chunked = false;
                    }
                }
                header::CONTENT_LENGTH => {
                    if is_te {
                        continue;
                    }
                    let len = headers::content_length_parse(&value)
                        .ok_or_else(Parse::content_length_invalid)?;
                    if let Some(prev) = con_len {
                        if prev != len {
                            debug!(
                                "multiple Content-Length headers with different values: [{}, {}]",
                                prev, len,
                            );
                            return Err(Parse::content_length_invalid());
                        }
                        // we don't need to append this secondary length
                        continue;
                    }
                    decoder = DecodedLength::checked_new(len)?;
                    con_len = Some(len);
                }
                header::CONNECTION => {
                    // keep_alive was previously set to default for Version
                    if keep_alive {
                        // HTTP/1.1
                        keep_alive = !headers::connection_close(&value);
                    } else {
                        // HTTP/1.0
                        keep_alive = headers::connection_keep_alive(&value);
                    }
                }
                header::EXPECT => {
                    // According to https://datatracker.ietf.org/doc/html/rfc2616#section-14.20
                    // Comparison of expectation values is case-insensitive for unquoted tokens
                    // (including the 100-continue token)
                    expect_continue = value.as_bytes().eq_ignore_ascii_case(b"100-continue");
                }
                header::UPGRADE => {
                    // Upgrades are only allowed with HTTP/1.1
                    wants_upgrade = is_http_11;
                }

                _ => (),
            }

            if let Some(ref mut header_case_map) = header_case_map {
                header_case_map.append(&name, slice.slice(header.name.0..header.name.1));
            }

            #[cfg(feature = "ffi")]
            if let Some(ref mut header_order) = header_order {
                header_order.append(&name);
            }

            headers.append(name, value);
        }

        if is_te && !is_te_chunked {
            debug!("request with transfer-encoding header, but not chunked, bad request");
            return Err(Parse::transfer_encoding_invalid());
        }

HTTP/1 request framing and message-length determination for the server role.

Transfer-Encoding takes precedence over Content-Length: once is_te is set, any Content-Length header is ignored (continue at line 277), and a request carrying Transfer-Encoding whose final coding is not chunked is rejected with transfer_encoding_invalid (line 331-334). Transfer-Encoding on an HTTP/1.0 request is rejected (line 263-266). Multiple Content-Length headers with differing values are rejected (line 281-291); equal duplicates are collapsed. This is the RFC 9112 framing precedence that defends against request smuggling and addresses the historical hyper 0.x advisories (RUSTSEC-2020-0008, RUSTSEC-2021-0020, RUSTSEC-2021-0078).

Justifies protocol-impl-safe, network-safe.

src/proto/h1/role.rs, line 149-256

        // Both headers_indices and headers are using uninitialized memory,
        // but we *never* read any of it until after httparse has assigned
        // values into it. By not zeroing out the stack memory, this saves
        // a good ~5% on pipeline benchmarks.
        let mut headers_indices: SmallVec<[MaybeUninit<HeaderIndices>; DEFAULT_MAX_HEADERS]> =
            match ctx.h1_max_headers {
                Some(cap) => smallvec![MaybeUninit::uninit(); cap],
                None => smallvec_inline![MaybeUninit::uninit(); DEFAULT_MAX_HEADERS],
            };
        {
            let mut headers: SmallVec<[MaybeUninit<httparse::Header<'_>>; DEFAULT_MAX_HEADERS]> =
                match ctx.h1_max_headers {
                    Some(cap) => smallvec![MaybeUninit::uninit(); cap],
                    None => smallvec_inline![MaybeUninit::uninit(); DEFAULT_MAX_HEADERS],
                };
            trace!(bytes = buf.len(), "Request.parse");
            let mut req = httparse::Request::new(&mut []);
            let bytes = buf.as_ref();
            match ctx.h1_parser_config.parse_request_with_uninit_headers(
                &mut req,
                bytes,
                &mut headers,
            ) {
                Ok(httparse::Status::Complete(parsed_len)) => {
                    trace!("Request.parse Complete({})", parsed_len);
                    len = parsed_len;
                    let uri = req.path.unwrap();
                    if uri.len() > MAX_URI_LEN {
                        return Err(Parse::UriTooLong);
                    }
                    method = Method::from_bytes(req.method.unwrap().as_bytes())?;
                    path_range = Server::record_path_range(bytes, uri);
                    version = if req.version.unwrap() == 1 {
                        keep_alive = true;
                        is_http_11 = true;
                        Version::HTTP_11
                    } else {
                        keep_alive = false;
                        is_http_11 = false;
                        Version::HTTP_10
                    };

                    record_header_indices(bytes, req.headers, &mut headers_indices)?;
                    headers_len = req.headers.len();
                }
                Ok(httparse::Status::Partial) => return Ok(None),
                // if invalid Token, try to determine if for method or path
                Err(httparse::Error::Token) => {
                    return Err({
                        if req.method.is_none() {
                            Parse::Method
                        } else {
                            debug_assert!(req.path.is_none());
                            Parse::Uri
                        }
                    })
                }
                Err(err) => return Err(err.into()),
            }
        };

        let slice = buf.split_to(len).freeze();
        let uri = {
            let uri_bytes = slice.slice_ref(&slice[path_range]);
            // TODO(lucab): switch to `Uri::from_shared()` once public.
            http::Uri::from_maybe_shared(uri_bytes)?
        };
        subject = RequestLine(method, uri);

        // According to https://tools.ietf.org/html/rfc7230#section-3.3.3
        // 1. (irrelevant to Request)
        // 2. (irrelevant to Request)
        // 3. Transfer-Encoding: chunked has a chunked body.
        // 4. If multiple differing Content-Length headers or invalid, close connection.
        // 5. Content-Length header has a sized body.
        // 6. Length 0.
        // 7. (irrelevant to Request)

        let mut decoder = DecodedLength::ZERO;
        let mut expect_continue = false;
        let mut con_len = None;
        let mut is_te = false;
        let mut is_te_chunked = false;
        let mut wants_upgrade = subject.0 == Method::CONNECT;

        let mut header_case_map = if ctx.preserve_header_case {
            Some(HeaderCaseMap::default())
        } else {
            None
        };

        #[cfg(feature = "ffi")]
        let mut header_order = if ctx.preserve_header_order {
            Some(OriginalHeaderOrder::default())
        } else {
            None
        };

        let mut headers = ctx.cached_headers.take().unwrap_or_default();

        headers.reserve(headers_len);

        for header in &headers_indices[..headers_len] {
            // SAFETY: array is valid up to `headers_len`
            let header = unsafe { header.assume_init_ref() };
            let name = header_name!(&slice[header.name.0..header.name.1]);
            let value = header_value!(slice.slice(header.value.0..header.value.1));

Header parsing into uninitialized stack/inline storage. headers_indices and the httparse::Header array are SmallVec<[MaybeUninit<_>; DEFAULT_MAX_HEADERS]> left uninitialized to save zeroing cost. record_header_indices writes one entry per header that httparse actually parsed, and the subsequent loops read only &headers_indices[..headers_len], where headers_len equals the count httparse filled in. The unsafe { header.assume_init_ref() } calls (lines 253, 1105) and assume_init_mut (line 1078) are therefore only reached for entries that were written, upholding the MaybeUninit invariant noted in the SAFETY comments. record_header_indices also rejects any header name >= 64 KiB (Parse::TooLarge). The HeaderValue::from_maybe_shared_unchecked macro (line 50) is fed bytes that httparse already validated as a header value, and the slice ranges are recorded from the same buffer the values point into.

Justifies uses-unsafe, unsafe-safe, unsafe-documented, unsafe-minimal.

src/proto/h2/server.rs

src/proto/h2/server.rs, line 30-147

// Our defaults are chosen for the "majority" case, which usually are not
// resource constrained, and so the spec default of 64kb can be too limiting
// for performance.
//
// At the same time, a server more often has multiple clients connected, and
// so is more likely to use more resources than a client would.
const DEFAULT_CONN_WINDOW: u32 = 1024 * 1024; // 1mb
const DEFAULT_STREAM_WINDOW: u32 = 1024 * 1024; // 1mb
const DEFAULT_MAX_FRAME_SIZE: u32 = 1024 * 16; // 16kb
const DEFAULT_MAX_SEND_BUF_SIZE: usize = 1024 * 400; // 400kb
const DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE: u32 = 1024 * 16; // 16kb
const DEFAULT_MAX_LOCAL_ERROR_RESET_STREAMS: usize = 1024;

#[derive(Clone, Debug)]
pub(crate) struct Config {
    pub(crate) adaptive_window: bool,
    pub(crate) initial_conn_window_size: u32,
    pub(crate) initial_stream_window_size: u32,
    pub(crate) max_frame_size: u32,
    pub(crate) enable_connect_protocol: bool,
    pub(crate) max_concurrent_streams: Option<u32>,
    pub(crate) max_pending_accept_reset_streams: Option<usize>,
    pub(crate) max_local_error_reset_streams: Option<usize>,
    pub(crate) keep_alive_interval: Option<Duration>,
    pub(crate) keep_alive_timeout: Duration,
    pub(crate) max_send_buffer_size: usize,
    pub(crate) max_header_list_size: u32,
    pub(crate) date_header: bool,
}

impl Default for Config {
    fn default() -> Config {
        Config {
            adaptive_window: false,
            initial_conn_window_size: DEFAULT_CONN_WINDOW,
            initial_stream_window_size: DEFAULT_STREAM_WINDOW,
            max_frame_size: DEFAULT_MAX_FRAME_SIZE,
            enable_connect_protocol: false,
            max_concurrent_streams: Some(200),
            max_pending_accept_reset_streams: None,
            max_local_error_reset_streams: Some(DEFAULT_MAX_LOCAL_ERROR_RESET_STREAMS),
            keep_alive_interval: None,
            keep_alive_timeout: Duration::from_secs(20),
            max_send_buffer_size: DEFAULT_MAX_SEND_BUF_SIZE,
            max_header_list_size: DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE,
            date_header: true,
        }
    }
}

pin_project! {
    pub(crate) struct Server<T, S, B, E>
    where
        S: HttpService<IncomingBody>,
        B: Body,
    {
        exec: E,
        timer: Time,
        service: S,
        state: State<T, B>,
        date_header: bool,
        close_pending: bool
    }
}

enum State<T, B>
where
    B: Body,
{
    Handshaking {
        ping_config: ping::Config,
        hs: Handshake<Compat<T>, SendBuf<B::Data>>,
    },
    Serving(Serving<T, B>),
}

struct Serving<T, B>
where
    B: Body,
{
    ping: Option<(ping::Recorder, ping::Ponger)>,
    conn: Connection<Compat<T>, SendBuf<B::Data>>,
    closing: Option<crate::Error>,
    date_header: bool,
}

impl<T, S, B, E> Server<T, S, B, E>
where
    T: Read + Write + Unpin,
    S: HttpService<IncomingBody, ResBody = B>,
    S::Error: Into<Box<dyn StdError + Send + Sync>>,
    B: Body + 'static,
    E: Http2ServerConnExec<S::Future, B>,
{
    pub(crate) fn new(
        io: T,
        service: S,
        config: &Config,
        exec: E,
        timer: Time,
    ) -> Server<T, S, B, E> {
        let mut builder = h2::server::Builder::default();
        builder
            .initial_window_size(config.initial_stream_window_size)
            .initial_connection_window_size(config.initial_conn_window_size)
            .max_frame_size(config.max_frame_size)
            .max_header_list_size(config.max_header_list_size)
            .max_local_error_reset_streams(config.max_local_error_reset_streams)
            .max_send_buffer_size(config.max_send_buffer_size);
        if let Some(max) = config.max_concurrent_streams {
            builder.max_concurrent_streams(max);
        }
        if let Some(max) = config.max_pending_accept_reset_streams {
            builder.max_pending_accept_reset_streams(max);
        }
        if config.enable_connect_protocol {
            builder.enable_connect_protocol();
        }

HTTP/2 server configuration. hyper delegates HTTP/2 frame parsing and stream management to the h2 crate; this file wires hyper's Config into the h2::server::Builder. Defaults set max_concurrent_streams = 200, max_local_error_reset_streams = 1024, and max_header_list_size = 16 KiB, and forward max_pending_accept_reset_streams when set. The Rapid Reset (CVE-2023-44487) and CONTINUATION-flood mitigations themselves live in h2, which enforces a pending-reset cap and a header-list-size limit; the relevant knobs are surfaced and bounded here.

Justifies protocol-impl-safe.

src/rt/io.rs

src/rt/io.rs, line 234-344

impl<'data> ReadBuf<'data> {
    /// Create a new `ReadBuf` with a slice of initialized bytes.
    #[inline]
    pub fn new(raw: &'data mut [u8]) -> Self {
        let len = raw.len();
        Self {
            // SAFETY: We never de-init the bytes ourselves.
            raw: unsafe { &mut *(raw as *mut [u8] as *mut [MaybeUninit<u8>]) },
            filled: 0,
            init: len,
        }
    }

    /// Create a new `ReadBuf` with a slice of uninitialized bytes.
    #[inline]
    pub fn uninit(raw: &'data mut [MaybeUninit<u8>]) -> Self {
        Self {
            raw,
            filled: 0,
            init: 0,
        }
    }

    /// Get a slice of the buffer that has been filled in with bytes.
    #[inline]
    pub fn filled(&self) -> &[u8] {
        // SAFETY: We only slice the filled part of the buffer, which is always valid
        unsafe { &*(&self.raw[0..self.filled] as *const [MaybeUninit<u8>] as *const [u8]) }
    }

    /// Get a cursor to the unfilled portion of the buffer.
    #[inline]
    pub fn unfilled<'cursor>(&'cursor mut self) -> ReadBufCursor<'cursor> {
        ReadBufCursor {
            // SAFETY: self.buf is never re-assigned, so its safe to narrow
            // the lifetime.
            buf: unsafe {
                std::mem::transmute::<&'cursor mut ReadBuf<'data>, &'cursor mut ReadBuf<'cursor>>(
                    self,
                )
            },
        }
    }

    #[inline]
    #[cfg(all(any(feature = "client", feature = "server"), feature = "http2"))]
    pub(crate) unsafe fn set_init(&mut self, n: usize) {
        self.init = self.init.max(n);
    }

    #[inline]
    #[cfg(all(any(feature = "client", feature = "server"), feature = "http2"))]
    pub(crate) unsafe fn set_filled(&mut self, n: usize) {
        self.filled = self.filled.max(n);
    }

    #[inline]
    #[cfg(all(any(feature = "client", feature = "server"), feature = "http2"))]
    pub(crate) fn len(&self) -> usize {
        self.filled
    }

    #[inline]
    #[cfg(all(any(feature = "client", feature = "server"), feature = "http2"))]
    pub(crate) fn init_len(&self) -> usize {
        self.init
    }

    #[inline]
    fn remaining(&self) -> usize {
        self.capacity() - self.filled
    }

    #[inline]
    fn capacity(&self) -> usize {
        self.raw.len()
    }
}

impl fmt::Debug for ReadBuf<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ReadBuf")
            .field("filled", &self.filled)
            .field("init", &self.init)
            .field("capacity", &self.capacity())
            .finish()
    }
}

impl ReadBufCursor<'_> {
    /// Access the unfilled part of the buffer.
    ///
    /// # Safety
    ///
    /// The caller must not uninitialize any bytes that may have been
    /// initialized before.
    #[inline]
    pub unsafe fn as_mut(&mut self) -> &mut [MaybeUninit<u8>] {
        &mut self.buf.raw[self.buf.filled..]
    }

    /// Advance the `filled` cursor by `n` bytes.
    ///
    /// # Safety
    ///
    /// The caller must take care that `n` more bytes have been initialized.
    #[inline]
    pub unsafe fn advance(&mut self, n: usize) {
        self.buf.filled = self.buf.filled.checked_add(n).expect("overflow");
        self.buf.init = self.buf.filled.max(self.buf.init);
    }

ReadBuf/ReadBufCursor, hyper's re-export of the tokio-style uninitialized read-buffer abstraction. The unsafe casts between [u8] and [MaybeUninit<u8>] only ever expose the filled prefix as initialized (filled() at line 259-262 slices 0..self.filled), and advance uses checked_add so the filled cursor cannot overflow or exceed initialized bytes. unfilled narrows the buffer's lifetime with a transmute that is sound because self.buf is never reassigned (SAFETY comment at line 268-269). The public as_mut/advance/set_init/ set_filled are unsafe fn whose contracts are documented, pushing the initialization obligation onto the caller.

Justifies unsafe-safe, unsafe-documented, unsafe-minimal.

src/upgrade.rs

src/upgrade.rs, line 298-315

impl dyn Io + Send {
    fn __hyper_is<T: Io>(&self) -> bool {
        let t = TypeId::of::<T>();
        self.__hyper_type_id() == t
    }

    fn __hyper_downcast<T: Io>(self: Box<Self>) -> Result<Box<T>, Box<Self>> {
        if self.__hyper_is::<T>() {
            // Taken from `std::error::Error::downcast()`.
            unsafe {
                let raw: *mut dyn Io = Box::into_raw(self);
                Ok(Box::from_raw(raw as *mut T))
            }
        } else {
            Err(self)
        }
    }
}

Connection-upgrade IO downcast. The unsafe block reconstructs a Box<T> from a Box<dyn Io + Send> only after __hyper_is::<T>() confirms the concrete TypeId matches, mirroring the standard std::error::Error::downcast idiom. The type check makes the pointer cast sound.

Justifies unsafe-safe.