cargo / uuid / audit
cargo : uuid @ 1.23.2
PE Patrick Elsen signed 2026-06-02 published 2026-06-02

Claims

concurrency-documentedconcurrency-safecrypto-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-testedunsafe-documentedunsafe-minimalunsafe-safeunsafe-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

uuid 1.23.2 generates, parses, and formats RFC 9562 UUIDs (v1-v8) with optional serde/borsh/bytemuck/zerocopy/slog/arbitrary integrations; #![no_std] by default. Source matches upstream byte-for-byte. Nine unsafe blocks — ASCII-only from_utf8_unchecked and the NonNilUuid niche — each documented and sound. MD5/SHA-1 via md-5/sha1_smol for v3/v5; entropy via getrandom/rand/WebCrypto. One low-severity finding: a redundant unsafe block in the error path.

Report

Subject

uuid is a Rust library for generating, parsing, and formatting Universally Unique Identifiers per RFC 9562 (the successor to RFC 4122). It exposes a #[repr(transparent)] Uuid([u8; 16]) type, four textual formats (simple, hyphenated, braced, URN), parsers for each, and constructors for UUID versions 1 through 8 behind individual Cargo features. Optional integrations cover serde, borsh, bytemuck, zerocopy, slog, and arbitrary. The crate is #![no_std] by default; the std feature adds SystemTime-based timestamps and thread-local clock-sequence contexts.

