rust-lang/rust · error

range should be nonempty

Error message

range should be nonempty

What it means

`.expect("range should be nonempty")` in `InitMask::prepare_copy` fires when `range_as_init_chunks(range)` yields zero chunks, i.e. the supplied `AllocRange` covers no bytes. `prepare_copy` builds a run-length encoding of initialization bits to replay during repeated `mem_copy_repeatedly` operations; an empty source has no first chunk to peek at, so the initialization state is undefined. Callers are contractually required to pass a nonempty range.

Source

Thrown at compiler/rustc_middle/src/mir/interpret/allocation/init_mask.rs:686

    /// `InitMask::range_as_init_chunks(...).collect::<Vec<_>>()`.
    pub fn prepare_copy(&self, range: AllocRange) -> InitCopy {
        // Since we are copying `size` bytes from `src` to `dest + i * size` (`for i in 0..repeat`),
        // a naive initialization mask copying algorithm would repeatedly have to read the initialization mask from
        // the source and write it to the destination. Even if we optimized the memory accesses,
        // we'd be doing all of this `repeat` times.
        // Therefore we precompute a compressed version of the initialization mask of the source value and
        // then write it back `repeat` times without computing any more information from the source.

        // A precomputed cache for ranges of initialized / uninitialized bits
        // 0000010010001110 will become
        // `[5, 1, 2, 1, 3, 3, 1]`,
        // where each element toggles the state.

        let mut ranges = smallvec::SmallVec::<[u64; 1]>::new();

        let mut chunks = self.range_as_init_chunks(range).peekable();

        let initial = chunks.peek().expect("range should be nonempty").is_init();

        // Here we rely on `range_as_init_chunks` to yield alternating init/uninit chunks.
        for chunk in chunks {
            let len = chunk.range().end.bytes() - chunk.range().start.bytes();
            ranges.push(len);
        }

        InitCopy { ranges, initial }
    }

    /// Applies multiple instances of the run-length encoding to the initialization mask.
    pub fn apply_copy(&mut self, defined: InitCopy, range: AllocRange, repeat: u64) {
        // An optimization where we can just overwrite an entire range of initialization bits if
        // they are going to be uniformly `1` or `0`. If this happens to be a full-range overwrite,
        // we won't need materialized blocks either.
        if defined.ranges.len() <= 1 {
            let start = range.start;
            let end = range.start + range.size * repeat; // `Size` operations

View on GitHub (pinned to 22057b88b0)

Solutions

  1. Guard the caller: skip `prepare_copy` (and `apply_copy`) when `range.size.bytes() == 0`, since copying zero bytes is a no-op.
  2. Audit `mem_copy_repeatedly` and its callers to ensure `size > 0` is enforced before precomputing the init mask.
  3. Reproduce under Miri with `MIRIFLAGS=-Zmiri-track-raw-pointers` to find the producing instruction, then fix the upstream size computation.
  4. Add a debug_assert at the caller boundary documenting the nonempty precondition.

Example fix

// before
let copy = mask.prepare_copy(AllocRange::from(start..end)); // end == start → panic

// after
if end > start {
    let copy = mask.prepare_copy(AllocRange::from(start..end));
    mask.apply_copy(copy, dest_range, repeat);
}
Defensive patterns

Strategy: validation

Validate before calling

// prepare_copy panics when the requested range is empty (no init chunks).
// Reject zero-length / backwards ranges at the boundary.
fn nonempty_range(range: AllocRange) -> Option<AllocRange> {
    if range.start.bytes() < range.end.bytes() {
        Some(range)
    } else {
        None
    }
}

let copy = match nonempty_range(range) {
    Some(r) => mask.prepare_copy(r),
    None => InitCopy { ranges: Default::default(), initial: true }, // nothing to copy
};

Try / catch

let copy = std::panic::catch_unwind(|| mask.prepare_copy(range));
match copy {
    Ok(c) => /* use c */,
    Err(_) => /* treat as empty copy, log the offending range */,
}

Prevention

When it happens

Trigger: Calling `InitMask::prepare_copy(AllocRange { start, size: Size::ZERO })`, or any caller path that computes a copy range whose `end - start == 0` bytes (e.g. a zero-sized memcpy/repeat with `repeat > 0` but `size == 0`, or an off-by-one that produced an empty sub-range).

Common situations: Const-eval / Miri bugs handling ZST copies or zero-sized array initializers; MIR optimizations that fold a copy down to zero bytes but still route it through `prepare_copy`; custom allocators in interpreted code returning size-0 blocks; regressions after changes to `mem_copy_repeatedly` repeat/size handling.

Related errors


AI-assisted analysis of rust-lang/rust@22057b88b0 (2026-08-03). Data as JSON: /data/errors/4c442eb1c995916a.json. Report an issue: GitHub.