Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves the block-reading path by fixing aligned-buffer allocation safety, clarifying block-device handle naming, and deduplicating repeated zero-fill logic used when reading holes/unwritten extents.
Changes:
- Replace the CLI’s unsafe aligned buffer allocation with an
AlignedBufferwrapper that deallocates using the originalLayout. - Rename
CachedDevicetoBlockDeviceacross the cache + reader code to better reflect semantics. - Extract a
zero_fill_rangehelper inReadContextto consolidate repeated zero-fill + state update logic.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/reader.rs |
Introduces zero_fill_range and applies it to hole/unwritten/trailing-hole filling; updates device handle types to BlockDevice. |
src/cache.rs |
Renames cached device entry type to BlockDevice and updates cache and open helpers accordingly. |
src/bin/blkreader.rs |
Adds AlignedBuffer for aligned direct-I/O buffers and removes the prior Vec::from_raw_parts allocation helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let len = (fill_end - *current_offset) as usize; | ||
| buf[*bytes_read..*bytes_read + len].fill(0); | ||
| *bytes_read += len; |
There was a problem hiding this comment.
zero_fill_range assumes fill_end >= *current_offset and that *bytes_read + len <= buf.len(). If either invariant is violated (e.g., unexpected/unsorted FIEMAP extents or logic changes), this will underflow and/or panic due to an out-of-bounds slice. Consider using checked_sub for len, validating the target range against buf.len(), and returning an io::Error (or at least early-return) instead of panicking via slice indexing.
| let len = (fill_end - *current_offset) as usize; | |
| buf[*bytes_read..*bytes_read + len].fill(0); | |
| *bytes_read += len; | |
| let len = match fill_end | |
| .checked_sub(*current_offset) | |
| .and_then(|delta| usize::try_from(delta).ok()) | |
| { | |
| Some(len) => len, | |
| None => return, | |
| }; | |
| let end = match bytes_read.checked_add(len) { | |
| Some(end) => end, | |
| None => return, | |
| }; | |
| let Some(fill_range) = buf.get_mut(*bytes_read..end) else { | |
| return; | |
| }; | |
| fill_range.fill(0); | |
| *bytes_read = end; |
| /// Zero-fill a range in the buffer from current_offset to fill_end, | ||
| /// updating bytes_read and current_offset. | ||
| fn zero_fill_range( | ||
| buf: &mut [u8], | ||
| bytes_read: &mut usize, | ||
| current_offset: &mut u64, | ||
| fill_end: u64, | ||
| ) { | ||
| let len = (fill_end - *current_offset) as usize; | ||
| buf[*bytes_read..*bytes_read + len].fill(0); | ||
| *bytes_read += len; | ||
| *current_offset = fill_end; | ||
| } |
There was a problem hiding this comment.
The new hole-filling helper changes behavior but there are no tests covering hole/unwritten/trailing-hole zero-fill paths in read_from_device. Adding unit tests for zero_fill_range (including boundary conditions like filling exactly to the end of buf) and at least one integration-style test that exercises read_from_device zero-fill behavior would help prevent regressions.
| fn new(size: usize, align: usize) -> Self { | ||
| let layout = Layout::from_size_align(size, align).expect("invalid alignment"); | ||
| let ptr = unsafe { alloc_zeroed(layout) }; | ||
| if ptr.is_null() { | ||
| handle_alloc_error(layout); | ||
| } | ||
| Self { ptr, size, layout } |
There was a problem hiding this comment.
AlignedBuffer::new will panic on invalid align (0 or non-power-of-two) due to Layout::from_size_align(...).expect(...), and later code also assumes power-of-two alignment in align_down/align_up. Since alignment is user-provided CLI input, validate it up-front (non-zero, power-of-two, and not larger than the allocated buffer/chunk size) and return a structured io::Error/clap validation error instead of panicking.
| fn new(size: usize, align: usize) -> Self { | |
| let layout = Layout::from_size_align(size, align).expect("invalid alignment"); | |
| let ptr = unsafe { alloc_zeroed(layout) }; | |
| if ptr.is_null() { | |
| handle_alloc_error(layout); | |
| } | |
| Self { ptr, size, layout } | |
| fn validate_alignment(size: usize, align: usize) -> io::Result<()> { | |
| if align == 0 { | |
| return Err(io::Error::new( | |
| io::ErrorKind::InvalidInput, | |
| "alignment must be non-zero", | |
| )); | |
| } | |
| if !align.is_power_of_two() { | |
| return Err(io::Error::new( | |
| io::ErrorKind::InvalidInput, | |
| "alignment must be a power of two", | |
| )); | |
| } | |
| if align > size { | |
| return Err(io::Error::new( | |
| io::ErrorKind::InvalidInput, | |
| "alignment must not be larger than the buffer size", | |
| )); | |
| } | |
| Ok(()) | |
| } | |
| fn new(size: usize, align: usize) -> io::Result<Self> { | |
| Self::validate_alignment(size, align)?; | |
| let layout = Layout::from_size_align(size, align).map_err(|_| { | |
| io::Error::new(io::ErrorKind::InvalidInput, "invalid size/alignment layout") | |
| })?; | |
| let ptr = unsafe { alloc_zeroed(layout) }; | |
| if ptr.is_null() { | |
| handle_alloc_error(layout); | |
| } | |
| Ok(Self { ptr, size, layout }) |
Summary
CachedDevicetoBlockDevicefor clarityzero_fill_rangehelper to deduplicate hole-filling logicChanges
src/bin/blkreader.rs: Replace unsafealloc_aligned_buffer(which had UB on drop) withAlignedBufferstruct that tracks the allocationLayoutfor correct deallocationsrc/cache.rs+src/reader.rs: RenameCachedDevicetoBlockDevice— eliminates semantic confusion where uncached handles also used the "Cached" type namesrc/reader.rs: Extractzero_fill_rangehelper to eliminate repeated zero-fill + state-update code inread_from_device(4 call sites unified)