Skip to content

Undefined Behavior in Bitmap<SIZE>::try_from(&[u8]) Implementation #35

@shinmao

Description

@shinmao

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:

bitmaps/src/bitmap.rs

Lines 137 to 148 in b449cec

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

  1. When SIZE = 1, the backing store type <BitsImpl<1> as Bits>::Store is bool
  2. The bool type in Rust has strict validity requirements: only bit patterns 0x00 and 0x01 are valid
  3. Calling assume_init() on a MaybeUninit<bool> containing any other value (e.g., 0x02) is immediate undefined behavior, even before the value is used
  4. 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

  1. 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
  2. Conditional validation: For types with validity constraints (like bool), the compiler generates efficient validation code (typically a single comparison)
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions