Fix H1: bound untrusted array counts; resolve L1 (review #006)

- Replace the three Vec::with_capacity(count) sites in materialize.rs
  with Vec::new() — validate_bytes on an adversarial count no longer
  OOM-aborts the process (AGENTS.md §3).
- New compile-time caps in schema.rs: MAX_ARRAY_ELEMENTS (2^16,
  enforced at BastArray::parse — the choke point every consumer
  inherits, bounds the walkers' per-element entry loops) and
  MAX_ARRAY_BYTES (2^26, enforced per walker against the mode-specific
  stride: compile_array, walk_array, compute_array_field).
- L1: fixed_composite_size/fixed_plan_size now return
  Result<Option<usize>>; unwrap_or_default() gone, overflow is a clean
  Schema error instead of silent stride-0.
- Zero-progress guard: stride-0 arrays whose elements consume 0 bytes
  (legal empty-struct elements) now error in plan_walk_variable_array_
  size and materialize_array_packed instead of looping count times.
- Tests: 8 new (parse/build/compile rejections, cap boundary,
  short-buffer clean error) + array_count_large_u64_parses_on_64bit
  rewritten to assert the new cap rejection. In-tree tests assert only
  the safe (compile-time) half per review #006's Methodology warning.
- Review #006 updated: H1/L1 resolution blocks, new finding N2
  (unbounded align annotations, found while re-deriving the cap
  arithmetic), resolution log, recommended order.

Verified: 482 tests green (405+17+34+14+12, 2 pre-existing ignored),
clippy -D warnings clean, wasm32-unknown-unknown build green.
This commit is contained in:
glm-5.3-flash committed 2026-09-02 16:46:46 +00:00
1 parent 27be01af93
commit 2d166f567b
8 files changed
+453 -49

No files matched your search

+21 -11
View File
@@ -35,7 +35,9 @@
//! is used for any offset/count cast (AGENTS.md §4).
use crate::error::AlkTypeError;
use crate::schema::{AlkTypeKind, Endian, VariableEncoding};
use crate::schema::{
AlkTypeKind, Endian, VariableEncoding, MAX_ARRAY_ELEMENTS,
};
use serde_json::Value;
const DEFS_KEY: &str = "$defs";
@@ -851,6 +853,13 @@ impl BastArray {
"bast: array at {path} has no `count` (variable-length arrays are not supported in v1, D-BAST-004)"
))
})?;
if count > MAX_ARRAY_ELEMENTS {
return Err(AlkTypeError::Schema(format!(
"bast: array at {path} declares count {count}, which exceeds the \
compile-time limit of {MAX_ARRAY_ELEMENTS} elements (untrusted schemas \
must not be able to request unbounded per-element expansion)"
)));
}
Ok(Self {
element: Box::new(element),
count,
@@ -1750,7 +1759,10 @@ mod tests {
}
#[test]
fn array_count_large_u64_parses_on_64bit() {
fn array_count_large_u64_rejected_by_compile_cap() {
// Pre-0.3.1 this parsed u64::MAX verbatim (H1: the count fed
// unbounded per-element expansion downstream). The cap now
// rejects it at parse, before any walker sees the array.
let root = json!({
"$defs": {
"S": { "kind": "struct", "fields": [
@@ -1758,14 +1770,12 @@ mod tests {
] }
}
});
let d = BastDoc::new(&root, "S").expect("parses on usize>=u64 targets");
let s = match d.root_def().kind() {
BastDefKind::Struct(s) => s,
_ => unreachable!(),
};
match s.fields()[0].ty() {
BastType::Array(a) => assert_eq!(a.count(), usize::try_from(u64::MAX).unwrap()),
other => panic!("expected Array, got {other:?}"),
let err = BastDoc::new(&root, "S").unwrap_err();
match err {
AlkTypeError::Schema(reason) => {
assert!(reason.contains("compile-time limit"), "reason: {reason}");
}
other => panic!("expected Schema error, got {other:?}"),
}
}
}
}