cargo / bitflags / audit
cargo : bitflags @ 2.11.1
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

Claims

has-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

bitflags 2.11.1 is a declarative-macro crate generating typed flag-set wrappers; the crate body has forbid(unsafe_code) outside tests and the only unsafe is well-justified Pod/Zeroable impls emitted into consumers behind the optional bytemuck feature. One low-severity correctness finding in an example file (BitXor computes AND); safe to deploy.

Report

Subject

bitflags is a macro library that generates strongly-typed flag-set wrappers around integer primitives, with bitwise operators, named-flag iteration, and text formatting/parsing. It is no_std-compatible by default and offers opt-in integration with serde, arbitrary, and bytemuck.

Methodology

The published crate contents were compared against the upstream Git repository (commit recorded in .cargo_vcs_info.json) using diff -r. Source, examples, and benches matched byte-for-byte; manifest differences were limited to Cargo's standard normalisation. All Rust sources under contents/src/ (~3,150 lines across 9 top-level files plus 30 per-operation unit-test files) were read in full, including the optional serde, arbitrary, and bytemuck feature modules. The five example programs and the parsing benchmark were also reviewed. The repository's CI configuration (cargo-hack feature-powerset on stable/beta/nightly, trybuild compile-pass/compile-fail tests, a smoke-test sub-crate) was inspected from the VCS checkout. Tests were not executed locally.

The audit looked for unsafe code, FFI, network/filesystem/environment access, process spawning, dynamic code execution, cryptography, and concurrency primitives, and for any divergence between the documented grammar and the parser implementation.

Results

The published crate ships only text artefacts (source, manifests, license, readme, changelog, spec) and a Cargo.lock; there are no binaries (justifying has-binaries). The manifest sets build = false, the crate is not declared as proc-macro, and there is no install-time hook, so no compile-time or install-time code execution occurs (justifying has-build-exec and has-install-exec). The bitflags! macro is a macro_rules! declarative macro, which performs only token-tree substitution and does not run arbitrary user code at expansion time.

A codebase-wide search for std::process, std::net, std::fs, std::env, extern "C", tokio, thread, and spawn returned no hits in the crate's own sources. The only std:: reference in the library code is a std::error::Error impl on ParseError, gated behind the std feature. This justifies uses-network, uses-filesystem, uses-environment, uses-exec, uses-concurrency, uses-jit, uses-interpreter, uses-crypto, impl-crypto, impl-protocol, impl-interpreter, impl-jit, impl-algorithm, impl-datastructure, and impl-concurrency.

The crate declares #![cfg_attr(not(test), forbid(unsafe_code))], and direct inspection confirms no compiled unsafe blocks in the crate's own code. However, the bytemuck-feature macro (__impl_external_bitflags_bytemuck) emits unsafe impl Pod and unsafe impl Zeroable for the generated internal flags type into downstream crates. These impls are sound: the internal type is declared #[repr(transparent)] over the user-chosen primitive (src/internal.rs:19), the impls delegate the trait bounds to the underlying primitive, and SAFETY comments are present on both lines. The macro is also exercised by an in-crate test using bytemuck::cast. Because the package source contains these unsafe tokens, uses-unsafe is asserted, along with unsafe-safe, unsafe-documented, unsafe-minimal, and unsafe-tested.

The parser module implements the documented text grammar (Flags := (Whitespace Flag Whitespace)|*). The implementation is straightforward, uses safe Rust only, returns Result on every fallible path, and is exercised by dedicated unit tests under src/tests/parser.rs plus indirect coverage from the formatting tests. This justifies impl-parser, parser-impl-safe, parser-impl-tested, and parser-impl-correct.

The crate ships a comprehensive unit-test suite organised one file per operation (src/tests/), justifying has-unit-tests. Integration tests, trybuild UI tests, and a smoke-test sub-crate live under /tests in VCS but are excluded from the published archive (justifying has-integration-tests). No property tests or fuzz harnesses are present (justifying has-fuzz-tests and has-property-tests); the arbitrary integration is a downstream-facing trait impl, not in-crate fuzzing.

One low-severity correctness finding was identified (FINDING-1): in the standalone examples/custom_bits_type.rs, the BitXor impl for the demonstration CustomBits container computes a bitwise AND. This is a copy-paste error in tutorial code; it does not affect the library, but a user who copies the example will inherit broken ^ semantics on their custom bits type.

No malicious behaviour, obfuscated payloads, or supply-chain anomalies were observed; the published contents track the recorded VCS commit. This justifies is-benign.

Conclusion

bitflags is a small, focused, pure-logic crate with no runtime side effects, no FFI, and no compiled unsafe in its own code. The only unsafe it produces is the well-justified bytemuck::Pod/Zeroable impls emitted into consumers' crates, which rest on a #[repr(transparent)] guarantee that is structurally enforced by the macro. The unit-test coverage of the public API is strong, and the CI matrix exercises every feature combination. The single finding is cosmetic and confined to an example file. The crate is safe to use as a dependency.

Findings(1)

FINDING-1 correctness low