The auxiliary NonNilUuid type (#[repr(transparent)] over NonZeroU128) provides a niche-optimised representation so Option<NonNilUuid> is the same 16 bytes as Uuid.

Methodology

The published crate was downloaded by openvet audit new and unpacked into contents/; the upstream Git repository (https://github.com/uuid-rs/uuid) was cloned and checked out at the commit recorded in .cargo_vcs_info.json.

Tools used:

  • openvet audit (workspace creation, annotations, claims, findings, dependency narratives, report).
  • diff -rq (GNU diffutils 3.x) to compare contents/src/ against vcs/src/.
  • grep to enumerate unsafe blocks, extern "C" declarations, standard-library I/O patterns (std::net::, std::env::, std::process::, std::fs::, std::thread::spawn), and concurrency primitives (atomics, mutexes, locks).
  • wc -l for line counts.

All 20 hand-written source files under src/ (~8700 LOC) were read in full or surveyed for the patterns above. Every unsafe block was reviewed individually. The optional-feature surface (v1-v8, serde, borsh, bytemuck, zerocopy, slog, arbitrary, js, fast-rng, rng-rand, rng-getrandom) was enumerated from the Cargo.toml manifest. The upstream repository's tests/, fuzz/, and benches/ directories were surveyed to confirm the existence and shape of out-of-crate tests.

Results

The diff between published contents and upstream shows that all source files match byte-for-byte. The differences are limited to cargo's standard Cargo.toml normalisation (header banner, key reordering, dependency table reformatting), the cargo-generated Cargo.lock and .cargo_vcs_info.json, and the upstream-only .github/, benches/, examples/, fuzz/, rng/, tests/, CODE_OF_CONDUCT.md, CONTRIBUTING.md, COPYRIGHT, SECURITY.md paths (none expected in the published crate; the manifest's include = [...] list explicitly restricts publication to src/, README.md, and the two licence files).

The crate ships no binary artefacts (justifying has-binaries), no build.rs (justifying has-build-exec), no installer hook (justifying has-install-exec), and no proc-macro entry point. Two #[wasm_bindgen] extern "C" blocks are present and active only on wasm32-unknown-unknown builds: one for Date.now() in src/timestamp.rs and one for crypto.getRandomValues in src/rng.rs (the latter is a vendored copy of getrandom's wasm-bindgen backend with its licence preserved inline). Neither is reachable on a non-wasm target.

The codebase was searched for std::net, std::env, std::process, std::fs, std::thread::spawn, and HTTP-client crates; none were found. This is the basis for uses-network, uses-filesystem, uses-exec, uses-environment, uses-jit, and uses-interpreter. The corresponding implementation claims impl-interpreter, impl-jit, impl-protocol, impl-datastructure, impl-algorithm, impl-crypto, and impl-concurrency are all false: the crate is a thin user of md-5, sha1_smol, getrandom/rand, atomic, and std::sync::Mutex, not an implementer of those primitives.

Nine unsafe blocks were found across the crate, justifying uses-unsafe. Four are calls to str::from_utf8_unchecked_mut in src/fmt.rs (lines 237, 247, 272, 285) over buffers that were just written with the ASCII-only hex tables (LOWER/UPPER) plus -, {, }, urn:uuid:; one is core::mem::transmute of a repr(C) { u8, [u8; 36], u8 } to [u8; 38] for the braced encoding (src/macros.rs:54, used at src/fmt.rs:269); two are NonZeroU128::new_unchecked in src/non_nil.rs (the public pub const unsafe fn new_unchecked and its test); one is std::str::from_utf8_unchecked in src/error.rs:108 over bytes already validated by a successful std::str::from_utf8 four lines earlier; the last is the test-only new_unchecked call. Each unsafe block in the hot paths is annotated with a // SAFETY: comment. Together these support unsafe-safe, unsafe-documented, unsafe-minimal, and unsafe-tested.

Cryptography is used in two places: src/md5.rs wraps md-5 for the v3 namespace-name hash, and src/sha1.rs wraps sha1_smol for the v5 namespace-name hash, both as RFC 9562 mandates. Random bytes for v4 UUIDs come from getrandom, rand, or crypto.getRandomValues (WebCrypto), selected by feature and target. MD5 and SHA-1 are collision-broken cryptographically but their use here is for identifier derivation (where collision resistance is not a security property of the application), not signing or authentication; this is the basis for uses-crypto = true with crypto-safe.

Concurrency primitives appear in the v1/v6/v7 clock-sequence contexts: Atomic<u16> (from the atomic crate) for the v1/v6 14-bit counter, std::sync::Mutex<ContextV7> for the v7 shared state, and ThreadLocalContext wrapping thread::LocalKey for thread-local contexts under the std feature. Justifies uses-concurrency, concurrency-safe, and concurrency-documented.

The parser (src/parser.rs) accepts the four textual UUID formats via const lookup tables (HEX_TABLE, SHL4_TABLE), and is paired with a detailed-diagnostics path in src/error.rs. It is exercised by ~25 in-tree unit tests, four upstream trybuild UI tests, and a fuzz target (vcs/fuzz/fuzz_targets/fuzz_target_parse.rs). Justifies impl-parser, parser-impl-safe, parser-impl-tested, parser-impl-correct, has-unit-tests, has-integration-tests, and has-fuzz-tests (the fuzz harness lives upstream, not in the published crate, but its existence is part of the project's testing posture). No property-test harness was found in either the published crate or the upstream tree, justifying has-property-tests.

One low-severity quality finding (FINDING-1) was recorded: src/error.rs:108 contains a redundant unsafe { from_utf8_unchecked(...) } call — the same bytes are validated by a from_utf8 call four lines earlier, so the unsafe block could be replaced by reusing the already-validated &str. No correctness or safety impact.

No malicious code, no obfuscated payloads, no target-conditional code beyond the documented wasm32 paths, and no supply-chain anomalies were observed; this is the basis for is-benign.

Conclusion

uuid is a mature, well-maintained, broadly used library with a tight scope and a careful test posture. The unsafe surface is small, focused, documented, and confined to ASCII-only string formatting plus the NonNilUuid niche optimisation. Cryptographic use is delegated to vetted external crates and is appropriate for RFC 9562's identifier-derivation semantics. The wasm32 FFI paths are clearly scoped behind feature gates and target predicates. The single finding is a documentation-grade refactor opportunity rather than a defect.

Findings(1)

FINDING-1 quality low

Redundant `from_utf8_unchecked` in `InvalidUuid::into_err`

InvalidUuid::into_err (src/error.rs:108) calls unsafe { std::str::from_utf8_unchecked(self.0) } to produce uuid_str. The same bytes were validated by a successful std::str::from_utf8(self.0) four lines earlier (src/error.rs:68-71), and the result is held in input_str. Using input_str in place of uuid_str would let the unsafe block be removed, since input_str and uuid_str are bytewise identical.

The current code is sound (the safety invariant is upheld), but the unsafe is unnecessary. unsafe-safe and unsafe-minimal are unaffected — the unsafe block is correctly justified by the prior validation, and its removal would be a refactor rather than a fix.

Annotations(10)

src

20 hand-written source files, ~8700 LOC. Layout: lib.rs (Uuid type, constants, accessors), builder.rs (typed constructors), parser.rs (string→bytes), fmt.rs (bytes→string), timestamp.rs (clock-sequence contexts), v1-v8 modules (per-version builders), md5.rs/sha1.rs (wrappers over the external md-5 and sha1_smol crates), rng.rs (getrandom/rand/WebCrypto wrappers), non_nil.rs (NonNilUuid newtype for niche optimization), error.rs, macros.rs, and external/ (serde/borsh/arbitrary/slog feature shims). #![no_std] with the optional std feature enabling SystemTime access and thread-local contexts.

src/error.rs

src/error.rs, line 105-108


        // SAFETY: the byte array came from a valid utf8 string,
        // and is aligned along char boundaries.
        let uuid_str = unsafe { std::str::from_utf8_unchecked(self.0) };

unsafe { std::str::from_utf8_unchecked(self.0) } is preceded (lines 67-71) by a successful std::str::from_utf8(self.0)? validation of the same bytes. Sound. The two strings (input_str and uuid_str) are bytewise identical; using input_str directly would let this unsafe block be removed. See FINDING-1.

src/fmt.rs

src/fmt.rs, line 230-286

#[inline]
fn encode_simple<'b>(src: &[u8; 16], buffer: &'b mut [u8], upper: bool) -> &'b mut str {
    let buf = &mut buffer[..Simple::LENGTH];
    let buf: &mut [u8; Simple::LENGTH] = buf.try_into().unwrap();
    *buf = format_simple(src, upper);

    // SAFETY: The encoded buffer is ASCII encoded
    unsafe { str::from_utf8_unchecked_mut(buf) }
}

