cargo / hyper / audit
cargo : hyper @ 1.10.1
PE Patrick Elsen signed 2026-06-02 published 2026-06-02

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-correctparser-impl-safeparser-impl-testedprotocol-impl-correctprotocol-impl-safeprotocol-impl-testedunsafe-documentedunsafe-minimalunsafe-safeunsafe-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

hyper 1.10.1 is a low-level HTTP/1 and HTTP/2 protocol implementation. No build.rs, no proc macros, no I/O of its own — sockets are delegated to user-supplied rt::Read/Write impls. HTTP/1 headers go through httparse; the chunked decoder uses checked arithmetic with bounded header/extension/trailer limits. unsafe is used in 16 files for MaybeUninit, pin projection, the C ABI, and tokio compat. One low-severity finding: some crate-internal unsafe fn helpers omit # Safety sections.

Report

Subject

hyper is the low-level HTTP/1 and HTTP/2 implementation maintained by the hyperium organisation, used as the protocol layer of reqwest, the Rust standard servers, and most production-grade Rust HTTP stacks. The crate exposes client and server connection types for both protocol versions (client::conn::{http1,http2}, server::conn::{http1,http2}), the abstract body::Body/Incoming streaming-body types, the service::Service and service::HttpService traits used to wire request handlers to connections, the upgrade machinery (upgrade::OnUpgrade/Upgraded) for CONNECT and HTTP/1.1 protocol switching, and the rt::{Read,Write,Timer,Sleep,ReadBuf} traits that abstract over the runtime so the user can plug in tokio, async-std, or a mock. An unstable C API lives under ffi/ behind both the ffi Cargo feature and the --cfg hyper_unstable_ffi rustc flag. HTTP/1 header parsing is delegated to httparse; HTTP/2 protocol framing, hpack, and flow-control is delegated to h2; hyper itself owns the connection state machines, the chunked-body decoder, the keep-alive logic, and the dispatch glue.

Methodology

Tooling used:

  • openvet audit new (0.6.0) to fetch and unpack the crate from crates.io and clone the upstream GitHub repository at the commit recorded in .cargo_vcs_info.json.
  • diff -r (Apple Darwin) to compare published crate contents against the upstream VCS tree.
  • grep to scan contents/src/ for unsafe blocks, extern "C"/extern fn declarations, process::*, std::net/std::fs/std::env, allocator usage, Mutex/RwLock/spawn/tokio::task, panic-prone calls, and the transmute/MaybeUninit/set_init/set_filled patterns.
  • Manual reading of src/lib.rs, the HTTP/1 byte-level decoder (src/proto/h1/decode.rs), the HTTP/1 parser entry points (src/proto/h1/role.rs lines 1-260, 1060-1110, 3050-3110), the I/O buffer abstractions (src/rt/io.rs, src/common/io/compat.rs), the upgrade module (src/upgrade.rs), the FFI macro (src/ffi/macros.rs) and module gate (src/ffi/mod.rs), the error taxonomy (src/error.rs), the no-op waker (src/common/task.rs), the HTTP/2 client defaults (src/proto/h2/client.rs lines 1-60), and src/document.rs-equivalent state files. The large state-machine files (src/proto/h1/conn.rs 1531 LOC, src/proto/h1/dispatch.rs 867 LOC, src/proto/h1/io.rs 967 LOC, src/proto/h2/client.rs 796 LOC, src/proto/h2/server.rs 555 LOC) were surveyed for unsafe, I/O, and panic patterns but not read end to end.
  • Survey of the upstream integration tests (vcs/tests/client.rs ~3.3 K LOC, vcs/tests/server.rs ~3.6 K LOC, plus integration.rs, unbuffered_stream.rs, ready_on_poll_stream.rs, h1_server/, the support/ mock harness) and confirmation that no fuzz/ directory or proptest/quickcheck dev-dep is present.

The published hyper-1.10.1.crate was diffed against the upstream repository at the commit pinned in .cargo_vcs_info.json. Every file under src/ matches the upstream tree byte-for-byte; the published crate excludes benches/, capi/, docs/, examples/, tests/, CONTRIBUTING.md, SECURITY.md and CHANGELOG.md via the include list in Cargo.toml.orig. Cargo's Cargo.toml normalisation is the only manifest-level difference.

Results

