The following was reported by OpenAI via email:
- Bug Type: Incorrect slice metadata / API contract violation
- Affected Software: zerocopy
- Date: September 2026
- Discoverer: OpenAI (OutboundDisclosures@openai.com)
Summary
Ref::from_bytes_with_elems, from_prefix_with_elems, and from_suffix_with_elems can return an object whose trailing slice has more elements than the explicitly requested count. These constructors reduce the count to a padded byte size and then discard it. Dereferencing the wrapper reconstructs the maximum count fitting that byte size.
In the proof of concept, a request for one trailing byte returns eight through Ref, while the corresponding direct FromBytes API returns the requested one. A request for zero trailing u16 elements similarly returns one element for another layout.
Classification and threat model: This is a correctness bug. A parser using a validated length field as the count can accidentally process padding as additional elements. All additional bytes in this repro remain within the initialized input buffer; it does not demonstrate undefined behavior, an out-of-allocation read, or an application security bypass. Security consequences would require additional consumer behavior not established here.
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.
Sketch of the attack
A #[repr(C)] object with a u64 header and a trailing byte slice has eight-byte alignment. With one trailing byte its total padded size is sixteen bytes; with eight trailing bytes the size is also sixteen. The constructor initially computes the correct sixteen-byte extent, but the wrapper stores only that extent. Dereferencing it chooses eight trailing elements instead of the caller's one.
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 }
The commands below select the latest stable Rust, validated here with Rust 1.98.1.
Save this as src/bin/ref_count.rs:
#![forbid(unsafe_code)]
use zerocopy::{FromBytes, Immutable, KnownLayout, Ref};
#[derive(FromBytes, Immutable, KnownLayout)]
#[repr(C)]
struct Frame {
count: u64,
body: [u8],
}
#[derive(FromBytes, Immutable, KnownLayout)]
#[repr(C)]
struct OddHeader {
count: u32,
marker: u8,
body: [u16],
}
#[repr(align(8))]
struct Aligned<const N: usize>([u8; N]);
fn main() {
let mut bytes = Aligned([0u8; 32]);
bytes.0[8..16].copy_from_slice(b"APADDING");
let direct = Frame::ref_from_bytes_with_elems(&bytes.0[..16], 1).unwrap();
let whole = Ref::<_, Frame>::from_bytes_with_elems(&bytes.0[..16], 1).unwrap();
let (prefix, rest) = Ref::<_, Frame>::from_prefix_with_elems(&bytes.0[..], 1).unwrap();
let (before, suffix) = Ref::<_, Frame>::from_suffix_with_elems(&bytes.0[..24], 1).unwrap();
println!("Frame count=1; FromBytes body.len()={}", direct.body.len());
println!("Frame count=1; Ref whole body.len()={}", whole.body.len());
println!(
"Frame count=1; Ref prefix body.len()={}, remainder={}",
prefix.body.len(),
rest.len()
);
println!(
"Frame count=1; Ref suffix body.len()={}, prefix={}",
suffix.body.len(),
before.len()
);
println!(
"Frame count=1; direct bytes={:?}; Ref bytes={:?}",
&direct.body, &whole.body
);
let empty = Ref::<_, OddHeader>::from_bytes_with_elems(&bytes.0[..8], 0).unwrap();
println!("OddHeader count=0; Ref body.len()={}", empty.body.len());
assert_eq!(direct.body.len(), 1);
assert_eq!(whole.body.len(), 8);
assert_eq!(prefix.body.len(), 8);
assert_eq!(suffix.body.len(), 8);
assert_eq!(empty.body.len(), 1);
}
Run:
cargo +stable generate-lockfile
cargo +stable run --locked --release --bin ref_count
Observed output:
Frame count=1; FromBytes body.len()=1
Frame count=1; Ref whole body.len()=8
Frame count=1; Ref prefix body.len()=8, remainder=16
Frame count=1; Ref suffix body.len()=8, prefix=8
Frame count=1; direct bytes=[65]; Ref bytes=[65, 80, 65, 68, 68, 73, 78, 71]
OddHeader count=0; Ref body.len()=1
The direct FromBytes result contains A. The Ref result contains APADDING, despite both constructors being given count 1. The assertions deliberately confirm the current incorrect behavior; after a fix, the three Ref length assertions should expect 1 and the OddHeader assertion should expect 0.
Root cause
from_bytes_with_elems computes T::size_for_metadata(count) and checks the input length, then calls Self::from_bytes(source) without preserving count.
from_prefix_with_elems and from_suffix_with_elems likewise select the byte extent and delegate without the metadata.
Deref::deref reconstructs the reference with try_cast_into_no_leftover(..., None), allowing the layout code to select the largest element count fitting the bytes. With dynamic trailing padding, byte size does not uniquely determine the original count.
Preserve explicit metadata in the wrapper, or reject ambiguous counts if the representation cannot retain them. The direct FromBytes::*_with_elems APIs preserve the requested metadata and provide the passing comparison in this repro.
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
Ref::from_bytes_with_elems,from_prefix_with_elems, andfrom_suffix_with_elemscan return an object whose trailing slice has more elements than the explicitly requested count. These constructors reduce the count to a padded byte size and then discard it. Dereferencing the wrapper reconstructs the maximum count fitting that byte size.In the proof of concept, a request for one trailing byte returns eight through
Ref, while the corresponding directFromBytesAPI returns the requested one. A request for zero trailingu16elements similarly returns one element for another layout.Classification and threat model: This is a correctness bug. A parser using a validated length field as the count can accidentally process padding as additional elements. All additional bytes in this repro remain within the initialized input buffer; it does not demonstrate undefined behavior, an out-of-allocation read, or an application security bypass. Security consequences would require additional consumer behavior not established here.
Affected version: Reproduced using the published
zerocopy 0.8.56crate. Its release source contains the same affected code as audited revision2dad389b030e9268d6645ac0bf0626b867e96068, which the source references below use. Earlier releases have not been exhaustively tested.Environment: macOS arm64 (
aarch64-apple-darwin), Rust 1.98.1.Sketch of the attack
A
#[repr(C)]object with au64header and a trailing byte slice has eight-byte alignment. With one trailing byte its total padded size is sixteen bytes; with eight trailing bytes the size is also sixteen. The constructor initially computes the correct sixteen-byte extent, but the wrapper stores only that extent. Dereferencing it chooses eight trailing elements instead of the caller's one.Full repro
Save this as
Cargo.toml:The commands below select the latest stable Rust, validated here with Rust 1.98.1.
Save this as
src/bin/ref_count.rs:Run:
Observed output:
The direct
FromBytesresult containsA. TheRefresult containsAPADDING, despite both constructors being given count1. The assertions deliberately confirm the current incorrect behavior; after a fix, the threeReflength assertions should expect1and theOddHeaderassertion should expect0.Root cause
from_bytes_with_elemscomputesT::size_for_metadata(count)and checks the input length, then callsSelf::from_bytes(source)without preservingcount.from_prefix_with_elemsandfrom_suffix_with_elemslikewise select the byte extent and delegate without the metadata.Deref::derefreconstructs the reference withtry_cast_into_no_leftover(..., None), allowing the layout code to select the largest element count fitting the bytes. With dynamic trailing padding, byte size does not uniquely determine the original count.Preserve explicit metadata in the wrapper, or reject ambiguous counts if the representation cannot retain them. The direct
FromBytes::*_with_elemsAPIs preserve the requested metadata and provide the passing comparison in this repro.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.