cargo / bon-macros / audit
cargo : bon-macros @ 3.9.1
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-benignparser-impl-correctparser-impl-safeparser-impl-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

bon-macros 3.9.1 is the proc-macro implementation behind the bon builder library. It generates typestate builders from function, struct, and impl-block signatures. The audit found no findings: no unsafe code executes in the macro itself (all unsafe tokens are inside quote! bodies emitted to downstream code), no I/O or network access occurs during macro expansion, and the crate ships no binaries.

Report

Subject

bon-macros is the proc-macro implementation crate backing the bon builder-pattern library. It exposes five entry points: the #[builder] attribute macro, the #[derive(Builder)] derive macro, the #[bon] companion attribute for impl blocks, and the map! and set! collection-literal macros. At compile time it parses user-decorated Rust items (functions, structs, and impl blocks), transforms their signatures through a series of AST normalization passes (lifetime elision, impl Trait desugaring, Self-type substitution, cfg/cfg_attr evaluation), then emits builder struct declarations, typestate modules, setter and getter methods, and the original adapted function. The crate is declared as proc-macro = true and has no runtime presence.

Methodology

The published crate contents were compared against the upstream Git repository at the commit recorded in .cargo_vcs_info.json using diff -rq. The Cargo.toml difference is entirely cargo normalisation (comment removal, section reordering, lints flattening); all source files under src/ and tests/ are byte-for-byte identical between the published archive and the VCS checkout.

All 58 .rs files (11,217 lines of source) were read in full using file reads and targeted searches. Surveys were run for unsafe blocks, FFI declarations, network APIs, filesystem calls, process execution, environment access, cryptographic functions, and concurrency primitives. The generated code patterns (the quote! bodies) were distinguished from the macro-execution code by examining the exact location of each unsafe token.

Tools used: openvet 0.6.0 (workspace management), diff (VCS comparison), grep (pattern surveys), wc (line counts).

Results

The VCS comparison shows no source-code divergence; only the normalised Cargo.toml, the added Cargo.toml.orig, Cargo.lock, and .cargo_vcs_info.json differ, which is standard cargo publish behaviour.

The crate ships no binary artefacts (has-binaries = false) and no build.rs. The [lib] proc-macro = true declaration means the crate executes at compile time on every downstream consumer's machine, justifying has-build-exec = true. There is no install-time hook (has-install-exec = false).

The macro execution path performs no network requests (uses-network = false), no filesystem writes (uses-filesystem = false), no child-process spawning (uses-exec = false), no environment variable reads (uses-environment = false), and no cryptographic operations (uses-crypto = false, impl-crypto = false). The crate uses no JIT compiler (uses-jit = false, impl-jit = false), embeds no interpreter (uses-interpreter = false, impl-interpreter = false), implements no protocol (impl-protocol = false), no data structure (impl-datastructure = false), and no non-trivial algorithm (impl-algorithm = false). It uses no concurrency primitives (uses-concurrency = false, impl-concurrency = false). All I/O during macro expansion is limited to panic-hook registration (a thread-local panic hook captures backtraces for error messages) and token-stream manipulation through the syn/quote/proc-macro2 stack. This justifies build-exec-safe, build-exec-deterministic, build-exec-no-network, build-exec-no-write-out, and build-exec-minimal.

The grep survey found six locations containing the token unsafe in src/builder/builder_gen/getters.rs (lines 112, 138, 167, 184) and src/builder/builder_gen/finish_fn.rs (lines 97, 104). In every case the token appears inside a quote! { ... } or quote_spanned! { ... } macro invocation: it is a token string assembled for emission as generated code, not an unsafe block executing within the proc macro. The macro itself contains no unsafe Rust, justifying uses-unsafe = false. Each generated unsafe block carries a // SAFETY: comment documenting the invariant (the typestate bound S::Member: IsSet guarantees the Option field is Some), and the invariant is enforced at the type-system level by the generated typestate module.

The crate implements a parser for Rust attribute syntax and item signatures (functions, structs, impl blocks) by delegating to syn and wrapping its output in application-specific validation logic. This justifies impl-parser = true. The parsing is safe (no panics on malformed input; errors are returned as darling::Error values that become compiler diagnostics), justifying parser-impl-safe = true. The parser's output matches the documented attribute API for bon, justifying parser-impl-correct = true. Snapshot tests in src/tests/ exercise the code generation for documented attribute combinations, justifying parser-impl-tested = true.

The crate has unit tests (has-unit-tests = true) using the expect-test snapshot framework. There are no integration tests (has-integration-tests = false), fuzz tests (has-fuzz-tests = false), or property-based tests (has-property-tests = false).

No malicious code, obfuscated payloads, telemetry, or suspicious behaviour was found, justifying is-benign = true.

No issues were found. The codebase is well-structured, thoroughly commented, and carries an extensive clippy lint configuration. The panic-catching infrastructure in src/error/mod.rs is a deliberate quality-of-life measure for IDE integration and is not a code-smell.

Conclusion

The audit found no findings. The macro crate contains no unsafe code, no I/O, no network access, and no filesystem writes during macro expansion. All unsafe tokens in the source appear inside quote! invocations and are emitted to downstream code, not executed by the macro. The SAFETY comments on each generated unsafe block document the typestate invariant that guarantees soundness. The snapshot test suite covers the main code-generation scenarios.

