In these manual implementations, the code unconditionally accesses union variants regardless of which variant was initialized. For example, in __c_anonymous_ptrace_syscall_info_data (spanning 64 bytes):
Full Gemini Unsafe Code Audit Report
Unsafe Rust Review: libc (v0_2)
Overall Safety Assessment
The libc crate (v0_2, audited at version 0.2.183) is the foundational
Foreign Function Interface (FFI) binding library for the Rust ecosystem. It
defines cross-platform C types, constants, struct layouts, unions, and extern "C" function signatures corresponding to standard C libraries and system call
ABIs across dozens of target operating systems and architectures.
The explicit unsafe surface area of libc is immense in raw line count, but
structurally monolithic in nature:
- Almost the entire codebase consists of external C function declarations
inside extern blocks. Prior to Rust Edition 2024, these declare unsafe fn items by default.
- Target data structures are defined with explicit C layout representations
(#[repr(C)], #[repr(packed)], #[repr(align(...))], and
#[repr(transparent)]) via declarative macro systems (s!,
s_no_extra_traits!, s_paren!, c_enum!, f!, and safe_f!). Notably,
the s! macro soundly prevents deriving PartialEq, Eq, and Hash on C
union types by triggering an explicit compile_error!.
- Executable Rust code within the crate is limited to macro-generated accessor
methods on complex kernel structures (e.g. siginfo_t), polyfills for
platform compatibility (e.g. unix/solarish/compat.rs), CPU affinity
bitmask operations (CPU_COUNT_S, etc.), and manual trait implementations
under the optional extra_traits feature flag.
Architecturally, the crate adheres to strict FFI compatibility principles.
However, rigorous security auditing under unsafe-rust-review-joshlf
proof-obligation standards reveals 2 Critical findings related to Undefined
Behavior (UB) when implementing traits on C union types and accessing flexible
array members under the extra_traits feature flag. Additionally, several
Fishy findings were identified regarding undocumented FFI hazards in
platform polyfills, accompanied by systematic omissions of // SAFETY: comments
and # Safety contracts on executable Rust code.
Critical Findings
1. Undefined Behavior reading uninitialized union memory in manual PartialEq and Hash implementations under extra_traits 🔴 🤦
- Severity: 🔴 High
- Threat Vector: 🤦 Accidental Misuse
- Bug Type: Uninitialized Memory Read
Locations:
src/unix/linux_like/linux/gnu/mod.rs:467-485
(__c_anonymous_ptrace_syscall_info_data)
src/unix/bsd/apple/mod.rs:1675-1685 (semun)
src/unix/bsd/apple/mod.rs:1712-1765 (__c_anonymous_ifk_data,
__c_anonymous_ifr_ifru)
src/vxworks/mod.rs:619-657 (sa_u_t, sigval)
src/unix/solarish/mod.rs:626-660 (pad128_t, upad128_t)
src/unix/bsd/freebsdlike/freebsd/mod.rs:1791-1830 (__c_anonymous_cr_pid,
__c_anonymous_elf32_auxv_union)
Description: Standard Rust derive macros reject union types because
unions overlay multiple fields of different types and sizes across the same
memory address. The libc authors correctly enforced this restriction in the
standard s! macro (compile_error!("unions cannot derive extra traits...")).
However, for several platform-specific C unions defined via
s_no_extra_traits!, the authors manually implemented PartialEq and Hash
when #[cfg(feature = "extra_traits")] is enabled.
In these manual implementations, the code unconditionally accesses union
variants. For example, in __c_anonymous_ptrace_syscall_info_data (spanning 64
bytes):
impl PartialEq for __c_anonymous_ptrace_syscall_info_data {
fn eq(&self, other: &__c_anonymous_ptrace_syscall_info_data) -> bool {
unsafe {
self.entry == other.entry
|| self.exit == other.exit
|| self.seccomp == other.seccomp
}
}
}
When an instance is initialized in Rust using the smaller exit variant
(occupying 16 bytes), the remaining 48 bytes of the union remain uninitialized
memory. When PartialEq::eq evaluates self.entry == other.entry (which spans
56 bytes), it reads 40 bytes of uninitialized union padding as u64 integers.
According to authoritative Rust validity rules (Rust Reference § Behavior
considered undefined) and verified by the Miri execution model, reading
uninitialized memory as an initialized integer, floating-point number, or
non-null pointer type is immediate Undefined Behavior.
Coherence & Law Violations: Furthermore, these manual implementations
violate standard PartialEq and Hash trait invariants (coherence theorem):
- In
ptrace_syscall_info_data, if two instances have identical entry
fields but uninitialized/differing memory in their exit variants, a == b
evaluates to true. However, Hash::hash unconditionally hashes all three
fields (self.entry.hash(state); self.exit.hash(state); self.seccomp.hash(state)), resulting in hash(a) != hash(b). This breaks
HashMap/HashSet invariants.
- In
__c_anonymous_elf32_auxv_union on FreeBSD (freebsd/mod.rs:1827),
PartialEq is implemented, but Hash::hash unconditionally panics with
unimplemented!("traits").
2. Out-of-bounds slice reads in flexible array member idiom af_alg_iv 🔴 🤦
- Severity: 🔴 High
- Threat Vector: 🤦 Accidental Misuse
- Bug Type: Out-of-Bounds Read
Locations:
src/unix/linux_like/linux/mod.rs:1287-1295
src/unix/linux_like/linux/mod.rs:1389-1410
Description: The Linux kernel AF_ALG socket initialization vector structure
is defined as:
pub struct af_alg_iv {
pub ivlen: u32,
pub iv: [c_uchar; 0],
}
Here, iv is a C flexible array member idiom ([u8; 0]). The helper method
as_slice() is implemented as:
impl af_alg_iv {
fn as_slice(&self) -> &[u8] {
unsafe { ::core::slice::from_raw_parts(self.iv.as_ptr(), self.ivlen as usize) }
}
}
When extra_traits is enabled, PartialEq::eq and Hash::hash unconditionally
invoke self.as_slice(). If a user allocates a standalone af_alg_iv instance
on the stack or heap without consecutive trailing buffer allocation (e.g. let iv = af_alg_iv { ivlen: 32, iv: [] };), the allocated object size is exactly 4
bytes. Invoking as_slice() (directly or via == / hash()) constructs a
32-byte slice reference extending past the allocated memory bounds into
arbitrary memory. Reading this slice is immediate Undefined Behavior.
Although the crate authors deprecated af_alg_iv in version 0.2.80 with the
explicit warning "WARNING: The PartialEq, Eq and Hash implementations of
this type are unsound and will be removed in the future", the unsound trait
implementations remain publicly exposed under extra_traits.
Fishy Findings
1. Public unsafe FFI polyfills exported without # Safety contracts 🟡 🤦
- Severity: 🟡 Low
- Threat Vector: 🤦 Accidental Misuse
- Bug Type: Undocumented Safety Contract
Location: src/unix/solarish/compat.rs:12, 37, 59, 133, 184, 204 Unlike raw
extern "C" declarations where # Safety documentation is omitted by ecosystem
convention, compat.rs implements executable Rust polyfills (cfmakeraw,
cfsetspeed, openpty, forkpty, getpwent_r, getgrent_r) marked pub unsafe fn. These functions perform raw pointer dereferencing, system ioctls,
and unchecked buffer copies (strcpy). None of these public exports document a
# Safety section detailing caller preconditions (such as buffer allocation
capacities and non-null requirements).
Notably, comments inside openpty (compat.rs:120) explicitly warn of a
classic FFI buffer overflow vulnerability: "Note that this is a terrible
interface: there appears to be no standard upper bound on the copy length for
this pointer". Yet no proof obligation contract is specified for callers
passing name.
2. Inconsistent safety classification of errno() retrieval 🟡 🤦
- Severity: 🟡 Low
- Threat Vector: 🤦 Accidental Misuse
- Bug Type: API Inconsistency
Locations: src/teeos/mod.rs:1333 vs src/new/qurt/errno.rs:148 In
teeos/mod.rs, errno() is exported as a safe function (pub fn errno() -> c_int), whereas in new/qurt/errno.rs, it is exported as pub unsafe fn errno() -> c_int. Retrieving the current thread's error number via
*__errno_location() has no safety preconditions in standard C runtime
implementations and is universally sound to call from safe code. Marking it
unsafe fn on QURT creates unnecessary caller proof obligations.
Missing Safety Comments
Executable Rust code across libc systematically omits // SAFETY: comments
inside unsafe blocks and unsafe fn bodies. The following enumerate
representative sites along with rigorous proposed proof obligations:
1. src/macros.rs:454 (offset_of! macro) 🟠
(Note: This site correctly includes a safety comment, serving as a positive
architectural baseline):
// SAFETY: computed address is inbounds since we have a stack alloc for T
let fptr = unsafe { core::ptr::addr_of!((*ptr).$field) };
2. src/unix/linux_like/mod.rs:1790 & 1794 (SIGRTMAX and SIGRTMIN) 🔴
safe_f! {
pub fn SIGRTMAX() -> c_int {
// SAFETY: `__libc_current_sigrtmax()` queries runtime real-time signal bounds from libc. It has no caller safety preconditions (AXIOM: POSIX signal specification).
unsafe { __libc_current_sigrtmax() }
}
3. src/teeos/mod.rs:1334 (errno) 🔴
pub fn errno() -> c_int {
// SAFETY: `__errno_location()` returns a valid, non-null, thread-local pointer to `c_int` (AXIOM: C standard library contract). Dereferencing it for reads is always valid on the active thread.
unsafe { *__errno_location() }
}
4. src/unix/linux_like/linux/mod.rs:1391 (af_alg_iv::as_slice) 🔴
(Assuming sound allocation bounds precondition):
impl af_alg_iv {
fn as_slice(&self) -> &[u8] {
// SAFETY: `self.iv.as_ptr()` points to the flexible array member trailing `self`. Assuming caller allocated at least `self.ivlen` consecutive bytes after the struct header, the memory range `[as_ptr(), ivlen]` is in-bounds, aligned, and initialized.
unsafe { ::core::slice::from_raw_parts(self.iv.as_ptr(), self.ivlen as usize) }
}
}
5. src/unix/linux_like/linux/gnu/mod.rs:389, 402, 433 (siginfo_t accessors) 🔴
pub unsafe fn si_addr(&self) -> *mut c_void {
// ... struct definitions ...
// SAFETY: `self` is a valid reference to `siginfo_t`. Casting `self` to layout-compatible union overlay structs (`siginfo_sigfault`, `siginfo_timer`, etc.) and accessing fields is valid provided the caller verified `si_signo` and `si_code` correspond to this active union variant.
(*(self as *const siginfo_t).cast::<siginfo_sigfault>()).si_addr
}
6. src/wasi/mod.rs:66-67 (clockid_t Send and Sync) 🔴
// SAFETY: `clockid_t` wraps a raw pointer `*const u8` acting as an opaque, immutable WASI clock identifier handle. Clock handles are thread-safe constants managed by the WASI host environment, so transferring or sharing them across threads is sound.
unsafe impl Send for clockid_t {}
// SAFETY: See Send implementation.
unsafe impl Sync for clockid_t {}
7. src/unix/solarish/compat.rs:59 (openpty FFI polyfill export) 🔴
/// # Safety
/// - `amain` and `asubord` must be valid, aligned pointers valid for writes of `c_int`.
/// - If `name` is non-null, it must point to a buffer large enough to contain the null-terminated PTS device path (typically `PATH_MAX` bytes) without overflowing.
/// - `termp` and `winp` may be null or must point to valid initialized instances of `termios` and `winsize`.
pub unsafe fn openpty( ... ) -> c_int { ... }
Note
This finding was identified during an agentic unsafe Rust code review performed by Gemini AI, followed by human review and verification.
The Issue
Standard Rust
derivemacros rejectuniontypes because unions overlay multiple fields of different types and sizes across the same memory address. This crate correctly enforces this restriction in the standards!macro (compile_error!("unions cannot derive extra traits...")). However, for several platform-specific C unions defined vias_no_extra_traits!,PartialEqandHashare manually implemented when#[cfg(feature = "extra_traits")]is enabled.In these manual implementations, the code unconditionally accesses union variants regardless of which variant was initialized. For example, in
__c_anonymous_ptrace_syscall_info_data(spanning 64 bytes):libc/src/unix/linux_like/linux/gnu/mod.rs
Lines 467 to 485 in 5660e6f
When an instance is initialized in Rust using the smaller
exitvariant (occupying 16 bytes), the remaining 48 bytes of the union remain uninitialized memory. WhenPartialEq::eqevaluatesself.entry == other.entry(which spans 56 bytes), it reads 40 bytes of uninitialized union padding asu64integers.Other affected union types across
libcinclude:src/unix/bsd/apple/mod.rs(semun,__c_anonymous_ifk_data,__c_anonymous_ifr_ifru)src/vxworks/mod.rs(sa_u_t,sigval)src/unix/solarish/mod.rs(pad128_t,upad128_t)src/unix/bsd/freebsdlike/freebsd/mod.rs(__c_anonymous_cr_pid,__c_anonymous_elf32_auxv_union)Furthermore, these manual implementations violate standard
PartialEqandHashtrait coherence invariants: if two instances have identicalentryfields but differing uninitialized memory in theirexitvariants,a == bevaluates totrue, butHash::hashunconditionally hashes all fields, resulting inhash(a) != hash(b).Minimal Reproduction (Miri)
Suggested Fix
Unions in C/C++ FFI bindings cannot soundly implement
PartialEq,Eq, orHashbecause there is no runtime tag indicating which union variant iscurrently active.
We suggest removing manual
PartialEq,Eq, andHashimplementations for alluniontypes defined vias_no_extra_traits!, ensuring they behave identically to unions defined vias!where deriving extra traits is explicitly prohibited.Note
The full audit report below also contains additional minor findings (such as missing safety comments or undocumented FFI assumptions) that are probably worth fixing as well but not the primary goal of this issue.
Full Gemini Unsafe Code Audit Report
Unsafe Rust Review:
libc(v0_2)Overall Safety Assessment
The
libccrate (v0_2, audited at version0.2.183) is the foundationalForeign Function Interface (FFI) binding library for the Rust ecosystem. It
defines cross-platform C types, constants, struct layouts, unions, and
extern "C"function signatures corresponding to standard C libraries and system callABIs across dozens of target operating systems and architectures.
The explicit
unsafesurface area oflibcis immense in raw line count, butstructurally monolithic in nature:
inside
externblocks. Prior to Rust Edition 2024, these declareunsafe fnitems by default.(
#[repr(C)],#[repr(packed)],#[repr(align(...))], and#[repr(transparent)]) via declarative macro systems (s!,s_no_extra_traits!,s_paren!,c_enum!,f!, andsafe_f!). Notably,the
s!macro soundly prevents derivingPartialEq,Eq, andHashon Cuniontypes by triggering an explicitcompile_error!.methods on complex kernel structures (e.g.
siginfo_t), polyfills forplatform compatibility (e.g.
unix/solarish/compat.rs), CPU affinitybitmask operations (
CPU_COUNT_S, etc.), and manual trait implementationsunder the optional
extra_traitsfeature flag.Architecturally, the crate adheres to strict FFI compatibility principles.
However, rigorous security auditing under
unsafe-rust-review-joshlfproof-obligation standards reveals 2 Critical findings related to Undefined
Behavior (UB) when implementing traits on C union types and accessing flexible
array members under the
extra_traitsfeature flag. Additionally, severalFishy findings were identified regarding undocumented FFI hazards in
platform polyfills, accompanied by systematic omissions of
// SAFETY:commentsand
# Safetycontracts on executable Rust code.Critical Findings
1. Undefined Behavior reading uninitialized union memory in manual
PartialEqandHashimplementations underextra_traits🔴 🤦Locations:
src/unix/linux_like/linux/gnu/mod.rs:467-485(
__c_anonymous_ptrace_syscall_info_data)src/unix/bsd/apple/mod.rs:1675-1685(semun)src/unix/bsd/apple/mod.rs:1712-1765(__c_anonymous_ifk_data,__c_anonymous_ifr_ifru)src/vxworks/mod.rs:619-657(sa_u_t,sigval)src/unix/solarish/mod.rs:626-660(pad128_t,upad128_t)src/unix/bsd/freebsdlike/freebsd/mod.rs:1791-1830(__c_anonymous_cr_pid,__c_anonymous_elf32_auxv_union)Description: Standard Rust
derivemacros rejectuniontypes becauseunions overlay multiple fields of different types and sizes across the same
memory address. The
libcauthors correctly enforced this restriction in thestandard
s!macro (compile_error!("unions cannot derive extra traits...")).However, for several platform-specific C unions defined via
s_no_extra_traits!, the authors manually implementedPartialEqandHashwhen
#[cfg(feature = "extra_traits")]is enabled.In these manual implementations, the code unconditionally accesses union
variants. For example, in
__c_anonymous_ptrace_syscall_info_data(spanning 64bytes):
When an instance is initialized in Rust using the smaller
exitvariant(occupying 16 bytes), the remaining 48 bytes of the union remain uninitialized
memory. When
PartialEq::eqevaluatesself.entry == other.entry(which spans56 bytes), it reads 40 bytes of uninitialized union padding as
u64integers.According to authoritative Rust validity rules (Rust Reference § Behavior
considered undefined) and verified by the Miri execution model, reading
uninitialized memory as an initialized integer, floating-point number, or
non-null pointer type is immediate Undefined Behavior.
Coherence & Law Violations: Furthermore, these manual implementations
violate standard
PartialEqandHashtrait invariants (coherence theorem):ptrace_syscall_info_data, if two instances have identicalentryfields but uninitialized/differing memory in their
exitvariants,a == bevaluates to
true. However,Hash::hashunconditionally hashes all threefields (
self.entry.hash(state); self.exit.hash(state); self.seccomp.hash(state)), resulting inhash(a) != hash(b). This breaksHashMap/HashSetinvariants.__c_anonymous_elf32_auxv_unionon FreeBSD (freebsd/mod.rs:1827),PartialEqis implemented, butHash::hashunconditionally panics withunimplemented!("traits").2. Out-of-bounds slice reads in flexible array member idiom
af_alg_iv🔴 🤦Locations:
src/unix/linux_like/linux/mod.rs:1287-1295src/unix/linux_like/linux/mod.rs:1389-1410Description: The Linux kernel AF_ALG socket initialization vector structure
is defined as:
Here,
ivis a C flexible array member idiom ([u8; 0]). The helper methodas_slice()is implemented as:When
extra_traitsis enabled,PartialEq::eqandHash::hashunconditionallyinvoke
self.as_slice(). If a user allocates a standaloneaf_alg_ivinstanceon the stack or heap without consecutive trailing buffer allocation (e.g.
let iv = af_alg_iv { ivlen: 32, iv: [] };), the allocated object size is exactly 4bytes. Invoking
as_slice()(directly or via==/hash()) constructs a32-byte slice reference extending past the allocated memory bounds into
arbitrary memory. Reading this slice is immediate Undefined Behavior.
Although the crate authors deprecated
af_alg_ivin version0.2.80with theexplicit warning "WARNING: The
PartialEq,EqandHashimplementations ofthis type are unsound and will be removed in the future", the unsound trait
implementations remain publicly exposed under
extra_traits.Fishy Findings
1. Public unsafe FFI polyfills exported without
# Safetycontracts 🟡 🤦Location:
src/unix/solarish/compat.rs:12, 37, 59, 133, 184, 204Unlike rawextern "C"declarations where# Safetydocumentation is omitted by ecosystemconvention,
compat.rsimplements executable Rust polyfills (cfmakeraw,cfsetspeed,openpty,forkpty,getpwent_r,getgrent_r) markedpub unsafe fn. These functions perform raw pointer dereferencing, system ioctls,and unchecked buffer copies (
strcpy). None of these public exports document a# Safetysection detailing caller preconditions (such as buffer allocationcapacities and non-null requirements).
Notably, comments inside
openpty(compat.rs:120) explicitly warn of aclassic FFI buffer overflow vulnerability: "Note that this is a terrible
interface: there appears to be no standard upper bound on the copy length for
this pointer". Yet no proof obligation contract is specified for callers
passing
name.2. Inconsistent safety classification of
errno()retrieval 🟡 🤦Locations:
src/teeos/mod.rs:1333vssrc/new/qurt/errno.rs:148Inteeos/mod.rs,errno()is exported as a safe function (pub fn errno() -> c_int), whereas innew/qurt/errno.rs, it is exported aspub unsafe fn errno() -> c_int. Retrieving the current thread's error number via*__errno_location()has no safety preconditions in standard C runtimeimplementations and is universally sound to call from safe code. Marking it
unsafe fnon QURT creates unnecessary caller proof obligations.Missing Safety Comments
Executable Rust code across
libcsystematically omits// SAFETY:commentsinside
unsafeblocks andunsafe fnbodies. The following enumeraterepresentative sites along with rigorous proposed proof obligations:
1.
src/macros.rs:454(offset_of!macro) 🟠(Note: This site correctly includes a safety comment, serving as a positive
architectural baseline):
2.
src/unix/linux_like/mod.rs:1790&1794(SIGRTMAXandSIGRTMIN) 🔴3.
src/teeos/mod.rs:1334(errno) 🔴4.
src/unix/linux_like/linux/mod.rs:1391(af_alg_iv::as_slice) 🔴(Assuming sound allocation bounds precondition):
5.
src/unix/linux_like/linux/gnu/mod.rs:389,402,433(siginfo_taccessors) 🔴6.
src/wasi/mod.rs:66-67(clockid_tSend and Sync) 🔴7.
src/unix/solarish/compat.rs:59(openptyFFI polyfill export) 🔴