Skip to content

Commit 6941ef7

Browse files
hyperpolymathclaude
andcommitted
fix(bounds): .take(LIMIT) on external-input reads (batch 1/N)
Self-scan found 14 UnboundedAllocation criticals in panic-attack's own src/ — all TPs by detector semantics. This batch bounds the three highest-risk reads (external or user-supplied input, biggest attack surface) using the same pattern applied to 007-lang: bridge/reachability.rs — Rust source file scan (64 MiB cap) bridge/intelligence.rs — OSV API HTTP response body (256 MiB for success, 64 KiB for error-body; uses ureq v3's Body::with_config().limit()) amuck/mod.rs — mutation target source (64 MiB cap) and spec JSON/YAML (4 MiB cap) Self-scan after this batch: UnboundedAllocation 14 -> 11 critical findings (confirmed the three flagged files are now disarmed). Remaining 11: self-produced reports, config files, /proc metadata, and the analyzer's own manifest-file reads — all lower-risk but still on the sweep list for the next batch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4c1c128 commit 6941ef7

3 files changed

Lines changed: 70 additions & 10 deletions

File tree

src/amuck/mod.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,22 @@
44
55
use anyhow::{anyhow, Context, Result};
66
use serde::{Deserialize, Serialize};
7-
use std::fs;
7+
use std::fs::{self, File};
8+
use std::io::Read;
89
use std::path::{Path, PathBuf};
910
use std::process::{Command, Stdio};
1011
use std::time::Instant;
1112

13+
/// Upper bound on mutation-target source file reads. Amuck operates on a
14+
/// single source file at a time; 64 MiB is well beyond any realistic
15+
/// target and prevents a hostile or runaway input from exhausting memory.
16+
const TARGET_SOURCE_READ_LIMIT: u64 = 64 * 1024 * 1024;
17+
18+
/// Upper bound on mutation spec JSON/YAML reads. Specs are curated,
19+
/// short documents — 4 MiB is two orders of magnitude beyond realistic
20+
/// spec size and still catches a malformed or hostile config.
21+
const SPEC_FILE_READ_LIMIT: u64 = 4 * 1024 * 1024;
22+
1223
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1324
pub enum AmuckPreset {
1425
Light,
@@ -114,9 +125,18 @@ pub fn run(config: AmuckConfig) -> Result<AmuckReport> {
114125
));
115126
}
116127

117-
// Source text is loaded once and each combo is applied from the pristine baseline.
118-
let source = fs::read_to_string(&config.target)
119-
.with_context(|| format!("reading target file {}", config.target.display()))?;
128+
// Source text is loaded once and each combo is applied from the pristine
129+
// baseline. Cap to TARGET_SOURCE_READ_LIMIT — a larger file is truncated
130+
// rather than swallowed whole, and mutations apply to that prefix only.
131+
let source = {
132+
let mut buf = String::new();
133+
File::open(&config.target)
134+
.with_context(|| format!("opening target file {}", config.target.display()))?
135+
.take(TARGET_SOURCE_READ_LIMIT)
136+
.read_to_string(&mut buf)
137+
.with_context(|| format!("reading target file {}", config.target.display()))?;
138+
buf
139+
};
120140

121141
let mut combos = if let Some(spec_path) = &config.spec_path {
122142
let spec = load_spec(spec_path)?;
@@ -281,8 +301,16 @@ fn mutation_path(target: &Path, output_dir: &Path, id: usize) -> PathBuf {
281301
}
282302

283303
fn load_spec(path: &Path) -> Result<MutationSpecFile> {
284-
let content =
285-
fs::read_to_string(path).with_context(|| format!("reading spec {}", path.display()))?;
304+
// Cap spec reads at SPEC_FILE_READ_LIMIT — specs are short curated docs.
305+
let content = {
306+
let mut buf = String::new();
307+
File::open(path)
308+
.with_context(|| format!("opening spec {}", path.display()))?
309+
.take(SPEC_FILE_READ_LIMIT)
310+
.read_to_string(&mut buf)
311+
.with_context(|| format!("reading spec {}", path.display()))?;
312+
buf
313+
};
286314
let spec =
287315
match path.extension().and_then(|ext| ext.to_str()) {
288316
Some("json") => serde_json::from_str(&content)

src/bridge/intelligence.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,26 @@ pub fn query_osv_batch(deps: &[LockedDependency]) -> Result<Vec<Vulnerability>>
143143

144144
let status = resp.status().as_u16();
145145
if !(200..300).contains(&status) {
146-
let buf = resp.body_mut().read_to_string().unwrap_or_default();
146+
// Cap error-body reads at 64 KiB — for error paths we only
147+
// need enough context to report, not the full OSV error blob.
148+
let buf = resp
149+
.body_mut()
150+
.with_config()
151+
.limit(64 * 1024)
152+
.read_to_string()
153+
.unwrap_or_default();
147154
anyhow::bail!("OSV API returned HTTP {}: {}", status, buf);
148155
}
149156

150-
let response_text = resp.body_mut().read_to_string()?;
157+
// Cap success-body reads at 256 MiB. OSV batch responses for
158+
// ~1000-dep projects run a few MiB at most; 256 MiB is an order
159+
// of magnitude beyond any realistic response and prevents a
160+
// misbehaving or hostile OSV endpoint from exhausting memory.
161+
let response_text = resp
162+
.body_mut()
163+
.with_config()
164+
.limit(256 * 1024 * 1024)
165+
.read_to_string()?;
151166
let response: OsvBatchResponse = serde_json::from_str(&response_text)?;
152167

153168
// Map OSV results back to dependencies

src/bridge/reachability.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,17 @@
1111
1212
use super::{ImportSite, ReachabilityEvidence, ReachabilityStatus};
1313
use anyhow::Result;
14+
use std::fs::File;
15+
use std::io::Read;
1416
use std::path::Path;
1517
use walkdir::WalkDir;
1618

19+
/// Upper bound on single-file reads during reachability scans.
20+
/// Rust source files are almost always well under 16 MiB; capping at 64 MiB
21+
/// prevents a pathological or malicious input (e.g. a minified vendor blob
22+
/// masquerading as .rs) from exhausting memory during a mass-panic sweep.
23+
const SOURCE_FILE_READ_LIMIT: u64 = 64 * 1024 * 1024;
24+
1725
/// Check whether a crate is actually imported in the project's Rust source files.
1826
///
1927
/// Scans all .rs files under `project_dir` for patterns that indicate the
@@ -50,8 +58,17 @@ pub fn check_reachability(project_dir: &Path, crate_name: &str) -> Result<Reacha
5058
continue;
5159
}
5260

53-
// Read file and scan for import patterns
54-
let content = match std::fs::read_to_string(path) {
61+
// Read file and scan for import patterns. Cap per-file size to
62+
// avoid an arbitrarily-large file consuming memory; take(N) is
63+
// an upper bound, not a guarantee the file is <= N bytes — if a
64+
// file is larger we silently truncate to the first N bytes and
65+
// scan that prefix (imports live at the top of a Rust file).
66+
let content = match File::open(path).and_then(|f| {
67+
let mut buf = String::new();
68+
f.take(SOURCE_FILE_READ_LIMIT)
69+
.read_to_string(&mut buf)
70+
.map(|_| buf)
71+
}) {
5572
Ok(c) => c,
5673
Err(_) => continue, // Skip unreadable files (binary, encoding issues)
5774
};

0 commit comments

Comments
 (0)