cargo : zerocopy-derive @ 0.8.50
PE Patrick Elsen signed 2026-06-02 published 2026-06-02

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-benignuses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

zerocopy-derive 0.8.50 is the proc-macro companion to zerocopy: 10 derives (KnownLayout, Immutable, TryFromBytes, FromZeros/FromBytes, IntoBytes, Unaligned, ByteHash, ByteEq, SplitAt) emitting unsafe-trait impls based on type shape and #[repr(...)]. Source matches upstream byte-for-byte. The proc-macro runs pure safe Rust — no I/O, no unsafe in its executable path. Coverage is broad: 35 integration tests, 26 trybuild UI tests, 21 golden-output tests. No findings.

Report

Subject

zerocopy-derive is the proc-macro companion to the zerocopy crate. It provides 10 published #[derive(...)] macros — KnownLayout, Immutable, TryFromBytes, FromZeros, FromBytes, IntoBytes, Unaligned, ByteHash, ByteEq, SplitAt — plus deprecated aliases (FromZeroes, AsBytes). Each derive inspects the input type's shape, #[repr(...)] attributes, and field types, then emits trait impls (mostly unsafe impls) for the corresponding zerocopy trait, together with the bound predicates that the user's type must satisfy.

The emitted code is the unsafe-trait scaffolding that lets downstream consumers move between [u8] and typed values without manual transmutes; the soundness contract lives on the zerocopy side. This proc-macro's role is to mechanically produce that scaffolding from the input shape and to refuse to compile when the shape doesn't satisfy the relevant safety invariant.

Methodology

