The following was reported by OpenAI via email:
- Vulnerability Type: Uninitialized-memory read / information disclosure / invalid value construction
- Affected Software: zerocopy-derive
- Date: September 2026
- Discoverer: OpenAI (OutboundDisclosures@openai.com)
Summary
zerocopy-derive silently ignores representation attributes written as #[r#repr(...)], although the Rust compiler applies them. Consequently, safe application code can derive IntoBytes for a type containing padding, TryFromBytes with a validator that checks a different byte than the actual field, or KnownLayout with an incorrect size. We reproduce disclosure of controlled padding residue, acceptance of an invalid NonZeroU8 value, and construction of an eight-byte reference backed by a one-byte allocation.
Exploitation requires an application to already use this valid raw-identifier spelling in a derived type's schema. In a server, an attacker could then supply bytes to the affected parser or observe bytes serialized in a response. An attacker controlling only network input cannot change these compile-time attributes; a schema using ordinary repr spelling does not meet this prerequisite.
The disclosure example deliberately seeds storage with SECRET!! to make the retained bytes observable. It demonstrates that serialization exposes bytes outside the initialized fields; disclosure of another request's secrets depends on the application's storage contents and reuse. The enum parser example accepts attacker-selected bytes that violate a field's validity requirement. The layout parser example accepts an input allocation too small for the returned reference. All three examples produce explicit undefined-behavior diagnostics under Miri. We have not demonstrated remote code execution or exploitation of a deployed service.
Affected versions: The original audit examined public Git revision 2dad389b030e9268d6645ac0bf0626b867e96068. The standalone reproduction below uses published zerocopy 0.8.56 and zerocopy-derive 0.8.56, whose package metadata identifies revision 6dc429c451bdf1d7202ec1ec2cf426514e00d8eb. The affected code is unchanged between these sources. We have not established the affected range of other published releases.
Environment: Native verification used Rust 1.98.1 on macOS arm64. Miri reproduction with nightly-2026-09-04, Rust 1.100.0-nightly (a69a63265, 2026-09-03), and Miri 0.1.0 (a69a63265c).
Sketch of the attack
The application defines a response with one initialized byte:
#[derive(FromBytes, Immutable, IntoBytes)]
#[repr(C)]
#[r#repr(align(8))]
struct Response {
status: u8,
}
The compiler gives Response size eight and alignment eight. Its only field occupies byte zero; the remaining seven bytes are padding. The derive parser sees repr(C) but misses the alignment attribute, so it incorrectly accepts IntoBytes. Calling as_bytes() exposes all eight bytes.
The second reproduction uses #[repr(C, align(8))] together with #[r#repr(u8)] on an enum. On the tested target, the actual NonZeroU8 field is at byte offset one, but the generated validator checks offset four. A packet containing zero at offset one and one at offset four passes validation.
The third reproduction derives KnownLayout for a one-field struct with the same omitted align(8) attribute. The generated layout records a size of one, while the actual type requires eight bytes. A one-byte input allocation is accepted as backing storage for a reference to the eight-byte type.
Full repro
Save the following as Cargo.toml:
[package]
name = "zerocopy-audit-repro"
version = "0.1.0"
edition = "2024"
[workspace]
[dependencies]
zerocopy = { version = "=0.8.56", features = ["derive", "std", "simd"] }
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 }
Save the first program before generating the lockfile so that Cargo has a binary target.
Padding disclosure
Save the following as src/bin/raw_repr_serialization.rs:
#![forbid(unsafe_code)]
use zerocopy::{FromBytes, Immutable, IntoBytes};
#[derive(FromBytes, Immutable, IntoBytes)]
#[repr(C)]
#[r#repr(align(8))]
struct Response {
status: u8,
}
fn main() {
let mut response = Response::read_from_bytes(b"SECRET!!").unwrap();
response.status = 42;
println!("{:?}", response.as_bytes());
assert_eq!(&response.as_bytes()[1..], b"ECRET!!");
}
Run:
cargo +stable generate-lockfile
cargo +stable run --release --locked --bin raw_repr_serialization
The native run with the published packages exited successfully and printed:
[42, 69, 67, 82, 69, 84, 33, 33]
The response's only field is now 42, but the serialized suffix is still ECRET!!. This is a controlled residue demonstration: the program explicitly supplied the earlier bytes. It does not depend on, or demonstrate, an allocator choosing a particular previously freed allocation.
Run the same program under Miri:
cargo +nightly-2026-09-04 miri run --locked --bin raw_repr_serialization
With the published packages, Miri terminated with an explicit undefined-behavior report:
error: Undefined Behavior: reading memory at alloc294[0x1..0x2], but memory is uninitialized at [0x1..0x2], and this operation requires initialized memory
The allocation identifier can vary. The failure is at the first padding byte when the returned byte slice is read for formatting. Typed values do not guarantee that their padding remains initialized, even when an earlier byte buffer contained values there.
Enum validation at the wrong offset
In the same project, save the following as src/bin/raw_repr_enum.rs:
#![forbid(unsafe_code)]
use zerocopy::{Immutable, KnownLayout, TryFromBytes};
#[derive(Debug, Immutable, KnownLayout, TryFromBytes)]
#[repr(C, align(8))]
#[r#repr(u8)]
enum Packet {
Value(core::num::NonZeroU8),
End,
}
#[repr(align(8))]
struct Wire([u8; 8]);
fn main() {
println!(
"actual size={} alignment={}",
core::mem::size_of::<Packet>(),
core::mem::align_of::<Packet>(),
);
let wire = Wire([0, 0, 0, 0, 1, 0, 0, 0]);
let packet = Packet::try_ref_from_bytes(&wire.0).unwrap();
println!("{packet:?}");
}
Run:
cargo +nightly-2026-09-04 miri run --locked --bin raw_repr_enum
The packet should be rejected because its Value payload at offset one is zero. In the published-package run, validation succeeded and Miri reported the invalid value when the program formatted the result:
actual size=8 alignment=8
error: Undefined Behavior: constructing invalid value of type std::num::NonZero<u8>: at .0.0, encountered 0, but expected something greater or equal to 1
The Wire wrapper gives the input the required alignment. This reproduction therefore isolates the incorrect field validation.
Reference extends beyond its allocation
In the same project, save the following as src/bin/raw_repr_layout.rs:
#![forbid(unsafe_code)]
use zerocopy::{FromBytes, Immutable, KnownLayout};
#[derive(Debug, FromBytes, Immutable, KnownLayout)]
#[repr(C)]
#[r#repr(align(8))]
struct AlignedPacket(u8);
fn main() {
println!(
"actual size={} alignment={}",
core::mem::size_of::<AlignedPacket>(),
core::mem::align_of::<AlignedPacket>(),
);
let wire = Box::new([42u8]);
let packet = AlignedPacket::ref_from_bytes(wire.as_slice()).unwrap();
println!("{packet:?}");
}
Run with the strict-provenance option used for this validation:
MIRIFLAGS='-Zmiri-strict-provenance' \
cargo +nightly-2026-09-04 miri run --locked --bin raw_repr_layout
With the published packages, Miri reported undefined behavior when ref_from_bytes constructed the reference:
actual size=8 alignment=8
error: Undefined Behavior: constructing invalid value of type &AlignedPacket: encountered a dangling reference (going beyond the bounds of its allocation)
The box contains a one-byte allocation. The compiler applies align(8), so a reference to AlignedPacket requires eight bytes of backing storage. The derived KnownLayout incorrectly describes a one-byte type, allowing the undersized input through the size check. The failure occurs inside NonNull::as_ref before the reference is returned to the caller.
Root cause
The links below refer to the original audited revision 2dad389b030e9268d6645ac0bf0626b867e96068. The same affected code is present in published zerocopy 0.8.56 and zerocopy-derive 0.8.56, whose package metadata identifies revision 6dc429c451bdf1d7202ec1ec2cf426514e00d8eb.
- The representation-attribute parser processes an attribute only when
meta_list.path.is_ident("repr") succeeds. That comparison does not match the raw spelling r#repr, so the parser silently omits its contents. Rust's raw-identifier rule says that the r# prefix is not part of the actual identifier; the compiler applies the omitted attribute.
- The single-field
repr(C) branch of IntoBytes skips the padding check when it sees no alignment modifier greater than one. For Response, that premise is false. IntoBytes::as_bytes then uses the actual size_of_val and trusts the generated trait implementation to guarantee initialization of every exposed byte.
- For the enum,
generate_tag_enum and derive_is_bit_valid build a repr(C) helper after the parser has discarded repr(u8). On the tested target, the helper's tag occupies four bytes and its payload starts at offset four; the original enum's tag occupies one byte and its payload starts at offset one.
- The generated validator treats this helper as layout-equivalent to the original enum and validates the helper's fields. Both types have total size eight in the reproduction, so a size check cannot detect the different field offsets. The bytes at offsets zero through three decode as the helper's
Value tag, and the nonzero byte at offset four passes its field check, leaving the actual zero-valued NonZeroU8 at offset one unvalidated.
KnownLayout generation extracts the alignment from the same incomplete representation and constructs LAYOUT from the field layouts. For AlignedPacket(u8), this records size one and alignment one instead of eight. FromBytes::ref_from_bytes trusts that layout when checking the input, then constructs a reference whose actual size exceeds the allocation.
The parser should normalize raw identifiers before recognizing representation attributes, and regression tests should exercise both spellings. A fix must prevent deriving IntoBytes for the padded response, must either reject the enum representation or validate the actual enum fields, and must compute the full backing-storage requirements for KnownLayout.
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.
The following was reported by OpenAI via email:
Summary
zerocopy-derivesilently ignores representation attributes written as#[r#repr(...)], although the Rust compiler applies them. Consequently, safe application code can deriveIntoBytesfor a type containing padding,TryFromByteswith a validator that checks a different byte than the actual field, orKnownLayoutwith an incorrect size. We reproduce disclosure of controlled padding residue, acceptance of an invalidNonZeroU8value, and construction of an eight-byte reference backed by a one-byte allocation.Exploitation requires an application to already use this valid raw-identifier spelling in a derived type's schema. In a server, an attacker could then supply bytes to the affected parser or observe bytes serialized in a response. An attacker controlling only network input cannot change these compile-time attributes; a schema using ordinary
reprspelling does not meet this prerequisite.The disclosure example deliberately seeds storage with
SECRET!!to make the retained bytes observable. It demonstrates that serialization exposes bytes outside the initialized fields; disclosure of another request's secrets depends on the application's storage contents and reuse. The enum parser example accepts attacker-selected bytes that violate a field's validity requirement. The layout parser example accepts an input allocation too small for the returned reference. All three examples produce explicit undefined-behavior diagnostics under Miri. We have not demonstrated remote code execution or exploitation of a deployed service.Affected versions: The original audit examined public Git revision
2dad389b030e9268d6645ac0bf0626b867e96068. The standalone reproduction below uses publishedzerocopy 0.8.56andzerocopy-derive 0.8.56, whose package metadata identifies revision6dc429c451bdf1d7202ec1ec2cf426514e00d8eb. The affected code is unchanged between these sources. We have not established the affected range of other published releases.Environment: Native verification used Rust
1.98.1on macOS arm64. Miri reproduction withnightly-2026-09-04, Rust1.100.0-nightly(a69a63265,2026-09-03), and Miri0.1.0(a69a63265c).Sketch of the attack
The application defines a response with one initialized byte:
The compiler gives
Responsesize eight and alignment eight. Its only field occupies byte zero; the remaining seven bytes are padding. The derive parser seesrepr(C)but misses the alignment attribute, so it incorrectly acceptsIntoBytes. Callingas_bytes()exposes all eight bytes.The second reproduction uses
#[repr(C, align(8))]together with#[r#repr(u8)]on an enum. On the tested target, the actualNonZeroU8field is at byte offset one, but the generated validator checks offset four. A packet containing zero at offset one and one at offset four passes validation.The third reproduction derives
KnownLayoutfor a one-field struct with the same omittedalign(8)attribute. The generated layout records a size of one, while the actual type requires eight bytes. A one-byte input allocation is accepted as backing storage for a reference to the eight-byte type.Full repro
Save the following as
Cargo.toml:Save the first program before generating the lockfile so that Cargo has a binary target.
Padding disclosure
Save the following as
src/bin/raw_repr_serialization.rs:Run:
The native run with the published packages exited successfully and printed:
The response's only field is now
42, but the serialized suffix is stillECRET!!. This is a controlled residue demonstration: the program explicitly supplied the earlier bytes. It does not depend on, or demonstrate, an allocator choosing a particular previously freed allocation.Run the same program under Miri:
With the published packages, Miri terminated with an explicit undefined-behavior report:
The allocation identifier can vary. The failure is at the first padding byte when the returned byte slice is read for formatting. Typed values do not guarantee that their padding remains initialized, even when an earlier byte buffer contained values there.
Enum validation at the wrong offset
In the same project, save the following as
src/bin/raw_repr_enum.rs:Run:
The packet should be rejected because its
Valuepayload at offset one is zero. In the published-package run, validation succeeded and Miri reported the invalid value when the program formatted the result:The
Wirewrapper gives the input the required alignment. This reproduction therefore isolates the incorrect field validation.Reference extends beyond its allocation
In the same project, save the following as
src/bin/raw_repr_layout.rs:Run with the strict-provenance option used for this validation:
MIRIFLAGS='-Zmiri-strict-provenance' \ cargo +nightly-2026-09-04 miri run --locked --bin raw_repr_layoutWith the published packages, Miri reported undefined behavior when
ref_from_bytesconstructed the reference:The box contains a one-byte allocation. The compiler applies
align(8), so a reference toAlignedPacketrequires eight bytes of backing storage. The derivedKnownLayoutincorrectly describes a one-byte type, allowing the undersized input through the size check. The failure occurs insideNonNull::as_refbefore the reference is returned to the caller.Root cause
The links below refer to the original audited revision
2dad389b030e9268d6645ac0bf0626b867e96068. The same affected code is present in publishedzerocopy 0.8.56andzerocopy-derive 0.8.56, whose package metadata identifies revision6dc429c451bdf1d7202ec1ec2cf426514e00d8eb.meta_list.path.is_ident("repr")succeeds. That comparison does not match the raw spellingr#repr, so the parser silently omits its contents. Rust's raw-identifier rule says that ther#prefix is not part of the actual identifier; the compiler applies the omitted attribute.repr(C)branch ofIntoBytesskips the padding check when it sees no alignment modifier greater than one. ForResponse, that premise is false.IntoBytes::as_bytesthen uses the actualsize_of_valand trusts the generated trait implementation to guarantee initialization of every exposed byte.generate_tag_enumandderive_is_bit_validbuild arepr(C)helper after the parser has discardedrepr(u8). On the tested target, the helper's tag occupies four bytes and its payload starts at offset four; the original enum's tag occupies one byte and its payload starts at offset one.Valuetag, and the nonzero byte at offset four passes its field check, leaving the actual zero-valuedNonZeroU8at offset one unvalidated.KnownLayoutgeneration extracts the alignment from the same incomplete representation and constructsLAYOUTfrom the field layouts. ForAlignedPacket(u8), this records size one and alignment one instead of eight.FromBytes::ref_from_bytestrusts that layout when checking the input, then constructs a reference whose actual size exceeds the allocation.The parser should normalize raw identifiers before recognizing representation attributes, and regression tests should exercise both spellings. A fix must prevent deriving
IntoBytesfor the padded response, must either reject the enum representation or validate the actual enum fields, and must compute the full backing-storage requirements forKnownLayout.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.