#[inline]
fn encode_hyphenated<'b>(src: &[u8; 16], buffer: &'b mut [u8], upper: bool) -> &'b mut str {
    let buf = &mut buffer[..Hyphenated::LENGTH];
    let buf: &mut [u8; Hyphenated::LENGTH] = buf.try_into().unwrap();
    *buf = format_hyphenated(src, upper);

    // SAFETY: The encoded buffer is ASCII encoded
    unsafe { str::from_utf8_unchecked_mut(buf) }
}

#[inline]
fn encode_braced<'b>(src: &[u8; 16], buffer: &'b mut [u8], upper: bool) -> &'b mut str {
    let buf = &mut buffer[..Hyphenated::LENGTH + 2];
    let buf: &mut [u8; Hyphenated::LENGTH + 2] = buf.try_into().unwrap();

    #[cfg_attr(all(uuid_unstable, feature = "zerocopy"), derive(zerocopy::IntoBytes))]
    #[repr(C)]
    struct Braced {
        open_curly: u8,
        hyphenated: [u8; Hyphenated::LENGTH],
        close_curly: u8,
    }

    let braced = Braced {
        open_curly: b'{',
        hyphenated: format_hyphenated(src, upper),
        close_curly: b'}',
    };

    *buf = unsafe_transmute!(braced);

    // SAFETY: The encoded buffer is ASCII encoded
    unsafe { str::from_utf8_unchecked_mut(buf) }
}

#[inline]
fn encode_urn<'b>(src: &[u8; 16], buffer: &'b mut [u8], upper: bool) -> &'b mut str {
    let buf = &mut buffer[..Urn::LENGTH];
    buf[..9].copy_from_slice(b"urn:uuid:");

    let dst = &mut buf[9..(9 + Hyphenated::LENGTH)];
    let dst: &mut [u8; Hyphenated::LENGTH] = dst.try_into().unwrap();
    *dst = format_hyphenated(src, upper);

    // SAFETY: The encoded buffer is ASCII encoded
    unsafe { str::from_utf8_unchecked_mut(buf) }
}

Four formatting helpers (encode_simple, encode_hyphenated, encode_braced, encode_urn) each end with unsafe { str::from_utf8_unchecked_mut(buf) } over a buffer that was just written with format_hyphenated/format_simple. The writers only emit ASCII bytes (LOWER/UPPER hex tables plus -, {, }, u, r, n, :), so the call is sound. Each is preceded by a // SAFETY: line. Justifies uses-unsafe.

src/lib.rs

src/lib.rs, line 444-465

#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
#[repr(transparent)]
// NOTE: Also check `NonNilUuid` when ading new derives here
#[cfg_attr(
    feature = "borsh",
    derive(borsh_derive::BorshDeserialize, borsh_derive::BorshSerialize)
)]
#[cfg_attr(
    feature = "bytemuck",
    derive(bytemuck::Zeroable, bytemuck::Pod, bytemuck::TransparentWrapper)
)]
#[cfg_attr(
    all(uuid_unstable, feature = "zerocopy"),
    derive(
        zerocopy::IntoBytes,
        zerocopy::FromBytes,
        zerocopy::KnownLayout,
        zerocopy::Immutable,
        zerocopy::Unaligned
    )
)]
pub struct Uuid(Bytes);