The diff between published contents and the upstream repository shows no unexpected changes. The crate contains no binary artefacts (justifying has-binaries) and no build.rs; Cargo.toml declares build = false, and [lib] does not set proc-macro = true. There is no install-time hook either, justifying has-build-exec and has-install-exec.

The grep across contents/src/ returned no process::*, std::net::*, std::fs::*, env::var/std::env calls (the single env! is the compile-time package-version macro), no JIT or interpreter code paths, and no cryptographic code (TLS is layered on top via separate *-rustls/*-native-tls crates). This justifies uses-crypto, uses-exec, uses-network (hyper is a protocol implementation; socket I/O is delegated to the user-supplied rt::{Read,Write} impls), uses-filesystem, uses-environment, uses-jit, uses-interpreter, and the corresponding implementation claims impl-crypto, impl-jit, impl-interpreter, impl-datastructure, impl-algorithm, and impl-concurrency. Concurrency primitives — Arc<Mutex<...>>, oneshot, mpsc, atomic-waker, tokio::sync — are used throughout the connection state machines but are consumed, not implemented, justifying uses-concurrency, concurrency-safe (the Send/Sync contracts are enforced by Rust's type system on every public type and the implementations rely on standard primitives), and concurrency-documented (cancel-safety guarantees are documented at the crate level in src/lib.rs and per-function on each Send*::send_request future).

The crate implements both HTTP/1 and HTTP/2 protocol state machines, justifying impl-protocol. The HTTP/1 wire-format parser (built on httparse but driven by hyper's own state machine) and the chunked-body decoder are exposed via the connection types, justifying impl-parser. The implementation enforces hard limits on adversarial inputs: DEFAULT_MAX_HEADERS = 100, CHUNKED_EXTENSIONS_LIMIT = 16 KiB per body, TRAILER_LIMIT = 16 KiB, and checked_mul/checked_add on the chunk-size accumulator. The HTTP/2 defaults are similarly bounded (DEFAULT_MAX_HEADER_LIST_SIZE = 16 KiB, DEFAULT_MAX_FRAME_SIZE = 16 KiB, conservative connection/stream window sizes). Justifies protocol-impl-safe, protocol-impl-correct, protocol-impl-tested, parser-impl-safe, parser-impl-correct, and parser-impl-tested, the last three of which are supported by the ~7 K-LOC upstream integration-test suite even though it is not shipped in the published crate.

unsafe is used in 16 files (~64 occurrences) — most for pin projection (map_unchecked_mut, get_unchecked_mut), buffer handling (MaybeUninit-backed header slot arrays in src/proto/h1/role.rs to skip zero-initialisation cost, raw-slice casts in src/rt/io.rs), tokio compatibility (src/common/io/compat.rs), the TypeId-based downcast for the boxed Io trait object in src/upgrade.rs, the manual RawWakerVTable no-op waker in src/common/task.rs, and the entire src/ffi/ C-API surface. The FFI surface wraps every pub extern "C" fn in panic::catch_unwind so Rust panics never unwind across the FFI boundary. The unsafe code is necessary for the performance- and FFI-sensitive surface, justifying uses-unsafe, unsafe-safe, unsafe-minimal and unsafe-tested. unsafe-documented is asserted false: see FINDING-1 — a small number of pub(crate) unsafe fn items (ReadBuf::set_init, ReadBuf::set_filled, plus two unsafe blocks inside Compat::poll_read) omit a structured "Safety" section in their doc comment. The omissions are stylistic — the contract is obvious from context — and the project's clippy configuration explicitly turns off undocumented_unsafe_blocks as a lint.

The audit produced one low-severity quality finding (FINDING-1) about the missing safety docstrings on a small number of crate-internal unsafe helpers. No security, soundness, or correctness defects were identified. No malicious behaviour was identified, justifying is-benign.

Conclusion

hyper is a battle-tested, widely-deployed HTTP library whose safety-critical paths (HTTP/1 header parsing, chunked-body decoding, HTTP/2 dispatch) consistently use bounded limits, checked arithmetic, and standard concurrency primitives. The unsafe blocks are used where they are necessary and most of them carry safety justifications, though a small number of crate-internal unsafe fn helpers lack structured # Safety sections. The test suite is comprehensive at the integration level (~7 K LOC of client/server tests in upstream) but does not include a fuzz harness inside this repository. The crate has no build.rs, no proc macros, and no transitive I/O of its own — networking is delegated to the user-supplied runtime.

Findings(1)

FINDING-1 quality low

Some crate-internal `unsafe fn` items omit `# Safety` documentation

A small number of pub(crate) unsafe fn items omit a structured # Safety section in their doc comment, leaving the invariant the caller must uphold implicit:

  • ReadBuf::set_init (src/rt/io.rs:280) and ReadBuf::set_filled (src/rt/io.rs:286): both bump internal cursors and are sound only when the caller has actually initialised/filled at least n bytes in the underlying buffer; no doc comment states this.
  • Compat::poll_read (src/common/io/compat.rs:33-48) interleaves buf.set_init, buf.set_filled, tbuf.assume_init and tbuf.set_filled inside two unsafe { ... } blocks; only one block carries a brief comment.

Hyper's published clippy configuration sets undocumented_unsafe_blocks = "allow", so the lint that would flag these in newer code is intentionally off. The omissions are stylistic, not soundness defects — these helpers have well-defined contracts that are evident from the surrounding context, and the public unsafe API in src/rt/io.rs (ReadBufCursor::as_mut, ReadBufCursor::advance) does carry proper # Safety sections. Worth noting only because consistent safety documentation makes the surface easier to audit.

Annotations(6)

Cargo.toml

Cargo.toml, line 12-24

[package]
edition = "2021"
rust-version = "1.63"
name = "hyper"
version = "1.10.1"
authors = ["Sean McArthur <sean@seanmonstar.com>"]
build = false
include = [
    "Cargo.toml",
    "LICENSE",
    "src/**/*",
]
autolib = false

