Files
buildroot/package/clamav/0006-fix-alz-parser-robustness-and-scan-coverage-63.patch
Titouan Christophe 6ae2b74470 package/clamav: add patch for CVE-2026-20243
Signed-off-by: Titouan Christophe <titouan.christophe@mind.be>
Signed-off-by: Raphaël Mélotte <raphael.melotte@mind.be>
2026-09-04 13:05:17 +02:00

1504 lines
51 KiB
Diff

From: "Val S." <valsnyde@cisco.com>
Date: Wed, 17 Jun 2026 10:07:43 -0400
Subject: Fix ALZ parser robustness and scan coverage (#63)
Malformed ALZ entries could panic or abort archive processing before
payloads and metadata were scanned. Header size claims and decompression
behavior could bypass scan limits or skip valid members, and the deflate
path could inflate oversized output before applying limits.
Harden ALZ size arithmetic, extract stored, bzip2, and deflate members
under scan budgets, and keep scanning after metadata, extraction, and
later parse errors. Report accurate metadata including file positions
and encryption state. Use bounded flate2 reads for deflate output, drop
the old inflate dependency, and add Rust and clamscan regressions.
Keep ALZ per-member and archive-total limit accounting separate so
archive totals do not get reported as MaxFileSize for archives with
multiple smaller members. Preserve MaxScanSize exceeds-max reporting for
truncated archive totals and cover the behavior with Rust and clamscan
tests.
Credit: Yazdan Soltani
CLAM-2988
---
Upstream: https://github.com/Cisco-Talos/clamav/commit/610c1d1eeb67ced3645c205d62bcd9eb7e2000ed
CVE: CVE-2026-20243
[Titouan: fix merge conflict with clamav-1.4.3]
Signed-off-by: Titouan Christophe <titouan.christophe@mind.be>
---
Cargo.lock | 16 -
libclamav/libclamav.map | 1 +
libclamav_rust/Cargo.toml | 1 -
libclamav_rust/build.rs | 1 +
libclamav_rust/src/alz.rs | 622 +++++++++++++++++++++++++++-----
libclamav_rust/src/scanners.rs | 123 ++++++-
libclamav_rust/src/sys.rs | 6 +
libclamav_rust/src/util.rs | 31 +-
unit_tests/clamscan/alz_test.py | 321 ++++++++++++++++
9 files changed, 1006 insertions(+), 116 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index e96f29ffa..8ec8d3400 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,12 +8,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
-[[package]]
-name = "adler32"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
-
[[package]]
name = "aho-corasick"
version = "1.1.3"
@@ -199,7 +193,6 @@ dependencies = [
"hex",
"hex-literal",
"image",
- "inflate",
"libc",
"log",
"num-traits",
@@ -523,12 +516,14 @@ dependencies = [
]
[[package]]
-name = "inflate"
-version = "0.4.5"
+name = "interpolate_name"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff"
+checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
- "adler32",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.113",
]
[[package]]
diff --git a/libclamav/libclamav.map b/libclamav/libclamav.map
index a34a05dbb..f8cf8a299 100644
--- a/libclamav/libclamav.map
+++ b/libclamav/libclamav.map
@@ -292,6 +292,7 @@ CLAMAV_PRIVATE {
cli_getdsig;
cli_infomsg_simple;
cli_set_debug_flag;
+ cli_append_potentially_unwanted_if_heur_exceedsmax;
fmap_dump_to_file;
init_allow_list;
init_domain_list;
diff --git a/libclamav_rust/Cargo.toml b/libclamav_rust/Cargo.toml
index efab908656..b75185b6c7 100644
--- a/libclamav_rust/Cargo.toml
+++ b/libclamav_rust/Cargo.toml
@@ -22,7 +22,6 @@ unicode-segmentation = "1.10"
bindgen = "0.65"
onenote_parser = { git = "https://github.com/Cisco-Talos/onenote.rs.git", branch = "CLAM-2329-new-from-slice" }
hex-literal = "0.4"
-inflate = "0.4"
bzip2-rs = "0.1"
byteorder = "1.5"
delharc = "0.6"
diff --git a/libclamav_rust/build.rs b/libclamav_rust/build.rs
index ede1743f41..a35bc6211a 100644
--- a/libclamav_rust/build.rs
+++ b/libclamav_rust/build.rs
@@ -48,6 +48,7 @@ const BINDGEN_FUNCTIONS: &[&str] = &[
"cli_infomsg_simple",
"cli_errmsg",
"cli_append_virus",
+ "cli_append_potentially_unwanted_if_heur_exceedsmax",
"lsig_increment_subsig_match",
"cli_versig2",
"cli_getdsig",
diff --git a/libclamav_rust/src/alz.rs b/libclamav_rust/src/alz.rs
index fef676a0db..0ee30a0a04 100644
--- a/libclamav_rust/src/alz.rs
+++ b/libclamav_rust/src/alz.rs
@@ -30,11 +30,12 @@
)]
*/
+use std::convert::TryFrom;
use std::io::{Cursor, Read};
use byteorder::{LittleEndian, ReadBytesExt};
use bzip2_rs::DecoderReader;
-use inflate::InflateStream;
+use flate2::read::DeflateDecoder;
use log::debug;
/// File header
@@ -61,6 +62,12 @@ pub enum Error {
#[error("Failed to extract file")]
Extract,
+ #[error("Failed to allocate extracted file")]
+ Alloc,
+
+ #[error("Extracted file exceeds scan limits")]
+ ScanLimitExceeded(u64),
+
#[error("Failed to read field: {0}")]
Read(&'static str),
}
@@ -273,16 +280,20 @@ impl AlzLocalFileHeader {
}
}
- #[allow(clippy::cast_possible_truncation)]
- let idx0: usize = cursor.position() as usize;
- let idx1: usize = idx0 + self.head.file_name_length as usize;
+ let idx0: usize = usize::try_from(cursor.position())
+ .map_err(|_| Error::Parse("Invalid file name offset"))?;
+ let idx1: usize = idx0
+ .checked_add(usize::from(self.head.file_name_length))
+ .ok_or(Error::Parse("Invalid file name length"))?;
if idx1 > cursor.get_ref().len() {
return Err(Error::Parse("Invalid file name length"));
}
let filename = &cursor.get_ref().as_slice()[idx0..idx1];
- cursor.set_position(idx1 as u64);
+ cursor.set_position(
+ u64::try_from(idx1).map_err(|_| Error::Parse("Invalid file name length"))?,
+ );
self.file_name = String::from_utf8_lossy(filename).into_owned();
@@ -293,12 +304,20 @@ impl AlzLocalFileHeader {
}
self.start_of_compressed_data = cursor.position();
- cursor.set_position(self.start_of_compressed_data + self.compressed_size);
-
- if self.start_of_compressed_data + self.compressed_size > cursor.get_ref().len() as u64 {
+ let end_of_compressed_data = self
+ .start_of_compressed_data
+ .checked_add(self.compressed_size)
+ .ok_or(Error::Parse("Invalid compressed data length"))?;
+
+ if end_of_compressed_data
+ > u64::try_from(cursor.get_ref().len())
+ .map_err(|_| Error::Parse("Invalid compressed data length"))?
+ {
return Err(Error::Parse("Invalid compressed data length"));
}
+ cursor.set_position(end_of_compressed_data);
+
Ok(())
}
@@ -323,120 +342,196 @@ impl AlzLocalFileHeader {
&mut self,
cursor: &std::io::Cursor<&Vec<u8>>,
files: &mut Vec<ExtractedFile>,
+ max_extracted_size: u64,
) -> Result<(), Error> {
- #[allow(clippy::cast_possible_truncation)]
- let start: usize = self.start_of_compressed_data as usize;
- #[allow(clippy::cast_possible_truncation)]
- let end: usize = start + self.compressed_size as usize;
- if end >= cursor.get_ref().len() {
- return Err(Error::Extract);
- }
- let data: &[u8] = &cursor.get_ref().as_slice()[start..end];
+ let start: usize =
+ usize::try_from(self.start_of_compressed_data).map_err(|_| Error::Extract)?;
+ let len: usize = usize::try_from(self.compressed_size).map_err(|_| Error::Extract)?;
+ let end: usize = start.checked_add(len).ok_or(Error::Extract)?;
+ let data: &[u8] = cursor
+ .get_ref()
+ .as_slice()
+ .get(start..end)
+ .ok_or(Error::Extract)?;
- let mut inflater = InflateStream::new();
let mut out: Vec<u8> = Vec::<u8>::new();
- let mut n: usize = 0;
-
- while n < data.len() {
- let res = inflater.update(&data[n..]);
- if let Ok((num_bytes_read, result)) = res {
- n += num_bytes_read;
- out.extend(result.iter().copied());
- } else {
- return Err(Error::Extract);
+ let mut decompressor = DeflateDecoder::new(data);
+ let mut buffer = [0u8; 8192];
+
+ loop {
+ let len = decompressor.read(&mut buffer).map_err(|_| {
+ debug!("Unable to decompress deflate data");
+ Error::Extract
+ })?;
+ if len == 0 {
+ break;
+ }
+
+ if let Some(needed) =
+ self.append_output(&mut out, &buffer[..len], max_extracted_size)?
+ {
+ self.push_file(out, files)?;
+ return Err(Error::ScanLimitExceeded(needed));
}
}
- self.write_file(&out, files);
+ self.push_file(out, files)
+ }
- Ok(())
+ fn append_output(
+ &self,
+ out: &mut Vec<u8>,
+ buffer: &[u8],
+ max_extracted_size: u64,
+ ) -> Result<Option<u64>, Error> {
+ let needed = out.len().checked_add(buffer.len()).ok_or(Error::Extract)?;
+ let needed_u64 = u64::try_from(needed).map_err(|_| Error::Extract)?;
+
+ if needed_u64 > max_extracted_size {
+ let current = u64::try_from(out.len()).map_err(|_| Error::Extract)?;
+ let remaining = max_extracted_size.saturating_sub(current);
+ let copy_len = usize::try_from(remaining)
+ .unwrap_or(usize::MAX)
+ .min(buffer.len());
+
+ out.try_reserve(copy_len).map_err(|_| Error::Alloc)?;
+ out.extend_from_slice(&buffer[..copy_len]);
+
+ return Ok(Some(needed_u64));
+ }
+
+ out.try_reserve(buffer.len()).map_err(|_| Error::Alloc)?;
+ out.extend_from_slice(buffer);
+
+ Ok(None)
}
- fn write_file(&mut self, buffer: &[u8], files: &mut Vec<ExtractedFile>) {
+ fn push_file(&mut self, data: Vec<u8>, files: &mut Vec<ExtractedFile>) -> Result<(), Error> {
+ if data.is_empty() {
+ return Ok(());
+ }
+
+ let mut name = String::new();
+ name.try_reserve(self.file_name.len())
+ .map_err(|_| Error::Alloc)?;
+ name.push_str(&self.file_name);
+
let extracted_file: ExtractedFile = ExtractedFile {
- name: Some(self.file_name.to_string()),
- data: buffer.to_vec(),
+ name: Some(name),
+ data,
};
- if 0 != extracted_file.data.len() {
- files.push(extracted_file);
- }
+ files.try_reserve(1).map_err(|_| Error::Alloc)?;
+ files.push(extracted_file);
+
+ Ok(())
+ }
+
+ fn write_file(&mut self, buffer: &[u8], files: &mut Vec<ExtractedFile>) -> Result<(), Error> {
+ let mut data: Vec<u8> = Vec::new();
+ data.try_reserve_exact(buffer.len())
+ .map_err(|_| Error::Alloc)?;
+ data.extend_from_slice(buffer);
+
+ self.push_file(data, files)
}
fn extract_file_nocomp(
&mut self,
- cursor: &mut std::io::Cursor<&Vec<u8>>,
+ cursor: &std::io::Cursor<&Vec<u8>>,
files: &mut Vec<ExtractedFile>,
+ max_extracted_size: u64,
) -> Result<(), Error> {
- #[allow(clippy::cast_possible_truncation)]
- let idx0: usize = self.start_of_compressed_data as usize;
+ let idx0: usize =
+ usize::try_from(self.start_of_compressed_data).map_err(|_| Error::Extract)?;
- let mut len = self.compressed_size;
if self.compressed_size != self.uncompressed_size {
- debug!("Uncompressed file has different lengths for compressed vs uncompressed, using the shorter");
- if self.compressed_size > self.uncompressed_size {
- len = self.uncompressed_size;
- }
+ debug!("Uncompressed file has different lengths for compressed vs uncompressed, using the stored size");
}
- #[allow(clippy::cast_possible_truncation)]
- let idx1: usize = idx0 + len as usize;
- if idx1 > cursor.get_ref().len() {
- debug!("Invalid data length");
- return Err(Error::Extract);
+ let len: usize = usize::try_from(self.compressed_size).map_err(|_| Error::Extract)?;
+ let idx1: usize = idx0.checked_add(len).ok_or(Error::Extract)?;
+
+ let contents = cursor
+ .get_ref()
+ .as_slice()
+ .get(idx0..idx1)
+ .ok_or(Error::Extract)?;
+
+ let contents_len = u64::try_from(contents.len()).map_err(|_| Error::Extract)?;
+ if contents_len > max_extracted_size {
+ let copy_len = usize::try_from(max_extracted_size)
+ .unwrap_or(usize::MAX)
+ .min(contents.len());
+ self.write_file(&contents[..copy_len], files)?;
+ return Err(Error::ScanLimitExceeded(contents_len));
}
- let contents = &cursor.get_ref().as_slice()[idx0..idx1];
- cursor.set_position(idx1 as u64);
-
- self.write_file(contents, files);
- Ok(())
+ self.write_file(contents, files)
}
fn extract_file_bzip2(
&mut self,
cursor: &std::io::Cursor<&Vec<u8>>,
files: &mut Vec<ExtractedFile>,
+ max_extracted_size: u64,
) -> Result<(), Error> {
- #[allow(clippy::cast_possible_truncation)]
- let idx0: usize = self.start_of_compressed_data as usize;
- #[allow(clippy::cast_possible_truncation)]
- let idx1: usize = idx0 + self.compressed_size as usize;
+ let idx0: usize =
+ usize::try_from(self.start_of_compressed_data).map_err(|_| Error::Extract)?;
+ let len: usize = usize::try_from(self.compressed_size).map_err(|_| Error::Extract)?;
+ let idx1: usize = idx0.checked_add(len).ok_or(Error::Extract)?;
- let contents = &cursor.get_ref().as_slice()[idx0..idx1];
+ let contents = cursor
+ .get_ref()
+ .as_slice()
+ .get(idx0..idx1)
+ .ok_or(Error::Extract)?;
- /*
- * Create vector of the needed capacity.
- */
let mut out: Vec<u8> = Vec::new();
- for _i in 0..self.uncompressed_size {
- out.push(0);
+ let mut decompressor = DecoderReader::new(contents);
+ let mut buffer = [0u8; 8192];
+ loop {
+ let len = decompressor.read(&mut buffer).map_err(|_| {
+ debug!("Unable to decompress bz2 data");
+ Error::Extract
+ })?;
+ if len == 0 {
+ break;
+ }
+
+ if let Some(needed) =
+ self.append_output(&mut out, &buffer[..len], max_extracted_size)?
+ {
+ self.push_file(out, files)?;
+ return Err(Error::ScanLimitExceeded(needed));
+ }
}
- let mut decompressor = DecoderReader::new(contents);
- let ret = decompressor.read_exact(&mut out);
- if ret.is_err() {
- debug!("Unable to decompress bz2 data");
- return Err(Error::Extract);
+ if let Ok(uncompressed_size) = usize::try_from(self.uncompressed_size) {
+ if out.len() != uncompressed_size {
+ debug!(
+ "Bzip2 file has different lengths for declared vs decompressed data, using the decompressed size"
+ );
+ }
}
- self.write_file(&out, files);
- Ok(())
+ self.push_file(out, files)
}
fn extract_file(
&mut self,
cursor: &mut std::io::Cursor<&Vec<u8>>,
files: &mut Vec<ExtractedFile>,
+ max_extracted_size: u64,
) -> Result<(), Error> {
const ALZ_COMP_NOCOMP: u8 = 0;
const ALZ_COMP_BZIP2: u8 = 1;
const ALZ_COMP_DEFLATE: u8 = 2;
match self.compression_method {
- ALZ_COMP_NOCOMP => self.extract_file_nocomp(cursor, files),
- ALZ_COMP_BZIP2 => self.extract_file_bzip2(cursor, files),
- ALZ_COMP_DEFLATE => self.extract_file_deflate(cursor, files),
+ ALZ_COMP_NOCOMP => self.extract_file_nocomp(cursor, files, max_extracted_size),
+ ALZ_COMP_BZIP2 => self.extract_file_bzip2(cursor, files, max_extracted_size),
+ ALZ_COMP_DEFLATE => self.extract_file_deflate(cursor, files, max_extracted_size),
_ => Err(Error::Extract),
}
}
@@ -448,9 +543,27 @@ pub struct ExtractedFile {
pub data: Vec<u8>,
}
+pub struct AlzFileMetadata<'a> {
+ pub file_name: &'a str,
+ pub compressed_size: u64,
+ pub uncompressed_size: u64,
+ pub is_encrypted: bool,
+ pub file_crc: u32,
+ pub filepos: usize,
+}
+
+pub struct AlzExtractionLimits {
+ pub max_file_size: u64,
+ pub max_total_size: u64,
+}
+
#[derive(Default)]
pub struct Alz {
pub embedded_files: Vec<ExtractedFile>,
+ pub file_limit_exceeded_size: Option<u64>,
+ pub total_limit_exceeded_size: Option<u64>,
+ extracted_size: u64,
+ parse_error: bool,
}
impl<'aa> Alz {
@@ -462,29 +575,97 @@ impl<'aa> Alz {
.map_or(false, |n| ALZ_FILE_HEADER == n)
}
- fn parse_local_fileheader(&mut self, cursor: &mut std::io::Cursor<&Vec<u8>>) -> bool {
+ fn parse_local_fileheader<F>(
+ &mut self,
+ cursor: &mut std::io::Cursor<&Vec<u8>>,
+ filepos: &mut usize,
+ should_extract: &mut F,
+ ) -> Result<(), Error>
+ where
+ F: FnMut(&AlzFileMetadata<'_>) -> Option<AlzExtractionLimits>,
+ {
let mut local_fileheader = AlzLocalFileHeader::new();
- if let Err(err) = local_fileheader.parse(cursor) {
- debug!("{err}");
- return false;
- }
+ local_fileheader.parse(cursor)?;
- if let Err(err) = local_fileheader.is_supported() {
- debug!("{err}");
- return false;
- }
+ let metadata_filepos = *filepos;
+ *filepos = metadata_filepos.saturating_add(1);
if !local_fileheader.is_directory() {
/* The is_file flag doesn't appear to always be set, so we'll just assume it's a file if
* it's not marked as a directory.*/
- let res2 = local_fileheader.extract_file(cursor, &mut self.embedded_files);
- if res2.is_err() {
- return false;
+ let metadata = AlzFileMetadata {
+ file_name: &local_fileheader.file_name,
+ compressed_size: local_fileheader.compressed_size,
+ uncompressed_size: local_fileheader.uncompressed_size,
+ is_encrypted: local_fileheader.is_encrypted(),
+ file_crc: local_fileheader.file_crc,
+ filepos: metadata_filepos,
+ };
+
+ let limits = should_extract(&metadata);
+
+ if let Err(err) = local_fileheader.is_supported() {
+ debug!("{err}");
+ return Ok(());
+ }
+
+ let Some(limits) = limits else {
+ return Ok(());
+ };
+
+ let base_extracted_size = self.extracted_size;
+ let max_total_remaining = limits.max_total_size.saturating_sub(base_extracted_size);
+ let max_extracted_size = limits.max_file_size.min(max_total_remaining);
+ let files_start = self.embedded_files.len();
+
+ match local_fileheader.extract_file(
+ cursor,
+ &mut self.embedded_files,
+ max_extracted_size,
+ ) {
+ Ok(()) => {}
+ Err(Error::ScanLimitExceeded(needed)) => {
+ debug!(
+ "ALZ file {:?} exceeded extraction size limits. Scanning truncated content.",
+ local_fileheader.file_name
+ );
+ if needed > limits.max_file_size
+ && self
+ .file_limit_exceeded_size
+ .map_or(true, |current| current < needed)
+ {
+ self.file_limit_exceeded_size = Some(needed);
+ }
+
+ let total_needed = base_extracted_size.saturating_add(needed);
+ if total_needed > limits.max_total_size
+ && self
+ .total_limit_exceeded_size
+ .map_or(true, |current| current < total_needed)
+ {
+ self.total_limit_exceeded_size = Some(total_needed);
+ }
+ }
+ Err(Error::Extract) => {
+ debug!(
+ "Failed to extract ALZ file {:?}. Continuing with next entry.",
+ local_fileheader.file_name
+ );
+ }
+ Err(err) => return Err(err),
+ }
+
+ for file in &self.embedded_files[files_start..] {
+ let Ok(file_size) = u64::try_from(file.data.len()) else {
+ self.extracted_size = u64::MAX;
+ break;
+ };
+ self.extracted_size = self.extracted_size.saturating_add(file_size);
}
}
- true
+ Ok(())
}
#[allow(clippy::unused_self)]
@@ -503,16 +684,39 @@ impl<'aa> Alz {
pub const fn new() -> Self {
Self {
embedded_files: Vec::new(),
+ file_limit_exceeded_size: None,
+ total_limit_exceeded_size: None,
+ extracted_size: 0,
+ parse_error: false,
}
}
+ pub const fn has_parse_error(&self) -> bool {
+ self.parse_error
+ }
+
/// # Errors
/// Will return `Error::Parse` if file headers are not correct or are inconsistent.
pub fn from_bytes(bytes: &'aa [u8]) -> Result<Self, Error> {
+ Self::from_bytes_with_filter(bytes, |_| {
+ Some(AlzExtractionLimits {
+ max_file_size: u64::MAX,
+ max_total_size: u64::MAX,
+ })
+ })
+ }
+
+ /// # Errors
+ /// Will return `Error::Parse` if file headers are not correct or are inconsistent.
+ pub fn from_bytes_with_filter<F>(bytes: &'aa [u8], mut should_extract: F) -> Result<Self, Error>
+ where
+ F: FnMut(&AlzFileMetadata<'_>) -> Option<AlzExtractionLimits>,
+ {
let binding = bytes.to_vec();
let mut cursor = Cursor::new(&binding);
let mut alz: Self = Self::new();
+ let mut filepos: usize = 1;
if !alz.is_alz(&mut cursor) {
return Err(Error::Parse("No ALZ file header"));
@@ -531,9 +735,21 @@ impl<'aa> Alz {
match sig {
ALZ_LOCAL_FILE_HEADER => {
- if alz.parse_local_fileheader(&mut cursor) {
- continue;
+ match alz.parse_local_fileheader(&mut cursor, &mut filepos, &mut should_extract)
+ {
+ Ok(()) => {}
+ Err(Error::Alloc) => return Err(Error::Alloc),
+ Err(err) => {
+ if filepos == 1 {
+ return Err(err);
+ }
+
+ debug!("Failed to parse ALZ local file header: {err}");
+ alz.parse_error = true;
+ break;
+ }
}
+ continue;
}
ALZ_CENTRAL_DIRECTORY_HEADER => {
if alz.parse_central_directoryheader(&mut cursor) {
@@ -546,7 +762,14 @@ impl<'aa> Alz {
}
_ => {
#[allow(clippy::uninlined_format_args)]
- return Err(Error::UnrecognizedSig(format!("{:x}", sig)));
+ let err = Error::UnrecognizedSig(format!("{:x}", sig));
+ if filepos == 1 {
+ return Err(err);
+ }
+
+ debug!("Failed to parse ALZ archive: {err}");
+ alz.parse_error = true;
+ break;
}
}
}
@@ -554,3 +777,226 @@ impl<'aa> Alz {
Ok(alz)
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::io::Write;
+
+ fn append_local_file(
+ alz: &mut Vec<u8>,
+ name: &str,
+ compression_method: u8,
+ uncompressed_size: u8,
+ data: &[u8],
+ ) {
+ append_local_entry(
+ alz,
+ name,
+ AlzFileAttribute::File as u8,
+ 0x10,
+ compression_method,
+ uncompressed_size,
+ data,
+ );
+ }
+
+ fn append_local_entry(
+ alz: &mut Vec<u8>,
+ name: &str,
+ file_attribute: u8,
+ file_descriptor: u8,
+ compression_method: u8,
+ uncompressed_size: u8,
+ data: &[u8],
+ ) {
+ let name = name.as_bytes();
+ let name_len = u16::try_from(name.len()).unwrap();
+ let compressed_size = u8::try_from(data.len()).unwrap();
+
+ alz.extend_from_slice(&ALZ_LOCAL_FILE_HEADER.to_le_bytes());
+ alz.extend_from_slice(&name_len.to_le_bytes());
+ alz.push(file_attribute);
+ alz.extend_from_slice(&0u32.to_le_bytes());
+ alz.push(file_descriptor);
+ alz.push(0);
+ alz.push(compression_method);
+ alz.push(0);
+ alz.extend_from_slice(&0u32.to_le_bytes());
+ alz.push(compressed_size);
+ alz.push(uncompressed_size);
+ alz.extend_from_slice(name);
+ if file_descriptor & 0x01 != 0 {
+ alz.extend_from_slice(&[0; ALZ_ENCR_HEADER_LEN as usize]);
+ }
+ alz.extend_from_slice(data);
+ }
+
+ fn extraction_limits() -> AlzExtractionLimits {
+ AlzExtractionLimits {
+ max_file_size: u64::MAX,
+ max_total_size: u64::MAX,
+ }
+ }
+
+ fn raw_deflate(data: &[u8]) -> Vec<u8> {
+ let mut encoder =
+ flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default());
+ encoder.write_all(data).unwrap();
+ encoder.finish().unwrap()
+ }
+
+ #[test]
+ fn extraction_error_does_not_stop_later_entries() {
+ const ALZ_COMP_NOCOMP: u8 = 0;
+ const ALZ_COMP_BZIP2: u8 = 1;
+
+ let mut bytes = Vec::new();
+ bytes.extend_from_slice(&ALZ_FILE_HEADER.to_le_bytes());
+ bytes.extend_from_slice(&0u32.to_le_bytes());
+ append_local_file(&mut bytes, "bad.bz2", ALZ_COMP_BZIP2, 1, &[0]);
+ append_local_file(&mut bytes, "good.txt", ALZ_COMP_NOCOMP, 4, b"good");
+ bytes.extend_from_slice(&ALZ_END_OF_CENTRAL_DIRECTORY_HEADER.to_le_bytes());
+
+ let alz = Alz::from_bytes_with_filter(&bytes, |_| Some(extraction_limits())).unwrap();
+
+ assert_eq!(alz.embedded_files.len(), 1);
+ assert_eq!(alz.embedded_files[0].name.as_deref(), Some("good.txt"));
+ assert_eq!(alz.embedded_files[0].data, b"good");
+ }
+
+ #[test]
+ fn later_parse_error_preserves_earlier_extracted_entries() {
+ const ALZ_COMP_NOCOMP: u8 = 0;
+
+ let mut bytes = Vec::new();
+ bytes.extend_from_slice(&ALZ_FILE_HEADER.to_le_bytes());
+ bytes.extend_from_slice(&0u32.to_le_bytes());
+ append_local_file(&mut bytes, "good.txt", ALZ_COMP_NOCOMP, 4, b"good");
+ bytes.extend_from_slice(&ALZ_LOCAL_FILE_HEADER.to_le_bytes());
+
+ let alz = Alz::from_bytes_with_filter(&bytes, |_| Some(extraction_limits())).unwrap();
+
+ assert!(alz.has_parse_error());
+ assert_eq!(alz.embedded_files.len(), 1);
+ assert_eq!(alz.embedded_files[0].name.as_deref(), Some("good.txt"));
+ assert_eq!(alz.embedded_files[0].data, b"good");
+ }
+
+ #[test]
+ fn deflate_limit_uses_decompressed_size_not_header() {
+ const ALZ_COMP_DEFLATE: u8 = 2;
+ let payload = vec![b'A'; 1024];
+ let compressed = raw_deflate(&payload);
+ assert!(compressed.len() <= u8::MAX.into());
+
+ let mut bytes = Vec::new();
+ bytes.extend_from_slice(&ALZ_FILE_HEADER.to_le_bytes());
+ bytes.extend_from_slice(&0u32.to_le_bytes());
+ append_local_file(&mut bytes, "big.txt", ALZ_COMP_DEFLATE, 1, &compressed);
+ bytes.extend_from_slice(&ALZ_END_OF_CENTRAL_DIRECTORY_HEADER.to_le_bytes());
+
+ let alz = Alz::from_bytes_with_filter(&bytes, |_| {
+ Some(AlzExtractionLimits {
+ max_file_size: 64,
+ max_total_size: u64::MAX,
+ })
+ })
+ .unwrap();
+
+ assert_eq!(alz.file_limit_exceeded_size, Some(payload.len() as u64));
+ assert_eq!(alz.embedded_files.len(), 1);
+ assert_eq!(alz.embedded_files[0].name.as_deref(), Some("big.txt"));
+ assert_eq!(alz.embedded_files[0].data.as_slice(), &payload[..64]);
+ }
+
+ #[test]
+ fn total_limit_does_not_set_per_file_limit() {
+ const ALZ_COMP_NOCOMP: u8 = 0;
+ let payload = vec![b'A'; 60];
+
+ let mut bytes = Vec::new();
+ bytes.extend_from_slice(&ALZ_FILE_HEADER.to_le_bytes());
+ bytes.extend_from_slice(&0u32.to_le_bytes());
+ append_local_file(
+ &mut bytes,
+ "a",
+ ALZ_COMP_NOCOMP,
+ payload.len() as u8,
+ &payload,
+ );
+ append_local_file(
+ &mut bytes,
+ "b",
+ ALZ_COMP_NOCOMP,
+ payload.len() as u8,
+ &payload,
+ );
+ bytes.extend_from_slice(&ALZ_END_OF_CENTRAL_DIRECTORY_HEADER.to_le_bytes());
+
+ let alz = Alz::from_bytes_with_filter(&bytes, |_| {
+ Some(AlzExtractionLimits {
+ max_file_size: 64,
+ max_total_size: 100,
+ })
+ })
+ .unwrap();
+
+ assert_eq!(alz.file_limit_exceeded_size, None);
+ assert_eq!(alz.total_limit_exceeded_size, Some(120));
+ assert_eq!(alz.embedded_files.len(), 2);
+ assert_eq!(alz.embedded_files[0].data.len(), 60);
+ assert_eq!(alz.embedded_files[1].data.len(), 40);
+ }
+
+ #[test]
+ fn metadata_file_positions_are_one_based_and_include_skipped_entries() {
+ const ALZ_COMP_NOCOMP: u8 = 0;
+
+ let mut bytes = Vec::new();
+ bytes.extend_from_slice(&ALZ_FILE_HEADER.to_le_bytes());
+ bytes.extend_from_slice(&0u32.to_le_bytes());
+ append_local_entry(
+ &mut bytes,
+ "dir/",
+ AlzFileAttribute::Directory as u8,
+ 0x10,
+ ALZ_COMP_NOCOMP,
+ 0,
+ b"",
+ );
+ append_local_entry(
+ &mut bytes,
+ "encrypted.bin",
+ AlzFileAttribute::File as u8,
+ 0x11,
+ ALZ_COMP_NOCOMP,
+ 0,
+ b"",
+ );
+ append_local_file(&mut bytes, "first.txt", ALZ_COMP_NOCOMP, 5, b"first");
+ append_local_file(&mut bytes, "second.txt", ALZ_COMP_NOCOMP, 6, b"second");
+ bytes.extend_from_slice(&ALZ_END_OF_CENTRAL_DIRECTORY_HEADER.to_le_bytes());
+
+ let mut metadata_positions = Vec::new();
+ let alz = Alz::from_bytes_with_filter(&bytes, |metadata| {
+ metadata_positions.push((
+ metadata.file_name.to_owned(),
+ metadata.filepos,
+ metadata.is_encrypted,
+ ));
+ None
+ })
+ .unwrap();
+
+ assert!(alz.embedded_files.is_empty());
+ assert_eq!(
+ metadata_positions,
+ vec![
+ ("encrypted.bin".to_owned(), 2, true),
+ ("first.txt".to_owned(), 3, false),
+ ("second.txt".to_owned(), 4, false),
+ ]
+ );
+ }
+}
diff --git a/libclamav_rust/src/scanners.rs b/libclamav_rust/src/scanners.rs
index be8c52acd1..862b0f46e1 100644
--- a/libclamav_rust/src/scanners.rs
+++ b/libclamav_rust/src/scanners.rs
@@ -21,6 +21,7 @@
*/
use std::{
+ convert::TryFrom,
ffi::{c_char, CString},
io::Read,
panic,
@@ -33,14 +34,17 @@ use libc::c_void;
use log::{debug, error, warn};
use crate::{
- alz::Alz,
+ alz::{Alz, AlzExtractionLimits, Error as AlzError},
ctx,
onenote::OneNote,
sys::{
- cl_error_t, cl_error_t_CL_EFORMAT, cl_error_t_CL_ERROR, cl_error_t_CL_SUCCESS, cli_ctx,
+ cl_error_t, cl_error_t_CL_EFORMAT, cl_error_t_CL_EMAXSIZE, cl_error_t_CL_EMEM,
+ cl_error_t_CL_ERROR, cl_error_t_CL_SUCCESS, cl_error_t_CL_VIRUS, cli_ctx,
cli_magic_scan_buff,
},
- util::{check_scan_limits, scan_archive_metadata},
+ util::{
+ append_potentially_unwanted_if_heur_exceedsmax, check_scan_limits, scan_archive_metadata,
+ },
};
/// Rust wrapper of libclamav's cli_magic_scan_buff() function.
@@ -307,6 +311,30 @@ pub unsafe extern "C" fn scan_lha_lzh(ctx: *mut cli_ctx) -> cl_error_t {
cl_error_t_CL_SUCCESS
}
+unsafe fn alz_extraction_limits(ctx: *mut cli_ctx) -> AlzExtractionLimits {
+ if ctx.is_null() || (*ctx).engine.is_null() {
+ return AlzExtractionLimits {
+ max_file_size: u64::MAX,
+ max_total_size: u64::MAX,
+ };
+ }
+
+ let engine = &*(*ctx).engine;
+
+ AlzExtractionLimits {
+ max_file_size: if engine.maxfilesize == 0 {
+ u64::MAX
+ } else {
+ engine.maxfilesize
+ },
+ max_total_size: if engine.maxscansize == 0 {
+ u64::MAX
+ } else {
+ engine.maxscansize.saturating_sub((*ctx).scansize)
+ },
+ }
+}
+
/// Scan an Alz file for attachments
///
/// # Safety
@@ -333,14 +361,89 @@ pub unsafe extern "C" fn cli_scanalz(ctx: *mut cli_ctx) -> cl_error_t {
}
};
- let alz = match Alz::from_bytes(file_bytes) {
- Ok(x) => x,
- Err(err) => {
- error!("Failed to parse Alz file: {}", err.to_string());
- return cl_error_t_CL_ERROR;
+ let mut alz_metadata_ret = cl_error_t_CL_SUCCESS;
+ let alz_result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
+ Alz::from_bytes_with_filter(file_bytes, |metadata| {
+ if check_scan_limits("ALZ", ctx, 0, 0, 0) != cl_error_t_CL_SUCCESS {
+ debug!("Exceeded scan limits. Bailing out.");
+ return None;
+ }
+
+ match (
+ usize::try_from(metadata.compressed_size),
+ usize::try_from(metadata.uncompressed_size),
+ ) {
+ (Ok(compressed_size), Ok(uncompressed_size)) => {
+ let metadata_ret = scan_archive_metadata(
+ ctx,
+ metadata.file_name,
+ compressed_size,
+ uncompressed_size,
+ metadata.is_encrypted,
+ metadata.filepos,
+ metadata.file_crc as i32,
+ );
+ if metadata_ret == cl_error_t_CL_VIRUS {
+ alz_metadata_ret = metadata_ret;
+ debug!(
+ "ALZ file {:?} metadata did not pass scan checks. Skipping extraction.",
+ metadata.file_name
+ );
+ return None;
+ } else if metadata_ret != cl_error_t_CL_SUCCESS {
+ debug!(
+ "ALZ file {:?} metadata scan failed with {}. Continuing extraction.",
+ metadata.file_name, metadata_ret
+ );
+ }
+ }
+ _ => debug!(
+ "ALZ file {:?} metadata sizes are too large for this platform. Continuing extraction.",
+ metadata.file_name
+ ),
+ }
+
+ Some(alz_extraction_limits(ctx))
+ })
+ }));
+
+ let alz = match alz_result {
+ Ok(Ok(x)) => x,
+ Ok(Err(AlzError::Alloc)) => {
+ debug!("Failed to allocate memory when parsing ALZ archive");
+ return cl_error_t_CL_EMEM;
+ }
+ Ok(Err(err)) => {
+ debug!("Failed to parse Alz file: {}", err.to_string());
+ return cl_error_t_CL_EFORMAT;
+ }
+ Err(_) => {
+ debug!("Panic occurred when trying to parse ALZ archive");
+ return cl_error_t_CL_EFORMAT;
}
};
+ if alz_metadata_ret != cl_error_t_CL_SUCCESS {
+ return alz_metadata_ret;
+ }
+
+ if let Some(needed) = alz.file_limit_exceeded_size {
+ let ret = check_scan_limits("ALZ", ctx, needed, 0, 0);
+ if ret != cl_error_t_CL_SUCCESS && ret != cl_error_t_CL_EMAXSIZE {
+ return ret;
+ }
+ }
+
+ if alz.total_limit_exceeded_size.is_some() {
+ let ret = append_potentially_unwanted_if_heur_exceedsmax(
+ ctx,
+ "Heuristics.Limits.Exceeded.MaxScanSize",
+ );
+ if ret != cl_error_t_CL_SUCCESS {
+ return ret;
+ }
+ }
+
for i in 0..alz.embedded_files.len() {
let ret = magic_scan(
ctx,
@@ -352,5 +455,9 @@ pub unsafe extern "C" fn cli_scanalz(ctx: *mut cli_ctx) -> cl_error_t {
}
}
+ if alz.has_parse_error() {
+ return cl_error_t_CL_EFORMAT;
+ }
+
cl_error_t_CL_SUCCESS
}
diff --git a/libclamav_rust/src/sys.rs b/libclamav_rust/src/sys.rs
index 399ec438a4..9e36a11709 100644
--- a/libclamav_rust/src/sys.rs
+++ b/libclamav_rust/src/sys.rs
@@ -795,6 +795,12 @@ extern "C" {
virname: *const ::std::os::raw::c_char,
) -> cl_error_t;
}
+extern "C" {
+ pub fn cli_append_potentially_unwanted_if_heur_exceedsmax(
+ ctx: *mut cli_ctx,
+ vname: *mut ::std::os::raw::c_char,
+ );
+}
extern "C" {
pub fn cli_warnmsg(str_: *const ::std::os::raw::c_char, ...);
}
diff --git a/libclamav_rust/src/util.rs b/libclamav_rust/src/util.rs
index 086fdf2786..5a459919f3 100644
--- a/libclamav_rust/src/util.rs
+++ b/libclamav_rust/src/util.rs
@@ -22,7 +22,7 @@
use std::{ffi::CStr, fs::File};
-use log::error;
+use log::{debug, error};
use crate::sys;
@@ -85,7 +85,7 @@ pub unsafe fn check_scan_limits(
let module_name = match std::ffi::CString::new(module_name) {
Ok(name) => name,
Err(_) => {
- error!("Invalid module_name: {}", module_name);
+ error!("Invalid module_name: {:?}", module_name);
return sys::cl_error_t_CL_EFORMAT;
}
};
@@ -93,6 +93,31 @@ pub unsafe fn check_scan_limits(
unsafe { sys::cli_checklimits(module_name.as_ptr(), ctx, need1, need2, need3) }
}
+/// Append an exceeds-max heuristic alert or metadata entry.
+///
+/// # Safety
+///
+/// ctx must be a valid pointer to a clamav scan context structure
+///
+pub unsafe fn append_potentially_unwanted_if_heur_exceedsmax(
+ ctx: *mut sys::cli_ctx,
+ virname: &str,
+) -> sys::cl_error_t {
+ let virname = match std::ffi::CString::new(virname) {
+ Ok(name) => name,
+ Err(_) => {
+ error!("Invalid virname: {:?}", virname);
+ return sys::cl_error_t_CL_EFORMAT;
+ }
+ };
+
+ unsafe {
+ sys::cli_append_potentially_unwanted_if_heur_exceedsmax(ctx, virname.as_ptr().cast_mut());
+ }
+
+ sys::cl_error_t_CL_SUCCESS
+}
+
/// Scan archive metadata.
///
/// # Safety
@@ -111,7 +136,7 @@ pub unsafe fn scan_archive_metadata(
let module_name = match std::ffi::CString::new(filename) {
Ok(name) => name,
Err(_) => {
- error!("Invalid module_name: {}", filename);
+ debug!("Invalid archive metadata filename: {:?}", filename);
return sys::cl_error_t_CL_EFORMAT;
}
};
diff --git a/unit_tests/clamscan/alz_test.py b/unit_tests/clamscan/alz_test.py
index 78b294b339..995c4a5156 100644
--- a/unit_tests/clamscan/alz_test.py
+++ b/unit_tests/clamscan/alz_test.py
@@ -4,12 +4,58 @@
Run clamscan tests.
"""
+import hashlib
import sys
+import zlib
sys.path.append('../unit_tests')
import testcase
+ALZ_FILE_HEADER = 0x015A4C41
+ALZ_LOCAL_FILE_HEADER = 0x015A4C42
+ALZ_END_OF_CENTRAL_DIRECTORY_HEADER = 0x025A4C43
+ALZ_ATTR_DIRECTORY = 0x10
+ALZ_ATTR_FILE = 0x20
+ALZ_COMP_NOCOMP = 0
+ALZ_COMP_BZIP2 = 1
+ALZ_COMP_DEFLATE = 2
+ALZ_ENCR_HEADER_LEN = 12
+
+
+def append_alz_file(
+ alz,
+ name,
+ compression_method,
+ uncompressed_size,
+ data,
+ file_attribute=ALZ_ATTR_FILE,
+ file_descriptor=0x10,
+):
+ name = name.encode('utf-8')
+
+ alz.extend(ALZ_LOCAL_FILE_HEADER.to_bytes(4, 'little'))
+ alz.extend(len(name).to_bytes(2, 'little'))
+ alz.append(file_attribute)
+ alz.extend((0).to_bytes(4, 'little')) # file time/date
+ alz.append(file_descriptor)
+ alz.append(0) # unknown
+ alz.append(compression_method)
+ alz.append(0) # unknown
+ alz.extend((0).to_bytes(4, 'little')) # crc
+ alz.append(len(data))
+ alz.append(uncompressed_size)
+ alz.extend(name)
+ if file_descriptor & 0x01:
+ alz.extend(b'\x00' * ALZ_ENCR_HEADER_LEN)
+ alz.extend(data)
+
+
+def raw_deflate(data):
+ compressor = zlib.compressobj(wbits=-15)
+ return compressor.compress(data) + compressor.flush()
+
+
class TC(testcase.TestCase):
@classmethod
def setUpClass(cls):
@@ -116,6 +162,281 @@ def test_uncompressed_with_binary(self):
]
self.verify_output(output.out, expected=expected_results)
+ def test_extraction_error_does_not_abort_archive_scan(self):
+ self.step_name('Test alz scan continues after a malformed member')
+
+ payload = b'alz-later-member'
+ testfile = TC.path_tmp / 'bad-then-good.alz'
+ path_db = TC.path_tmp / 'bad-then-good.hdb'
+
+ alz = bytearray()
+ alz.extend(ALZ_FILE_HEADER.to_bytes(4, 'little'))
+ alz.extend((0).to_bytes(4, 'little'))
+ append_alz_file(alz, 'bad.bz2', ALZ_COMP_BZIP2, 1, b'\x00')
+ append_alz_file(alz, 'good.txt', ALZ_COMP_NOCOMP, len(payload), payload)
+ alz.extend(ALZ_END_OF_CENTRAL_DIRECTORY_HEADER.to_bytes(4, 'little'))
+
+ testfile.write_bytes(alz)
+ path_db.write_text(
+ '{}:{}:ALZ_LATER_FILE\n'.format(hashlib.sha1(payload).hexdigest(), len(payload))
+ )
+
+ command = '{valgrind} {valgrind_args} {clamscan} -d {path_db} {testfile}'.format(
+ valgrind=TC.valgrind, valgrind_args=TC.valgrind_args, clamscan=TC.clamscan,
+ path_db=path_db,
+ testfile=testfile,
+ )
+ output = self.execute_command(command)
+
+ assert output.ec == 1 # virus
+
+ expected_results = [
+ 'ALZ_LATER_FILE.UNOFFICIAL FOUND',
+ ]
+ self.verify_output(output.out, expected=expected_results)
+
+ def test_later_parse_error_does_not_skip_earlier_member_scan(self):
+ self.step_name('Test alz scan checks earlier members before later parse errors')
+
+ payload = b'alz-earlier-member'
+ testfile = TC.path_tmp / 'good-then-bad-header.alz'
+ path_db = TC.path_tmp / 'good-then-bad-header.hdb'
+
+ alz = bytearray()
+ alz.extend(ALZ_FILE_HEADER.to_bytes(4, 'little'))
+ alz.extend((0).to_bytes(4, 'little'))
+ append_alz_file(alz, 'good.txt', ALZ_COMP_NOCOMP, len(payload), payload)
+ alz.extend(ALZ_LOCAL_FILE_HEADER.to_bytes(4, 'little'))
+
+ testfile.write_bytes(alz)
+ path_db.write_text(
+ '{}:{}:ALZ_EARLIER_FILE\n'.format(hashlib.sha1(payload).hexdigest(), len(payload))
+ )
+
+ command = '{valgrind} {valgrind_args} {clamscan} -d {path_db} {testfile}'.format(
+ valgrind=TC.valgrind, valgrind_args=TC.valgrind_args, clamscan=TC.clamscan,
+ path_db=path_db,
+ testfile=testfile,
+ )
+ output = self.execute_command(command)
+
+ assert output.ec == 1 # virus
+
+ expected_results = [
+ 'ALZ_EARLIER_FILE.UNOFFICIAL FOUND',
+ ]
+ self.verify_output(output.out, expected=expected_results)
+
+ def test_deflate_limit_uses_decompressed_size(self):
+ self.step_name('Test alz deflate scan limits use decompressed output size')
+
+ truncated_payload = b'A' * 64
+ payload = b'A' * 4096
+ compressed = raw_deflate(payload)
+ assert len(compressed) <= 255
+
+ testfile = TC.path_tmp / 'deflate-limit.alz'
+ path_db = TC.path_tmp / 'deflate-limit.hdb'
+
+ alz = bytearray()
+ alz.extend(ALZ_FILE_HEADER.to_bytes(4, 'little'))
+ alz.extend((0).to_bytes(4, 'little'))
+ append_alz_file(alz, 'x', ALZ_COMP_DEFLATE, 1, compressed)
+ alz.extend(ALZ_END_OF_CENTRAL_DIRECTORY_HEADER.to_bytes(4, 'little'))
+ assert len(alz) <= 64
+
+ testfile.write_bytes(alz)
+ path_db.write_text(
+ '{}:{}:ALZ_DEFLATE_LIMIT\n'.format(
+ hashlib.sha1(truncated_payload).hexdigest(),
+ len(truncated_payload),
+ )
+ )
+ command = (
+ '{valgrind} {valgrind_args} {clamscan} --max-filesize=64 '
+ '--max-scansize=4096 -d {path_db} {testfile}'
+ ).format(
+ valgrind=TC.valgrind,
+ valgrind_args=TC.valgrind_args,
+ clamscan=TC.clamscan,
+ path_db=path_db,
+ testfile=testfile,
+ )
+ output = self.execute_command(command)
+
+ assert output.ec == 1 # virus
+
+ expected_results = [
+ 'ALZ_DEFLATE_LIMIT.UNOFFICIAL FOUND',
+ ]
+ self.verify_output(output.out, expected=expected_results)
+ def test_inflated_header_size_does_not_skip_extraction(self):
+ self.step_name('Test alz scan ignores inflated header size for extraction gating')
+
+ payload = b'alz-small-hit'
+ testfile = TC.path_tmp / 'inflated-header-size.alz'
+ path_db = TC.path_tmp / 'inflated-header-size.hdb'
+
+ alz = bytearray()
+ alz.extend(ALZ_FILE_HEADER.to_bytes(4, 'little'))
+ alz.extend((0).to_bytes(4, 'little'))
+ append_alz_file(alz, 'x', ALZ_COMP_NOCOMP, 80, payload)
+ alz.extend(ALZ_END_OF_CENTRAL_DIRECTORY_HEADER.to_bytes(4, 'little'))
+
+ testfile.write_bytes(alz)
+ path_db.write_text(
+ '{}:{}:ALZ_INFLATED_HEADER_SIZE\n'.format(
+ hashlib.sha1(payload).hexdigest(),
+ len(payload),
+ )
+ )
+ command = (
+ '{valgrind} {valgrind_args} {clamscan} --max-filesize=64 '
+ '--max-scansize=1024 -d {path_db} {testfile}'
+ ).format(
+ valgrind=TC.valgrind,
+ valgrind_args=TC.valgrind_args,
+ clamscan=TC.clamscan,
+ path_db=path_db,
+ testfile=testfile,
+ )
+ output = self.execute_command(command)
+
+ assert output.ec == 1 # virus
+
+ expected_results = [
+ 'ALZ_INFLATED_HEADER_SIZE.UNOFFICIAL FOUND',
+ ]
+ self.verify_output(output.out, expected=expected_results)
+
+ def test_total_limit_does_not_report_max_file_size(self):
+ self.step_name('Test alz archive total limit is not reported as max file size')
+
+ payload = b'A' * 60
+ testfile = TC.path_tmp / 'total-limit.alz'
+ path_db = TC.path_tmp / 'total-limit.hdb'
+
+ alz = bytearray()
+ alz.extend(ALZ_FILE_HEADER.to_bytes(4, 'little'))
+ alz.extend((0).to_bytes(4, 'little'))
+ append_alz_file(alz, 'a', ALZ_COMP_NOCOMP, len(payload), payload)
+ append_alz_file(alz, 'b', ALZ_COMP_NOCOMP, len(payload), payload)
+ alz.extend(ALZ_END_OF_CENTRAL_DIRECTORY_HEADER.to_bytes(4, 'little'))
+ assert len(alz) <= 200
+
+ testfile.write_bytes(alz)
+ path_db.write_text(
+ '{}:{}:ALZ_UNUSED\n'.format(
+ hashlib.sha1(b'not-present').hexdigest(),
+ len(b'not-present'),
+ )
+ )
+
+ command = (
+ '{valgrind} {valgrind_args} {clamscan} --alert-exceeds-max=yes '
+ '--max-filesize=200 --max-scansize=100 -d {path_db} {testfile}'
+ ).format(
+ valgrind=TC.valgrind,
+ valgrind_args=TC.valgrind_args,
+ clamscan=TC.clamscan,
+ path_db=path_db,
+ testfile=testfile,
+ )
+ output = self.execute_command(command)
+
+ assert output.ec == 1 # virus
+
+ expected_results = [
+ 'Heuristics.Limits.Exceeded.MaxScanSize FOUND',
+ ]
+ unexpected_results = [
+ 'Heuristics.Limits.Exceeded.MaxFileSize FOUND',
+ ]
+ self.verify_output(output.out, expected=expected_results, unexpected=unexpected_results)
+
+ def test_metadata_filepos_counts_skipped_entries_from_one(self):
+ self.step_name('Test alz metadata file position counts skipped entries from one')
+
+ payload = b'alz-filepos-member'
+ testfile = TC.path_tmp / 'filepos-skipped.alz'
+ path_db = TC.path_tmp / 'filepos-skipped.cdb'
+
+ alz = bytearray()
+ alz.extend(ALZ_FILE_HEADER.to_bytes(4, 'little'))
+ alz.extend((0).to_bytes(4, 'little'))
+ append_alz_file(
+ alz,
+ 'dir/',
+ ALZ_COMP_NOCOMP,
+ 0,
+ b'',
+ file_attribute=ALZ_ATTR_DIRECTORY,
+ )
+ append_alz_file(
+ alz,
+ 'encrypted.bin',
+ ALZ_COMP_NOCOMP,
+ 0,
+ b'',
+ file_descriptor=0x11,
+ )
+ append_alz_file(alz, 'good.txt', ALZ_COMP_NOCOMP, len(payload), payload)
+ alz.extend(ALZ_END_OF_CENTRAL_DIRECTORY_HEADER.to_bytes(4, 'little'))
+
+ testfile.write_bytes(alz)
+ path_db.write_text('ALZ_FILEPOS_TEST:CL_TYPE_ALZ:*:good.txt:*:*:0:3:*:*\n')
+
+ command = '{valgrind} {valgrind_args} {clamscan} -d {path_db} {testfile}'.format(
+ valgrind=TC.valgrind, valgrind_args=TC.valgrind_args, clamscan=TC.clamscan,
+ path_db=path_db,
+ testfile=testfile,
+ )
+ output = self.execute_command(command)
+
+ assert output.ec == 1 # virus
+
+ expected_results = [
+ 'ALZ_FILEPOS_TEST.UNOFFICIAL FOUND',
+ ]
+ self.verify_output(output.out, expected=expected_results)
+
+ def test_encrypted_metadata_matches_before_extraction_skip(self):
+ self.step_name('Test alz encrypted entries are matched before extraction skip')
+
+ testfile = TC.path_tmp / 'encrypted-metadata.alz'
+ path_db = TC.path_tmp / 'encrypted-metadata.cdb'
+
+ alz = bytearray()
+ alz.extend(ALZ_FILE_HEADER.to_bytes(4, 'little'))
+ alz.extend((0).to_bytes(4, 'little'))
+ append_alz_file(
+ alz,
+ 'encrypted.bin',
+ ALZ_COMP_NOCOMP,
+ 0,
+ b'',
+ file_descriptor=0x11,
+ )
+ alz.extend(ALZ_END_OF_CENTRAL_DIRECTORY_HEADER.to_bytes(4, 'little'))
+
+ testfile.write_bytes(alz)
+ path_db.write_text(
+ 'ALZ_ENCRYPTED_METADATA_TEST:CL_TYPE_ALZ:*:encrypted.bin:*:*:1:1:*:*\n'
+ )
+
+ command = '{valgrind} {valgrind_args} {clamscan} -d {path_db} {testfile}'.format(
+ valgrind=TC.valgrind, valgrind_args=TC.valgrind_args, clamscan=TC.clamscan,
+ path_db=path_db,
+ testfile=testfile,
+ )
+ output = self.execute_command(command)
+
+ assert output.ec == 1 # virus
+
+ expected_results = [
+ 'ALZ_ENCRYPTED_METADATA_TEST.UNOFFICIAL FOUND',
+ ]
+ self.verify_output(output.out, expected=expected_results)