cargo / anyhow / audit
cargo : anyhow @ 1.0.102
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

Claims

build-exec-deterministicbuild-exec-minimalbuild-exec-no-networkbuild-exec-no-write-outbuild-exec-safehas-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignunsafe-documentedunsafe-minimalunsafe-safeunsafe-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

anyhow 1.0.102 is a type-erased Rust error wrapper with no runtime dependencies. One low-severity quality finding: unsafe blocks in src/ptr.rs and src/ensure.rs lack // SAFETY: comments, unlike the consistently annotated src/error.rs; the invariants hold on inspection.

Report

Subject

anyhow 1.0.102, authored by David Tolnay, is a Rust error-handling library. It exposes anyhow::Error, a single-word owning pointer to any dyn StdError + Send + Sync + 'static value, along with anyhow::Result<T>, the Context trait for attaching contextual messages, and the anyhow!, bail!, and ensure! macros. The crate targets both std (default) and no_std (with a global allocator) environments. It has no runtime dependencies.

Methodology

All source files under contents/src/ were read in full: lib.rs, error.rs, ptr.rs, backtrace.rs, chain.rs, context.rs, ensure.rs, fmt.rs, kind.rs, macros.rs, nightly.rs, wrapper.rs (3918 lines total). build.rs and Cargo.toml/Cargo.toml.orig were also read in full. README.md was reviewed for context.

Source surveys were run for unsafe blocks, FFI, network, filesystem, process execution, environment variables, cryptographic patterns, RNG, and concurrency. A diff -rq contents vcs was run to verify byte-equivalence between the published crate and the VCS checkout. Tools: openvet 0.6.0, grep, diff, git.

Results

The diff between contents/ and vcs/ shows only expected differences: Cargo.toml normalisation, .cargo_vcs_info.json, Cargo.lock, Cargo.toml.orig, and test files present in VCS but not published. No source file diverges between published and VCS. is-benign: no obfuscated code, no network endpoints, no base64 blobs, no telemetry.

The crate has no runtime dependencies and no I/O. Network (uses-network=false), filesystem (uses-filesystem=false), environment (uses-environment=false), concurrency (uses-concurrency=false), process execution (uses-exec=false), JIT (uses-jit=false), interpreter (uses-interpreter=false), and cryptographic (uses-crypto=false) patterns are all absent from the runtime source. No pre-compiled binaries are included (has-binaries=false) and no install-time execution is configured (has-install-exec=false).

The crate implements no cryptographic algorithms (impl-crypto=false), no parser (impl-parser=false), no interpreter (impl-interpreter=false), no JIT (impl-jit=false), no protocol (impl-protocol=false), no data structure (impl-datastructure=false), no algorithm (impl-algorithm=false), and no concurrency primitives (impl-concurrency=false).

The build.rs script (has-build-exec=true) invokes rustc on a probe file to detect nightly error_generic_member_access support. It reads only Cargo-standard environment variables, writes only to OUT_DIR/probe, makes no network requests, and emits cargo:rustc-cfg directives (build-exec-safe=true, build-exec-no-network=true, build-exec-no-write-out=true, build-exec-minimal=true, build-exec-deterministic=true).

