clawhdf5-remote: no overflow on lengths near u64::MAX

A server can claim any length in Content-Range. block_len computed
start + block_size, which overflowed in the last blocks of a file claimed
to be near u64::MAX (a panic in debug builds, a wrapped value in
release); insert() multiplied block indices unchecked. The cache's block
arithmetic is now saturating/checked, and a run that does not split into
whole blocks is an error instead of an endless loop or a slice panic.

The test server gains fake_total (claim a length, serve zeros past the
data); a test reads the last bytes of such files and opens a file whose
superblock EOF and root addresses sit near u64::MAX.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 18:24:48 -05:00
co-authored by Claude Opus 5.5
parent 5062b907bd
commit e8aaf050be
3 changed files with 118 additions and 10 deletions
+23 -2
View File
@@ -48,6 +48,10 @@ pub struct Shared {
pub fail_next: AtomicU32,
/// Sleep this long before answering each request.
pub delay_ms: AtomicU64,
/// When non-zero, claim the file is this long (in `Content-Range` and
/// when checking ranges) and serve zeros past its real end: a hostile
/// server lying about the length.
pub fake_total: AtomicU64,
/// Requests for a served path (every status); requests for other
/// paths are not counted.
pub requests: AtomicU64,
@@ -262,7 +266,8 @@ fn serve(conn: TcpStream, s: &Shared) -> std::io::Result<()> {
)?;
continue;
}
let len = data.len() as u64;
let fake = s.fake_total.load(Ordering::SeqCst);
let len = if fake > 0 { fake } else { data.len() as u64 };
let mut validators = String::new();
if s.weak_etag.load(Ordering::SeqCst) {
validators = format!("ETag: W/{etag}\r\n");
@@ -291,6 +296,7 @@ fn serve(conn: TcpStream, s: &Shared) -> std::io::Result<()> {
.unwrap()
.push((path.clone(), range.and_then(Result::ok)));
}
let mut padded = Vec::new();
let (status, body, extra) = match range {
Some(Err(())) => {
write!(
@@ -302,7 +308,7 @@ fn serve(conn: TcpStream, s: &Shared) -> std::io::Result<()> {
}
Some(Ok((a, b))) => (
"206 Partial Content",
&data[a as usize..=b as usize],
slice_or_zeros(&data, a, b, &mut padded),
format!("Content-Range: bytes {a}-{b}/{len}\r\n"),
),
None => ("200 OK", &data[..], String::new()),
@@ -342,3 +348,18 @@ fn serve(conn: TcpStream, s: &Shared) -> std::io::Result<()> {
}
}
}
/// `data[a..=b]`, padded with zeros past its end (into `padded`).
fn slice_or_zeros<'a>(data: &'a [u8], a: u64, b: u64, padded: &'a mut Vec<u8>) -> &'a [u8] {
let real = data.len() as u64;
if b < real {
return &data[a as usize..=b as usize];
}
let n = usize::try_from(b - a + 1).expect("range fits in memory");
padded.resize(n, 0);
if a < real {
let have = (real - a) as usize;
padded[..have].copy_from_slice(&data[a as usize..]);
}
padded
}
+33
View File
@@ -544,3 +544,36 @@ fn requests_for_other_paths_are_not_counted() {
assert_eq!(server.requests(), 1, "only the open counts");
assert_eq!(server.log().len(), 1);
}
/// A server claiming a length near `u64::MAX` (and serving zeros past the
/// real data), with a file whose addresses point at the end of that range:
/// clean errors or zeros, never an arithmetic overflow (a panic in debug).
#[test]
fn a_server_claiming_a_huge_length_does_not_overflow() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let mut bytes =
std::fs::read(root.join("../clawhdf5-format/tests/fixtures/legacy/h5ex_g_iterate.h5"))
.unwrap();
// Superblock v0: end-of-file address, then the root symbol table
// entry's object header, B-tree and heap addresses.
bytes[0x28..0x30].copy_from_slice(&(u64::MAX - 1).to_le_bytes());
for at in [0x40, 0x50, 0x58] {
bytes[at..at + 8].copy_from_slice(&0xFFFF_FFFF_FFFF_F000u64.to_le_bytes());
}
let server = Server::start(vec![("/h.h5".into(), bytes)]);
for total in [u64::MAX, u64::MAX - 1, 1 << 62] {
server.shared.fake_total.store(total, Ordering::SeqCst);
let url = server.url("/h.h5");
let storage = storage_for_url(&url, &quick()).unwrap();
assert_eq!(clawhdf5_format::storage::Storage::len(&*storage), total);
for (off, n) in [(total - 100, 50), (total - 10, 100), (total - 1, 1)] {
let got = clawhdf5_format::storage::Storage::read_at(&*storage, off, n).unwrap();
assert_eq!(got.len() as u64, (total - off).min(n as u64));
assert!(got.iter().all(|&b| b == 0));
}
if let Ok(f) = File::open_storage(storage) {
let _ = transcript(&f);
}
let _ = open_url_with(&url, &quick()).map(|f| transcript(&f));
}
}