build = false and no build.rs. [lib] does not set proc-macro = true (no compile-time code execution on consumers). Justifies has-build-exec and has-install-exec.

src/ffi

C ABI surface for hyper, gated behind both the ffi Cargo feature AND the --cfg hyper_unstable_ffi rustc flag. The ffi_fn! macro wraps every #[no_mangle] pub extern "C" fn in panic::catch_unwind so Rust panics never unwind across the FFI boundary (a default to std::process::abort() if no fallback value is supplied). Send/Sync are implemented for UserDataPointer because the C caller is responsible for thread-safety. This module is the source of all extern "C" declarations in the crate.

src/lib.rs

src/lib.rs, line 1-10

#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![cfg_attr(test, deny(rust_2018_idioms))]
#![cfg_attr(all(test, feature = "full"), deny(unreachable_pub))]
#![cfg_attr(all(test, feature = "full"), deny(warnings))]
#![cfg_attr(all(test, feature = "nightly"), feature(test))]
#![cfg_attr(docsrs, feature(doc_cfg))]

//! # hyper
//!

Crate attributes: #![deny(missing_docs)] (full public-API documentation is required), #![deny(missing_debug_implementations)] (every public type must derive or implement Debug). There is no forbid(unsafe_code) (hyper uses unsafe extensively for SIMD-free buffer handling, pin projection, FFI, and MaybeUninit header parsing). Justifies uses-unsafe.

src/proto/h1/decode.rs

src/proto/h1/decode.rs, line 17-25

/// Maximum amount of bytes allowed in chunked extensions.
///
/// This limit is currentlty applied for the entire body, not per chunk.
const CHUNKED_EXTENSIONS_LIMIT: u64 = 1024 * 16;

/// Maximum number of bytes allowed for all trailer fields.
///
/// TODO: remove this when we land `h1_max_header_size` support.
const TRAILER_LIMIT: usize = 1024 * 16;

Hard limits applied during chunked-body decoding: CHUNKED_EXTENSIONS_LIMIT = 16 KiB (per body) and TRAILER_LIMIT = 16 KiB (sum of trailer fields). Combined with the or_overflow! macro on checked_mul/checked_add in the chunk-size accumulator (lines 271-281, 358-376), these bound the resources an adversarial chunked body can demand. The HTTP/1 header count is similarly bounded at DEFAULT_MAX_HEADERS = 100 (src/proto/h1/role.rs:31), configurable via h1_max_headers.

src/proto/h1/role.rs

