Unsound APIs (`Layout::new`, `Layout::contiguous_with_offset`) — unchecked `start_offset` leads to heap-buffer-overflow
Summary
Layout::new and Layout::contiguous_with_offset accept an arbitrary start_offset value without any validation. Downstream unsafe code in cpu_backend::utils::unary_map unconditionally trusts this offset via get_unchecked, resulting in out-of-bounds memory access. This is a soundness bug: undefined behavior is reachable without writing any unsafe code.
Confirmed on 0.11.0
Root Cause
The Layout type stores a start_offset: usize field that determines where element access begins within a backing storage buffer. The two public constructors accept any start_offset without validation:
// layout.rs:14 — accepts any usize
pub fn new(shape: Shape, stride: Vec<usize>, start_offset: usize) -> Self
// layout.rs:22 — same
pub fn contiguous_with_offset<S: Into<Shape>>(shape: S, start_offset: usize) -> SelfOnce a Layout is constructed with a valid start_offset, all derived operations (narrow, transpose, permute, broadcast_as) preserve the invariant that start_offset stays within bounds — narrow's start + len <= dims[dim] check guarantees the new offset cannot overflow given a valid initial state. The bug is therefore entirely in the constructors: they are the sole entry point for an invalid start_offset.
PoC
#[test]
fn poc_layout_unchecked_offset_oob() {
use candle_core::backend::BackendStorage;
use candle_core::cpu_backend::CpuStorage;
use candle_core::Layout;
// Step 1: Create a layout with start_offset = usize::MAX.
// Layout::contiguous_with_offset accepts this without any validation.
let layout = Layout::contiguous_with_offset(&[4usize, 3usize], usize::MAX);
// Step 2: narrow(dim=1, start=0, len=1)
// New offset: usize::MAX + stride[1] * 0 = usize::MAX (unchanged).
let layout = layout.narrow(1, 0, 1).unwrap();
// Step 3: Trigger OOB read via elu().
// unary_map → get_unchecked(usize::MAX + ...) → heap-buffer-overflow
let storage = CpuStorage::F32(vec![0.5f32; 20]);
let _result = storage.elu(&layout, 1.0);
}ASan output:
==540235==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7116f01e001c
SUMMARY: AddressSanitizer: heap-buffer-overflow
candle_core::cpu_backend::utils::unary_map::<f32, f32, ...elu::{closure#2}>Impact
- Heap-buffer-overflow : confirmed via ASan.
- Reachable from safe Rust through public API:
Layout::contiguous_with_offset→Layout::narrow→ any unary/binary storage operation (elu,relu,gelu,cos,sin, etc.).
Source: huggingface/candle