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` operationsView on GitHub (pinned to 22057b88b0)
Solutions
- Guard the caller: skip `prepare_copy` (and `apply_copy`) when `range.size.bytes() == 0`, since copying zero bytes is a no-op.
- Audit `mem_copy_repeatedly` and its callers to ensure `size > 0` is enforced before precomputing the init mask.
- Reproduce under Miri with `MIRIFLAGS=-Zmiri-track-raw-pointers` to find the producing instruction, then fix the upstream size computation.
- 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
- Guard every allocation-range operation with a start < end check; the init-mask code assumes nonempty ranges throughout.
- Track byte sizes as Size/Bytes newtypes and reject zero at construction rather than at use.
- When copying between allocations, short-circuit on equal start/end before touching the mask.
- In interpreters/translators that build ranges programmatically, assert non-emptiness once at the source rather than at each sink.
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
- got a pointer where a ScalarInt was expected
- there must be provenance somewhere here
- an interpreter error got improperly discarded; use `discard_
- statics should not have generic parameters
- destructed mir constant of adt without variant idx
AI-assisted analysis of rust-lang/rust@22057b88b0 (2026-08-03).
Data as JSON: /data/errors/4c442eb1c995916a.json.
Report an issue: GitHub.