Example file BitXor implementation uses bitwise AND instead of XOR

In examples/custom_bits_type.rs, the BitXor implementation for the demo CustomBits container computes a bitwise AND rather than an XOR:

impl BitXor for CustomBits {
    type Output = Self;

    fn bitxor(self, other: Self) -> Self {
        CustomBits([
            self.0[0] & other.0[0],
            self.0[1] & other.0[1],
            self.0[2] & other.0[2],
        ])
    }
}

The trait contract for BitXor (and the operator ^) requires exclusive-or semantics. The body is identical to the BitAnd impl above it, which is incorrect for users who copy this example as a template.

This affects only the example and is not exercised by the crate itself. Severity is therefore low.

Annotations(7)

Cargo.toml

Cargo.toml, line 47-50

[features]
example_generated = []
serde = ["serde_core"]
std = []

Three opt-in features: std enables Error impls, serde pulls in serde_core for de/serialisation, example_generated exposes a documentation-only module. The crate has no default features.

examples/custom_bits_type.rs

examples/custom_bits_type.rs, line 49-59

    type Output = Self;

    fn bitxor(self, other: Self) -> Self {
        CustomBits([
            self.0[0] & other.0[0],
            self.0[1] & other.0[1],
            self.0[2] & other.0[2],
        ])
    }
}

The BitXor impl computes self.0[i] & other.0[i], which is bitwise AND, not XOR. See FINDING-1.

src/external.rs

src/external.rs, line 234-247

        // SAFETY: $InternalBitFlags is guaranteed to have the same ABI as $T,
        // and $T implements Pod
        unsafe impl $crate::__private::bytemuck::Pod for $InternalBitFlags where
            $T: $crate::__private::bytemuck::Pod
        {
        }

        // SAFETY: $InternalBitFlags is guaranteed to have the same ABI as $T,
        // and $T implements Zeroable
        unsafe impl $crate::__private::bytemuck::Zeroable for $InternalBitFlags where
            $T: $crate::__private::bytemuck::Zeroable
        {
        }
    };

The bytemuck feature macro emits unsafe impl Pod and unsafe impl Zeroable for the generated InternalBitFlags type into downstream crates. SAFETY comments justify the impls by appealing to #[repr(transparent)] on the internal struct (declared at src/internal.rs:19) plus the requirement that $T already implements the target trait. Justifies uses-unsafe and unsafe-safe, unsafe-documented, unsafe-minimal.

src/internal.rs

src/internal.rs, line 15-21

        // NOTE: The ABI of this type is _guaranteed_ to be the same as `T`
        // This is relied on by some external libraries like `bytemuck` to make
        // its `unsafe` trait impls sound.
        #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
        #[repr(transparent)]
        $vis struct $InternalBitFlags($T);
    };

The internal flags struct is declared as a #[repr(transparent)] newtype over the user-chosen primitive bits type, which is what makes the bytemuck Pod/Zeroable impls sound.

src/lib.rs

src/lib.rs, line 250-252

#![cfg_attr(not(any(feature = "std", test)), no_std)]
#![cfg_attr(not(test), forbid(unsafe_code))]
#![cfg_attr(test, allow(mixed_script_confusables))]

Crate-level lints: no_std unless std or test, and forbid(unsafe_code) unless test. The crate body itself contains no compiled unsafe blocks; the unsafe impl Pod/Zeroable is only emitted from macros into downstream crates.

src/parser.rs

src/parser.rs, line 99-137

pub fn from_str<B: Flags>(input: &str) -> Result<B, ParseError>
where
    B::Bits: ParseHex,
{
    let mut parsed_flags = B::empty();

    // If the input is empty then return an empty set of flags
    if input.trim().is_empty() {
        return Ok(parsed_flags);
    }

    for flag in input.split('|') {
        let flag = flag.trim();

        // If the flag is empty then we've got missing input
        if flag.is_empty() {
            return Err(ParseError::empty_flag());
        }

        // If the flag starts with `0x` then it's a hex number
        // Parse it directly to the underlying bits type
        let parsed_flag = if let Some(flag) = flag.strip_prefix("0x") {
            let bits =
                <B::Bits>::parse_hex(flag).map_err(|_| ParseError::invalid_hex_flag(flag))?;

            B::from_bits_retain(bits)
        }
        // Otherwise the flag is a name
        // The generated flags type will determine whether
        // or not it's a valid identifier
        else {
            B::from_name(flag).ok_or_else(|| ParseError::invalid_named_flag(flag))?
        };

        parsed_flags.insert(parsed_flag);
    }

    Ok(parsed_flags)
}

Text parser for the documented grammar. Operates on &str, splits on |, validates hex via the bits type's from_str_radix, and looks up named flags via linear scan over B::FLAGS. No panics: all integer parsing routes through from_str_radix(...).map_err(...), all lookups return Option/Result. Justifies impl-parser and parser-impl-safe.

src/tests.rs

Comprehensive unit-test suite organised into per-operation submodules under src/tests/ (38 files), exercising every public flags operation plus parser edge cases. Justifies has-unit-tests.