The published crate was downloaded by openvet audit new and unpacked into contents/. The upstream Git repository (https://github.com/google/zerocopy) was cloned into vcs-root/, with vcs/ symlinked to vcs-root/zerocopy-derive (the audited crate is a workspace member). The checkout matches the commit recorded in .cargo_vcs_info.json.

Tools used:

  • openvet audit (workspace creation, annotations, claims, findings, dependency narratives, report).
  • diff -rq to compare contents/src/ and contents/tests/ against the symlinked vcs/.
  • grep to enumerate unsafe keywords, extern "C" declarations, and standard-library I/O (std::net::, std::env::, std::process::, std::fs::, std::thread::).
  • wc -l for line counts.

Reading: src/lib.rs in full; src/derive/mod.rs, src/derive/from_bytes.rs, src/derive/into_bytes.rs (head), src/derive/known_layout.rs (sampled), src/derive/try_from_bytes.rs (head and around its unsafe-emission sites at lines 415, 515, 640, 683), src/derive/unaligned.rs; src/repr.rs (head); src/util.rs (structure scan); src/output_tests/mod.rs head to confirm cfg(test) gating of std::env::var/std::fs::write. Tests surveyed by file count (82) and aggregate line count (~5760 lines). The [[test]] table in Cargo.toml enumerates 35 integration-test targets.

Results

The diff between the published crate and the upstream tree shows that src/ and tests/ match byte-for-byte; the only structural difference is tests/enum_from_bytes.rs, which is explicitly listed in the exclude field of Cargo.toml.orig and so is omitted from the published artefact by design. The Cargo.toml diff is the usual cargo normalisation.

The crate ships no binary artefacts (justifying has-binaries) and no installer hook (justifying has-install-exec). It is, however, a procedural macro: Cargo.toml sets [lib] proc-macro = true, so the crate's compiled code is loaded by rustc and run on every downstream consumer's machine at compile time. This is the basis for has-build-exec = true. The conditional build-exec claims hold by construction: the proc-macro is a pure function from input token stream to output token stream, it makes no syscalls during expansion (no file I/O, no network, no process spawning, no environment scraping); the test-time std::env::var/std::fs::write calls in src/output_tests/mod.rs are behind #[cfg(test)] and are unreachable in published proc-macro path. This supports build-exec-safe, build-exec-deterministic, build-exec-no-network, build-exec-no-write-out, and build-exec-minimal.

The proc-macro itself contains no unsafe blocks in its executable path. A grep across contents/src/ for the unsafe keyword returns hits only in two contexts: (1) tokens emitted inside quote!{ ... } invocations (which become part of the consumer's source, not part of the proc-macro's behaviour), and (2) the *.expected.rs fixtures under src/output_tests/expected/ (text files used as test oracles, never compiled as part of the proc-macro). Both are addressed at src/lib.rs:17-37 and src/derive/try_from_bytes.rs:415-421. The proc-macro's executable code is pure safe Rust. This is the basis for uses-unsafe.

The crate-level lints enable deny(clippy::missing_safety_doc, clippy::multiple_unsafe_ops_per_block, clippy::undocumented_unsafe_blocks), which apply to the proc-macro's own source. Inspection confirmed that every // SAFETY: comment in the source either sits inside a quote! block (so it becomes a token in the emitted output, justifying the unsafe it precedes) or annotates a piece of design-level reasoning about the emitted code.

No use of std::net, std::env, std::process, std::fs, std::thread, tokio, or other I/O was found outside the #[cfg(test)] bless-mode path. This is the basis for uses-network, uses-filesystem, uses-environment, uses-exec, uses-jit, uses-interpreter, uses-crypto, and uses-concurrency. The crate does not itself implement a parser in the data-format sense (it consumes syn's already-parsed DeriveInput and walks its variants), a network protocol, an interpreter, a JIT, a data structure, an algorithm, a concurrency primitive, or any cryptography; the corresponding impl-* claims (impl-parser, impl-interpreter, impl-jit, impl-protocol, impl-datastructure, impl-algorithm, impl-crypto, impl-concurrency) are all false.

Test coverage is dense: 35 integration-test targets under tests/ (covering struct, enum, and union shapes for every derive plus repr variants), 26 trybuild UI tests under tests/ui/ exercising compile-error cases, and 21 #[cfg(test)] output tests under src/output_tests/ that pretty-print the emitted token stream and string-compare it against checked-in *.expected.rs fixtures. The bless mechanism (ZEROCOPY_BLESS=1) is the single I/O path in the test code. Together this is the basis for has-unit-tests and has-integration-tests. No fuzz or property-test harness ships with the crate or appears under the workspace member's path; has-fuzz-tests and has-property-tests are both false.

No findings were recorded. The proc-macro is authored by the zerocopy maintainers and matches its documented purpose; no malicious code, no obfuscated payloads, no target-conditional shenanigans, no supply-chain anomalies were observed. This is the basis for is-benign.

Conclusion

zerocopy-derive is a well-scoped, safety-focused proc-macro: pure safe Rust at expansion time, no I/O, no concurrency, three standard proc-macro dependencies (proc-macro2, quote, syn). Its compile-time execution is fully deterministic and its emitted code's safety story is the responsibility of the zerocopy runtime crate. The test suite covers every derive across structs, enums, and unions in both happy-path and trybuild-UI-error forms, plus golden-output fixtures.

Findings

No findings.

Annotations(7)

Cargo.toml

Cargo.toml, line 37-39

name = "zerocopy_derive"
path = "src/lib.rs"
proc-macro = true

[lib] proc-macro = true declaration. Proc macros execute at compile time on every downstream consumer's machine, justifying has-build-exec.

src/derive/try_from_bytes.rs

The most complex derive: generates a TryFromBytes impl per data shape (struct, enum, union). For enums it constructs a per-variant validity check by reading the tag through the zerocopy-side pointer machinery and dispatching to is_bit_valid on each variant's fields. ~760 LOC. All unsafe text inside the file is inside quote! emissions into downstream code, each preceded by a // SAFETY: comment (also tokenized) explaining the invariant.

src/derive/try_from_bytes.rs, line 415-421

                let tag_ptr = unsafe {
                    candidate.reborrow().project_transmute_unchecked::<
                        _,
                        #zerocopy_crate::invariant::Initialized,
                        #zerocopy_crate::pointer::cast::CastSized
                    >()
                };

Example of unsafe text appearing INSIDE a quote!-emitted token stream — these tokens are placed into the consumer's crate, not executed by the proc-macro. Each such block is paired with a // SAFETY: comment in the source (also passed through as a token) and references zerocopy-side invariants. The proc-macro itself executes no unsafe code.

src/lib.rs

src/lib.rs, line 17-37

#![allow(unknown_lints)]
#![deny(renamed_and_removed_lints)]
#![deny(
    clippy::all,
    clippy::missing_safety_doc,
    clippy::multiple_unsafe_ops_per_block,
    clippy::undocumented_unsafe_blocks
)]
// We defer to own discretion on type complexity.
#![allow(clippy::type_complexity)]
// Inlining format args isn't supported on our MSRV.
#![allow(clippy::uninlined_format_args)]
#![deny(
    rustdoc::bare_urls,
    rustdoc::broken_intra_doc_links,
    rustdoc::invalid_codeblock_attributes,
    rustdoc::invalid_html_tags,
    rustdoc::invalid_rust_codeblocks,
    rustdoc::missing_crate_level_docs,
    rustdoc::private_intra_doc_links
)]

