Finding
check_disk_space estimates the space required for extraction as archive_size * 1.1, where archive_size is the sum of the compressed on-disk sizes of the archive files. Compression ratios of 10:1 to 1000:1 are routine, so a hostile or pathological archive that is 100 MB compressed but expands to 10 GB passes the needed = 110 MB check and then exhausts the disk during extraction. The same needed computation also misbehaves at the upper bound: (archive_size as f64 * 1.1) as u64 loses precision near u64::MAX, and an archive_size close to u64::MAX produces +Inf, which Rust's saturating float-to-int cast converts to u64::MAX, yielding a spurious InsufficientDiskSpace error for very large but legitimate archives.
Evidence
crates/ergasia/src/extract/pipeline.rs:168-169
let archive_size = calculate_archive_size(download_path);
let needed = (archive_size as f64 * 1.1) as u64;
crates/ergasia/src/extract/pipeline.rs:181-196 — calculate_archive_size sums m.len() (the compressed file size from OS metadata) rather than the declared uncompressed sizes from the archive header:
fn calculate_archive_size(dir: &Path) -> u64 {
let Ok(entries) = std::fs::read_dir(dir) else {
return 0;
};
entries
.flatten()
.filter_map(|e| {
let path = e.path();
if path.is_file() && detect_archive_format(&path).is_some() {
path.metadata().ok().map(|m| m.len())
} else {
None
}
})
.sum()
}
Why this matters
A single crafted archive (e.g. zbsm.zip, 42 kB → 5 GB) trivially defeats the guard. On a sovereign phone OS hardened against a capable adversary, an attacker who can place an archive into the download path can deliberately fill the device's storage, exhausting the system disk and inducing broader service disruption (failed writes across other subsystems, denial of the device's primary functions). Even without an active adversary, extracting onto a near-full disk leaves partial extractions and corrupts media. The bound-precision defect additionally rejects legitimate large archives, an availability failure for the user.
Desired correction
Derive the required space from the declared uncompressed sizes in the archive headers, not the compressed on-disk sizes. Cap the expansion: either (a) sum the entry size fields from the header and reject when the decompression ratio exceeds a configurable maximum (e.g. 100×), or (b) enforce a configurable absolute maximum uncompressed-bytes limit checked against that sum. Replace the float cast with saturating integer arithmetic, e.g. archive_size.saturating_add(archive_size / 10). Done when: a unit test with a mock archive whose header declares 100 GB of uncompressed content from 1 MB of compressed data is rejected before extraction starts, and the bound computation no longer saturates to u64::MAX for archive sizes near u64::MAX.
Finding
check_disk_spaceestimates the space required for extraction asarchive_size * 1.1, wherearchive_sizeis the sum of the compressed on-disk sizes of the archive files. Compression ratios of 10:1 to 1000:1 are routine, so a hostile or pathological archive that is 100 MB compressed but expands to 10 GB passes theneeded = 110 MBcheck and then exhausts the disk during extraction. The sameneededcomputation also misbehaves at the upper bound:(archive_size as f64 * 1.1) as u64loses precision nearu64::MAX, and anarchive_sizeclose tou64::MAXproduces+Inf, which Rust's saturating float-to-int cast converts tou64::MAX, yielding a spuriousInsufficientDiskSpaceerror for very large but legitimate archives.Evidence
crates/ergasia/src/extract/pipeline.rs:168-169crates/ergasia/src/extract/pipeline.rs:181-196—calculate_archive_sizesumsm.len()(the compressed file size from OS metadata) rather than the declared uncompressed sizes from the archive header:Why this matters
A single crafted archive (e.g.
zbsm.zip, 42 kB → 5 GB) trivially defeats the guard. On a sovereign phone OS hardened against a capable adversary, an attacker who can place an archive into the download path can deliberately fill the device's storage, exhausting the system disk and inducing broader service disruption (failed writes across other subsystems, denial of the device's primary functions). Even without an active adversary, extracting onto a near-full disk leaves partial extractions and corrupts media. The bound-precision defect additionally rejects legitimate large archives, an availability failure for the user.Desired correction
Derive the required space from the declared uncompressed sizes in the archive headers, not the compressed on-disk sizes. Cap the expansion: either (a) sum the entry
sizefields from the header and reject when the decompression ratio exceeds a configurable maximum (e.g. 100×), or (b) enforce a configurable absolute maximum uncompressed-bytes limit checked against that sum. Replace the float cast with saturating integer arithmetic, e.g.archive_size.saturating_add(archive_size / 10). Done when: a unit test with a mock archive whose header declares 100 GB of uncompressed content from 1 MB of compressed data is rejected before extraction starts, and the bound computation no longer saturates tou64::MAXfor archive sizes nearu64::MAX.