The crate uses unsafe extensively (uses-unsafe=true) to implement its type-erasure design. Error stores a thin Own<ErrorImpl> pointer (a #[repr(transparent)] wrapper around NonNull). ErrorImpl<E> is #[repr(C)] with vtable first, so error.rs's vtable() function can read the vtable via a raw pointer cast to the first field. Every vtable function pointer in ErrorVTable is unsafe fn and each is documented with a // Safety: requires layout of *e to match ErrorImpl<E> comment. Call sites in error.rs carry their own // Safety: annotations. The invariants hold: casts from ErrorImpl to ErrorImpl<E> are sound because the vtable pointers are always populated at the same construction step as the E value, and #[repr(C)] ensures stable field offsets. The downcast paths compare TypeId before any pointer cast, preventing unsound casts to the wrong type. The ManuallyDrop pattern in downcast correctly separates value extraction from deallocation. The crate's unsafe is unsafe-minimal=true and unsafe-safe=true.

However, src/ptr.rs and src/ensure.rs contain unsafe blocks without // SAFETY: comments (unsafe-documented=false). The unsafe impl Send and unsafe impl Sync for Own<T> in ptr.rs (lines 13-15) have no justification comment; soundness depends on E: Send + Sync being enforced at Error::construct, which is not stated at the impl site. The Buf::as_str and write_str methods in ensure.rs use str::from_utf8_unchecked, slice::from_raw_parts, and copy_nonoverlapping without safety annotations. A quality finding was raised.

unsafe-tested=false: the published package contains no fuzzer or Miri CI configuration, and no fuzz tests (has-fuzz-tests=false) or property tests (has-property-tests=false) are present.

Testing: the crate ships 73 #[test] functions across 14 integration test files (has-integration-tests=true) covering downcasting, chain traversal, context, formatting, FFI, macros, repr, and more. Unit tests appear in src/fmt.rs (has-unit-tests=true).

Conclusion

anyhow 1.0.102 implements a type-erased error wrapper using a hand-rolled vtable for safe thin-pointer erasure. The design is sound: all pointer casts are guarded by TypeId comparison or enforced at construction, #[repr(C)] is used to fix field offsets that the vtable functions depend on, and the scope of unsafe is limited to the type-erasure mechanism and a small buffer in ensure.rs. One low-severity quality finding was raised: src/ptr.rs and src/ensure.rs contain unsafe blocks without // SAFETY: comments, unlike src/error.rs where every unsafe block is annotated. The crate has no runtime dependencies and no I/O.

Findings(1)

FINDING-1 quality low

Missing safety comments in ptr.rs and ensure.rs

Several unsafe blocks in src/ptr.rs and src/ensure.rs lack // SAFETY: comments explaining the invariants they rely on.

In src/ptr.rs, the unsafe impl Send and unsafe impl Sync for Own<T> (lines 13-15) carry no comment justifying why these impls are sound. The correctness argument is that Own<T> is only constructed from error types that satisfy E: Send + Sync + 'static (enforced at the Error::construct call sites), but this reasoning appears nowhere in ptr.rs itself. The four unsafe fn definitions (boxed, deref, deref_mut, read) likewise have no precondition comments.

In src/ensure.rs, the Buf::as_str method (lines 49-54) calls str::from_utf8_unchecked and slice::from_raw_parts with no SAFETY annotation. The invariant that makes this safe — that every byte written to Buf originated from a valid &str slice via copy_nonoverlapping, and that written never exceeds bytes.len() — is only discernible by reading write_str. Similarly, the copy_nonoverlapping call in write_str (lines 69-74) has no safety comment, though bounds are checked just above.

By contrast, src/error.rs documents its unsafe blocks consistently with // Safety: annotations on every call site.

Justifies unsafe-documented=false.

Annotations(4)

build.rs

Build script probes whether the current compiler supports the error_generic_member_access nightly API by invoking rustc on src/nightly.rs under OUT_DIR/probe. It reads RUSTC, OUT_DIR, TARGET, RUSTC_BOOTSTRAP, RUSTC_STAGE, RUSTC_WRAPPER, RUSTC_WORKSPACE_WRAPPER, and CARGO_ENCODED_RUSTFLAGS from the environment, all of which are standard Cargo-supplied or user-intentional variables. Writes occur only to OUT_DIR/probe (created and removed within the script). No network access. Emits cargo:rustc-cfg directives to the compiler. Justifies has-build-exec, build-exec-safe, build-exec-no-network, build-exec-no-write-out, build-exec-minimal, build-exec-deterministic.

src/ensure.rs

src/ensure.rs, line 35-79

struct Buf {
    bytes: [MaybeUninit<u8>; 40],
    written: usize,
}

impl Buf {
    fn new() -> Self {
        Buf {
            bytes: [MaybeUninit::uninit(); 40],
            written: 0,
        }
    }

    fn as_str(&self) -> &str {
        unsafe {
            str::from_utf8_unchecked(slice::from_raw_parts(
                self.bytes.as_ptr().cast::<u8>(),
                self.written,
            ))
        }
    }
}

impl Write for Buf {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        if s.bytes().any(|b| b == b' ' || b == b'\n') {
            return Err(fmt::Error);
        }

        let remaining = self.bytes.len() - self.written;
        if s.len() > remaining {
            return Err(fmt::Error);
        }

        unsafe {
            ptr::copy_nonoverlapping(
                s.as_ptr(),
                self.bytes.as_mut_ptr().add(self.written).cast::<u8>(),
                s.len(),
            );
        }
        self.written += s.len();
        Ok(())
    }
}

