Skip to content

Commit 1f057ce

Browse files
hyperpolymathclaude
andcommitted
fix: bound three unbounded file reads (resolves self-scan Critical)
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) <noreply@anthropic.com>
1 parent ea02d50 commit 1f057ce

3 files changed

Lines changed: 43 additions & 7 deletions

File tree

src/assail/mod.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,25 @@ pub fn load_user_classifications(project_root: &Path) -> Vec<UserClassification>
225225
.join("assail-classifications.a2ml"),
226226
project_root.join(".panic-attack-classifications.a2ml"),
227227
];
228+
// User-classification a2ml files are hand-edited audit registries. A
229+
// legitimate one rarely exceeds a few dozen KiB. Capping at 4 MiB
230+
// stops a malicious or accidental input from exhausting memory during
231+
// a multi-thousand-repo mass-panic sweep.
232+
use std::io::Read;
233+
const CLASSIFICATIONS_FILE_READ_LIMIT: u64 = 4 * 1024 * 1024;
234+
228235
let mut content = String::new();
229236
for p in &candidate_paths {
230-
if let Ok(c) = fs::read_to_string(p) {
231-
content = c;
232-
break;
237+
if let Ok(mut f) = fs::File::open(p) {
238+
let mut buf = String::new();
239+
if (&mut f)
240+
.take(CLASSIFICATIONS_FILE_READ_LIMIT)
241+
.read_to_string(&mut buf)
242+
.is_ok()
243+
{
244+
content = buf;
245+
break;
246+
}
233247
}
234248
}
235249
if content.is_empty() {

src/assemblyline.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,20 @@ impl FingerprintCache {
115115
Ok(())
116116
}
117117

118-
/// Load fingerprint cache from a standalone cache JSON file
118+
/// Load fingerprint cache from a standalone cache JSON file.
119+
///
120+
/// Bounded at 256 MiB — fingerprint caches grow with the size of the
121+
/// estate being scanned, but even an aggressive multi-thousand-repo
122+
/// rollup should land well under this cap. A larger file is almost
123+
/// certainly corrupted or hostile.
119124
pub fn load_cache_file(path: &Path) -> Result<Self> {
120-
let content = fs::read_to_string(path)?;
125+
use std::io::Read;
126+
const CACHE_FILE_READ_LIMIT: u64 = 256 * 1024 * 1024;
127+
128+
let mut content = String::new();
129+
fs::File::open(path)?
130+
.take(CACHE_FILE_READ_LIMIT)
131+
.read_to_string(&mut content)?;
121132
let cache: Self = serde_json::from_str(&content)?;
122133
Ok(cache)
123134
}

src/attestation/mod.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,19 @@ pub enum VerifyResult {
6767
/// with a list of failure reasons otherwise.
6868
pub fn verify_attestation_file(path: &std::path::Path) -> anyhow::Result<VerifyResult> {
6969
use sha2::{Digest, Sha256};
70-
71-
let content = std::fs::read_to_string(path)
70+
use std::io::Read;
71+
72+
// Attestation envelopes are JSON. They embed three hashes plus a small
73+
// signature; legitimate envelopes are well under 1 MiB. Capping at
74+
// 16 MiB stops a malicious or accidental input from exhausting memory
75+
// before verification can even begin.
76+
const ATTESTATION_FILE_READ_LIMIT: u64 = 16 * 1024 * 1024;
77+
78+
let mut content = String::new();
79+
std::fs::File::open(path)
80+
.map_err(|e| anyhow::anyhow!("opening {}: {}", path.display(), e))?
81+
.take(ATTESTATION_FILE_READ_LIMIT)
82+
.read_to_string(&mut content)
7283
.map_err(|e| anyhow::anyhow!("reading {}: {}", path.display(), e))?;
7384

7485
let envelope: A2mlEnvelope = serde_json::from_str(&content)

0 commit comments

Comments
 (0)