fix: W3-3 — read_field/write_field misread aligned maxLength reservations

The running validate_pair campaign found a third crash: in aligned
mode a maxLength reservation (ADR-003 strategy 2, VARCHAR(N)) stores
RAW zero-padded data with no length prefix — materialize and
validate_bytes implement exactly that — but read_field read the entry
through data_access::read_string, i.e. parsed the window's first four
bytes as a u32 length prefix. Raw reservation bytes that look like a
large prefix then fail bounds with Access while validate_bytes says
Ok: the validate⇒read agreement lattice breaks on every aligned
maxLength string/bytes field (any schema declaring maxLength in
aligned mode). write_field had the same mismatch (prefix+data into a
raw window).

Engine fix:
- VariableEncoding gains MaxLengthReserved (additive variant, ADR-003
  strategy 2). OffsetMap::compute records it for maxLength fields with
  the default encoding; maxLength+offset-indirect stays OffsetIndirect
  (the pair read is intentional, the window reserves max_len bytes),
  preserving the W3-1 combination semantics.
- read_field String/Bytes arms dispatch on MaxLengthReserved → new
  data_access::read_reservation_string / read_reservation (raw window
  inside-buffer check + NUL trim — the materializer's exact semantics).
- write_field dispatches → new data_access::write_reservation (zero-
  pads the window, rejects oversized values with Access).
- materialize_aligned reads MaxLengthReserved through the same new
  read_reservation paths (single source of truth; replaces the inline
  trim logic with an identical implementation).
- offset_map compute rejects a MaxLengthReserved encoding reaching the
  walk with a clean Offset error (recorded, never declared).
- builder round-trips: MaxLengthReserved serializes via maxLength (the
  document form), never as an encoding value.
- three engine regression tests: raw-not-prefixed read, zero-pad
  write + oversize rejection, validate⇒read_field agreement.
- fuzz/shared validate_pair invariant updated: the W3-1
  shorter-than-reservation exemption now applies to offset-indirect
  only; reservations assert the full window in-bounds (fixed engine).
- corpus regenerated for generator-consistent numbering (seeds 037-044
  relabeled; W3-1/W3-2 artifacts remain 044/045-047 → now 044, 048-050
  region) — 48 seeds, replay 30/30 green.

Verification: main crate 573 tests pass; clippy -D warnings clean
(crate + shared); wasm clean; cargo fuzz build clean.
This commit is contained in:
glm-5.3-flash committed 2026-09-30 08:20:13 +00:00
1 parent b7ead99724
commit a0dd3d2de4
15 files changed
+307 -34

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+14 -13
View File
@@ -408,19 +408,20 @@ fn drive_pair(engine: &AlkTypeEngine, doc: &Value, root: &str, buffer: &[u8]) {
) {
continue;
}
// W3-1 (pinned contract, not engine behavior to
// change): an offset-indirect entry's range is
// the pair/reservation window (a declared
// maxLength contributes its full size), but the
// {data_offset, data_length} pair points
// absolutely into the whole buffer — the data
// may live anywhere in the buffer and the
// buffer may be shorter than the reservation
// (the wave-1 data_access bounds partition is
// the contract: the pointed-to window sits
// inside the buffer, nothing about the
// reservation). For every other encoding a
// successful read implies range.end ≤ len.
// W3-1 (pinned contract): an offset-indirect
// entry's range is the absolute pair window
// and the {data_offset, data_length} pair
// points anywhere in the buffer, so only the
// pointed-to window must sit inside the buffer.
// W3-3 fix (engine change): maxLength
// reservations now read as raw NUL-trimmed
// windows — the read requires range.end ≤ len,
// so a validated buffer shorter than the
// reservation window fails both validate and
// the read; the exemption is unnecessary and
// the full assertion applies. For every other
// encoding a successful read implies
// range.end ≤ len.
let indirect =
entry.meta.encoding == alktype::VariableEncoding::OffsetIndirect;
if !indirect {