security: Tier 3 — Android JNI length validation, pyo3 bump, WAL caps
CI / test (push) Failing after 2s
CI / test (push) Failing after 2s
- clawhdf5-android: validate embedding_len/query_embedding_len against the handle's configured embedding_dim (and reject null pointers) before constructing a slice via from_raw_parts in edgehdf5_save and edgehdf5_hybrid_search. Strengthen the # Safety docs to state the now-enforced invariant and its limits. Add unit tests covering mismatched length and null-pointer rejection. - clawhdf5-py: bump pyo3/numpy 0.28 -> 0.29, clearing RUSTSEC-2026-0176 (OOB read in PyList/PyTuple iterator) and RUSTSEC-2026-0177 (missing Sync bound on PyCFunction::new_closure). No source changes needed; confirmed via cargo audit that both advisories no longer appear. - clawhdf5-agent/wal.rs: cap read_len_prefixed_str/read_embedding's length claims at a new MAX_WAL_FIELD_LEN (64 MiB) before allocating, so a corrupted/truncated WAL length field fails cleanly instead of attempting a huge allocation. Add regression tests for both. - BENCHMARKS.md: add a top-of-file traceability note distinguishing the dated/hardware-cited/reproducible h5bench and tank-validation sections from the older sections that don't yet meet that bar.
This commit is contained in:
@@ -12,6 +12,11 @@ use crate::MemoryError;
|
||||
const WAL_MAGIC: [u8; 4] = [0x45, 0x48, 0x57, 0x4C]; // "EHWL"
|
||||
const WAL_VERSION: u8 = 1;
|
||||
|
||||
/// Upper bound on a single length-prefixed WAL field (string bytes, or
|
||||
/// embedding element count), to reject a corrupted/truncated WAL length
|
||||
/// claim before allocating a large buffer for it.
|
||||
const MAX_WAL_FIELD_LEN: usize = 64 * 1024 * 1024;
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WalEntryType {
|
||||
@@ -349,6 +354,11 @@ fn read_len_prefixed_str(f: &mut File) -> Result<String, MemoryError> {
|
||||
let mut len_buf = [0u8; 4];
|
||||
f.read_exact(&mut len_buf)?;
|
||||
let len = u32::from_le_bytes(len_buf) as usize;
|
||||
if len > MAX_WAL_FIELD_LEN {
|
||||
return Err(MemoryError::Schema(format!(
|
||||
"WAL string field length {len} exceeds max {MAX_WAL_FIELD_LEN}"
|
||||
)));
|
||||
}
|
||||
let mut buf = vec![0u8; len];
|
||||
f.read_exact(&mut buf)?;
|
||||
String::from_utf8(buf).map_err(|e| MemoryError::Schema(format!("invalid UTF-8 in WAL: {e}")))
|
||||
@@ -358,6 +368,12 @@ fn read_embedding(f: &mut File) -> Result<Vec<f32>, MemoryError> {
|
||||
let mut len_buf = [0u8; 4];
|
||||
f.read_exact(&mut len_buf)?;
|
||||
let count = u32::from_le_bytes(len_buf) as usize;
|
||||
if count > MAX_WAL_FIELD_LEN / 4 {
|
||||
return Err(MemoryError::Schema(format!(
|
||||
"WAL embedding element count {count} exceeds max {}",
|
||||
MAX_WAL_FIELD_LEN / 4
|
||||
)));
|
||||
}
|
||||
let mut vals = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let mut val_buf = [0u8; 4];
|
||||
@@ -427,6 +443,40 @@ mod tests {
|
||||
assert_eq!(entries[2].embedding, vec![5.0, 6.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_len_prefixed_str_rejects_oversized_len_claim() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("oversized_str.bin");
|
||||
{
|
||||
let mut f = File::create(&path).unwrap();
|
||||
// Claim a length far beyond MAX_WAL_FIELD_LEN; no payload follows.
|
||||
f.write_all(&(u32::MAX).to_le_bytes()).unwrap();
|
||||
}
|
||||
let mut f = File::open(&path).unwrap();
|
||||
let result = read_len_prefixed_str(&mut f);
|
||||
assert!(
|
||||
matches!(result, Err(MemoryError::Schema(_))),
|
||||
"expected a clean Schema error, got {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_embedding_rejects_oversized_count_claim() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("oversized_embedding.bin");
|
||||
{
|
||||
let mut f = File::create(&path).unwrap();
|
||||
// Claim a count far beyond MAX_WAL_FIELD_LEN / 4; no payload follows.
|
||||
f.write_all(&(u32::MAX).to_le_bytes()).unwrap();
|
||||
}
|
||||
let mut f = File::open(&path).unwrap();
|
||||
let result = read_embedding(&mut f);
|
||||
assert!(
|
||||
matches!(result, Err(MemoryError::Schema(_))),
|
||||
"expected a clean Schema error, got {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wal_truncate() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user