Skip to content

Commit eb172f4

Browse files
committed
fix(zstd): return decode error instead of panicking on corrupt view lengths
reconstruct_views read a u32 view-length prefix from ZSTD-decompressed (untrusted) data and used it as a slice bound without validation. A corrupt or fuzzed length produced an out-of-bounds slice panic ("range end index ... out of range for slice of length ...") in reconstruct_views. Make reconstruct_views return VortexResult: bounds-check both the length prefix read and the value slice (with a checked_add to avoid usize overflow), returning a decode error instead of panicking. Both zstd callers and the vortex-cuda caller propagate the error. Closes #8822
1 parent c5e075a commit eb172f4

2 files changed

Lines changed: 37 additions & 10 deletions

File tree

encodings/zstd/src/array.rs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ fn collect_valid_vbv(
475475
pub fn reconstruct_views(
476476
buffer: &ByteBuffer,
477477
max_buffer_len: usize,
478-
) -> (Vec<ByteBuffer>, Buffer<BinaryView>) {
478+
) -> VortexResult<(Vec<ByteBuffer>, Buffer<BinaryView>)> {
479479
let mut views = BufferMut::<BinaryView>::empty();
480480
let mut buffers = Vec::new();
481481
let mut segment_start: usize = 0;
@@ -485,7 +485,12 @@ pub fn reconstruct_views(
485485
let str_len = ViewLen::from_le_bytes(
486486
buffer
487487
.get(offset..offset + size_of::<ViewLen>())
488-
.vortex_expect("corrupted zstd length")
488+
.ok_or_else(|| {
489+
vortex_err!(
490+
"corrupted zstd data: truncated view length prefix at offset {offset} (buffer length {})",
491+
buffer.len()
492+
)
493+
})?
489494
.try_into()
490495
.ok()
491496
.vortex_expect("must fit ViewLen size"),
@@ -502,16 +507,27 @@ pub fn reconstruct_views(
502507
let local_offset = u32::try_from(value_data_offset - segment_start)
503508
.vortex_expect("local offset within segment must fit in u32");
504509
let buf_index = u32::try_from(buffers.len()).vortex_expect("buffer index must fit in u32");
505-
let value = &buffer[value_data_offset..value_data_offset + str_len];
510+
let value_end = value_data_offset.checked_add(str_len).ok_or_else(|| {
511+
vortex_err!(
512+
"corrupted zstd data: view of length {str_len} at offset {value_data_offset} exceeds buffer length {}",
513+
buffer.len()
514+
)
515+
})?;
516+
let value = buffer.get(value_data_offset..value_end).ok_or_else(|| {
517+
vortex_err!(
518+
"corrupted zstd data: view of length {str_len} at offset {value_data_offset} exceeds buffer length {}",
519+
buffer.len()
520+
)
521+
})?;
506522
views.push(BinaryView::make_view(value, buf_index, local_offset));
507-
offset = value_data_offset + str_len;
523+
offset = value_end;
508524
}
509525

510526
if segment_start < buffer.len() {
511527
buffers.push(buffer.slice(segment_start..buffer.len()));
512528
}
513529

514-
(buffers, views.freeze())
530+
Ok((buffers, views.freeze()))
515531
}
516532

517533
impl ZstdData {
@@ -1016,7 +1032,8 @@ impl ZstdData {
10161032
DType::Binary(_) | DType::Utf8(_) => {
10171033
match slice_validity.execute_mask(slice_n_rows, ctx)?.indices() {
10181034
AllOr::All => {
1019-
let (buffers, all_views) = reconstruct_views(&decompressed, MAX_BUFFER_LEN);
1035+
let (buffers, all_views) =
1036+
reconstruct_views(&decompressed, MAX_BUFFER_LEN)?;
10201037
let valid_views = all_views.slice(
10211038
slice_value_idx_start - n_skipped_values
10221039
..slice_value_idx_stop - n_skipped_values,
@@ -1039,7 +1056,8 @@ impl ZstdData {
10391056
)
10401057
.into_array()),
10411058
AllOr::Some(valid_indices) => {
1042-
let (buffers, all_views) = reconstruct_views(&decompressed, MAX_BUFFER_LEN);
1059+
let (buffers, all_views) =
1060+
reconstruct_views(&decompressed, MAX_BUFFER_LEN)?;
10431061
let valid_views = all_views.slice(
10441062
slice_value_idx_start - n_skipped_values
10451063
..slice_value_idx_stop - n_skipped_values,
@@ -1151,7 +1169,7 @@ mod tests {
11511169
fn test_reconstruct_views_no_split() {
11521170
let strings: &[&[u8]] = &[b"hello", b"world"];
11531171
let buf = make_interleaved(strings);
1154-
let (buffers, views) = reconstruct_views(&buf, 1024);
1172+
let (buffers, views) = reconstruct_views(&buf, 1024).unwrap();
11551173

11561174
assert_eq!(buffers.len(), 1);
11571175
assert_eq!(views.len(), 2);
@@ -1168,12 +1186,21 @@ mod tests {
11681186
// so it rolls into a second segment.
11691187
let strings: &[&[u8]] = &[b"aaaaaaaaaaaaa", b"bbbbbbbbbbbbb"];
11701188
let buf = make_interleaved(strings);
1171-
let (buffers, views) = reconstruct_views(&buf, 20);
1189+
let (buffers, views) = reconstruct_views(&buf, 20).unwrap();
11721190

11731191
assert_eq!(buffers.len(), 2);
11741192
assert_eq!(views.len(), 2);
11751193
assert_eq!(views[0], BinaryView::make_view(b"aaaaaaaaaaaaa", 0, 4));
11761194
// Second entry starts a new segment at byte 17 (the length prefix), so local offset = 4.
11771195
assert_eq!(views[1], BinaryView::make_view(b"bbbbbbbbbbbbb", 1, 4));
11781196
}
1197+
1198+
#[test]
1199+
fn test_reconstruct_views_corrupt_length_returns_error() {
1200+
let buf = ByteBuffer::copy_from([0xff_u8, 0xff, 0xff, 0x7f, 0x01, 0x02].as_slice());
1201+
assert!(reconstruct_views(&buf, 1024).is_err());
1202+
1203+
let truncated_prefix = ByteBuffer::copy_from([0x01_u8, 0x02].as_slice());
1204+
assert!(reconstruct_views(&truncated_prefix, 1024).is_err());
1205+
}
11791206
}

vortex-cuda/src/kernel/encodings/zstd.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ async fn decode_zstd(array: ZstdArray, ctx: &mut CudaExecutionCtx) -> VortexResu
331331
.indices()
332332
{
333333
AllOr::All => {
334-
let (buffers, all_views) = reconstruct_views(&host_buffer, MAX_BUFFER_LEN);
334+
let (buffers, all_views) = reconstruct_views(&host_buffer, MAX_BUFFER_LEN)?;
335335
let sliced_views = all_views.slice(slice_value_idx_start..slice_value_idx_stop);
336336

337337
Ok(Canonical::VarBinView(unsafe {

0 commit comments

Comments
 (0)