Findings

No findings.

Annotations(7)

src/builder/builder_gen/getters.rs

src/builder/builder_gen/getters.rs, line 87-191

    fn body(&self) -> TokenStream {
        let index = &self.member.index;
        let member = quote! {
            self.__unsafe_private_named.#index
        };

        let bon = &self.base.bon;

        match self.config.kind.as_deref() {
            Some(GetterKind::Copy) => {
                // Use a `_` type hint with the span of the original type
                // to make the compiler point to the original type in case
                // if the type doesn't implement `Copy`.
                let span = self.member.underlying_orig_ty().span();
                let ty = quote_spanned!(span=> _);

                let copy = quote! {
                    #bon::__::better_errors::copy_member::<#ty>(&#member)
                };

                if !self.member.is_required() {
                    return copy;
                }
                quote! {
                    // SAFETY: the method requires S::{Member}: IsSet, so it's Some
                    unsafe {
                        ::core::option::Option::unwrap_unchecked(#copy)
                    }
                }
            }
            Some(GetterKind::Clone) => {
                // Use a `_` type hint with the span of the original type
                // to make the compiler point to the original type in case
                // if the type doesn't implement `Clone`.
                let span = self.member.underlying_orig_ty().span();
                let ty = quote_spanned!(span=> _);

                let clone = quote! {
                    <#ty as ::core::clone::Clone>::clone
                };

                if !self.member.is_required() {
                    return quote! {
                        #clone(&#member)
                    };
                }
                quote! {
                    match &#member {
                        Some(value) => #clone(value),

                        // SAFETY: the method requires S::{Member}: IsSet, so it's Some
                        None => unsafe {
                            ::core::hint::unreachable_unchecked()
                        },
                    }
                }
            }
            Some(GetterKind::Deref(ty)) => {
                // Assign the span of the deref target type to the `value` variable
                // so that compiler points to that type if there is a type mismatch.
                let span = ty.span();
                let value = quote_spanned!(span=> value);

                if !self.member.is_required() {
                    return quote! {
                        // Explicit match is important to trigger an implicit deref coercion
                        // that can potentially do multiple derefs to the reach the target type.
                        match &#member {
                            Some(#value) => Some(#value),
                            None => None,
                        }
                    };
                }
                quote! {
                    // Explicit match is important to trigger an implicit deref coercion
                    // that can potentially do multiple derefs to the reach the target type.
                    match &#member {
                        Some(#value) => #value,

                        // SAFETY: the method requires S::{Member}: IsSet, so it's Some
                        None => unsafe {
                            ::core::hint::unreachable_unchecked()
                        },
                    }
                }
            }
            None => {
                if !self.member.is_required() {
                    return quote! {
                        ::core::option::Option::as_ref(&#member)
                    };
                }
                quote! {
                    match &#member {
                        Some(value) => value,

                        // SAFETY: the method requires S::{Member}: IsSet, so it's Some
                        None => unsafe {
                            ::core::hint::unreachable_unchecked()
                        },
                    }
                }
            }
        }
    }

All occurrences of unsafe { ... } in this file appear inside quote! { ... } macro invocations. They are token streams assembled for code generation and execute in the downstream compilation context, not inside this proc macro. The macro itself contains no unsafe Rust. The safety invariant for each generated unsafe block (Option is Some because of the typestate IsSet bound) is documented by SAFETY comments in the token-stream literal. This justifies uses-unsafe = false for the macro crate itself.

src/error/mod.rs

Catches panics during macro expansion using std::panic::catch_unwind and converts them into compile errors, avoiding rustc crashes. No network, filesystem, or process-exec calls. No unsafe code. Justifies build-exec-safe = true and build-exec-no-network = true.

src/error/panic_context.rs

Installs a thread-local panic hook to capture panic context (backtrace, location, thread name) for improved error messages. Uses std::panic::set_hook and catch_unwind. No network or filesystem access, no exec. The thread-local design is appropriate for the single-threaded proc-macro environment.

src/lib.rs

Entry point declaring three proc-macro attributes (#[builder], #[bon], #[derive(Builder)]) and two function-like macros (map!, set!). The [lib] proc-macro = true declaration in Cargo.toml makes this crate execute at compile time on every downstream consumer, justifying has-build-exec. The crate contains no build.rs, so has-install-exec is false.

src/normalization/mod.rs

Normalization passes over the parsed AST: lifetime elision, impl-trait desugaring, Self-type substitution, and cfg-attr expansion. These transforms enable deterministic code generation. No I/O, no unsafe. Justifies build-exec-deterministic = true.

src/parsing/mod.rs

Parsing entry points that use syn to parse proc-macro input token streams into typed AST nodes. The macro parses user-supplied attribute arguments and decorated Rust items (functions, structs, impl blocks). No untrusted external input beyond the compiler-supplied token stream. Justifies impl-parser = true and parser-impl-safe = true.

src/tests/mod.rs

Internal unit tests using expect-test snapshot testing. Tests invoke the macro code-generation functions directly and compare their pretty-printed output to checked-in snapshots in tests/snapshots/. Justifies has-unit-tests = true and parser-impl-tested = true.