security: bound decompression output to prevent memory-exhaustion DoS
decompress_chunk() already threaded chunk_size (the pipeline's declared decompressed size) into the scale-offset/nbit/szip decoders to bound their output, but not into deflate/lz4/zstd/pcodec, all four of which allocated based on attacker-controlled input with no cap: - lz4: read a raw u32 "orig_size" straight from the compressed payload's first 4 bytes and passed it directly to lz4_flex::block::decompress with no upper bound — a 4-byte attacker-controlled field could request ~4 GiB. - deflate (non-macOS path): unbounded flate2 read_to_end into a fresh Vec. - zstd: zstd::decode_all with no output cap (classic decompression-bomb vector, ratios can exceed 1000:1). - pcodec: simple_decompress with no cap. All four now take the expected chunk size and reject output that exceeds it (or a 256 MiB absolute ceiling when the size is unavailable), matching the pattern the other three filters already used. Also fixes the same unbounded read_to_end in clawhdf5-filters' fast_deflate streaming fallback (used when no size hint is available). Added tests for each codec plus one exercising the actually-exploited path through the public decompress_chunk() entrypoint. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
88195d1c33
commit
b9898c2a9c
@@ -270,14 +270,29 @@ pub(crate) fn flate2_decompress_preallocated(
|
|||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Absolute ceiling on decompressed output when the caller has no size hint,
|
||||||
|
/// preventing unbounded allocation from a hostile/corrupted zlib stream.
|
||||||
|
const MAX_DECOMPRESS_SIZE: usize = 256 * 1024 * 1024;
|
||||||
|
|
||||||
/// Streaming decompress with dynamic sizing (when output size is unknown).
|
/// Streaming decompress with dynamic sizing (when output size is unknown).
|
||||||
|
///
|
||||||
|
/// Bounded by [`MAX_DECOMPRESS_SIZE`] since there is no chunk-size hint to
|
||||||
|
/// validate against here — an unbounded `read_to_end` would let a hostile
|
||||||
|
/// zlib stream force arbitrarily large allocation (a "zlib bomb").
|
||||||
pub(crate) fn flate2_decompress_streaming(data: &[u8]) -> Result<Vec<u8>, String> {
|
pub(crate) fn flate2_decompress_streaming(data: &[u8]) -> Result<Vec<u8>, String> {
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
let mut decoder = flate2::read::ZlibDecoder::new(data);
|
let decoder = flate2::read::ZlibDecoder::new(data);
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
decoder
|
decoder
|
||||||
|
.take(MAX_DECOMPRESS_SIZE as u64 + 1)
|
||||||
.read_to_end(&mut result)
|
.read_to_end(&mut result)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
if result.len() > MAX_DECOMPRESS_SIZE {
|
||||||
|
return Err(format!(
|
||||||
|
"decompressed output exceeds {} MiB limit",
|
||||||
|
MAX_DECOMPRESS_SIZE / 1024 / 1024
|
||||||
|
));
|
||||||
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ use crate::filter_pipeline::{
|
|||||||
FILTER_SCALEOFFSET, FILTER_SHUFFLE, FILTER_SZIP, FILTER_ZSTD, FilterPipeline,
|
FILTER_SCALEOFFSET, FILTER_SHUFFLE, FILTER_SZIP, FILTER_ZSTD, FilterPipeline,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Absolute ceiling on a single decompressed chunk's output size, used only
|
||||||
|
/// when the pipeline's declared `chunk_size` is unavailable (0). Prevents
|
||||||
|
/// unbounded-allocation DoS from a malicious/corrupted compressed chunk.
|
||||||
|
pub(crate) const MAX_DECOMPRESS_SIZE: usize = 256 * 1024 * 1024;
|
||||||
|
|
||||||
/// Apply a filter pipeline to decompress a chunk.
|
/// Apply a filter pipeline to decompress a chunk.
|
||||||
/// Filters are applied in REVERSE order for decompression.
|
/// Filters are applied in REVERSE order for decompression.
|
||||||
pub fn decompress_chunk(
|
pub fn decompress_chunk(
|
||||||
@@ -25,11 +30,15 @@ pub fn decompress_chunk(
|
|||||||
for filter in pipeline.filters.iter().rev() {
|
for filter in pipeline.filters.iter().rev() {
|
||||||
data = match filter.filter_id {
|
data = match filter.filter_id {
|
||||||
FILTER_SHUFFLE => shuffle_decompress(&data, element_size as usize)?,
|
FILTER_SHUFFLE => shuffle_decompress(&data, element_size as usize)?,
|
||||||
FILTER_DEFLATE => deflate_decompress(&data)?,
|
// `chunk_size` is the expected decompressed size (shuffle/fletcher32
|
||||||
FILTER_LZ4 => lz4_decompress(&data)?,
|
// are size-preserving, so it bounds these too); pass it so these
|
||||||
FILTER_ZSTD => zstd_decompress(&data)?,
|
// decoders can't be forced into unbounded allocation by a hostile
|
||||||
|
// or corrupted compressed payload.
|
||||||
|
FILTER_DEFLATE => deflate_decompress(&data, chunk_size)?,
|
||||||
|
FILTER_LZ4 => lz4_decompress(&data, chunk_size)?,
|
||||||
|
FILTER_ZSTD => zstd_decompress(&data, chunk_size)?,
|
||||||
FILTER_FLETCHER32 => fletcher32_verify(&data)?,
|
FILTER_FLETCHER32 => fletcher32_verify(&data)?,
|
||||||
FILTER_PCODEC => pcodec_decompress(&data, element_size as usize)?,
|
FILTER_PCODEC => pcodec_decompress(&data, element_size as usize, chunk_size)?,
|
||||||
// `chunk_size` is the expected decompressed size; pass it so these
|
// `chunk_size` is the expected decompressed size; pass it so these
|
||||||
// decoders can reject an element count that would over-allocate.
|
// decoders can reject an element count that would over-allocate.
|
||||||
FILTER_SCALEOFFSET => scaleoffset_decompress(&data, &filter.client_data, chunk_size)?,
|
FILTER_SCALEOFFSET => scaleoffset_decompress(&data, &filter.client_data, chunk_size)?,
|
||||||
@@ -569,24 +578,48 @@ fn nbit_decompress(data: &[u8], cd: &[u32], expected_bytes: usize) -> Result<Vec
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decompress zlib-compressed data.
|
/// Decompress zlib-compressed data.
|
||||||
|
///
|
||||||
|
/// `expected_bytes` is the pipeline's declared decompressed chunk size (0 if
|
||||||
|
/// unavailable); output is rejected if it exceeds this bound (or, when
|
||||||
|
/// unavailable, [`MAX_DECOMPRESS_SIZE`]), preventing a hostile/corrupted
|
||||||
|
/// compressed payload from forcing unbounded allocation (a "zlib bomb").
|
||||||
#[cfg(feature = "deflate")]
|
#[cfg(feature = "deflate")]
|
||||||
fn deflate_decompress(data: &[u8]) -> Result<Vec<u8>, FormatError> {
|
fn deflate_decompress(data: &[u8], expected_bytes: usize) -> Result<Vec<u8>, FormatError> {
|
||||||
|
let limit = if expected_bytes != 0 {
|
||||||
|
expected_bytes
|
||||||
|
} else {
|
||||||
|
MAX_DECOMPRESS_SIZE
|
||||||
|
};
|
||||||
|
|
||||||
// Try system zlib first on macOS (Apple's ARM64-optimized libz is ~1.4x
|
// Try system zlib first on macOS (Apple's ARM64-optimized libz is ~1.4x
|
||||||
// faster at decompression than zlib-ng on Apple Silicon).
|
// faster at decompression than zlib-ng on Apple Silicon).
|
||||||
#[cfg(all(target_os = "macos", feature = "system-zlib-decompress"))]
|
#[cfg(all(target_os = "macos", feature = "system-zlib-decompress"))]
|
||||||
{
|
{
|
||||||
if let Ok(result) = sysz::decompress(data) {
|
if let Ok(result) = sysz::decompress(data) {
|
||||||
|
if result.len() > limit {
|
||||||
|
return Err(FormatError::DecompressionError(
|
||||||
|
"deflate: output exceeds expected chunk size".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
// Fall through to flate2 on error
|
// Fall through to flate2 on error
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
let mut decoder = flate2::read::ZlibDecoder::new(data);
|
let decoder = flate2::read::ZlibDecoder::new(data);
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::with_capacity(limit.min(1 << 20));
|
||||||
|
// Read one byte past the limit so an over-size stream is distinguishable
|
||||||
|
// from one that legitimately ends exactly at the limit.
|
||||||
decoder
|
decoder
|
||||||
|
.take(limit as u64 + 1)
|
||||||
.read_to_end(&mut result)
|
.read_to_end(&mut result)
|
||||||
.map_err(|e| FormatError::DecompressionError(e.to_string()))?;
|
.map_err(|e| FormatError::DecompressionError(e.to_string()))?;
|
||||||
|
if result.len() > limit {
|
||||||
|
return Err(FormatError::DecompressionError(
|
||||||
|
"deflate: output exceeds size limit".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,7 +692,7 @@ mod sysz {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "deflate"))]
|
#[cfg(not(feature = "deflate"))]
|
||||||
fn deflate_decompress(_data: &[u8]) -> Result<Vec<u8>, FormatError> {
|
fn deflate_decompress(_data: &[u8], _expected_bytes: usize) -> Result<Vec<u8>, FormatError> {
|
||||||
Err(FormatError::UnsupportedFilter(FILTER_DEFLATE))
|
Err(FormatError::UnsupportedFilter(FILTER_DEFLATE))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,20 +715,35 @@ fn deflate_compress(_data: &[u8], _level: u32) -> Result<Vec<u8>, FormatError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decompress LZ4 data. Format: 4 bytes LE original size + LZ4 block data.
|
/// Decompress LZ4 data. Format: 4 bytes LE original size + LZ4 block data.
|
||||||
|
///
|
||||||
|
/// The 4-byte "original size" header is part of the attacker-controlled
|
||||||
|
/// compressed payload itself, so it is bounded against `expected_bytes` (the
|
||||||
|
/// pipeline's declared chunk size) before being used to size the output
|
||||||
|
/// allocation — otherwise a crafted 4-byte value can request up to ~4 GiB.
|
||||||
#[cfg(feature = "lz4")]
|
#[cfg(feature = "lz4")]
|
||||||
fn lz4_decompress(data: &[u8]) -> Result<Vec<u8>, FormatError> {
|
fn lz4_decompress(data: &[u8], expected_bytes: usize) -> Result<Vec<u8>, FormatError> {
|
||||||
if data.len() < 4 {
|
if data.len() < 4 {
|
||||||
return Err(FormatError::DecompressionError(
|
return Err(FormatError::DecompressionError(
|
||||||
"lz4: data too short".into(),
|
"lz4: data too short".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let orig_size = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
|
let orig_size = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
|
||||||
|
if expected_bytes != 0 && orig_size > expected_bytes {
|
||||||
|
return Err(FormatError::DecompressionError(
|
||||||
|
"lz4: declared size exceeds chunk size".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if orig_size > MAX_DECOMPRESS_SIZE {
|
||||||
|
return Err(FormatError::DecompressionError(
|
||||||
|
"lz4: declared size exceeds limit".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
lz4_flex::block::decompress(&data[4..], orig_size)
|
lz4_flex::block::decompress(&data[4..], orig_size)
|
||||||
.map_err(|e| FormatError::DecompressionError(format!("lz4: {e}")))
|
.map_err(|e| FormatError::DecompressionError(format!("lz4: {e}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "lz4"))]
|
#[cfg(not(feature = "lz4"))]
|
||||||
fn lz4_decompress(_data: &[u8]) -> Result<Vec<u8>, FormatError> {
|
fn lz4_decompress(_data: &[u8], _expected_bytes: usize) -> Result<Vec<u8>, FormatError> {
|
||||||
Err(FormatError::UnsupportedFilter(FILTER_LZ4))
|
Err(FormatError::UnsupportedFilter(FILTER_LZ4))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,13 +763,35 @@ fn lz4_compress(_data: &[u8]) -> Result<Vec<u8>, FormatError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decompress zstd data.
|
/// Decompress zstd data.
|
||||||
|
///
|
||||||
|
/// `expected_bytes` bounds the output (or [`MAX_DECOMPRESS_SIZE`] when
|
||||||
|
/// unavailable) to guard against a zstd decompression bomb, since zstd's
|
||||||
|
/// compression ratio can exceed 1000:1.
|
||||||
#[cfg(feature = "zstd")]
|
#[cfg(feature = "zstd")]
|
||||||
fn zstd_decompress(data: &[u8]) -> Result<Vec<u8>, FormatError> {
|
fn zstd_decompress(data: &[u8], expected_bytes: usize) -> Result<Vec<u8>, FormatError> {
|
||||||
zstd::decode_all(data).map_err(|e| FormatError::DecompressionError(format!("zstd: {e}")))
|
use std::io::Read;
|
||||||
|
let limit = if expected_bytes != 0 {
|
||||||
|
expected_bytes
|
||||||
|
} else {
|
||||||
|
MAX_DECOMPRESS_SIZE
|
||||||
|
};
|
||||||
|
let decoder = zstd::stream::Decoder::new(data)
|
||||||
|
.map_err(|e| FormatError::DecompressionError(format!("zstd: {e}")))?;
|
||||||
|
let mut out = Vec::with_capacity(limit.min(1 << 20));
|
||||||
|
decoder
|
||||||
|
.take(limit as u64 + 1)
|
||||||
|
.read_to_end(&mut out)
|
||||||
|
.map_err(|e| FormatError::DecompressionError(format!("zstd: {e}")))?;
|
||||||
|
if out.len() > limit {
|
||||||
|
return Err(FormatError::DecompressionError(
|
||||||
|
"zstd: output exceeds chunk size".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "zstd"))]
|
#[cfg(not(feature = "zstd"))]
|
||||||
fn zstd_decompress(_data: &[u8]) -> Result<Vec<u8>, FormatError> {
|
fn zstd_decompress(_data: &[u8], _expected_bytes: usize) -> Result<Vec<u8>, FormatError> {
|
||||||
Err(FormatError::UnsupportedFilter(FILTER_ZSTD))
|
Err(FormatError::UnsupportedFilter(FILTER_ZSTD))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,30 +1052,74 @@ fn pcodec_compress(_data: &[u8], _element_size: usize) -> Result<Vec<u8>, Format
|
|||||||
Err(FormatError::UnsupportedFilter(FILTER_PCODEC))
|
Err(FormatError::UnsupportedFilter(FILTER_PCODEC))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `expected_bytes` bounds the number of elements decoded: the output buffer
|
||||||
|
/// is pre-sized to exactly `expected_bytes / element_size` elements and
|
||||||
|
/// `simple_decompress_into` never writes past it, so a corrupted/hostile pco
|
||||||
|
/// stream cannot force over-allocation the way an unbounded `simple_decompress`
|
||||||
|
/// (which allocates however many elements the stream claims) could.
|
||||||
#[cfg(feature = "pcodec")]
|
#[cfg(feature = "pcodec")]
|
||||||
fn pcodec_decompress(data: &[u8], element_size: usize) -> Result<Vec<u8>, FormatError> {
|
fn pcodec_decompress(
|
||||||
use pco::standalone::simple_decompress;
|
data: &[u8],
|
||||||
|
element_size: usize,
|
||||||
|
expected_bytes: usize,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
|
use pco::standalone::simple_decompress_into;
|
||||||
|
let limit_bytes = if expected_bytes != 0 {
|
||||||
|
expected_bytes
|
||||||
|
} else {
|
||||||
|
MAX_DECOMPRESS_SIZE
|
||||||
|
};
|
||||||
|
let n = if element_size != 0 {
|
||||||
|
limit_bytes / element_size
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
match element_size {
|
match element_size {
|
||||||
4 => {
|
4 => {
|
||||||
let nums = simple_decompress::<f32>(data)
|
let mut buf = vec![0f32; n];
|
||||||
|
let progress = simple_decompress_into(data, &mut buf)
|
||||||
.map_err(|e| FormatError::DecompressionError(format!("pco: {e}")))?;
|
.map_err(|e| FormatError::DecompressionError(format!("pco: {e}")))?;
|
||||||
Ok(nums.iter().flat_map(|x| x.to_le_bytes()).collect())
|
if !progress.finished {
|
||||||
|
return Err(FormatError::DecompressionError(
|
||||||
|
"pco: stream contains more data than expected chunk size allows".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
buf.truncate(progress.n_processed);
|
||||||
|
Ok(buf.iter().flat_map(|x| x.to_le_bytes()).collect())
|
||||||
}
|
}
|
||||||
8 => {
|
8 => {
|
||||||
let nums = simple_decompress::<f64>(data)
|
let mut buf = vec![0f64; n];
|
||||||
|
let progress = simple_decompress_into(data, &mut buf)
|
||||||
.map_err(|e| FormatError::DecompressionError(format!("pco: {e}")))?;
|
.map_err(|e| FormatError::DecompressionError(format!("pco: {e}")))?;
|
||||||
Ok(nums.iter().flat_map(|x| x.to_le_bytes()).collect())
|
if !progress.finished {
|
||||||
|
return Err(FormatError::DecompressionError(
|
||||||
|
"pco: stream contains more data than expected chunk size allows".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
buf.truncate(progress.n_processed);
|
||||||
|
Ok(buf.iter().flat_map(|x| x.to_le_bytes()).collect())
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let nums = simple_decompress::<u32>(data)
|
let mut buf = vec![0u32; n];
|
||||||
|
let progress = simple_decompress_into(data, &mut buf)
|
||||||
.map_err(|e| FormatError::DecompressionError(format!("pco: {e}")))?;
|
.map_err(|e| FormatError::DecompressionError(format!("pco: {e}")))?;
|
||||||
Ok(nums.iter().flat_map(|x| x.to_le_bytes()).collect())
|
if !progress.finished {
|
||||||
|
return Err(FormatError::DecompressionError(
|
||||||
|
"pco: stream contains more data than expected chunk size allows".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
buf.truncate(progress.n_processed);
|
||||||
|
Ok(buf.iter().flat_map(|x| x.to_le_bytes()).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "pcodec"))]
|
#[cfg(not(feature = "pcodec"))]
|
||||||
fn pcodec_decompress(_data: &[u8], _element_size: usize) -> Result<Vec<u8>, FormatError> {
|
fn pcodec_decompress(
|
||||||
|
_data: &[u8],
|
||||||
|
_element_size: usize,
|
||||||
|
_expected_bytes: usize,
|
||||||
|
) -> Result<Vec<u8>, FormatError> {
|
||||||
Err(FormatError::UnsupportedFilter(FILTER_PCODEC))
|
Err(FormatError::UnsupportedFilter(FILTER_PCODEC))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1021,7 +1135,7 @@ mod tests {
|
|||||||
fn deflate_compress_decompress_roundtrip() {
|
fn deflate_compress_decompress_roundtrip() {
|
||||||
let data: Vec<u8> = (0..256).map(|i| (i % 256) as u8).collect();
|
let data: Vec<u8> = (0..256).map(|i| (i % 256) as u8).collect();
|
||||||
let compressed = deflate_compress(&data, 6).unwrap();
|
let compressed = deflate_compress(&data, 6).unwrap();
|
||||||
let decompressed = deflate_decompress(&compressed).unwrap();
|
let decompressed = deflate_decompress(&compressed, data.len()).unwrap();
|
||||||
assert_eq!(decompressed, data);
|
assert_eq!(decompressed, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1034,7 +1148,7 @@ mod tests {
|
|||||||
let compressed: Vec<u8> = vec![
|
let compressed: Vec<u8> = vec![
|
||||||
120, 156, 99, 96, 100, 98, 102, 97, 101, 99, 231, 224, 4, 0, 0, 175, 0, 46,
|
120, 156, 99, 96, 100, 98, 102, 97, 101, 99, 231, 224, 4, 0, 0, 175, 0, 46,
|
||||||
];
|
];
|
||||||
let decompressed = deflate_decompress(&compressed).unwrap();
|
let decompressed = deflate_decompress(&compressed, 10).unwrap();
|
||||||
assert_eq!(decompressed, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
assert_eq!(decompressed, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,7 +1159,7 @@ mod tests {
|
|||||||
let data = vec![0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
let data = vec![0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
let compressed = deflate_compress(&data, 6).unwrap();
|
let compressed = deflate_compress(&data, 6).unwrap();
|
||||||
assert!(!compressed.is_empty());
|
assert!(!compressed.is_empty());
|
||||||
let decompressed = deflate_decompress(&compressed).unwrap();
|
let decompressed = deflate_decompress(&compressed, data.len()).unwrap();
|
||||||
assert_eq!(decompressed, data);
|
assert_eq!(decompressed, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1246,7 +1360,7 @@ mod tests {
|
|||||||
fn lz4_compress_decompress_roundtrip() {
|
fn lz4_compress_decompress_roundtrip() {
|
||||||
let data: Vec<u8> = (0..256).map(|i| (i % 256) as u8).collect();
|
let data: Vec<u8> = (0..256).map(|i| (i % 256) as u8).collect();
|
||||||
let compressed = lz4_compress(&data).unwrap();
|
let compressed = lz4_compress(&data).unwrap();
|
||||||
let decompressed = lz4_decompress(&compressed).unwrap();
|
let decompressed = lz4_decompress(&compressed, data.len()).unwrap();
|
||||||
assert_eq!(decompressed, data);
|
assert_eq!(decompressed, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1301,7 +1415,7 @@ mod tests {
|
|||||||
fn zstd_compress_decompress_roundtrip() {
|
fn zstd_compress_decompress_roundtrip() {
|
||||||
let data: Vec<u8> = (0..256).map(|i| (i % 256) as u8).collect();
|
let data: Vec<u8> = (0..256).map(|i| (i % 256) as u8).collect();
|
||||||
let compressed = zstd_compress(&data, 3).unwrap();
|
let compressed = zstd_compress(&data, 3).unwrap();
|
||||||
let decompressed = zstd_decompress(&compressed).unwrap();
|
let decompressed = zstd_decompress(&compressed, data.len()).unwrap();
|
||||||
assert_eq!(decompressed, data);
|
assert_eq!(decompressed, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1623,4 +1737,79 @@ mod tests {
|
|||||||
// Missing client data entirely.
|
// Missing client data entirely.
|
||||||
assert!(scaleoffset_decompress(&[0u8; 32], &[2, 0], 4).is_err());
|
assert!(scaleoffset_decompress(&[0u8; 32], &[2, 0], 4).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- Decompression-bomb hardening: hostile compressed data must not -----
|
||||||
|
// ----- force unbounded allocation. -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "lz4")]
|
||||||
|
fn lz4_decompress_rejects_oversized_orig_size() {
|
||||||
|
// 4-byte LE header claiming ~4 GiB, followed by a few garbage bytes.
|
||||||
|
let mut data = u32::MAX.to_le_bytes().to_vec();
|
||||||
|
data.extend_from_slice(&[0u8; 8]);
|
||||||
|
assert!(lz4_decompress(&data, 64).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "lz4")]
|
||||||
|
fn lz4_decompress_rejects_size_exceeding_chunk_size() {
|
||||||
|
// orig_size (1000) is well under MAX_DECOMPRESS_SIZE but exceeds the
|
||||||
|
// pipeline's declared chunk size (64) — must be rejected by the
|
||||||
|
// chunk-size check specifically, not just the absolute cap.
|
||||||
|
let mut data = 1000u32.to_le_bytes().to_vec();
|
||||||
|
data.extend_from_slice(&[0u8; 8]);
|
||||||
|
assert!(lz4_decompress(&data, 64).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "deflate")]
|
||||||
|
fn deflate_decompress_rejects_output_exceeding_chunk_size() {
|
||||||
|
// A highly-compressible deflate bomb (1 MiB of zeros compresses to a
|
||||||
|
// tiny payload); declared chunk size is far smaller than the real
|
||||||
|
// decompressed size, so this must be rejected rather than allocating
|
||||||
|
// the full 1 MiB.
|
||||||
|
let data = vec![0u8; 1024 * 1024];
|
||||||
|
let compressed = deflate_compress(&data, 6).unwrap();
|
||||||
|
assert!(deflate_decompress(&compressed, 64).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "zstd")]
|
||||||
|
fn zstd_decompress_rejects_output_exceeding_chunk_size() {
|
||||||
|
let data = vec![0u8; 1024 * 1024];
|
||||||
|
let compressed = zstd_compress(&data, 3).unwrap();
|
||||||
|
assert!(zstd_decompress(&compressed, 64).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "pcodec")]
|
||||||
|
fn pcodec_decompress_rejects_element_count_exceeding_chunk_size() {
|
||||||
|
let data: Vec<f32> = (0..1000).map(|i| i as f32).collect();
|
||||||
|
let raw: Vec<u8> = data.iter().flat_map(|x| x.to_le_bytes()).collect();
|
||||||
|
let compressed = pcodec_compress(&raw, 4).unwrap();
|
||||||
|
// Declared chunk size only fits 4 f32 elements, far fewer than the
|
||||||
|
// 1000 the stream actually contains.
|
||||||
|
assert!(pcodec_decompress(&compressed, 4, 16).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "lz4")]
|
||||||
|
fn decompress_chunk_rejects_hostile_lz4_size_via_public_entrypoint() {
|
||||||
|
// The actually-exploited path: a FilterPipeline claiming a small
|
||||||
|
// chunk_size, but whose LZ4-compressed data header claims a huge
|
||||||
|
// decompressed size.
|
||||||
|
use crate::filter_pipeline::{FilterDescription, FilterPipeline};
|
||||||
|
let mut data = u32::MAX.to_le_bytes().to_vec();
|
||||||
|
data.extend_from_slice(&[0u8; 8]);
|
||||||
|
let pipeline = FilterPipeline {
|
||||||
|
version: 2,
|
||||||
|
filters: vec![FilterDescription {
|
||||||
|
filter_id: FILTER_LZ4,
|
||||||
|
name: None,
|
||||||
|
flags: 0,
|
||||||
|
client_data: vec![],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
assert!(decompress_chunk(&data, &pipeline, 16, 1).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user