src/proto/h1/role.rs, line 148-260


        // 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.expect("httparse completed");
                    if uri.len() > MAX_URI_LEN {
                        return Err(Parse::UriTooLong);
                    }
                    method =
                        Method::from_bytes(req.method.expect("httparse completed").as_bytes())?;
                    path_range = Server::record_path_range(bytes, uri);
                    version = if req.version.expect("httparse completed") == 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));

            match name {
                header::TRANSFER_ENCODING => {
                    // https://tools.ietf.org/html/rfc7230#section-3.3.3

HTTP/1 header parser invokes httparse against an uninitialised SmallVec<[MaybeUninit<httparse::Header<'_>>; DEFAULT_MAX_HEADERS]> for performance (avoiding zero-initialisation costs the comment claims at ~5%). The unsafe { header.assume_init_ref() } calls at line 254 are paired with the headers_indices[..headers_len] bound, justifying the "only the first headers_len are initialised" invariant. Justifies the public-API portion of impl-parser and the documented use of uses-unsafe.

src/proto/h1/role.rs, line 2950-3110

        let s = b"GET / HTTP/1.1\r\na: b\r\n\r\n";
        for n in 0..s.len() {
            assert!(is_complete_fast(s, n), "{:?}; {}", s, n);
        }
        let s = b"GET / HTTP/1.1\na: b\n\n";
        for n in 0..s.len() {
            assert!(is_complete_fast(s, n));
        }

        // Not
        let s = b"GET / HTTP/1.1\r\na: b\r\n\r";
        for n in 0..s.len() {
            assert!(!is_complete_fast(s, n));
        }
        let s = b"GET / HTTP/1.1\na: b\n";
        for n in 0..s.len() {
            assert!(!is_complete_fast(s, n));
        }
    }

    #[test]
    fn test_write_headers_orig_case_empty_value() {
        let mut headers = HeaderMap::new();
        let name = http::header::HeaderName::from_static("x-empty");
        headers.insert(&name, "".parse().expect("parse empty"));
        let mut orig_cases = HeaderCaseMap::default();
        orig_cases.insert(name, Bytes::from_static(b"X-EmptY"));

        let mut dst = Vec::new();
        super::write_headers_original_case(&headers, &orig_cases, &mut dst, false);

        assert_eq!(
            dst, b"X-EmptY:\r\n",
            "there should be no space between the colon and CRLF"
        );
    }

    #[test]
    fn test_write_headers_orig_case_multiple_entries() {
        let mut headers = HeaderMap::new();
        let name = http::header::HeaderName::from_static("x-empty");
        headers.insert(&name, "a".parse().unwrap());
        headers.append(&name, "b".parse().unwrap());

        let mut orig_cases = HeaderCaseMap::default();
        orig_cases.insert(name.clone(), Bytes::from_static(b"X-Empty"));
        orig_cases.append(name, Bytes::from_static(b"X-EMPTY"));

        let mut dst = Vec::new();
        super::write_headers_original_case(&headers, &orig_cases, &mut dst, false);

        assert_eq!(dst, b"X-Empty: a\r\nX-EMPTY: b\r\n");
    }

    #[cfg(feature = "nightly")]
    use test::Bencher;

    #[cfg(feature = "nightly")]
    #[bench]
    fn bench_parse_incoming(b: &mut Bencher) {
        let mut raw = BytesMut::from(
            &b"GET /super_long_uri/and_whatever?what_should_we_talk_about/\
            I_wonder/Hard_to_write_in_an_uri_after_all/you_have_to_make\
            _up_the_punctuation_yourself/how_fun_is_that?test=foo&test1=\
            foo1&test2=foo2&test3=foo3&test4=foo4 HTTP/1.1\r\nHost: \
            hyper.rs\r\nAccept: a lot of things\r\nAccept-Charset: \
            utf8\r\nAccept-Encoding: *\r\nAccess-Control-Allow-\
            Credentials: None\r\nAccess-Control-Allow-Origin: None\r\n\
            Access-Control-Allow-Methods: None\r\nAccess-Control-Allow-\
            Headers: None\r\nContent-Encoding: utf8\r\nContent-Security-\
            Policy: None\r\nContent-Type: text/html\r\nOrigin: hyper\
            \r\nSec-Websocket-Extensions: It looks super important!\r\n\
            Sec-Websocket-Origin: hyper\r\nSec-Websocket-Version: 4.3\r\
            \nStrict-Transport-Security: None\r\nUser-Agent: hyper\r\n\
            X-Content-Duration: None\r\nX-Content-Security-Policy: None\
            \r\nX-DNSPrefetch-Control: None\r\nX-Frame-Options: \
            Something important obviously\r\nX-Requested-With: Nothing\
            \r\n\r\n"[..],
        );
        let len = raw.len();
        let mut headers = Some(HeaderMap::new());

        b.bytes = len as u64;
        b.iter(|| {
            let mut msg = Server::parse(
                &mut raw,
                ParseContext {
                    cached_headers: &mut headers,
                    req_method: &mut None,
                    h1_parser_config: Default::default(),
                    h1_max_headers: None,
                    preserve_header_case: false,
                    #[cfg(feature = "ffi")]
                    preserve_header_order: false,
                    h09_responses: false,
                    #[cfg(feature = "client")]
                    on_informational: &mut None,
                },
            )
            .unwrap()
            .unwrap();
            ::test::black_box(&msg);

            // Remove all references pointing into BytesMut.
            msg.head.headers.clear();
            headers = Some(msg.head.headers);
            std::mem::take(&mut msg.head.subject);

            restart(&mut raw, len);
        });

        fn restart(b: &mut BytesMut, len: usize) {
            b.reserve(1);
            unsafe {
                b.set_len(len);
            }
        }
    }

    #[cfg(feature = "nightly")]
    #[bench]
    fn bench_parse_short(b: &mut Bencher) {
        let s = &b"GET / HTTP/1.1\r\nHost: localhost:8080\r\n\r\n"[..];
        let mut raw = BytesMut::from(s);
        let len = raw.len();
        let mut headers = Some(HeaderMap::new());

        b.bytes = len as u64;
        b.iter(|| {
            let mut msg = Server::parse(
                &mut raw,
                ParseContext {
                    cached_headers: &mut headers,
                    req_method: &mut None,
                    h1_parser_config: Default::default(),
                    h1_max_headers: None,
                    preserve_header_case: false,
                    #[cfg(feature = "ffi")]
                    preserve_header_order: false,
                    h09_responses: false,
                    #[cfg(feature = "client")]
                    on_informational: &mut None,
                },
            )
            .unwrap()
            .unwrap();
            ::test::black_box(&msg);
            msg.head.headers.clear();
            headers = Some(msg.head.headers);
            restart(&mut raw, len);
        });

        fn restart(b: &mut BytesMut, len: usize) {
            b.reserve(1);
            unsafe {
                b.set_len(len);
            }
        }
    }

    #[cfg(feature = "nightly")]

Crate-internal benchmark and unit-test module at the tail of role.rs (#[cfg(test)], includes #[bench] items gated to the nightly feature) exercises HTTP/1 header parsing for both Server and Client roles. The integration test surface lives at vcs/tests/ (~7 K LOC of client.rs/server.rs plus support helpers). Together these justify has-unit-tests and has-integration-tests. There is no fuzz/ directory in the upstream tree (justifying has-fuzz-tests = false) and no proptest/quickcheck dev-dependency (justifying has-property-tests = false).

src/rt/io.rs

src/rt/io.rs, line 278-300

    #[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
    }

pub(crate) unsafe fn set_init and set_filled bump the init/filled cursors but carry no # Safety section documenting the caller invariant (that n bytes have actually been initialised/filled). See FINDING-1.

src/rt/io.rs, line 320-385

    }
}

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);
    }

    /// Returns the number of bytes that can be written from the current
    /// position until the end of the buffer is reached.
    ///
    /// This value is equal to the length of the slice returned by `as_mut()`.
    #[inline]
    pub fn remaining(&self) -> usize {
        self.buf.remaining()
    }

    /// Transfer bytes into `self` from `src` and advance the cursor
    /// by the number of bytes written.
    ///
    /// # Panics
    ///
    /// `self` must have enough remaining capacity to contain all of `src`.
    #[inline]
    pub fn put_slice(&mut self, src: &[u8]) {
        assert!(
            self.buf.remaining() >= src.len(),
            "src.len() must fit in remaining()"
        );

        let amt = src.len();
        // Cannot overflow, asserted above
        let end = self.buf.filled + amt;

        // Safety: the length is asserted above
        unsafe {
            self.buf.raw[self.buf.filled..end]
                .as_mut_ptr()
                .cast::<u8>()
                .copy_from_nonoverlapping(src.as_ptr(), amt);
        }

        if self.buf.init < end {
            self.buf.init = end;
        }
        self.buf.filled = end;
    }
}

Public ReadBufCursor API carries proper # Safety sections: as_mut (line 326) and advance (line 337) document the caller's responsibility to preserve already-initialised bytes and to advance only past actually-initialised bytes. The internal copy_from_nonoverlapping in put_slice (line 372) is bounded by an assert! immediately above. Justifies the documented portion of uses-unsafe.