use super::SIGNATURE_HEADER_SIZE;
use super::header::BlockIndex;
use super::header::SevenZHeader;
use crate::ArchiveError;
use crate::EntryCompression;
use crate::PolicySetting;
use crate::Result;
use crate::SecurityConfig;
use crate::SolidBlock;
use crate::config::Validated;
use crate::error::QuotaResource;
use crate::formats::common;
use crate::inspection::report::IssueCategory;
use crate::inspection::report::IssueSeverity;
use crate::inspection::report::UnverifiedReason;
use crate::inspection::report::VerificationIssue;
use crate::security::zipbomb::DeclaredCompressedBudget;
use sevenz_rust2::BlockDecoder;
use sevenz_rust2::Password;
use std::io;
pub(super) fn packed_stream_count(block: &sevenz_rust2::Block) -> usize {
use sevenz_rust2::EncoderMethod;
let bcj2 = block
.coders
.iter()
.filter(|coder| coder.encoder_method_id() == EncoderMethod::ID_BCJ2)
.count();
1 + 3 * bcj2
}
pub fn sevenz_entry_compression(
header: &SevenZHeader,
source_len: u64,
) -> Result<Vec<EntryCompression>> {
let archive = header.archive();
let data_region = source_len
.checked_sub(SIGNATURE_HEADER_SIZE)
.and_then(|len| len.checked_sub(archive.pack_pos()))
.ok_or_else(|| {
ArchiveError::InvalidArchive("declared compressed size exceeds archive length".into())
})?;
let mut budget = DeclaredCompressedBudget::new(data_region);
for &pack_size in archive.pack_sizes() {
budget.charge(pack_size)?;
}
let pack_sizes = archive.pack_sizes();
let block_compressed = |block: BlockIndex| -> Result<u64> {
let start = header.block_first_pack(block);
let end = header.next_block_first_pack(block).unwrap_or_else(|| {
start
.saturating_add(packed_stream_count(header.block(block)))
.min(pack_sizes.len())
});
pack_sizes[start..end]
.iter()
.try_fold(0u64, |acc, &size| acc.checked_add(size))
.ok_or_else(|| ArchiveError::InvalidArchive("7z pack sizes overflow".into()))
};
let mut block_uncompressed = vec![0u64; header.block_count()];
let mut block_files = vec![0usize; header.block_count()];
for (index, file) in header.files().iter().enumerate() {
if let Some(block) = header.file_block(index) {
block_files[block.get()] += 1;
block_uncompressed[block.get()] = block_uncompressed[block.get()]
.checked_add(file.size)
.ok_or(ArchiveError::QuotaExceeded {
resource: QuotaResource::IntegerOverflow,
})?;
}
}
header
.files()
.iter()
.enumerate()
.map(|(index, file)| {
let Some(block) = header.file_block(index) else {
return Ok(EntryCompression::Unknown);
};
if !file.has_stream {
return Ok(EntryCompression::Unknown);
}
let compressed = block_compressed(block)?;
if block_files[block.get()] <= 1 {
return Ok(EntryCompression::Entry(compressed));
}
if header.block_first_file(block) == index {
Ok(EntryCompression::SolidBlockStart(SolidBlock::new(
compressed,
block_uncompressed[block.get()],
)))
} else {
Ok(EntryCompression::SolidBlockMember)
}
})
.collect()
}
#[derive(Debug)]
pub(super) struct Refused {
pub(super) reason: UnverifiedReason,
pub(super) error: ArchiveError,
}
impl Refused {
const fn new(reason: UnverifiedReason, error: ArchiveError) -> Self {
Self { reason, error }
}
}
pub(super) fn check_solid_policy(
is_solid: bool,
sizes: impl IntoIterator<Item = u64>,
config: &SecurityConfig<Validated>,
) -> std::result::Result<(), Refused> {
if !is_solid {
return Ok(());
}
if !config.allow_solid_archives {
return Err(Refused::new(
UnverifiedReason::SolidPolicy,
ArchiveError::relaxable_violation(
PolicySetting::SolidArchives,
"solid 7z archives are not allowed (enable allow_solid_archives)".into(),
),
));
}
let total_uncompressed = sizes
.into_iter()
.try_fold(0u64, u64::checked_add)
.ok_or_else(|| {
Refused::new(
UnverifiedReason::ResourceLimit,
ArchiveError::QuotaExceeded {
resource: QuotaResource::IntegerOverflow,
},
)
})?;
if total_uncompressed > config.max_solid_block_memory {
return Err(Refused::new(
UnverifiedReason::ResourceLimit,
ArchiveError::QuotaExceeded {
resource: QuotaResource::TotalSize {
current: total_uncompressed,
max: config.max_solid_block_memory,
},
},
));
}
Ok(())
}
pub(super) const BZIP2_DECODER_MEMORY: u64 = 4 * 1024 * 1024;
pub(super) fn lzma2_dict_size(props: &[u8]) -> Option<u32> {
let bits = u32::from(*props.first()?);
match bits {
0..=39 => Some((2 | (bits & 1)) << (bits / 2 + 11)),
40 => Some(u32::MAX),
_ => None,
}
}
pub(super) fn coder_memory(coder: &sevenz_rust2::Coder, unpack_size: u64) -> Option<u64> {
use sevenz_rust2::EncoderMethod;
let props = coder.properties();
let window = |dict: u64| dict.min(unpack_size.saturating_mul(2));
match coder.encoder_method_id() {
EncoderMethod::ID_COPY
| EncoderMethod::ID_DELTA
| EncoderMethod::ID_BCJ_X86
| EncoderMethod::ID_BCJ_PPC
| EncoderMethod::ID_BCJ_IA64
| EncoderMethod::ID_BCJ_ARM
| EncoderMethod::ID_BCJ_ARM64
| EncoderMethod::ID_BCJ_ARM_THUMB
| EncoderMethod::ID_BCJ_SPARC
| EncoderMethod::ID_BCJ_RISCV
| EncoderMethod::ID_BCJ2
| EncoderMethod::ID_DEFLATE => Some(0),
EncoderMethod::ID_LZMA => {
let dict = props.get(1..)?.first_chunk::<4>()?;
Some(window(u64::from(u32::from_le_bytes(*dict))))
}
EncoderMethod::ID_LZMA2 => Some(window(u64::from(lzma2_dict_size(props)?))),
EncoderMethod::ID_PPMD => {
let mem = props.get(1..)?.first_chunk::<4>()?;
Some(u64::from(u32::from_le_bytes(*mem)))
}
EncoderMethod::ID_BZIP2 => Some(BZIP2_DECODER_MEMORY),
_ => None,
}
}
#[derive(Debug, Default, Clone, Copy)]
pub(super) struct BlockShape {
declared_size: u64,
interleaved_entry: bool,
span_mismatch: bool,
}
pub(super) fn block_span(header: &SevenZHeader, block: BlockIndex) -> std::ops::Range<usize> {
let password = Password::empty();
let mut no_source = io::Cursor::new(&[][..]);
let count = BlockDecoder::new(1, block.get(), header.archive(), &password, &mut no_source)
.entry_count();
let first = header.block_first_file(block);
first..first.saturating_add(count)
}
pub(super) fn block_shapes(header: &SevenZHeader) -> Vec<BlockShape> {
let mut shapes = vec![BlockShape::default(); header.block_count()];
let spans: Vec<_> = header
.block_indices()
.map(|block| block_span(header, block))
.collect();
let mut mapped = vec![0usize; header.block_count()];
let mut outside_span = vec![false; header.block_count()];
for (index, file) in header.files().iter().enumerate() {
if let Some(block) = header.file_block(index) {
let block = block.get();
let shape = &mut shapes[block];
shape.declared_size = shape.declared_size.saturating_add(file.size);
shape.interleaved_entry |= !file.has_stream;
mapped[block] += 1;
outside_span[block] |= !spans[block].contains(&index);
}
}
for (block, shape) in shapes.iter_mut().enumerate() {
shape.span_mismatch = !spans[block].is_empty()
&& (outside_span[block] || mapped[block] != spans[block].len());
}
shapes
}
pub(super) fn block_charge(block: &sevenz_rust2::Block) -> u64 {
block
.coders
.iter()
.map(|coder| block.get_unpack_size_for_coder(coder))
.max()
.unwrap_or_default()
}
pub(super) fn admit_sevenz_block(
block: &sevenz_rust2::Block,
shape: BlockShape,
config: &SecurityConfig<Validated>,
) -> std::result::Result<(), Refused> {
let not_decodable = |error| Refused::new(UnverifiedReason::NotDecodable, error);
let output = block_charge(block).max(shape.declared_size);
let mut memory: u64 = 0;
for coder in &block.coders {
if coder.encoder_method_id() == sevenz_rust2::EncoderMethod::ID_AES256_SHA256 {
return Err(not_decodable(common::encrypted_archive_error()));
}
let needed = coder_memory(coder, output).ok_or_else(|| {
not_decodable(ArchiveError::InvalidArchive(
"7z block uses an unsupported coder or malformed coder properties".into(),
))
})?;
memory = memory.saturating_add(needed);
}
if memory > config.max_decoder_memory {
return Err(Refused::new(
UnverifiedReason::ResourceLimit,
ArchiveError::QuotaExceeded {
resource: QuotaResource::DecoderMemory {
max: config.max_decoder_memory,
},
},
));
}
if shape.interleaved_entry {
return Err(not_decodable(ArchiveError::InvalidArchive(
"7z block interleaves a stream-less entry, so a file of the block would be skipped"
.into(),
)));
}
if shape.span_mismatch {
return Err(not_decodable(ArchiveError::InvalidArchive(
"7z block decodes files other than the ones mapped to it".into(),
)));
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefusalScope {
Archive,
Block(BlockIndex),
}
#[derive(Debug)]
pub struct SevenZRefusal {
pub(super) scope: RefusalScope,
unverified: UnverifiedReason,
pub(super) error: ArchiveError,
}
impl SevenZRefusal {
fn new(scope: RefusalScope, refused: Refused) -> Self {
Self {
scope,
unverified: refused.reason,
error: refused.error,
}
}
pub(super) const fn unverified(&self) -> UnverifiedReason {
self.unverified
}
pub(crate) fn into_error(self) -> ArchiveError {
self.error
}
}
pub fn iter_sevenz_refusals<'a>(
header: &'a SevenZHeader,
config: &'a SecurityConfig<Validated>,
) -> impl Iterator<Item = SevenZRefusal> + 'a {
let solid = check_solid_policy(
header.is_solid(),
header.files().iter().map(|f| f.size),
config,
)
.err()
.map(|refused| SevenZRefusal::new(RefusalScope::Archive, refused));
let blocks =
header
.block_indices()
.zip(block_shapes(header))
.filter_map(move |(index, shape)| {
admit_sevenz_block(header.block(index), shape, config)
.err()
.map(|refused| SevenZRefusal::new(RefusalScope::Block(index), refused))
});
solid.into_iter().chain(blocks)
}
pub fn sevenz_refusals(
header: &SevenZHeader,
config: &SecurityConfig<Validated>,
) -> Vec<SevenZRefusal> {
iter_sevenz_refusals(header, config).collect()
}
pub(super) const MAX_REFUSAL_ISSUES: usize = 32;
pub fn refusal_issues(refusals: &[SevenZRefusal]) -> Vec<VerificationIssue> {
let mut issues: Vec<_> = refusals
.iter()
.take(MAX_REFUSAL_ISSUES)
.map(|r| VerificationIssue::from_error(&r.error, None))
.collect();
if let Some(omitted) = refusals.len().checked_sub(MAX_REFUSAL_ISSUES)
&& omitted > 0
{
issues.push(VerificationIssue::new(
IssueSeverity::Info,
IssueCategory::InvalidArchive,
format!("{omitted} further 7z refusals are not listed"),
));
}
issues
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::assert_matches;
fn config(allow_solid: bool, max_solid_memory: u64) -> SecurityConfig<Validated> {
SecurityConfig::default()
.with_allow_solid_archives(allow_solid)
.with_max_solid_block_memory(max_solid_memory)
.validate()
.unwrap()
}
#[test]
fn solid_policy_refusal_carries_its_own_reason() {
let refused = check_solid_policy(true, [1], &config(false, 100)).unwrap_err();
assert_eq!(refused.reason, UnverifiedReason::SolidPolicy);
assert_matches!(refused.error, ArchiveError::SecurityViolation { .. });
}
#[test]
fn solid_size_overflow_is_an_integer_overflow() {
let refused = check_solid_policy(true, [u64::MAX, 1], &config(true, 100)).unwrap_err();
assert_eq!(refused.reason, UnverifiedReason::ResourceLimit);
assert_matches!(
refused.error,
ArchiveError::QuotaExceeded {
resource: QuotaResource::IntegerOverflow
}
);
}
#[test]
fn solid_size_over_the_limit_is_a_resource_limit() {
let refused = check_solid_policy(true, [60, 60], &config(true, 100)).unwrap_err();
assert_eq!(refused.reason, UnverifiedReason::ResourceLimit);
assert_matches!(
refused.error,
ArchiveError::QuotaExceeded {
resource: QuotaResource::TotalSize {
current: 120,
max: 100
}
}
);
}
#[test]
fn non_solid_archive_passes_the_policy() {
assert!(check_solid_policy(false, [u64::MAX, 1], &config(false, 100)).is_ok());
}
}