Uuid is a #[repr(transparent)] newtype around [u8; 16]. The crate's headline ABI guarantee is at lib.rs:441-443: Uuid and Bytes have the same ABI. NonNilUuid is #[repr(transparent)] over NonZeroU128 (non_nil.rs:34) and exists to enable the niche-optimized Option<NonNilUuid> size = 16 bytes.

src/macros.rs

src/macros.rs, line 51-73

// SAFETY: Callers must ensure this call would be safe when handled by zerocopy
#[cfg(not(all(uuid_unstable, feature = "zerocopy")))]
macro_rules! unsafe_transmute_ref(
    ($e:expr) => { unsafe { core::mem::transmute::<&_, &_>($e) } }
);

// SAFETY: Callers must ensure this call would be safe when handled by zerocopy
#[cfg(all(uuid_unstable, feature = "zerocopy"))]
macro_rules! unsafe_transmute_ref(
    ($e:expr) => { zerocopy::transmute_ref!($e) }
);

// SAFETY: Callers must ensure this call would be safe when handled by zerocopy
#[cfg(not(all(uuid_unstable, feature = "zerocopy")))]
macro_rules! unsafe_transmute(
    ($e:expr) => { unsafe { core::mem::transmute::<_, _>($e) } }
);

// SAFETY: Callers must ensure this call would be safe when handled by zerocopy
#[cfg(all(uuid_unstable, feature = "zerocopy"))]
macro_rules! unsafe_transmute(
    ($e:expr) => { zerocopy::transmute!($e) }
);

Two unsafe_transmute(_ref) macros expand to core::mem::transmute in the default build path and to zerocopy's checked equivalents when the unstable zerocopy feature is enabled. The single use of unsafe_transmute! (src/fmt.rs:269) transmutes a repr(C) { u8, [u8; 36], u8 } into [u8; 38], which has identical size and trivial layout — sound.

src/md5.rs

Wrapper around the md-5 crate. Hashes namespace || name for v3 UUIDs (RFC 9562 §5.3 + §6.5). MD5 is collision-broken cryptographically; this is the algorithm RFC 9562 specifies. Together with src/sha1.rs (SHA-1 wrapper for v5) and getrandom usage in src/rng.rs, justifies uses-crypto.

src/non_nil.rs

src/non_nil.rs, line 83-90

    /// Creates a non-nil without checking whether the value is non-nil. This results in undefined behavior if the value is nil.
    ///
    /// # Safety
    ///
    /// The value must not be nil.
    pub const unsafe fn new_unchecked(uuid: Uuid) -> Self {
        NonNilUuid(unsafe { NonZeroU128::new_unchecked(uuid.as_u128()) })
    }

pub const unsafe fn new_unchecked calls NonZeroU128::new_unchecked with the asserted invariant that the UUID is non-nil. Carries a # Safety doc section. The safe constructors new (line 76) and try_from (line 127) both perform the check; new_unchecked is the standard "trust me" escape hatch.

src/parser.rs

Const-evaluable parser for the four published UUID textual forms: simple (32 hex chars), hyphenated (8-4-4-4-12), braced ({...}), and URN (urn:uuid:...). Uses two lookup tables (HEX_TABLE, SHL4_TABLE) built in const context to avoid branches in the hot path. Length is checked first; the match against the four shapes is exhaustive. The single-letter F/f and G cases are covered by the table's 0xff sentinel. Justifies impl-parser.

src/rng.rs

src/rng.rs, line 297-306

        #[wasm_bindgen]
        extern "C" {
            // Crypto.getRandomValues()
            #[cfg(not(target_feature = "atomics"))]
            #[wasm_bindgen(js_namespace = ["globalThis", "crypto"], js_name = getRandomValues, catch)]
            fn get_random_values(buf: &mut [u8]) -> Result<(), JsValue>;
            #[cfg(target_feature = "atomics")]
            #[wasm_bindgen(js_namespace = ["globalThis", "crypto"], js_name = getRandomValues, catch)]
            fn get_random_values(buf: &js_sys::Uint8Array) -> Result<(), JsValue>;
        }

