Skip to content

Undefined Behavior in with_size_unzeroed - Reading Uninitialized Memory #10

@biryukovmaxim

Description

@biryukovmaxim

Summary

The with_size_unzeroed function creates a ByteView with uninitialized memory, but accessing this memory through indexing (bytes[0]) causes undefined behavior as detected by Miri.

Reproduction

#[test]
fn test_with_size_unzeroed() {
    let bytes = ByteView::with_size_unzeroed(INLINE_SIZE + 1);
    let b = bytes[0]; // UB: reading uninitialized memory
    std::hint::black_box(b);
}

Miri Output

error: Undefined Behavior: reading memory at alloc106017[0x8..0x9], but memory is uninitialized at [0x8..0x9], and this operation requires initialized memory
    --> src/byteview.rs:1299:17
     |
1299 |         let b = bytes[0];
     |                 ^^^^^^^^ Undefined Behavior occurred here

Command used: cargo +nightly miri test -- test_with_size_unzeroed

Root Cause

Reading from uninitialized memory is undefined behavior in Rust, even if the memory was properly allocated. While creating a slice from uninitialized memory is technically safe, any operation that reads the actual bytes (indexing, iteration, copying, etc.) triggers UB.

Impact

  • Any code that reads from a ByteView created with with_size_unzeroed before writing to it is unsound
  • This makes the current API footgun-prone and potentially unsafe
  • Miri will catch this in tests, but release builds may exhibit unpredictable behavior

Proposed Solutions

Option 1: Remove from Public API (Breaking Change)

Remove with_size_unzeroed entirely from the public API if uninitialized memory access is not a core use case.

// Remove or make private
// pub fn with_size_unzeroed(size: usize) -> Self

Option 2: Add Initialization Flag to Header (Non-breaking)

Extend the internal header/metadata to track whether memory is initialized, preventing accidental reads from uninitialized regions.

struct ByteViewHeader {
    // existing fields...
    is_initialized: bool,
}

impl ByteView {
    pub fn with_size_unzeroed(size: usize) -> Self {
        // Create with is_initialized = false
    }
    
    // Add bounds checking in Index impl
    fn index(&self, index: usize) -> &u8 {
        if !self.header().is_initialized {
            panic!("Cannot read from uninitialized ByteView");
        }
        // existing indexing logic...
    }
}

Option 3: Use MaybeUninit-based API (Breaking Change)

Replace with_size_unzeroed with a MaybeUninit<u8>-based API:

impl ByteView {
    pub fn with_size_uninit(size: usize) -> ByteViewUninit {
        // Return a wrapper that only allows safe initialization
    }
}

pub struct ByteViewUninit {
    // Internal implementation
}

impl ByteViewUninit {
    pub fn write(&mut self, index: usize, value: u8) { /* ... */ }
    pub fn assume_init(self) -> ByteView { /* ... */ }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions