Skip to content

Disk-space pre-extraction check uses 1.1× compressed size; decompression-bomb ratio bypasses it #454

Description

@forkwright

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-196calculate_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.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions