Skip to content

Commit b0f9bb1

Browse files
hyperpolymathclaude
andcommitted
fix(integrity): bounded_read_config helper for solver manifest
Add src/rust/integrity/io.rs — sync bounded read for operator-controlled TOML manifest paths, capped at 1 MiB (MAX_CONFIG_BYTES). Same principle as provers/io.rs but sync rather than async. Wire into integrity/mod.rs (pub mod io; pub use io::bounded_read_config) and replace the bare std::fs::read_to_string call at solver_integrity.rs:85 with super::io::bounded_read_config. Closes the 1 remaining UnboundedAllocation in src/rust/ flagged by panic-attack. Two unit tests (small_config_reads_fully, oversized_config_errors) — both pass; lib total 670/0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a253bb1 commit b0f9bb1

3 files changed

Lines changed: 66 additions & 1 deletion

File tree

src/rust/integrity/io.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
4+
//! Sync bounded read for operator-controlled config files.
5+
//!
6+
//! Solver manifest paths are operator-controlled TOML files (not user-supplied
7+
//! proof files), so a sync read is appropriate. The same bound principle
8+
//! as [`crate::provers::io::bounded_read_proof_file`] applies: cap at
9+
//! [`MAX_CONFIG_BYTES`] and error rather than truncate.
10+
11+
use anyhow::{Context, Result};
12+
use std::path::Path;
13+
14+
/// Maximum byte length for a solver manifest config read (1 MiB).
15+
pub const MAX_CONFIG_BYTES: u64 = 1024 * 1024;
16+
17+
/// Read a UTF-8 config file (e.g. solver manifest TOML), bounded at [`MAX_CONFIG_BYTES`].
18+
///
19+
/// Uses `File::take(N+1)` so an oversized file is detected in a single
20+
/// read pass — no TOCTOU race against `metadata().len()`.
21+
pub fn bounded_read_config<P: AsRef<Path>>(path: P) -> Result<String> {
22+
use std::io::Read;
23+
let path = path.as_ref();
24+
let file = std::fs::File::open(path)
25+
.with_context(|| format!("opening config {}", path.display()))?;
26+
let mut buf = String::new();
27+
let mut limited = file.take(MAX_CONFIG_BYTES + 1);
28+
limited
29+
.read_to_string(&mut buf)
30+
.with_context(|| format!("reading config {}", path.display()))?;
31+
if buf.len() as u64 > MAX_CONFIG_BYTES {
32+
return Err(anyhow::anyhow!(
33+
"config file {} exceeds {} byte cap",
34+
path.display(),
35+
MAX_CONFIG_BYTES
36+
));
37+
}
38+
Ok(buf)
39+
}
40+
41+
#[cfg(test)]
42+
mod tests {
43+
use super::*;
44+
45+
#[test]
46+
fn small_config_reads_fully() {
47+
let dir = tempfile::tempdir().unwrap();
48+
let path = dir.path().join("solvers.toml");
49+
std::fs::write(&path, b"[solvers]\nz3 = \"/usr/bin/z3\"\n").unwrap();
50+
let s = bounded_read_config(&path).unwrap();
51+
assert!(s.contains("solvers"));
52+
}
53+
54+
#[test]
55+
fn oversized_config_errors() {
56+
let dir = tempfile::tempdir().unwrap();
57+
let path = dir.path().join("big.toml");
58+
let big = vec![b'x'; (MAX_CONFIG_BYTES + 1024) as usize];
59+
std::fs::write(&path, &big).unwrap();
60+
let err = bounded_read_config(&path).unwrap_err();
61+
assert!(err.to_string().contains("exceeds"));
62+
}
63+
}

src/rust/integrity/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
//! known-good manifest. If a mismatch is detected, the solver is marked as
88
//! untrusted and will not be used.
99
10+
pub mod io;
1011
pub mod solver_integrity;
1112

13+
pub use io::bounded_read_config;
1214
pub use solver_integrity::{
1315
IntegrityChecker, IntegrityStatus, SolverIntegrityReport, SolverManifest, SolverManifestEntry,
1416
};

src/rust/integrity/solver_integrity.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ pub struct IntegrityChecker {
8383
impl IntegrityChecker {
8484
/// Create a new integrity checker from a manifest file
8585
pub fn from_manifest_file(path: &Path) -> Result<Self> {
86-
let content = std::fs::read_to_string(path)
86+
let content = super::io::bounded_read_config(path)
8787
.with_context(|| format!("Failed to read solver manifest: {}", path.display()))?;
8888
let manifest: SolverManifest =
8989
toml::from_str(&content).with_context(|| "Failed to parse solver manifest TOML")?;

0 commit comments

Comments
 (0)