This is an automated email from the ASF dual-hosted git repository. martin-g pushed a commit to branch 06-empty-compression-level in repository https://gitbox.apache.org/repos/asf/avro-rs.git
commit 6f3a2d8b80e7bb36cea9847fe8014930bd170a4d Author: Martin Tzvetanov Grigorov <[email protected]> AuthorDate: Tue Aug 25 16:33:14 2026 +0300 fix: Empty avro.codec.compression_level metadata panics reader via bytes[0] Header parsing returns Err, never panics, on malformed metadata: all three sites use bytes.first(); an empty value yields the existing Details::BadCodecMetadata error. An explicit error (rather than silently falling back to the codec default level) was chosen because an empty level is malformed input, and failing closed on malformed attacker headers is the safer contract; the absent-key case still falls back to defaults as before. Reported-by: Security scans --- avro/src/reader/block.rs | 54 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/avro/src/reader/block.rs b/avro/src/reader/block.rs index b860e58..536a157 100644 --- a/avro/src/reader/block.rs +++ b/avro/src/reader/block.rs @@ -333,7 +333,10 @@ fn read_codec(metadata: &HashMap<String, Value>) -> AvroResult<Codec> { if let Some(Value::Bytes(bytes)) = metadata.get("avro.codec.compression_level") { - Ok(Codec::Bzip2(Bzip2Settings::new(bytes[0]))) + match bytes.first() { + Some(&level) => Ok(Codec::Bzip2(Bzip2Settings::new(level))), + None => Err(Details::BadCodecMetadata.into()), + } } else { Ok(codec) } @@ -344,7 +347,10 @@ fn read_codec(metadata: &HashMap<String, Value>) -> AvroResult<Codec> { if let Some(Value::Bytes(bytes)) = metadata.get("avro.codec.compression_level") { - Ok(Codec::Xz(XzSettings::new(bytes[0]))) + match bytes.first() { + Some(&level) => Ok(Codec::Xz(XzSettings::new(level))), + None => Err(Details::BadCodecMetadata.into()), + } } else { Ok(codec) } @@ -355,7 +361,10 @@ fn read_codec(metadata: &HashMap<String, Value>) -> AvroResult<Codec> { if let Some(Value::Bytes(bytes)) = metadata.get("avro.codec.compression_level") { - Ok(Codec::Zstandard(ZstandardSettings::new(bytes[0]))) + match bytes.first() { + Some(&level) => Ok(Codec::Zstandard(ZstandardSettings::new(level))), + None => Err(Details::BadCodecMetadata.into()), + } } else { Ok(codec) } @@ -377,6 +386,45 @@ mod tests { use crate::{Codec, Schema}; use apache_avro_test_helper::TestResult; + #[cfg(any(feature = "bzip", feature = "xz", feature = "zstandard"))] + fn empty_compression_level_errors_for(codec_name: &str) { + use crate::types::Value; + use std::collections::HashMap; + + let mut metadata = HashMap::new(); + metadata.insert( + "avro.codec".to_string(), + Value::Bytes(codec_name.as_bytes().to_vec()), + ); + metadata.insert( + "avro.codec.compression_level".to_string(), + Value::Bytes(vec![]), + ); + + // An empty compression_level in attacker-controlled metadata must be + // a clean error, not an index-out-of-bounds panic. + let err = super::read_codec(&metadata).unwrap_err().into_details(); + assert!(matches!(err, Details::BadCodecMetadata), "{err:?}"); + } + + #[cfg(feature = "bzip")] + #[test] + fn avro_rs_644_empty_bzip2_compression_level_is_rejected() { + empty_compression_level_errors_for("bzip2"); + } + + #[cfg(feature = "xz")] + #[test] + fn avro_rs_644_empty_xz_compression_level_is_rejected() { + empty_compression_level_errors_for("xz"); + } + + #[cfg(feature = "zstandard")] + #[test] + fn avro_rs_644_empty_zstandard_compression_level_is_rejected() { + empty_compression_level_errors_for("zstandard"); + } + #[test] fn avro_rs_643_huge_message_count_on_empty_block_is_rejected() -> TestResult { // Block header claiming 2^62 objects with a block size of 0: with a