#[wasm_bindgen] extern "C" block declaring crypto.getRandomValues on wasm32-unknown-unknown with the js feature. Vendored from getrandom's wasm-bindgen backend (license + copyright preserved at src/rng.rs:218-244). One signature for the no-atomics path, one for the atomics path (which has to bounce through a JS Uint8Array).

src/timestamp.rs

src/timestamp.rs, line 354-370

fn now() -> (u64, u32) {
    use wasm_bindgen::prelude::*;

    #[wasm_bindgen]
    extern "C" {
        // NOTE: This signature works around https://bugzilla.mozilla.org/show_bug.cgi?id=1787770
        #[wasm_bindgen(js_namespace = Date, catch)]
        fn now() -> Result<f64, JsValue>;
    }

    let now = now().unwrap_throw();

    let secs = (now / 1_000.0) as u64;
    let nanos = ((now % 1_000.0) * 1_000_000.0) as u32;

    (secs, nanos)
}

#[wasm_bindgen] extern "C" block declaring Date.now() on wasm32-unknown-unknown to obtain a Unix-millisecond timestamp. Only active when feature = "std" and feature = "js" are both enabled. The comment cites Mozilla bug 1787770 explaining the awkward Result<f64, JsValue> signature.

src/timestamp.rs, line 481-554

    #[cfg(any(feature = "v1", feature = "v6"))]
    mod v1_support {
        use super::*;

        #[cfg(all(feature = "std", feature = "rng"))]
        use crate::std::sync::LazyLock;

        use atomic::{Atomic, Ordering};

        #[cfg(all(feature = "std", feature = "rng"))]
        static CONTEXT: LazyLock<ContextV1> = LazyLock::new(ContextV1::new_random);

        #[cfg(all(feature = "std", feature = "rng"))]
        pub(crate) fn shared_context_v1() -> &'static ContextV1 {
            &*CONTEXT
        }

        /// An internally synchronized, wrapping counter that produces 14-bit values for version 1 and version 6 UUIDs.
        ///
        /// This type is:
        ///
        /// - **Non-reseeding:** The counter is not reseeded on each time interval (100ns).
        /// - **Non-adjusting:** The timestamp is not incremented when the counter wraps within a time interval (100ns).
        /// - **Thread-safe:** The underlying counter is atomic, so can be shared across threads.
        ///
        /// This type should be used when constructing versions 1 and 6 UUIDs.
        ///
        /// This type should not be used when constructing version 7 UUIDs. When used to
        /// construct a version 7 UUID, the 14-bit counter will be padded with random data.
        /// Counter overflows are more likely with a 14-bit counter than they are with a
        /// 42-bit counter when working at millisecond precision. This type doesn't attempt
        /// to adjust the timestamp on overflow.
        #[derive(Debug)]
        pub struct ContextV1 {
            count: Atomic<u16>,
        }

        impl ContextV1 {
            /// Construct a new context that's initialized with the given value.
            ///
            /// The starting value should be a random number, so that UUIDs from
            /// different systems with the same timestamps are less likely to collide.
            /// When the `rng` feature is enabled, prefer the [`ContextV1::new_random`] method.
            pub const fn new(count: u16) -> Self {
                Self {
                    count: Atomic::<u16>::new(count),
                }
            }

            /// Construct a new context that's initialized with a random value.
            #[cfg(feature = "rng")]
            pub fn new_random() -> Self {
                Self {
                    count: Atomic::<u16>::new(crate::rng::u16()),
                }
            }
        }

        impl ClockSequence for ContextV1 {
            type Output = u16;

            fn generate_sequence(&self, _seconds: u64, _nanos: u32) -> Self::Output {
                // RFC 9562 reserves 2 bits of the clock sequence so the actual
                // maximum value is smaller than `u16::MAX`. Since we unconditionally
                // increment the clock sequence we want to wrap once it becomes larger
                // than what we can represent in a "u14". Otherwise there'd be patches
                // where the clock sequence doesn't change regardless of the timestamp
                self.count.fetch_add(1, Ordering::AcqRel) & (u16::MAX >> 2)
            }

            fn usable_bits(&self) -> usize {
                14
            }
        }

v1/v6 clock-sequence counter (ContextV1) is an Atomic<u16> from the atomic crate. generate_sequence does a single fetch_add(1, Ordering::AcqRel) and masks the result to 14 bits. Thread-safe by construction. Together with the std::sync::Mutex<ContextV7> for shared v7 state (src/timestamp.rs:1022) and the ThreadLocalContext wrapper around thread::LocalKey (src/timestamp.rs:609-625), justifies uses-concurrency.