Summary
The TryFrom<&[u8]> implementation for Bitmap<SIZE> (lines 137-148 in src/bitmap.rs) contains a memory safety vulnerability that leads to undefined behavior when the backing store type has validity constraints (e.g., bool for SIZE=1). The function performs unchecked memory copying followed by assume_init(), which violates Rust's safety invariants.
Root Cause
The current implementation copies arbitrary byte data into MaybeUninit and immediately calls assume_init() without validating that the byte pattern is valid for the target type:
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> { |
|
if value.len() == size_of::<<BitsImpl<SIZE> as Bits>::Store>() { |
|
let mut data: MaybeUninit<<BitsImpl<SIZE> as Bits>::Store> = MaybeUninit::uninit(); |
|
let data_ptr: *mut u8 = data.as_mut_ptr().cast(); |
|
Ok(unsafe { |
|
data_ptr.copy_from_nonoverlapping(value.as_ptr(), value.len()); |
|
Self { |
|
data: data.assume_init(), |
|
} |
|
}) |
|
} else { |
|
Err(()) |
Why This Causes UB
- When
SIZE = 1, the backing store type <BitsImpl<1> as Bits>::Store is bool
- The
bool type in Rust has strict validity requirements: only bit patterns 0x00 and 0x01 are valid
- Calling
assume_init() on a MaybeUninit<bool> containing any other value (e.g., 0x02) is immediate undefined behavior, even before the value is used
- This violates the safety contract of
MaybeUninit:: assume_init(), which requires the memory to contain a valid instance of T
Proof of Concept
use bitmaps::Bitmap;
fn main() {
// This compiles and triggers UB at runtime
let _ = Bitmap::<1>::try_from(&[2u8][..]).unwrap();
// UB occurs during assume_init() due to invalid bool value
}
Verification with Miri:
error: Undefined Behavior: constructing invalid value: encountered 0x02, but expected a boolean
--> /home/usr/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bitmaps-3.2.1/src/bitmap.rs:144:27
|
144 | data: data.assume_init(),
| ^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
Impact
- Safe API exposing UB: This is a public safe function that allows users to trigger undefined behavior without using
unsafe blocks
- Compiler optimizations: UB can lead to unpredictable behavior due to compiler optimizations assuming valid values
- Potential for exploitation: Invalid values may lead to unexpected control flow or security vulnerabilities in downstream code
Proposed Fix (Zero Runtime Cost)
Replace the unchecked memory copy with read_unaligned followed by explicit validation:
fn try_from(value: &[u8]) -> Result<Self, Self:: Error> {
if value. len() == size_of::<<BitsImpl<SIZE> as Bits>::Store>() {
// Read the bytes as the target type (may contain invalid bit patterns)
let candidate = unsafe {
value.as_ptr()
.cast:: <<BitsImpl<SIZE> as Bits>::Store>()
.read_unaligned()
};
// Validate by round-tripping through as_bytes()
// This is optimized away by the compiler at -O2/-O3
let temp = Self { data: candidate };
if temp.as_bytes() == value {
Ok(temp)
} else {
Err(())
}
} else {
Err(())
}
}
Alternative approach (more explicit):
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
if value.len() != size_of::<<BitsImpl<SIZE> as Bits>::Store>() {
return Err(());
}
// Use from_ne_bytes if available, or similar deserialization
let data = <BitsImpl<SIZE> as Bits>::Store::from_bytes(value)
.ok_or(())?;
Ok(Self { data })
}
Why This Fix Has Zero Runtime Cost
- Compiler optimizations: At optimization levels
-O2 and above, the round-trip validation compiles down to a no-op for types like u8, u16, u32, etc. that have no invalid bit patterns
- Conditional validation: For types with validity constraints (like
bool), the compiler generates efficient validation code (typically a single comparison)
- The validation would be compiled away for most cases where the backing store is an integer type with no invalid bit patterns
Additional Notes
- The same issue potentially affects
AsMut<[u8]> (lines 117-129), which allows external code to write invalid bit patterns through the mutable slice reference
- Consider adding documentation warnings about the validity requirements when using these byte-level APIs
- The
as_bytes() and as_mut() methods expose raw memory representation, which should be used with caution
References
Summary
The
TryFrom<&[u8]>implementation forBitmap<SIZE>(lines 137-148 insrc/bitmap.rs) contains a memory safety vulnerability that leads to undefined behavior when the backing store type has validity constraints (e.g.,boolforSIZE=1). The function performs unchecked memory copying followed byassume_init(), which violates Rust's safety invariants.Root Cause
The current implementation copies arbitrary byte data into
MaybeUninitand immediately callsassume_init()without validating that the byte pattern is valid for the target type:bitmaps/src/bitmap.rs
Lines 137 to 148 in b449cec
Why This Causes UB
SIZE = 1, the backing store type<BitsImpl<1> as Bits>::Storeisboolbooltype in Rust has strict validity requirements: only bit patterns0x00and0x01are validassume_init()on aMaybeUninit<bool>containing any other value (e.g.,0x02) is immediate undefined behavior, even before the value is usedMaybeUninit:: assume_init(), which requires the memory to contain a valid instance ofTProof of Concept
Verification with Miri:
Impact
unsafeblocksProposed Fix (Zero Runtime Cost)
Replace the unchecked memory copy with
read_unalignedfollowed by explicit validation:Alternative approach (more explicit):
Why This Fix Has Zero Runtime Cost
-O2and above, the round-trip validation compiles down to a no-op for types likeu8,u16,u32, etc. that have no invalid bit patternsbool), the compiler generates efficient validation code (typically a single comparison)Additional Notes
AsMut<[u8]>(lines 117-129), which allows external code to write invalid bit patterns through the mutable slice referenceas_bytes()andas_mut()methods expose raw memory representation, which should be used with cautionReferences
bool