From 1f057ce3e3adf804d9b9b2e5f33a6fdeb3dc1edf Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 26 May 2026 11:21:53 +0100 Subject: [PATCH] fix: bound three unbounded file reads (resolves self-scan Critical) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-scanning panic-attack with the latest release surfaced a Critical UnboundedAllocation in attestation/mod.rs and, on closer inspection, two sibling sites the analyzer's per-file heuristic glossed over. All three follow the established pattern of every other read in the codebase (File::open + .take(LIMIT) + .read_to_string) and were the residual violations of the "every file read must be capacity-bounded" invariant. Fixes: 1. src/attestation/mod.rs::verify_attestation_file — was reading the attestation envelope with fs::read_to_string. JSON envelopes are small (well under 1 MiB legitimately); capped at 16 MiB. 2. src/assemblyline.rs::load_cache_file — was reading the fingerprint cache JSON with fs::read_to_string. Caches grow with estate size; capped at 256 MiB (large enough for multi-thousand-repo rollups). 3. src/assail/mod.rs::load_user_classifications — was reading the user-classification a2ml registry with fs::read_to_string. Hand- edited audit files; capped at 4 MiB. Verification: - cargo build / clippy --all-targets --features signing,http -D warnings — clean - cargo test --bin panic-attack --features signing,http — 236 passed, 0 failed - cargo fmt --check — clean - self-scan before: 12 findings (1 Critical UnboundedAllocation in attestation) - self-scan after: 11 findings (Critical resolved; residual findings are intentional — examples/vulnerable_program.rs unsafe blocks, test unwraps, etc.) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/assail/mod.rs | 20 +++++++++++++++++--- src/assemblyline.rs | 15 +++++++++++++-- src/attestation/mod.rs | 15 +++++++++++++-- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/assail/mod.rs b/src/assail/mod.rs index 0f985d1..ee63268 100644 --- a/src/assail/mod.rs +++ b/src/assail/mod.rs @@ -225,11 +225,25 @@ pub fn load_user_classifications(project_root: &Path) -> Vec .join("assail-classifications.a2ml"), project_root.join(".panic-attack-classifications.a2ml"), ]; + // User-classification a2ml files are hand-edited audit registries. A + // legitimate one rarely exceeds a few dozen KiB. Capping at 4 MiB + // stops a malicious or accidental input from exhausting memory during + // a multi-thousand-repo mass-panic sweep. + use std::io::Read; + const CLASSIFICATIONS_FILE_READ_LIMIT: u64 = 4 * 1024 * 1024; + let mut content = String::new(); for p in &candidate_paths { - if let Ok(c) = fs::read_to_string(p) { - content = c; - break; + if let Ok(mut f) = fs::File::open(p) { + let mut buf = String::new(); + if (&mut f) + .take(CLASSIFICATIONS_FILE_READ_LIMIT) + .read_to_string(&mut buf) + .is_ok() + { + content = buf; + break; + } } } if content.is_empty() { diff --git a/src/assemblyline.rs b/src/assemblyline.rs index 9749ef4..e2481c9 100644 --- a/src/assemblyline.rs +++ b/src/assemblyline.rs @@ -115,9 +115,20 @@ impl FingerprintCache { Ok(()) } - /// Load fingerprint cache from a standalone cache JSON file + /// Load fingerprint cache from a standalone cache JSON file. + /// + /// Bounded at 256 MiB — fingerprint caches grow with the size of the + /// estate being scanned, but even an aggressive multi-thousand-repo + /// rollup should land well under this cap. A larger file is almost + /// certainly corrupted or hostile. pub fn load_cache_file(path: &Path) -> Result { - let content = fs::read_to_string(path)?; + use std::io::Read; + const CACHE_FILE_READ_LIMIT: u64 = 256 * 1024 * 1024; + + let mut content = String::new(); + fs::File::open(path)? + .take(CACHE_FILE_READ_LIMIT) + .read_to_string(&mut content)?; let cache: Self = serde_json::from_str(&content)?; Ok(cache) } diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index 075b902..154ec9a 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -67,8 +67,19 @@ pub enum VerifyResult { /// with a list of failure reasons otherwise. pub fn verify_attestation_file(path: &std::path::Path) -> anyhow::Result { use sha2::{Digest, Sha256}; - - let content = std::fs::read_to_string(path) + use std::io::Read; + + // Attestation envelopes are JSON. They embed three hashes plus a small + // signature; legitimate envelopes are well under 1 MiB. Capping at + // 16 MiB stops a malicious or accidental input from exhausting memory + // before verification can even begin. + const ATTESTATION_FILE_READ_LIMIT: u64 = 16 * 1024 * 1024; + + let mut content = String::new(); + std::fs::File::open(path) + .map_err(|e| anyhow::anyhow!("opening {}: {}", path.display(), e))? + .take(ATTESTATION_FILE_READ_LIMIT) + .read_to_string(&mut content) .map_err(|e| anyhow::anyhow!("reading {}: {}", path.display(), e))?; let envelope: A2mlEnvelope = serde_json::from_str(&content)