Crate-level lints deny clippy::missing_safety_doc, clippy::multiple_unsafe_ops_per_block, and clippy::undocumented_unsafe_blocks. Rustdoc lints are also denied. The crate itself contains zero unsafe blocks in its proc-macro path — the only unsafe keyword appearances in src/ are inside quote! invocations that emit unsafe impl / unsafe { ... } tokens into downstream consumer crates. Justifies uses-unsafe.

src/lib.rs, line 47-48

#[cfg(test)]
mod output_tests;

#[cfg(test)] mod output_tests — gated so the test-fixture path containing std::env::var/std::fs::write only runs during cargo test of this crate, not during downstream proc-macro expansion.

src/lib.rs, line 79-98

macro_rules! derive {
    ($trait:ident => $outer:ident => $inner:path) => {
        #[proc_macro_derive($trait, attributes(zerocopy))]
        pub fn $outer(ts: proc_macro::TokenStream) -> proc_macro::TokenStream {
            let ast = syn::parse_macro_input!(ts as DeriveInput);
            let ctx = match Ctx::try_from_derive_input(ast) {
                Ok(ctx) => ctx,
                Err(e) => return e.into_compile_error().into(),
            };
            let ts = $inner(&ctx, Trait::$trait).into_ts();
            // We wrap in `const_block` as a backstop in case any derive fails
            // to wrap its output in `const_block` (and thus fails to annotate)
            // with the full set of `#[allow(...)]` attributes).
            let ts = const_block([Some(ts)]);
            #[cfg(test)]
            crate::util::testutil::check_hygiene(ts.clone());
            ts.into()
        }
    };
}

The derive! macro is the single entry-point shape for all 10 published derives (KnownLayout, Immutable, TryFromBytes, FromZeros, FromBytes, IntoBytes, Unaligned, ByteHash, ByteEq, SplitAt). Each parses a DeriveInput, builds a Ctx, runs the inner derive function, wraps the output in const_block for hygiene, and on cfg(test) also runs check_hygiene. Errors are turned into compile_error!() via into_compile_error.

src/output_tests/mod.rs

src/output_tests/mod.rs, line 81-91

            if std::env::var("ZEROCOPY_BLESS").is_ok() {
                let path = Path::new(env!("CARGO_MANIFEST_DIR"))
                    .join("src/output_tests")
                    .join($path);
                std::fs::write(&path, &actual).expect("failed to bless output");
            } else {
                let expected_str = include_str!($path);
                let expected_ts: proc_macro2::TokenStream = expected_str.parse().expect("failed to parse expected output");
                let expected = pretty_print(expected_ts);
                assert_eq_or_diff(&expected, &actual);
            }

std::env::var("ZEROCOPY_BLESS") and std::fs::write are only reachable inside #[cfg(test)] code (mod is gated at src/lib.rs:47-48). They are the bless mechanism for regenerating the *.expected.rs fixtures. Not in the proc-macro runtime path. Together with the lack of any other I/O in src/, justifies uses-environment, uses-filesystem (false: not reachable in the published proc-macro path).

src/repr.rs

Parses #[repr(...)] attributes into a typed Repr<Prim, Packed> enum with separate type aliases for struct/union (StructUnionRepr) and enum (EnumRepr) representations. The legal-combination rules (Transparent vs Compound, align/packed mutual exclusion) are encoded in the type. ~850 LOC.

src/util.rs

Shared helpers for the derives: Ctx holds the parsed input plus the path to the zerocopy crate (resolved from #[zerocopy(crate = "...")] for re-export support), ImplBlockBuilder constructs trait impls with field-bound predicates, PaddingCheck enumerates the safety-check strategies (struct, packed, repr-C struct), and testutil::check_hygiene (cfg(test)) parses every emitted token stream to catch hygiene mistakes early. ~840 LOC, no unsafe.

tests

82 test files: 36 in-tree integration tests under tests/ covering struct/enum/union variants of every derive, plus 26 tests/ui/*.rs trybuild UI tests (run via tests/ui.rs) covering compile-error cases. The crate also has 21 inline #[cfg(test)] output tests under src/output_tests/ that check the emitted token stream against *.expected.rs fixtures. Justifies has-unit-tests and has-integration-tests.