Fixed-size 40-byte stack buffer for rendering operand values in ensure! failure messages. write_str checks for spaces, newlines, and capacity before writing. The as_str method uses str::from_utf8_unchecked and slice::from_raw_parts with no SAFETY comment; the invariant holds because all bytes in 0..written were copied from valid &str slices, but the reasoning is implicit. The copy_nonoverlapping in write_str is also uncombented. Justifies unsafe-documented=false.

src/error.rs

src/error.rs, line 278-299

    unsafe fn construct<E>(
        error: E,
        vtable: &'static ErrorVTable,
        backtrace: Option<Backtrace>,
    ) -> Self
    where
        E: StdError + Send + Sync + 'static,
    {
        let inner: Box<ErrorImpl<E>> = Box::new(ErrorImpl {
            vtable,
            backtrace,
            _object: error,
        });
        // Erase the concrete type of E from the compile-time type system. This
        // is equivalent to the safe unsize coercion from Box<ErrorImpl<E>> to
        // Box<ErrorImpl<dyn StdError + Send + Sync + 'static>> except that the
        // result is a thin pointer. The necessary behavior for manipulating the
        // underlying ErrorImpl<E> is preserved in the vtable provided by the
        // caller rather than a builtin fat pointer vtable.
        let inner = Own::new(inner).cast::<ErrorImpl>();
        Error { inner }
    }

The type-erasure mechanism. ErrorImpl<E> stores a vtable pointer, an optional backtrace, and the concrete error value E, all in a #[repr(C)] struct so field offsets are stable. Error::construct boxes ErrorImpl<E>, converts it to Own<ErrorImpl> (erasing E from the type), and stores the resulting thin pointer. Every subsequent operation on the erased pointer goes through one of the object_* vtable function pointers, which cast back to ErrorImpl<E> using Own::cast (a pointer cast, no allocation). The vtable function at line 917 reads the vtable reference out of the first field of ErrorImpl using a raw-pointer dereference, relying on #[repr(C)] to guarantee that vtable is at offset 0. Each vtable function is marked unsafe fn and documented with // Safety: requires layout of *e to match ErrorImpl<E>. Justifies uses-unsafe, unsafe-safe.

src/ptr.rs

src/ptr.rs, line 1-30

use alloc::boxed::Box;
use core::marker::PhantomData;
use core::ptr::NonNull;

#[repr(transparent)]
pub struct Own<T>
where
    T: ?Sized,
{
    pub ptr: NonNull<T>,
}

unsafe impl<T> Send for Own<T> where T: ?Sized {}

unsafe impl<T> Sync for Own<T> where T: ?Sized {}

impl<T> Copy for Own<T> where T: ?Sized {}

impl<T> Clone for Own<T>
where
    T: ?Sized,
{
    fn clone(&self) -> Self {
        *self
    }
}

impl<T> Own<T>
where
    T: ?Sized,

Three thin-pointer wrappers: Own<T> (owning, analogous to Box), Ref<'a, T> (shared borrow), and Mut<'a, T> (mutable borrow). All three are #[repr(transparent)] over NonNull<T>. unsafe impl Send and unsafe impl Sync are applied to Own<T> unconditionally: these are sound because every Own<ErrorImpl> stored in Error is constructed from E: Send + Sync + 'static via Error::construct, but the impl carries no comment to that effect. The unsafe fn methods (boxed, deref, deref_mut, read) also lack precondition comments. Justifies uses-unsafe, unsafe-documented.