Skip to main content

relon_eval_api/
verifier.rs

1//! Host-walk bounds verifier for the binary handshake buffer.
2//!
3//! Modelled on the FlatBuffers verifier / rkyv `bytecheck` discipline:
4//! before the host dereferences any in-buffer offset it first proves the
5//! offset (and the length / payload it introduces) lands inside the
6//! buffer — and, for the return-side single-region invariant, inside the
7//! **one** region the value is supposed to be self-contained in. A walk
8//! that would step outside the region returns a precise
9//! [`VerifyError`] rather than reading garbage or panicking.
10//!
11//! This is a **read-only structural pass**: it never decodes payload
12//! semantics (no utf-8 validation, no integer interpretation). Its sole
13//! job is to certify that a subsequent [`crate::buffer::BufferReader`]
14//! walk over the same bytes can dereference every pointer it will follow
15//! without an out-of-bounds access. The reader already performs the same
16//! bounds checks inline (and returns its own `BufferError`); the
17//! verifier exists so a host can run one cheap up-front pass — and so the
18//! single-region invariant (every reachable offset is confined to
19//! `[region_start, region_end)`) can be asserted independently of the
20//! reader, which only checks against the whole-buffer end.
21//!
22//! ## Single-region invariant (the load-bearing wall)
23//!
24//! The compiled-backend ABI lays the arena out as
25//! `[const_data | in_buf | out_buf | scratch]`. A `#main` return value
26//! is required to be **self-contained inside one region**: a value built
27//! in `out_buf` (const-pool literals, copied records) references only
28//! `out_buf`; a value returned by identity from a parameter references
29//! only `in_buf`. The verifier takes the region bounds explicitly and
30//! rejects any offset that escapes them, so a cross-region pointer (the
31//! one shape the return marshaller must keep behind a loud capability)
32//! is caught as [`VerifyError::OutOfRegion`] instead of being silently
33//! dereferenced.
34//!
35//! ## Multi-region walk (F0 cross-region safety net)
36//!
37//! The S5 cross-region return shape (`-> Dict { servers: List<Cfg> }`)
38//! builds the object head in `out_buf` while the parameter-sourced field
39//! data still lives in `in_buf` — a single value graph that legitimately
40//! spans two regions. The single-region wall would (correctly, for S1–S6)
41//! reject it. [`MultiRegion`] + [`verify_value_at_multi`] /
42//! [`verify_record_multi`] are the multi-region-aware sibling pass: they
43//! walk over the **whole arena** in absolute coordinates, and at every
44//! pointer they (1) read the slot value as an **arena-absolute** offset
45//! (the convention F1 codegen will emit — see the plan's "F0 design
46//! decision" note: a single global arena-relative pointer convention),
47//! (2)
48//! classify which region that absolute offset lands in, and (3)
49//! bounds-check the introduced span against **that one** region. A
50//! pointer that lands in no region, or whose span runs off the region it
51//! starts in, is a loud [`VerifyError`] — never a wild read. The object
52//! positive-`bytes_written` return path runs this pass before any decode,
53//! closing the red-line "cross-region pointer into an object slot is not
54//! verified" gap.
55//!
56//! **F0 does not release any capability.** The multi-region pass is the
57//! safety net + the facilities that let F1 release the first cross-region
58//! shape behind a verified gate; the cross-region object/struct lowering
59//! stays capped until then.
60
61use crate::layout::{FieldKind, ListElementKind, OffsetTable, SchemaLayout};
62use crate::schema_canonical::{Field, Schema, TypeRepr};
63use thiserror::Error;
64
65/// A half-open byte window `[start, end)` inside the arena that a value
66/// is required to be self-contained in. `start <= end` is enforced by
67/// [`Region::new`]; every offset the verifier follows must satisfy
68/// `start <= off` and the introduced span must end at or before `end`.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct Region {
71    /// First byte of the region (inclusive), as an absolute index into
72    /// the verified byte slice.
73    pub start: usize,
74    /// One past the last byte of the region (exclusive).
75    pub end: usize,
76}
77
78impl Region {
79    /// Build a region, returning [`VerifyError::DegenerateRegion`] when
80    /// `start > end` (a caller bug — the arena layout never produces an
81    /// inverted window).
82    pub fn new(start: usize, end: usize) -> Result<Self, VerifyError> {
83        if start > end {
84            return Err(VerifyError::DegenerateRegion { start, end });
85        }
86        Ok(Self { start, end })
87    }
88
89    /// `true` when `[off, off+len)` is fully inside the region. `len`
90    /// uses checked arithmetic so an overflowing span is reported as
91    /// out-of-region rather than wrapping.
92    fn contains_span(&self, off: usize, len: usize) -> bool {
93        match off.checked_add(len) {
94            Some(end) => off >= self.start && end <= self.end,
95            None => false,
96        }
97    }
98}
99
100/// The four arena regions a cross-region return value may reach, in the
101/// fixed ABI layout order `[const_data | in_buf | out_buf | scratch]`.
102/// Used only to label which region a multi-region span landed in for
103/// diagnostics; the verifier itself treats them uniformly.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum RegionTag {
106    /// Const-data pool at arena offset 0.
107    Const,
108    /// Input buffer (`in_ptr..in_ptr+in_len`) — parameter-sourced data.
109    In,
110    /// Output buffer (`out_ptr..out_ptr+out_cap`) — the object head and
111    /// const-pool / copied tails.
112    Out,
113    /// Scratch region (`scratch_base..arena_size`).
114    Scratch,
115}
116
117impl RegionTag {
118    /// Short region label for diagnostics.
119    pub fn label(self) -> &'static str {
120        match self {
121            RegionTag::Const => "const",
122            RegionTag::In => "in",
123            RegionTag::Out => "out",
124            RegionTag::Scratch => "scratch",
125        }
126    }
127}
128
129/// A set of arena regions in **absolute arena coordinates**, the
130/// multi-region sibling of [`Region`]. Where a [`Region`] confines an
131/// entire value graph to one window, a `MultiRegion` lets the graph span
132/// regions: every followed pointer is an arena-absolute offset that must
133/// land fully inside *one* of these regions (`contains_span` picks the
134/// region whose window the span starts in and requires the whole span to
135/// fit it). A span that starts in no region, or starts in one region but
136/// runs past its end, is rejected — there is no "fell through to the next
137/// region" silent over-read.
138///
139/// The regions are half-open `[start, end)` absolute byte windows and may
140/// be empty (`start == end`, e.g. a zero-length `in_buf`); an empty
141/// region contains no span. They are expected non-overlapping (the ABI
142/// arena layout guarantees this), but `contains_span` only ever requires
143/// a span to fit *some* region, so a benign overlap could not cause a
144/// missed bounds check.
145#[derive(Debug, Clone, Copy)]
146pub struct MultiRegion {
147    regions: [(RegionTag, Region); 4],
148}
149
150impl MultiRegion {
151    /// Build the four-region map from absolute arena boundaries. Each
152    /// pair is a half-open `[start, end)` window; `start > end` for any
153    /// region is a caller bug surfaced as [`VerifyError::DegenerateRegion`].
154    pub fn new(
155        const_data: (usize, usize),
156        in_buf: (usize, usize),
157        out_buf: (usize, usize),
158        scratch: (usize, usize),
159    ) -> Result<Self, VerifyError> {
160        Ok(Self {
161            regions: [
162                (RegionTag::Const, Region::new(const_data.0, const_data.1)?),
163                (RegionTag::In, Region::new(in_buf.0, in_buf.1)?),
164                (RegionTag::Out, Region::new(out_buf.0, out_buf.1)?),
165                (RegionTag::Scratch, Region::new(scratch.0, scratch.1)?),
166            ],
167        })
168    }
169
170    /// The largest `end` across all regions — the minimum byte length the
171    /// verified arena slice must cover for every region bound to be
172    /// satisfiable.
173    fn max_end(&self) -> usize {
174        self.regions.iter().map(|(_, r)| r.end).max().unwrap_or(0)
175    }
176
177    /// Classify the absolute offset `off` (with span `len`) into the one
178    /// region that fully contains `[off, off+len)`. Returns the region
179    /// tag on success, or `None` when the span fits no single region.
180    fn classify_span(&self, off: usize, len: usize) -> Option<RegionTag> {
181        for (tag, region) in &self.regions {
182            // Empty regions contain nothing; `contains_span` already
183            // handles that via `off >= start && end <= end` with
184            // `start == end`.
185            if region.contains_span(off, len) {
186                return Some(*tag);
187            }
188        }
189        None
190    }
191}
192
193/// Why a verifier walk rejected a buffer. Every variant names the
194/// field (and where useful the offending offset / span) so a host can
195/// surface a precise diagnostic. The verifier returns these instead of
196/// reading out of bounds or panicking.
197#[derive(Debug, Error, Clone, PartialEq, Eq)]
198pub enum VerifyError {
199    /// A `Region` was constructed with `start > end`.
200    #[error("degenerate region: start {start} > end {end}")]
201    DegenerateRegion {
202        /// Requested region start.
203        start: usize,
204        /// Requested region end.
205        end: usize,
206    },
207    /// The byte slice handed to the verifier is shorter than the
208    /// region's `end` — the caller's region bounds don't fit the data.
209    #[error("buffer of {have} bytes is shorter than region end {end}")]
210    BufferShorterThanRegion {
211        /// Actual byte length of the slice.
212        have: usize,
213        /// Region end the caller asserted.
214        end: usize,
215    },
216    /// A fixed-area slot (or the `root_size` window) does not fit inside
217    /// the region. Indicates a truncated record or a wrong region base.
218    #[error("fixed-area slot for `{field}` at {offset}..+{size} escapes region [{start}, {end})")]
219    FixedSlotOutOfRegion {
220        /// Field whose fixed-area slot is out of bounds.
221        field: String,
222        /// Slot start offset.
223        offset: usize,
224        /// Slot byte size.
225        size: usize,
226        /// Region start.
227        start: usize,
228        /// Region end.
229        end: usize,
230    },
231    /// A pointer-indirect offset (a `u32` slot value, a list-entry
232    /// pointer, a length prefix, or a payload span) escapes the region.
233    /// This is the cross-region / out-of-bounds catch.
234    #[error("offset {offset}..+{len} for `{field}` ({what}) escapes region [{start}, {end})")]
235    OutOfRegion {
236        /// Field being walked.
237        field: String,
238        /// What kind of span tripped the check (e.g. `"length prefix"`).
239        what: &'static str,
240        /// Span start.
241        offset: usize,
242        /// Span byte length.
243        len: usize,
244        /// Region start.
245        start: usize,
246        /// Region end.
247        end: usize,
248    },
249    /// A `usize` add overflowed while computing a span end — a hostile
250    /// or corrupt buffer carrying a near-`u32::MAX` offset / length.
251    #[error("arithmetic overflow computing span for `{field}` ({what})")]
252    SpanOverflow {
253        /// Field being walked.
254        field: String,
255        /// What span overflowed.
256        what: &'static str,
257    },
258    /// The schema named a type the verifier does not model (it should
259    /// never reach here — the layout pass rejects unsupported types
260    /// first — but we surface it loudly rather than skip the check).
261    #[error("verifier does not model type `{ty}` in field `{field}`")]
262    UnsupportedType {
263        /// Field carrying the unmodelled type.
264        field: String,
265        /// Human-readable type label.
266        ty: &'static str,
267    },
268    /// A multi-region walk followed a pointer whose arena-absolute span
269    /// `[offset, offset+len)` fits inside **no** arena region (or starts
270    /// in one region but runs past its end). The cross-region catch: a
271    /// pointer must land fully inside exactly one of const / in / out /
272    /// scratch, never between or past them.
273    #[error(
274        "offset {offset}..+{len} for `{field}` ({what}) fits no arena region \
275         (const, in, out, scratch are all disjoint windows)"
276    )]
277    NoRegion {
278        /// Field being walked.
279        field: String,
280        /// What kind of span tripped the check.
281        what: &'static str,
282        /// Absolute span start.
283        offset: usize,
284        /// Span byte length.
285        len: usize,
286    },
287    /// A recursion-depth guard tripped: the schema nests deeper than
288    /// the verifier's bound. Protects against a hand-built schema that
289    /// recurses without bound (the runtime schemas are acyclic, but a
290    /// verifier must not be DoS-able by a hostile layout).
291    #[error("verifier recursion depth exceeded ({depth}) at field `{field}`")]
292    DepthExceeded {
293        /// Field at which the guard tripped.
294        field: String,
295        /// The depth bound that was hit.
296        depth: usize,
297    },
298}
299
300/// The bounds discipline a verifier walk enforces: either the
301/// single-region wall (S1–S6: the whole graph must stay in one region) or
302/// the multi-region map (F0 cross-region: each followed span must stay in
303/// *some* region). Both expose the same two operations the walk needs —
304/// "does this absolute span fit my bounds" and "what is the maximum end my
305/// bounds require the slice to cover" — so the recursive walk is written
306/// once and parameterised over this.
307#[derive(Debug, Clone, Copy)]
308enum Bounds {
309    /// S1–S6 single-region wall. Every offset is region-relative into a
310    /// region-sliced byte array; the whole graph must stay in `[start,
311    /// end)`.
312    Single(Region),
313    /// F0 multi-region map. Every offset is arena-absolute into the whole
314    /// arena slice; each span must fit one region, cross-region links
315    /// allowed.
316    Multi(MultiRegion),
317}
318
319impl Bounds {
320    /// The minimum byte length the verified slice must cover.
321    fn required_len(&self) -> usize {
322        match self {
323            Bounds::Single(r) => r.end,
324            Bounds::Multi(m) => m.max_end(),
325        }
326    }
327
328    /// Assert `[off, off+len)` (an absolute offset into the verified
329    /// slice) is legal under this bounds discipline. `field` / `what`
330    /// label the offending span for diagnostics.
331    fn require_span(
332        &self,
333        field: &str,
334        what: &'static str,
335        off: usize,
336        len: usize,
337    ) -> Result<(), VerifyError> {
338        if off.checked_add(len).is_none() {
339            return Err(VerifyError::SpanOverflow {
340                field: field.to_string(),
341                what,
342            });
343        }
344        match self {
345            Bounds::Single(region) => {
346                if region.contains_span(off, len) {
347                    Ok(())
348                } else {
349                    Err(VerifyError::OutOfRegion {
350                        field: field.to_string(),
351                        what,
352                        offset: off,
353                        len,
354                        start: region.start,
355                        end: region.end,
356                    })
357                }
358            }
359            Bounds::Multi(multi) => {
360                if multi.classify_span(off, len).is_some() {
361                    Ok(())
362                } else {
363                    Err(VerifyError::NoRegion {
364                        field: field.to_string(),
365                        what,
366                        offset: off,
367                        len,
368                    })
369                }
370            }
371        }
372    }
373
374    /// Assert a record fixed-area slot `[off, off+size)` fits one region,
375    /// reporting the dedicated [`VerifyError::FixedSlotOutOfRegion`] /
376    /// [`VerifyError::NoRegion`] diagnostic (a truncated record / wrong
377    /// base) rather than the generic pointer-span error.
378    fn require_fixed_slot(&self, field: &str, off: usize, size: usize) -> Result<(), VerifyError> {
379        match self {
380            Bounds::Single(region) => {
381                if region.contains_span(off, size) {
382                    Ok(())
383                } else {
384                    Err(VerifyError::FixedSlotOutOfRegion {
385                        field: field.to_string(),
386                        offset: off,
387                        size,
388                        start: region.start,
389                        end: region.end,
390                    })
391                }
392            }
393            Bounds::Multi(multi) => {
394                if multi.classify_span(off, size).is_some() {
395                    Ok(())
396                } else {
397                    Err(VerifyError::NoRegion {
398                        field: field.to_string(),
399                        what: "record fixed slot",
400                        offset: off,
401                        len: size,
402                    })
403                }
404            }
405        }
406    }
407}
408
409/// Maximum schema-nesting depth the verifier walks before bailing.
410/// Runtime `#main` schemas never approach this; the cap only guards a
411/// hand-built cyclic layout from spinning the verifier.
412const MAX_DEPTH: usize = 64;
413
414/// Verify that every offset reachable from a record laid out by
415/// `layout` / `fields` — anchored at `record_base` — stays inside
416/// `region` for the byte slice `bytes`.
417///
418/// This is the entry point a host calls before decoding a return value:
419/// pass the return layout, the return schema's fields, the buffer-base
420/// offset of the root record (`out_ptr`-relative `0` for a top-level
421/// return), and the `out_buf` region. A clean `Ok(())` certifies that a
422/// subsequent [`crate::buffer::BufferReader`] walk will not dereference
423/// out of region.
424///
425/// `bytes` must cover at least `region.end` bytes; otherwise the
426/// region bounds are themselves unsatisfiable and
427/// [`VerifyError::BufferShorterThanRegion`] is returned.
428pub fn verify_record(
429    bytes: &[u8],
430    layout: &OffsetTable,
431    fields: &[Field],
432    record_base: usize,
433    region: Region,
434) -> Result<(), VerifyError> {
435    verify_record_with_bounds(bytes, layout, fields, record_base, Bounds::Single(region))
436}
437
438/// Multi-region sibling of [`verify_record`]: verify a record laid out at
439/// the **arena-absolute** offset `record_base`, where every pointer slot
440/// holds an arena-absolute offset and the graph may legitimately span the
441/// `const` / `in` / `out` / `scratch` regions described by `multi`.
442///
443/// `bytes` is the **whole arena** slice (offsets are absolute into it),
444/// and must cover at least the largest region end. This is the entry
445/// point the object positive-`bytes_written` return path runs before
446/// decoding a cross-region object head — the gap the S5 design flagged as
447/// the red-line "object slot cross-region pointer is not verified".
448pub fn verify_record_multi(
449    bytes: &[u8],
450    layout: &OffsetTable,
451    fields: &[Field],
452    record_base: usize,
453    multi: MultiRegion,
454) -> Result<(), VerifyError> {
455    verify_record_with_bounds(bytes, layout, fields, record_base, Bounds::Multi(multi))
456}
457
458fn verify_record_with_bounds(
459    bytes: &[u8],
460    layout: &OffsetTable,
461    fields: &[Field],
462    record_base: usize,
463    bounds: Bounds,
464) -> Result<(), VerifyError> {
465    let required = bounds.required_len();
466    if bytes.len() < required {
467        return Err(VerifyError::BufferShorterThanRegion {
468            have: bytes.len(),
469            end: required,
470        });
471    }
472    verify_record_inner(bytes, layout, fields, record_base, bounds, 0)
473}
474
475/// Verify a **bare value** reachable from a direct pointer offset,
476/// rather than from a record's fixed-area slot. This is the entry point
477/// the in-place region-walk return ABI calls: the machine code reports
478/// the arena-absolute offset of a value's root (e.g. a
479/// `List<List<scalar>>` header) and the host rebases it to a
480/// region-relative offset, then asks the verifier to certify the whole
481/// reachable graph stays inside `region` before any decode.
482///
483/// `ty` is the declared type of the value at `root` (a pointer-indirect
484/// type — `String` / `List<…>` / `Schema`); `list_element` is the
485/// matching [`ListElementKind`] sidecar for a `List` root (recomputed by
486/// the caller from the return layout). `root` is region-relative (an
487/// offset into `bytes`, with `bytes` covering at least `region.end`).
488///
489/// A clean `Ok(())` means a subsequent direct in-place decode of the
490/// same value will not dereference outside `region`. Any escape is a
491/// loud [`VerifyError`] — the decode must not run.
492pub fn verify_value_at(
493    bytes: &[u8],
494    ty: &TypeRepr,
495    list_element: Option<ListElementKind>,
496    root: usize,
497    region: Region,
498) -> Result<(), VerifyError> {
499    verify_value_at_with_bounds(bytes, ty, list_element, root, Bounds::Single(region))
500}
501
502/// Multi-region sibling of [`verify_value_at`]: certify a bare value
503/// whose root pointer (and every pointer reachable from it) is an
504/// arena-absolute offset and whose graph may span regions. `bytes` is the
505/// whole arena slice; `root` is the arena-absolute offset of the value's
506/// root header. Any escape (a span fitting no region, or running off the
507/// region it starts in) is a loud [`VerifyError`] — the decode must not
508/// run.
509pub fn verify_value_at_multi(
510    bytes: &[u8],
511    ty: &TypeRepr,
512    list_element: Option<ListElementKind>,
513    root: usize,
514    multi: MultiRegion,
515) -> Result<(), VerifyError> {
516    verify_value_at_with_bounds(bytes, ty, list_element, root, Bounds::Multi(multi))
517}
518
519fn verify_value_at_with_bounds(
520    bytes: &[u8],
521    ty: &TypeRepr,
522    list_element: Option<ListElementKind>,
523    root: usize,
524    bounds: Bounds,
525) -> Result<(), VerifyError> {
526    let required = bounds.required_len();
527    if bytes.len() < required {
528        return Err(VerifyError::BufferShorterThanRegion {
529            have: bytes.len(),
530            end: required,
531        });
532    }
533    verify_pointer_target(bytes, "<in-place root>", ty, list_element, root, bounds, 0)
534}
535
536fn verify_record_inner(
537    bytes: &[u8],
538    layout: &OffsetTable,
539    fields: &[Field],
540    record_base: usize,
541    bounds: Bounds,
542    depth: usize,
543) -> Result<(), VerifyError> {
544    if depth >= MAX_DEPTH {
545        return Err(VerifyError::DepthExceeded {
546            field: "<record>".to_string(),
547            depth: MAX_DEPTH,
548        });
549    }
550    // The whole fixed area must land in one region before we read any
551    // slot. (Single-region: the one window; multi-region: the record
552    // head and all its slots must sit together in a single region — a
553    // record fixed area is never itself split across regions.)
554    bounds.require_fixed_slot("<root>", record_base, layout.root_size)?;
555    for fo in &layout.fields {
556        let slot_abs =
557            record_base
558                .checked_add(fo.offset)
559                .ok_or_else(|| VerifyError::SpanOverflow {
560                    field: fo.name.clone(),
561                    what: "fixed slot offset",
562                })?;
563        bounds.require_fixed_slot(&fo.name, slot_abs, fo.size)?;
564        let FieldKind::PointerIndirect { .. } = fo.kind else {
565            // Inline scalar: the fixed-slot span check above is the
566            // whole verification — no further pointer to follow.
567            continue;
568        };
569        // Find the declared field type so we know what the pointer
570        // introduces. A pointer-indirect slot whose name isn't in the
571        // schema is a layout/schema drift bug — surface it.
572        let declared = fields.iter().find(|f| f.name == fo.name);
573        let ty = match declared {
574            Some(f) => &f.ty,
575            None => {
576                return Err(VerifyError::UnsupportedType {
577                    field: fo.name.clone(),
578                    ty: "<missing schema field>",
579                });
580            }
581        };
582        // The pointer slot value: a buffer-relative `u32` under the
583        // single-region wall, an arena-absolute `u32` under the
584        // multi-region map.
585        let ptr = read_u32(bytes, slot_abs, &fo.name, "pointer slot", bounds)?;
586        verify_pointer_target(bytes, &fo.name, ty, fo.list_element, ptr, bounds, depth)?;
587    }
588    Ok(())
589}
590
591/// Follow one pointer-indirect slot's target and validate the tail
592/// record it introduces stays in-region. Dispatches on the declared
593/// type: `String` / inline-fixed lists carry a single `[len][payload]`
594/// record; pointer-array lists (`List<String>` / `List<Schema>` /
595/// `List<List<_>>`) carry a `[len][off_0]…` header whose entries are
596/// followed recursively; nested `Schema` slots recurse into the
597/// sub-record.
598fn verify_pointer_target(
599    bytes: &[u8],
600    field: &str,
601    ty: &TypeRepr,
602    list_element: Option<ListElementKind>,
603    ptr: usize,
604    bounds: Bounds,
605    depth: usize,
606) -> Result<(), VerifyError> {
607    match ty {
608        TypeRepr::String => {
609            // `[len: u32][len bytes]`.
610            let len = read_u32(bytes, ptr, field, "length prefix", bounds)?;
611            let payload = ptr
612                .checked_add(4)
613                .ok_or_else(|| VerifyError::SpanOverflow {
614                    field: field.to_string(),
615                    what: "string payload start",
616                })?;
617            bounds.require_span(field, "string payload", payload, len)
618        }
619        TypeRepr::Schema { schema } => {
620            // Nested sub-record: recurse with the sub-layout anchored at
621            // `ptr`. We rebuild the sub-layout from the canonical schema
622            // so the verifier doesn't depend on a cached table.
623            let sub_layout =
624                SchemaLayout::offsets_for(schema).map_err(|_| VerifyError::UnsupportedType {
625                    field: field.to_string(),
626                    ty: "Schema (unlayoutable)",
627                })?;
628            verify_record_inner(bytes, &sub_layout, &schema.fields, ptr, bounds, depth + 1)
629        }
630        TypeRepr::List { element } => {
631            verify_list_target(bytes, field, element, list_element, ptr, bounds, depth)
632        }
633        TypeRepr::Option { .. } | TypeRepr::Result { .. } | TypeRepr::Enum { .. } => {
634            verify_variant_target(bytes, field, ty, ptr, bounds, depth)
635        }
636        other => Err(VerifyError::UnsupportedType {
637            field: field.to_string(),
638            ty: type_label(other),
639        }),
640    }
641}
642
643fn verify_variant_target(
644    bytes: &[u8],
645    field: &str,
646    ty: &TypeRepr,
647    ptr: usize,
648    bounds: Bounds,
649    depth: usize,
650) -> Result<(), VerifyError> {
651    if depth >= MAX_DEPTH {
652        return Err(VerifyError::DepthExceeded {
653            field: field.to_string(),
654            depth: MAX_DEPTH,
655        });
656    }
657    bounds.require_span(field, "variant tag", ptr, 1)?;
658    let tag = bytes[ptr];
659    let Some(payload_ty) =
660        variant_payload_type(ty, tag).ok_or_else(|| VerifyError::UnsupportedType {
661            field: field.to_string(),
662            ty: type_label(ty),
663        })?
664    else {
665        return Ok(());
666    };
667    let (slot_size, slot_align) = variant_payload_slot_layout(&payload_ty);
668    let slot = align_up(
669        ptr.checked_add(1)
670            .ok_or_else(|| VerifyError::SpanOverflow {
671                field: field.to_string(),
672                what: "variant payload slot start",
673            })?,
674        slot_align,
675    )
676    .ok_or_else(|| VerifyError::SpanOverflow {
677        field: field.to_string(),
678        what: "variant payload slot alignment",
679    })?;
680    bounds.require_span(field, "variant payload slot", slot, slot_size)?;
681    match &payload_ty {
682        TypeRepr::Unit | TypeRepr::Bool | TypeRepr::Int | TypeRepr::Float => Ok(()),
683        TypeRepr::String
684        | TypeRepr::Schema { .. }
685        | TypeRepr::List { .. }
686        | TypeRepr::Option { .. }
687        | TypeRepr::Result { .. }
688        | TypeRepr::Enum { .. } => {
689            let target = read_u32(bytes, slot, field, "variant payload pointer", bounds)?;
690            verify_pointer_target(
691                bytes,
692                field,
693                &payload_ty,
694                list_kind_for_list_type(&payload_ty),
695                target,
696                bounds,
697                depth + 1,
698            )
699        }
700        TypeRepr::Closure { .. } => Err(VerifyError::UnsupportedType {
701            field: field.to_string(),
702            ty: "Closure",
703        }),
704    }
705}
706
707fn variant_payload_type(ty: &TypeRepr, tag: u8) -> Option<Option<TypeRepr>> {
708    match ty {
709        TypeRepr::Option { inner } => match tag {
710            0 => Some(None),
711            1 => Some(Some(inner.as_ref().clone())),
712            _ => None,
713        },
714        TypeRepr::Result { ok, err } => match tag {
715            0 => Some(Some(ok.as_ref().clone())),
716            1 => Some(Some(err.as_ref().clone())),
717            _ => None,
718        },
719        TypeRepr::Enum { name, variants } => variants
720            .iter()
721            .find(|variant| variant.tag == tag)
722            .map(|variant| {
723                variant.payload_schema(name).map(|schema| TypeRepr::Schema {
724                    schema: Box::new(schema),
725                })
726            }),
727        _ => None,
728    }
729}
730
731fn variant_payload_slot_layout(ty: &TypeRepr) -> (usize, usize) {
732    match ty {
733        TypeRepr::Unit | TypeRepr::Bool => (1, 1),
734        TypeRepr::Int | TypeRepr::Float => (8, 8),
735        _ => (4, 4),
736    }
737}
738
739/// Validate a `List<T>` tail record. `inline_fixed` payloads are one
740/// contiguous `[len][pad][elem*]` span; `pointer_array` payloads are a
741/// `[len][off_0]…` header whose entries point at per-element records we
742/// recurse into.
743fn verify_list_target(
744    bytes: &[u8],
745    field: &str,
746    element: &TypeRepr,
747    list_element: Option<ListElementKind>,
748    ptr: usize,
749    bounds: Bounds,
750    depth: usize,
751) -> Result<(), VerifyError> {
752    let count = read_u32(bytes, ptr, field, "list length prefix", bounds)?;
753    let entries_start = ptr
754        .checked_add(4)
755        .ok_or_else(|| VerifyError::SpanOverflow {
756            field: field.to_string(),
757            what: "list entries start",
758        })?;
759    match list_element {
760        Some(ListElementKind::InlineFixed {
761            elem_size,
762            elem_align,
763        }) => {
764            // Payload starts at `entries_start` padded up to `elem_align`.
765            let payload_start =
766                align_up(entries_start, elem_align).ok_or_else(|| VerifyError::SpanOverflow {
767                    field: field.to_string(),
768                    what: "inline list payload start",
769                })?;
770            let byte_len =
771                count
772                    .checked_mul(elem_size)
773                    .ok_or_else(|| VerifyError::SpanOverflow {
774                        field: field.to_string(),
775                        what: "inline list byte length",
776                    })?;
777            bounds.require_span(field, "inline list payload", payload_start, byte_len)
778        }
779        Some(ListElementKind::PointerArray { .. }) => {
780            // `[len][off_0: u32]…[off_{count-1}]`; each entry points at a
781            // per-element record we recurse into.
782            for i in 0..count {
783                let entry_off = entries_start
784                    .checked_add(i.checked_mul(4).ok_or_else(|| VerifyError::SpanOverflow {
785                        field: field.to_string(),
786                        what: "list entry index",
787                    })?)
788                    .ok_or_else(|| VerifyError::SpanOverflow {
789                        field: field.to_string(),
790                        what: "list entry offset",
791                    })?;
792                let entry_ptr = read_u32(bytes, entry_off, field, "list entry pointer", bounds)?;
793                // Recurse per element type. The element of a pointer-
794                // array list is itself String / Schema / List.
795                verify_pointer_target(
796                    bytes,
797                    field,
798                    element,
799                    element_list_kind(element),
800                    entry_ptr,
801                    bounds,
802                    depth + 1,
803                )?;
804            }
805            Ok(())
806        }
807        None => {
808            // A pointer-indirect list slot must carry a list_element
809            // sidecar; its absence is a layout-construction bug.
810            Err(VerifyError::UnsupportedType {
811                field: field.to_string(),
812                ty: "List (missing element layout)",
813            })
814        }
815    }
816}
817
818/// Resolve the [`ListElementKind`] for the element of a pointer-array
819/// list — used when recursing into a `List<List<_>>` inner list. We
820/// recompute it from the inner element type via a one-field probe
821/// schema so the verifier doesn't need the parent's cached sidecar for
822/// the inner level.
823fn element_list_kind(element: &TypeRepr) -> Option<ListElementKind> {
824    let TypeRepr::List { .. } = element else {
825        // String / Schema / Option / Result elements dispatch on the type
826        // directly and never read a nested list sidecar.
827        return None;
828    };
829    list_kind_for_list_type(element)
830}
831
832fn list_kind_for_list_type(ty: &TypeRepr) -> Option<ListElementKind> {
833    let TypeRepr::List { .. } = ty else {
834        return None;
835    };
836    let probe = Schema {
837        name: "<probe>".to_string(),
838        generics: vec![],
839        is_tuple: false,
840        fields: vec![Field {
841            name: "f".to_string(),
842            ty: ty.clone(),
843            default: None,
844        }],
845    };
846    SchemaLayout::offsets_for(&probe)
847        .ok()
848        .and_then(|t| t.fields.into_iter().next())
849        .and_then(|fo| fo.list_element)
850}
851
852/// Read a little-endian `u32` at `off`, first proving `[off, off+4)` is
853/// in-bounds under `bounds`. Returns the value as a `usize`.
854fn read_u32(
855    bytes: &[u8],
856    off: usize,
857    field: &str,
858    what: &'static str,
859    bounds: Bounds,
860) -> Result<usize, VerifyError> {
861    bounds.require_span(field, what, off, 4)?;
862    let mut buf = [0u8; 4];
863    buf.copy_from_slice(&bytes[off..off + 4]);
864    Ok(u32::from_le_bytes(buf) as usize)
865}
866
867/// Round `off` up to the next multiple of `align` (no-op for `align <=
868/// 1`). Returns `None` on overflow.
869fn align_up(off: usize, align: usize) -> Option<usize> {
870    if align <= 1 {
871        return Some(off);
872    }
873    off.checked_next_multiple_of(align)
874}
875
876/// Human-readable label for an unmodelled type in an error path.
877fn type_label(ty: &TypeRepr) -> &'static str {
878    match ty {
879        TypeRepr::Unit => "Unit",
880        TypeRepr::Bool => "Bool",
881        TypeRepr::Int => "Int",
882        TypeRepr::Float => "Float",
883        TypeRepr::String => "String",
884        TypeRepr::List { .. } => "List",
885        TypeRepr::Option { .. } => "Option",
886        TypeRepr::Result { .. } => "Result",
887        TypeRepr::Enum { .. } => "Enum",
888        TypeRepr::Schema { .. } => "Schema",
889        TypeRepr::Closure { .. } => "Closure",
890    }
891}
892
893#[cfg(test)]
894mod tests {
895    use super::*;
896    use crate::buffer::BufferBuilder;
897    use crate::layout::SchemaLayout;
898    use crate::schema_canonical::{Field, Schema};
899
900    fn field(name: &str, ty: TypeRepr) -> Field {
901        Field {
902            name: name.into(),
903            ty,
904            default: None,
905        }
906    }
907
908    fn list(inner: TypeRepr) -> TypeRepr {
909        TypeRepr::List {
910            element: Box::new(inner),
911        }
912    }
913
914    /// `{ name: String, age: Int }` — a String tail record alongside an
915    /// inline Int. The canonical "host reads out_buf" shape.
916    fn user_schema() -> Schema {
917        Schema {
918            name: "User".into(),
919            generics: vec![],
920            is_tuple: false,
921            fields: vec![field("name", TypeRepr::String), field("age", TypeRepr::Int)],
922        }
923    }
924
925    fn full_region(bytes: &[u8]) -> Region {
926        Region::new(0, bytes.len()).expect("region")
927    }
928
929    // ---- legal buffers verify clean -------------------------------------
930
931    #[test]
932    fn legal_string_int_record_verifies() {
933        let schema = user_schema();
934        let layout = SchemaLayout::offsets_for(&schema).expect("layout");
935        let mut b = BufferBuilder::new(&layout, &schema.fields);
936        b.write_string("name", "ada").unwrap();
937        b.write_int("age", 36).unwrap();
938        let bytes = b.finish();
939        verify_record(&bytes, &layout, &schema.fields, 0, full_region(&bytes))
940            .expect("legal buffer must verify");
941    }
942
943    #[test]
944    fn legal_list_string_record_verifies() {
945        let schema = Schema {
946            name: "Tags".into(),
947            generics: vec![],
948            is_tuple: false,
949            fields: vec![field("tags", list(TypeRepr::String))],
950        };
951        let layout = SchemaLayout::offsets_for(&schema).expect("layout");
952        let mut b = BufferBuilder::new(&layout, &schema.fields);
953        b.write_list_string("tags", &["a", "bb", "", "中文"])
954            .unwrap();
955        let bytes = b.finish();
956        verify_record(&bytes, &layout, &schema.fields, 0, full_region(&bytes))
957            .expect("legal list-string buffer must verify");
958    }
959
960    #[test]
961    fn legal_list_int_record_verifies() {
962        let schema = Schema {
963            name: "Nums".into(),
964            generics: vec![],
965            is_tuple: false,
966            fields: vec![field("nums", list(TypeRepr::Int))],
967        };
968        let layout = SchemaLayout::offsets_for(&schema).expect("layout");
969        let mut b = BufferBuilder::new(&layout, &schema.fields);
970        b.write_list_int("nums", &[1, -2, 3, i64::MIN, i64::MAX])
971            .unwrap();
972        let bytes = b.finish();
973        verify_record(&bytes, &layout, &schema.fields, 0, full_region(&bytes))
974            .expect("legal list-int buffer must verify");
975    }
976
977    #[test]
978    fn legal_nested_schema_record_verifies() {
979        let addr = Schema {
980            name: "Addr".into(),
981            generics: vec![],
982            is_tuple: false,
983            fields: vec![field("city", TypeRepr::String), field("zip", TypeRepr::Int)],
984        };
985        let usr = Schema {
986            name: "Usr".into(),
987            generics: vec![],
988            is_tuple: false,
989            fields: vec![
990                field(
991                    "addr",
992                    TypeRepr::Schema {
993                        schema: Box::new(addr.clone()),
994                    },
995                ),
996                field("name", TypeRepr::String),
997            ],
998        };
999        let usr_layout = SchemaLayout::offsets_for(&usr).expect("usr layout");
1000        let addr_layout = SchemaLayout::offsets_for(&addr).expect("addr layout");
1001        let mut b = BufferBuilder::new(&usr_layout, &usr.fields);
1002        let mut sub = b.sub_record("addr", &addr_layout, &addr.fields).unwrap();
1003        sub.write_string("city", "BJ").unwrap();
1004        sub.write_int("zip", 100000).unwrap();
1005        b.finish_sub_record("addr", sub).unwrap();
1006        b.write_string("name", "Bob").unwrap();
1007        let bytes = b.finish();
1008        verify_record(&bytes, &usr_layout, &usr.fields, 0, full_region(&bytes))
1009            .expect("legal nested-schema buffer must verify");
1010    }
1011
1012    // ---- malformed buffers report loudly (never panic / over-read) ------
1013
1014    #[test]
1015    fn out_of_range_string_pointer_rejected() {
1016        // Patch the String slot to point past the buffer end.
1017        let schema = user_schema();
1018        let layout = SchemaLayout::offsets_for(&schema).expect("layout");
1019        let mut b = BufferBuilder::new(&layout, &schema.fields);
1020        b.write_string("name", "ada").unwrap();
1021        b.write_int("age", 36).unwrap();
1022        let mut bytes = b.finish();
1023        // `name` slot is at offset 0; overwrite with a bogus far offset.
1024        let bogus = (bytes.len() as u32 + 9999).to_le_bytes();
1025        bytes[0..4].copy_from_slice(&bogus);
1026        let region = full_region(&bytes);
1027        let err = verify_record(&bytes, &layout, &schema.fields, 0, region)
1028            .expect_err("bogus string pointer must be rejected");
1029        assert!(
1030            matches!(
1031                err,
1032                VerifyError::OutOfRegion {
1033                    what: "length prefix",
1034                    ..
1035                }
1036            ),
1037            "got {err:?}"
1038        );
1039    }
1040
1041    #[test]
1042    fn overlong_string_len_prefix_rejected() {
1043        // Keep the pointer legal but inflate the length prefix so the
1044        // payload span runs off the end.
1045        let schema = user_schema();
1046        let layout = SchemaLayout::offsets_for(&schema).expect("layout");
1047        let mut b = BufferBuilder::new(&layout, &schema.fields);
1048        b.write_string("name", "ada").unwrap();
1049        b.write_int("age", 36).unwrap();
1050        let mut bytes = b.finish();
1051        let ptr = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
1052        // The len prefix lives at `ptr`; set it to a huge value.
1053        bytes[ptr..ptr + 4].copy_from_slice(&0xFFFF_F000u32.to_le_bytes());
1054        let region = full_region(&bytes);
1055        let err = verify_record(&bytes, &layout, &schema.fields, 0, region)
1056            .expect_err("overlong string len must be rejected");
1057        assert!(
1058            matches!(
1059                err,
1060                VerifyError::OutOfRegion {
1061                    what: "string payload",
1062                    ..
1063                }
1064            ),
1065            "got {err:?}"
1066        );
1067    }
1068
1069    #[test]
1070    fn cross_region_pointer_rejected() {
1071        // A legal whole-buffer walk, but with the region tightened to
1072        // just the fixed-area + part of the tail: the String payload now
1073        // lands *outside* the asserted region. This is the single-region
1074        // invariant catch — a pointer escaping its region is loud.
1075        let schema = user_schema();
1076        let layout = SchemaLayout::offsets_for(&schema).expect("layout");
1077        let mut b = BufferBuilder::new(&layout, &schema.fields);
1078        b.write_string("name", "ada").unwrap();
1079        b.write_int("age", 36).unwrap();
1080        let bytes = b.finish();
1081        let ptr = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
1082        // Region ends right after the len prefix but before the payload.
1083        let region = Region::new(0, ptr + 4).expect("region");
1084        let err = verify_record(&bytes, &layout, &schema.fields, 0, region)
1085            .expect_err("payload outside region must be rejected");
1086        assert!(
1087            matches!(
1088                err,
1089                VerifyError::OutOfRegion {
1090                    what: "string payload",
1091                    ..
1092                }
1093            ),
1094            "got {err:?}"
1095        );
1096    }
1097
1098    #[test]
1099    fn root_record_past_region_rejected() {
1100        // record_base shifted so the fixed area doesn't fit the region.
1101        let schema = user_schema();
1102        let layout = SchemaLayout::offsets_for(&schema).expect("layout");
1103        let mut b = BufferBuilder::new(&layout, &schema.fields);
1104        b.write_string("name", "ada").unwrap();
1105        b.write_int("age", 36).unwrap();
1106        let bytes = b.finish();
1107        let region = Region::new(0, 4).expect("region"); // too small for root_size
1108        let err = verify_record(&bytes, &layout, &schema.fields, 0, region)
1109            .expect_err("undersized region must reject the root record");
1110        assert!(
1111            matches!(err, VerifyError::FixedSlotOutOfRegion { ref field, .. } if field == "<root>"),
1112            "got {err:?}"
1113        );
1114    }
1115
1116    #[test]
1117    fn list_entry_pointer_out_of_range_rejected() {
1118        // Corrupt one pointer-array entry so it dereferences off-buffer.
1119        let schema = Schema {
1120            name: "Tags".into(),
1121            generics: vec![],
1122            is_tuple: false,
1123            fields: vec![field("tags", list(TypeRepr::String))],
1124        };
1125        let layout = SchemaLayout::offsets_for(&schema).expect("layout");
1126        let mut b = BufferBuilder::new(&layout, &schema.fields);
1127        b.write_list_string("tags", &["a", "bb"]).unwrap();
1128        let mut bytes = b.finish();
1129        // header ptr at slot 0 -> [len][off_0][off_1]. Corrupt off_0.
1130        let header = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
1131        let off0_pos = header + 4;
1132        let bogus = (bytes.len() as u32 + 5000).to_le_bytes();
1133        bytes[off0_pos..off0_pos + 4].copy_from_slice(&bogus);
1134        let region = full_region(&bytes);
1135        let err = verify_record(&bytes, &layout, &schema.fields, 0, region)
1136            .expect_err("bogus list entry pointer must reject");
1137        assert!(
1138            matches!(err, VerifyError::OutOfRegion { .. }),
1139            "got {err:?}"
1140        );
1141    }
1142
1143    #[test]
1144    fn buffer_shorter_than_region_rejected() {
1145        let schema = user_schema();
1146        let layout = SchemaLayout::offsets_for(&schema).expect("layout");
1147        let bytes = vec![0u8; 4];
1148        let region = Region::new(0, 64).expect("region");
1149        let err = verify_record(&bytes, &layout, &schema.fields, 0, region)
1150            .expect_err("region beyond slice must reject");
1151        assert!(matches!(
1152            err,
1153            VerifyError::BufferShorterThanRegion { have: 4, end: 64 }
1154        ));
1155    }
1156
1157    #[test]
1158    fn degenerate_region_rejected() {
1159        let err = Region::new(10, 4).expect_err("inverted region");
1160        assert!(matches!(
1161            err,
1162            VerifyError::DegenerateRegion { start: 10, end: 4 }
1163        ));
1164    }
1165
1166    // ---- in-place region-walk return ABI (`verify_value_at`) ------------
1167
1168    /// Build a single-field `Ret { value: List<List<Int>> }` buffer, the
1169    /// shape the S1 in-place return decodes. Returns the bytes plus the
1170    /// `value` slot's `(list_element, root header offset)` so a test can
1171    /// drive `verify_value_at` directly the way the host does.
1172    fn list_list_int_buffer() -> (Vec<u8>, Option<ListElementKind>, usize) {
1173        let schema = Schema {
1174            name: "Ret".into(),
1175            generics: vec![],
1176            is_tuple: false,
1177            fields: vec![field("value", list(list(TypeRepr::Int)))],
1178        };
1179        let layout = SchemaLayout::offsets_for(&schema).expect("layout");
1180        let mut b = BufferBuilder::new(&layout, &schema.fields);
1181        // Build the `Value` rows the host writer consumes:
1182        // `[[1, 2], [3], []]`.
1183        let rows: Vec<crate::value::Value> = vec![
1184            crate::value::Value::List(std::sync::Arc::new(vec![
1185                crate::value::Value::Int(1),
1186                crate::value::Value::Int(2),
1187            ])),
1188            crate::value::Value::List(std::sync::Arc::new(vec![crate::value::Value::Int(3)])),
1189            crate::value::Value::List(std::sync::Arc::new(vec![])),
1190        ];
1191        crate::buffer::write_nested_scalar_list(&mut b, "value", &TypeRepr::Int, &rows)
1192            .expect("write nested list");
1193        let bytes = b.finish();
1194        // The fixed-area slot for `value` holds the buffer-relative offset
1195        // of the outer pointer-array header — exactly the `root_abs` the
1196        // machine code reports (rebased to region-relative).
1197        let fo = &layout.fields[0];
1198        let mut slot = [0u8; 4];
1199        slot.copy_from_slice(&bytes[fo.offset..fo.offset + 4]);
1200        let header_off = u32::from_le_bytes(slot) as usize;
1201        (bytes, fo.list_element, header_off)
1202    }
1203
1204    #[test]
1205    fn inplace_list_list_verifies_clean() {
1206        let (bytes, list_element, root) = list_list_int_buffer();
1207        let region = Region::new(0, bytes.len()).expect("region");
1208        verify_value_at(
1209            &bytes,
1210            &list(list(TypeRepr::Int)),
1211            list_element,
1212            root,
1213            region,
1214        )
1215        .expect("a legal in-place List<List<Int>> root must verify");
1216    }
1217
1218    #[test]
1219    fn inplace_corrupt_inner_pointer_rejected() {
1220        // Corrupt one outer entry pointer so an inner row dereferences
1221        // off-buffer; `verify_value_at` must reject loudly, not over-read.
1222        let (mut bytes, list_element, root) = list_list_int_buffer();
1223        // Outer header at `root`: `[len][off_0][off_1][off_2]`. Smash
1224        // `off_0` (first entry) to a far offset.
1225        let off0_pos = root + 4;
1226        let bogus = (bytes.len() as u32 + 4096).to_le_bytes();
1227        bytes[off0_pos..off0_pos + 4].copy_from_slice(&bogus);
1228        let region = Region::new(0, bytes.len()).expect("region");
1229        let err = verify_value_at(
1230            &bytes,
1231            &list(list(TypeRepr::Int)),
1232            list_element,
1233            root,
1234            region,
1235        )
1236        .expect_err("a corrupt inner pointer must be rejected loudly");
1237        assert!(
1238            matches!(err, VerifyError::OutOfRegion { .. }),
1239            "got {err:?}"
1240        );
1241    }
1242
1243    #[test]
1244    fn inplace_root_outside_region_rejected() {
1245        // The root header is legal whole-buffer, but the asserted region
1246        // ends before it — the single-region catch turns "root in the
1247        // wrong region" into a loud error instead of a decode.
1248        let (bytes, list_element, root) = list_list_int_buffer();
1249        let region = Region::new(0, root).expect("region"); // excludes header
1250        let err = verify_value_at(
1251            &bytes,
1252            &list(list(TypeRepr::Int)),
1253            list_element,
1254            root,
1255            region,
1256        )
1257        .expect_err("a root outside the region must be rejected");
1258        assert!(
1259            matches!(err, VerifyError::OutOfRegion { .. }),
1260            "got {err:?}"
1261        );
1262    }
1263
1264    // ---- in-place `List<String>` root (S3 String-element layer) ---------
1265
1266    /// Build a single-field `Ret { value: List<String> }` buffer, the
1267    /// shape the S3 in-place return decodes. Returns the bytes plus the
1268    /// `value` slot's `(list_element, root header offset)` so a test can
1269    /// drive `verify_value_at` directly the way the host does — the root
1270    /// header is `[len][off_0..]` and each `off_i` points at a String
1271    /// `[slen][utf8]` record.
1272    fn list_string_buffer(items: &[&str]) -> (Vec<u8>, Option<ListElementKind>, usize) {
1273        let schema = Schema {
1274            name: "Ret".into(),
1275            generics: vec![],
1276            is_tuple: false,
1277            fields: vec![field("value", list(TypeRepr::String))],
1278        };
1279        let layout = SchemaLayout::offsets_for(&schema).expect("layout");
1280        let mut b = BufferBuilder::new(&layout, &schema.fields);
1281        b.write_list_string("value", items).expect("write list str");
1282        let bytes = b.finish();
1283        let fo = &layout.fields[0];
1284        let mut slot = [0u8; 4];
1285        slot.copy_from_slice(&bytes[fo.offset..fo.offset + 4]);
1286        let header_off = u32::from_le_bytes(slot) as usize;
1287        (bytes, fo.list_element, header_off)
1288    }
1289
1290    #[test]
1291    fn inplace_list_string_verifies_clean() {
1292        // Empty string, ascii, multibyte (built from code points so the
1293        // source stays ascii) — all must verify in-region.
1294        let multibyte: String = [0x4E2Du32, 0x6587]
1295            .iter()
1296            .map(|c| char::from_u32(*c).unwrap())
1297            .collect();
1298        let items = ["", "x", "abc", multibyte.as_str()];
1299        let (bytes, list_element, root) = list_string_buffer(&items);
1300        let region = Region::new(0, bytes.len()).expect("region");
1301        verify_value_at(&bytes, &list(TypeRepr::String), list_element, root, region)
1302            .expect("a legal in-place List<String> root must verify");
1303    }
1304
1305    #[test]
1306    fn inplace_list_string_corrupt_entry_pointer_rejected() {
1307        // Smash `off_0` (the first entry pointer) so the String record it
1308        // points at lands off-buffer; the verifier must reject loudly
1309        // rather than let the decode over-read a String record.
1310        let (mut bytes, list_element, root) = list_string_buffer(&["a", "bb"]);
1311        let off0_pos = root + 4;
1312        let bogus = (bytes.len() as u32 + 4096).to_le_bytes();
1313        bytes[off0_pos..off0_pos + 4].copy_from_slice(&bogus);
1314        let region = Region::new(0, bytes.len()).expect("region");
1315        let err = verify_value_at(&bytes, &list(TypeRepr::String), list_element, root, region)
1316            .expect_err("a corrupt entry pointer must be rejected");
1317        assert!(
1318            matches!(err, VerifyError::OutOfRegion { .. }),
1319            "got {err:?}"
1320        );
1321    }
1322
1323    #[test]
1324    fn inplace_list_string_overlong_str_len_rejected() {
1325        // Keep the entry pointers legal but inflate one String record's
1326        // length prefix so its utf8 payload span runs off the region —
1327        // the String-element bounds check must catch it.
1328        let (mut bytes, list_element, root) = list_string_buffer(&["ada", "bob"]);
1329        // off_0 lives at root+4; it points at the first String record's
1330        // `[slen][utf8]`. Overwrite that record's slen with a huge value.
1331        let mut o0 = [0u8; 4];
1332        o0.copy_from_slice(&bytes[root + 4..root + 8]);
1333        let rec0 = u32::from_le_bytes(o0) as usize;
1334        bytes[rec0..rec0 + 4].copy_from_slice(&0xFFFF_F000u32.to_le_bytes());
1335        let region = Region::new(0, bytes.len()).expect("region");
1336        let err = verify_value_at(&bytes, &list(TypeRepr::String), list_element, root, region)
1337            .expect_err("an overlong String len must be rejected");
1338        assert!(
1339            matches!(
1340                err,
1341                VerifyError::OutOfRegion {
1342                    what: "string payload",
1343                    ..
1344                }
1345            ),
1346            "got {err:?}"
1347        );
1348    }
1349
1350    #[test]
1351    fn inplace_list_string_outer_len_lies_rejected() {
1352        // Inflate the outer header's element count so the verifier walks
1353        // past the real entry array into unrelated bytes; the per-entry
1354        // pointer read (or its target) must escape the region loudly.
1355        let (mut bytes, list_element, root) = list_string_buffer(&["a"]);
1356        bytes[root..root + 4].copy_from_slice(&9999u32.to_le_bytes());
1357        let region = Region::new(0, bytes.len()).expect("region");
1358        let err = verify_value_at(&bytes, &list(TypeRepr::String), list_element, root, region)
1359            .expect_err("a lying outer len must be rejected");
1360        assert!(
1361            matches!(err, VerifyError::OutOfRegion { .. }),
1362            "got {err:?}"
1363        );
1364    }
1365
1366    #[test]
1367    fn inplace_list_string_root_outside_region_rejected() {
1368        // Legal whole-buffer header, but the asserted region ends before
1369        // it: the single-region catch turns "root in the wrong region"
1370        // into a loud error rather than a decode.
1371        let (bytes, list_element, root) = list_string_buffer(&["a", "b"]);
1372        let region = Region::new(0, root).expect("region"); // excludes header
1373        let err = verify_value_at(&bytes, &list(TypeRepr::String), list_element, root, region)
1374            .expect_err("a root outside the region must be rejected");
1375        assert!(
1376            matches!(err, VerifyError::OutOfRegion { .. }),
1377            "got {err:?}"
1378        );
1379    }
1380
1381    // ---- in-place `List<Schema>` root (S4 field-pointer layer) ----------
1382
1383    /// A `Cfg` schema whose fields interleave inline scalars with String
1384    /// and List<scalar>/List<String> tails at varied offsets — the exact
1385    /// historically-tricky mixed-offset layout (a String hiding after a
1386    /// Bool, a List between two scalars).
1387    fn cfg_schema() -> Schema {
1388        Schema {
1389            name: "Cfg".into(),
1390            generics: vec![],
1391            is_tuple: false,
1392            fields: vec![
1393                field("flag", TypeRepr::Bool),
1394                field("name", TypeRepr::String),
1395                field("port", TypeRepr::Int),
1396                field("tags", list(TypeRepr::String)),
1397                field("nums", list(TypeRepr::Int)),
1398            ],
1399        }
1400    }
1401
1402    /// Build a `Ret { value: List<Cfg> }` buffer with `n` sub-records, the
1403    /// shape the S4 in-place return decodes. Returns the bytes plus the
1404    /// `value` slot's `(list_element, root header offset)` so a test can
1405    /// drive `verify_value_at` the way the host does — the root header is
1406    /// `[len][off_i]` and each `off_i` points at a `Cfg` sub-record whose
1407    /// String / List fields point at their own tail records.
1408    fn list_cfg_buffer(n: usize) -> (Vec<u8>, Option<ListElementKind>, usize) {
1409        let cfg = cfg_schema();
1410        let cfg_layout = SchemaLayout::offsets_for(&cfg).expect("cfg layout");
1411        let ret = Schema {
1412            name: "Ret".into(),
1413            generics: vec![],
1414            is_tuple: false,
1415            fields: vec![field(
1416                "value",
1417                list(TypeRepr::Schema {
1418                    schema: Box::new(cfg.clone()),
1419                }),
1420            )],
1421        };
1422        let ret_layout = SchemaLayout::offsets_for(&ret).expect("ret layout");
1423        let mut b = BufferBuilder::new(&ret_layout, &ret.fields);
1424        let mut writer = b
1425            .list_record_writer("value", &cfg_layout, &cfg)
1426            .expect("list_record_writer");
1427        for i in 0..n {
1428            let mut child = writer.start_entry();
1429            child.write_bool("flag", i % 2 == 0).unwrap();
1430            child.write_string("name", &format!("cfg-{i}")).unwrap();
1431            child.write_int("port", (1000 + i) as i64).unwrap();
1432            child.write_list_string("tags", &["a", "", "bb"]).unwrap();
1433            child.write_list_int("nums", &[i as i64, -1, 7]).unwrap();
1434            writer.finish_entry(&mut b, child).expect("finish entry");
1435        }
1436        b.finish_list_record(writer).expect("finish list");
1437        let bytes = b.finish();
1438        let fo = &ret_layout.fields[0];
1439        let mut slot = [0u8; 4];
1440        slot.copy_from_slice(&bytes[fo.offset..fo.offset + 4]);
1441        let header_off = u32::from_le_bytes(slot) as usize;
1442        (bytes, fo.list_element, header_off)
1443    }
1444
1445    fn list_cfg_ty() -> TypeRepr {
1446        list(TypeRepr::Schema {
1447            schema: Box::new(cfg_schema()),
1448        })
1449    }
1450
1451    #[test]
1452    fn inplace_list_schema_verifies_clean() {
1453        let (bytes, list_element, root) = list_cfg_buffer(3);
1454        let region = Region::new(0, bytes.len()).expect("region");
1455        verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
1456            .expect("a legal in-place List<Schema> root must verify to the field-pointer layer");
1457    }
1458
1459    #[test]
1460    fn inplace_list_schema_empty_verifies_clean() {
1461        let (bytes, list_element, root) = list_cfg_buffer(0);
1462        let region = Region::new(0, bytes.len()).expect("region");
1463        verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
1464            .expect("an empty in-place List<Schema> must verify");
1465    }
1466
1467    #[test]
1468    fn inplace_list_schema_corrupt_entry_pointer_rejected() {
1469        // Smash `off_0` (the first sub-record pointer) so the Cfg record it
1470        // points at lands off-region; the verifier must reject before any
1471        // field-pointer is followed.
1472        let (mut bytes, list_element, root) = list_cfg_buffer(2);
1473        let off0_pos = root + 4;
1474        let bogus = (bytes.len() as u32 + 4096).to_le_bytes();
1475        bytes[off0_pos..off0_pos + 4].copy_from_slice(&bogus);
1476        let region = Region::new(0, bytes.len()).expect("region");
1477        let err = verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
1478            .expect_err("a corrupt sub-record pointer must be rejected");
1479        assert!(
1480            matches!(
1481                err,
1482                VerifyError::OutOfRegion { .. } | VerifyError::FixedSlotOutOfRegion { .. }
1483            ),
1484            "got {err:?}"
1485        );
1486    }
1487
1488    #[test]
1489    fn inplace_list_schema_corrupt_field_string_pointer_rejected() {
1490        // Keep every sub-record pointer legal, but smash one sub-record's
1491        // `name` String slot so the String record it points at escapes the
1492        // region. This is the field-pointer layer the verifier MUST reach —
1493        // the most easily-missed level.
1494        let (mut bytes, list_element, root) = list_cfg_buffer(1);
1495        // off_0 -> the single Cfg sub-record's fixed area.
1496        let mut o0 = [0u8; 4];
1497        o0.copy_from_slice(&bytes[root + 4..root + 8]);
1498        let sub_base = u32::from_le_bytes(o0) as usize;
1499        // Find the `name` slot offset within the Cfg fixed area.
1500        let cfg = cfg_schema();
1501        let cfg_layout = SchemaLayout::offsets_for(&cfg).expect("cfg layout");
1502        let name_fo = cfg_layout
1503            .fields
1504            .iter()
1505            .find(|fo| fo.name == "name")
1506            .expect("name field");
1507        let name_slot = sub_base + name_fo.offset;
1508        let bogus = (bytes.len() as u32 + 8192).to_le_bytes();
1509        bytes[name_slot..name_slot + 4].copy_from_slice(&bogus);
1510        let region = Region::new(0, bytes.len()).expect("region");
1511        let err = verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
1512            .expect_err("a corrupt sub-record String field pointer must be rejected");
1513        assert!(
1514            matches!(err, VerifyError::OutOfRegion { .. }),
1515            "got {err:?}"
1516        );
1517    }
1518
1519    #[test]
1520    fn inplace_list_schema_corrupt_field_list_pointer_rejected() {
1521        // Same as above, but smash a sub-record's `tags` (List<String>)
1522        // field slot so the list header escapes the region — proves the
1523        // verifier follows List field pointers inside the sub-record too.
1524        let (mut bytes, list_element, root) = list_cfg_buffer(1);
1525        let mut o0 = [0u8; 4];
1526        o0.copy_from_slice(&bytes[root + 4..root + 8]);
1527        let sub_base = u32::from_le_bytes(o0) as usize;
1528        let cfg = cfg_schema();
1529        let cfg_layout = SchemaLayout::offsets_for(&cfg).expect("cfg layout");
1530        let tags_fo = cfg_layout
1531            .fields
1532            .iter()
1533            .find(|fo| fo.name == "tags")
1534            .expect("tags field");
1535        let tags_slot = sub_base + tags_fo.offset;
1536        let bogus = (bytes.len() as u32 + 8192).to_le_bytes();
1537        bytes[tags_slot..tags_slot + 4].copy_from_slice(&bogus);
1538        let region = Region::new(0, bytes.len()).expect("region");
1539        let err = verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
1540            .expect_err("a corrupt sub-record List field pointer must be rejected");
1541        assert!(
1542            matches!(err, VerifyError::OutOfRegion { .. }),
1543            "got {err:?}"
1544        );
1545    }
1546
1547    #[test]
1548    fn inplace_list_schema_outer_len_lies_rejected() {
1549        // Inflate the outer header's element count so the verifier walks
1550        // past the real entry array; a per-entry pointer (or its target)
1551        // must escape the region loudly.
1552        let (mut bytes, list_element, root) = list_cfg_buffer(1);
1553        bytes[root..root + 4].copy_from_slice(&9999u32.to_le_bytes());
1554        let region = Region::new(0, bytes.len()).expect("region");
1555        let err = verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
1556            .expect_err("a lying outer len must be rejected");
1557        assert!(
1558            matches!(
1559                err,
1560                VerifyError::OutOfRegion { .. } | VerifyError::FixedSlotOutOfRegion { .. }
1561            ),
1562            "got {err:?}"
1563        );
1564    }
1565
1566    #[test]
1567    fn inplace_list_schema_root_outside_region_rejected() {
1568        let (bytes, list_element, root) = list_cfg_buffer(2);
1569        let region = Region::new(0, root).expect("region"); // excludes header
1570        let err = verify_value_at(&bytes, &list_cfg_ty(), list_element, root, region)
1571            .expect_err("a root outside the region must be rejected");
1572        assert!(
1573            matches!(err, VerifyError::OutOfRegion { .. }),
1574            "got {err:?}"
1575        );
1576    }
1577
1578    // ---- F5 doubly-nested pointer array (`List<List<String>>`) ----------
1579    //
1580    // The verifier must recurse all the way to the **innermost** String
1581    // record: outer entry -> inner list header -> inner entry -> String
1582    // record. These adversarial probes smash a pointer at each depth and
1583    // assert a loud reject (never a wild read past the region).
1584
1585    fn list_str(items: &[&str]) -> crate::value::Value {
1586        crate::value::Value::List(std::sync::Arc::new(
1587            items
1588                .iter()
1589                .map(|s| crate::value::Value::String((*s).into()))
1590                .collect(),
1591        ))
1592    }
1593
1594    /// Build a `Ret { value: List<List<String>> }` buffer and return the
1595    /// bytes plus the `value` slot's `(list_element, outer header offset)`.
1596    fn list_list_string_buffer() -> (Vec<u8>, Option<ListElementKind>, usize) {
1597        let schema = Schema {
1598            name: "Ret".into(),
1599            generics: vec![],
1600            is_tuple: false,
1601            fields: vec![field("value", list(list(TypeRepr::String)))],
1602        };
1603        let layout = SchemaLayout::offsets_for(&schema).expect("layout");
1604        let mut b = BufferBuilder::new(&layout, &schema.fields);
1605        let rows = vec![list_str(&["a", "", "bb"]), list_str(&[]), list_str(&["zz"])];
1606        crate::buffer::write_nested_pointer_array_list(&mut b, "value", &TypeRepr::String, &rows)
1607            .expect("write nested pointer-array list");
1608        let bytes = b.finish();
1609        let fo = &layout.fields[0];
1610        let mut slot = [0u8; 4];
1611        slot.copy_from_slice(&bytes[fo.offset..fo.offset + 4]);
1612        let header_off = u32::from_le_bytes(slot) as usize;
1613        (bytes, fo.list_element, header_off)
1614    }
1615
1616    fn list_list_string_ty() -> TypeRepr {
1617        list(list(TypeRepr::String))
1618    }
1619
1620    #[test]
1621    fn inplace_list_list_string_verifies_clean() {
1622        let (bytes, le, root) = list_list_string_buffer();
1623        let region = Region::new(0, bytes.len()).expect("region");
1624        verify_value_at(&bytes, &list_list_string_ty(), le, root, region)
1625            .expect("a legal List<List<String>> must verify to the innermost String");
1626    }
1627
1628    #[test]
1629    fn inplace_list_list_string_corrupt_outer_entry_rejected() {
1630        // Smash the first outer entry pointer so the inner list header it
1631        // names lands off-region.
1632        let (mut bytes, le, root) = list_list_string_buffer();
1633        let off0 = root + 4;
1634        let bogus = (bytes.len() as u32 + 4096).to_le_bytes();
1635        bytes[off0..off0 + 4].copy_from_slice(&bogus);
1636        let region = Region::new(0, bytes.len()).expect("region");
1637        let err = verify_value_at(&bytes, &list_list_string_ty(), le, root, region)
1638            .expect_err("a corrupt outer entry must be rejected");
1639        assert!(
1640            matches!(err, VerifyError::OutOfRegion { .. }),
1641            "got {err:?}"
1642        );
1643    }
1644
1645    #[test]
1646    fn inplace_list_list_string_corrupt_inner_entry_rejected() {
1647        // Follow the first outer entry to its inner list header, then smash
1648        // that header's first entry (which points at a String record) so the
1649        // String record escapes the region. Proves the verifier recurses
1650        // outer -> inner header -> inner entry.
1651        let (mut bytes, le, root) = list_list_string_buffer();
1652        // outer off_0 -> inner header.
1653        let mut o0 = [0u8; 4];
1654        o0.copy_from_slice(&bytes[root + 4..root + 8]);
1655        let inner_header = u32::from_le_bytes(o0) as usize;
1656        // inner header: [len][inner_off_0]…; smash inner_off_0.
1657        let inner_off0 = inner_header + 4;
1658        let bogus = (bytes.len() as u32 + 8192).to_le_bytes();
1659        bytes[inner_off0..inner_off0 + 4].copy_from_slice(&bogus);
1660        let region = Region::new(0, bytes.len()).expect("region");
1661        let err = verify_value_at(&bytes, &list_list_string_ty(), le, root, region)
1662            .expect_err("a corrupt inner entry must be rejected");
1663        assert!(
1664            matches!(err, VerifyError::OutOfRegion { .. }),
1665            "got {err:?}"
1666        );
1667    }
1668
1669    #[test]
1670    fn inplace_list_list_string_overlong_innermost_str_rejected() {
1671        // Reach the innermost String record (outer_0 -> inner header ->
1672        // inner_off_0 -> String record) and inflate its length prefix so the
1673        // utf8 payload runs off the region. The deepest-level bounds check
1674        // must catch it — proof the recursion reaches the String layer.
1675        let (mut bytes, le, root) = list_list_string_buffer();
1676        let mut o0 = [0u8; 4];
1677        o0.copy_from_slice(&bytes[root + 4..root + 8]);
1678        let inner_header = u32::from_le_bytes(o0) as usize;
1679        let mut i0 = [0u8; 4];
1680        i0.copy_from_slice(&bytes[inner_header + 4..inner_header + 8]);
1681        let str_rec = u32::from_le_bytes(i0) as usize;
1682        bytes[str_rec..str_rec + 4].copy_from_slice(&0xFFFF_F000u32.to_le_bytes());
1683        let region = Region::new(0, bytes.len()).expect("region");
1684        let err = verify_value_at(&bytes, &list_list_string_ty(), le, root, region)
1685            .expect_err("an overlong innermost String must be rejected");
1686        assert!(
1687            matches!(
1688                err,
1689                VerifyError::OutOfRegion {
1690                    what: "string payload",
1691                    ..
1692                }
1693            ),
1694            "got {err:?}"
1695        );
1696    }
1697
1698    // ---- multi-region walk (F0 cross-region safety net) -----------------
1699    //
1700    // These build a synthetic arena by hand the way the F1 cross-region
1701    // codegen will (object head in `out`, parameter-sourced field data in
1702    // `in`, every pointer arena-absolute) and drive the multi-region
1703    // verifier directly — F0 ships the safety net, not the cap release,
1704    // so the bytes are laid out here rather than produced by a compiled
1705    // backend.
1706
1707    /// Lay out a fixed four-region arena and return `(arena, multi)`.
1708    /// Layout (absolute byte offsets):
1709    /// `const = [0, 16)`, `in = [16, 16+in_len)`,
1710    /// `out = [out_start, out_start+out_len)`, `scratch = [.., end)`.
1711    /// Each region is generously padded so a test can place records freely.
1712    fn arena_with_regions(
1713        in_bytes: &[u8],
1714        out_bytes: &[u8],
1715    ) -> (Vec<u8>, MultiRegion, usize, usize) {
1716        let const_len = 16usize;
1717        let in_start = const_len;
1718        let in_len = in_bytes.len().max(4);
1719        let in_end = in_start + in_len;
1720        // 8-byte gap so the regions are clearly disjoint, never adjacent.
1721        let out_start = in_end + 8;
1722        let out_len = out_bytes.len().max(4);
1723        let out_end = out_start + out_len;
1724        let scratch_start = out_end + 8;
1725        let scratch_len = 16usize;
1726        let arena_size = scratch_start + scratch_len;
1727
1728        let mut arena = vec![0u8; arena_size];
1729        arena[in_start..in_start + in_bytes.len()].copy_from_slice(in_bytes);
1730        arena[out_start..out_start + out_bytes.len()].copy_from_slice(out_bytes);
1731
1732        let multi = MultiRegion::new(
1733            (0, const_len),
1734            (in_start, in_end),
1735            (out_start, out_end),
1736            (scratch_start, arena_size),
1737        )
1738        .expect("multi region");
1739        (arena, multi, in_start, out_start)
1740    }
1741
1742    /// Encode a String record `[len: u32 LE][utf8]` into `buf` and return
1743    /// its byte length.
1744    fn string_record(s: &str) -> Vec<u8> {
1745        let mut v = Vec::with_capacity(4 + s.len());
1746        v.extend_from_slice(&(s.len() as u32).to_le_bytes());
1747        v.extend_from_slice(s.as_bytes());
1748        v
1749    }
1750
1751    /// The F1 cross-region shape `Cfg { name: String, port: Int }`: the
1752    /// record head lives in `out`, but `name`'s String record lives in
1753    /// `in`, reached by an **arena-absolute** pointer in the head's slot.
1754    fn cross_region_cfg() -> Schema {
1755        Schema {
1756            name: "Cfg".into(),
1757            generics: vec![],
1758            is_tuple: false,
1759            fields: vec![
1760                field("name", TypeRepr::String),
1761                field("port", TypeRepr::Int),
1762            ],
1763        }
1764    }
1765
1766    /// Build the cross-region `Cfg` arena. Returns `(arena, multi, layout,
1767    /// schema, out_record_base, name_string_abs)`.
1768    #[allow(clippy::type_complexity)]
1769    fn cross_region_cfg_arena() -> (Vec<u8>, MultiRegion, OffsetTable, Schema, usize, usize) {
1770        let schema = cross_region_cfg();
1771        let layout = SchemaLayout::offsets_for(&schema).expect("cfg layout");
1772        // `in` region carries just the String record for `name`.
1773        let in_bytes = string_record("ada");
1774        // `out` region carries the Cfg fixed area; fill the slots after we
1775        // know the absolute offsets.
1776        let out_bytes = vec![0u8; layout.root_size];
1777        let (mut arena, multi, in_start, out_start) = arena_with_regions(&in_bytes, &out_bytes);
1778
1779        let name_fo = layout
1780            .fields
1781            .iter()
1782            .find(|fo| fo.name == "name")
1783            .expect("name fo");
1784        let port_fo = layout
1785            .fields
1786            .iter()
1787            .find(|fo| fo.name == "port")
1788            .expect("port fo");
1789        // `name` slot holds the arena-absolute offset of the String record
1790        // (which sits at `in_start`).
1791        let name_string_abs = in_start;
1792        let name_slot = out_start + name_fo.offset;
1793        arena[name_slot..name_slot + 4].copy_from_slice(&(name_string_abs as u32).to_le_bytes());
1794        // `port` is an inline Int.
1795        let port_slot = out_start + port_fo.offset;
1796        arena[port_slot..port_slot + 8].copy_from_slice(&8080i64.to_le_bytes());
1797
1798        (arena, multi, layout, schema, out_start, name_string_abs)
1799    }
1800
1801    #[test]
1802    fn multi_region_cross_region_record_verifies_clean() {
1803        // The legal F1 shape: head in `out`, String field in `in`. The
1804        // multi-region verifier must accept the cross-region pointer the
1805        // single-region wall would (correctly, for S1-S6) reject.
1806        let (arena, multi, layout, schema, base, _) = cross_region_cfg_arena();
1807        verify_record_multi(&arena, &layout, &schema.fields, base, multi)
1808            .expect("a legal cross-region record must verify under the multi-region map");
1809    }
1810
1811    #[test]
1812    fn multi_region_pointer_to_no_region_rejected() {
1813        // Smash the `name` slot so it points into the inter-region gap
1814        // (between `in` and `out`) — a span that fits no region. The
1815        // multi-region verifier must reject loudly, never over-read.
1816        let (mut arena, multi, layout, schema, base, _) = cross_region_cfg_arena();
1817        let name_fo = layout.fields.iter().find(|fo| fo.name == "name").unwrap();
1818        let name_slot = base + name_fo.offset;
1819        // The gap between `in_end` and `out_start`: in_start=16, in_len=8
1820        // (string "ada" record is 7 bytes -> max(7,4)=7? string_record is
1821        // 4+3=7, so in_len=max(7,4)=7, in_end=23, gap=[23,31)). Point at 24.
1822        let in_start = 16usize;
1823        let in_record_len = string_record("ada").len();
1824        let in_len = in_record_len.max(4);
1825        let gap_off = in_start + in_len + 1; // strictly inside the gap
1826        arena[name_slot..name_slot + 4].copy_from_slice(&(gap_off as u32).to_le_bytes());
1827        let err = verify_record_multi(&arena, &layout, &schema.fields, base, multi)
1828            .expect_err("a pointer into the inter-region gap must be rejected");
1829        assert!(matches!(err, VerifyError::NoRegion { .. }), "got {err:?}");
1830    }
1831
1832    #[test]
1833    fn multi_region_pointer_payload_runs_off_region_rejected() {
1834        // Keep the `name` pointer landing in `in`, but inflate the String
1835        // record's length prefix so the utf8 payload runs past `in_end`
1836        // into the gap. The payload span starts in `in` but escapes it —
1837        // must be rejected (no "fell into the next region" over-read).
1838        let (mut arena, multi, layout, schema, base, name_abs) = cross_region_cfg_arena();
1839        // The String record's len prefix sits at `name_abs`.
1840        arena[name_abs..name_abs + 4].copy_from_slice(&0xFFFF_F000u32.to_le_bytes());
1841        let err = verify_record_multi(&arena, &layout, &schema.fields, base, multi)
1842            .expect_err("an overlong String payload escaping its region must be rejected");
1843        assert!(matches!(err, VerifyError::NoRegion { .. }), "got {err:?}");
1844    }
1845
1846    #[test]
1847    fn multi_region_record_head_in_no_region_rejected() {
1848        // Anchor the record head at an absolute offset that fits no
1849        // region (in the gap). The fixed-area check must reject before any
1850        // slot is read.
1851        let (arena, multi, layout, schema, _, _) = cross_region_cfg_arena();
1852        let in_start = 16usize;
1853        let in_len = string_record("ada").len().max(4);
1854        let gap_base = in_start + in_len + 1;
1855        let err = verify_record_multi(&arena, &layout, &schema.fields, gap_base, multi)
1856            .expect_err("a record head fitting no region must be rejected");
1857        assert!(matches!(err, VerifyError::NoRegion { .. }), "got {err:?}");
1858    }
1859
1860    /// Build a cross-region `-> Dict { servers: List<Cfg>, n: Int }` style
1861    /// shape: an outer object head in `out`, a `servers` field pointing at
1862    /// a `List<Cfg>` header **in `in`** whose entries and sub-records all
1863    /// live in `in` (the parameter-sourced identity return). This is the
1864    /// canonical F1+ cross-region object the multi-region verifier must
1865    /// certify.
1866    #[allow(clippy::type_complexity)]
1867    fn cross_region_dict_of_list_cfg() -> (Vec<u8>, MultiRegion, OffsetTable, Schema, usize) {
1868        // Inner element schema.
1869        let cfg = cross_region_cfg();
1870        let cfg_layout = SchemaLayout::offsets_for(&cfg).expect("cfg layout");
1871        // Outer object schema: { servers: List<Cfg>, n: Int }.
1872        let outer = Schema {
1873            name: "Out".into(),
1874            generics: vec![],
1875            is_tuple: false,
1876            fields: vec![
1877                field(
1878                    "servers",
1879                    list(TypeRepr::Schema {
1880                        schema: Box::new(cfg.clone()),
1881                    }),
1882                ),
1883                field("n", TypeRepr::Int),
1884            ],
1885        };
1886        let outer_layout = SchemaLayout::offsets_for(&outer).expect("outer layout");
1887
1888        // --- build the `in` region: a `List<Cfg>` with 2 entries ---------
1889        // Layout inside `in` (relative offsets we fix up to absolute):
1890        //   [list header: len=2][off_0][off_1]
1891        //   [cfg_0 fixed area][cfg_1 fixed area]
1892        //   [name_0 string][name_1 string]
1893        // We assemble it region-locally then place at `in_start`.
1894        let two = 2u32;
1895        let header_rel = 0usize;
1896        let entries_rel = header_rel + 4; // off_0, off_1
1897        let cfg0_rel = entries_rel + 8;
1898        let cfg1_rel = cfg0_rel + cfg_layout.root_size;
1899        let name0_rec = string_record("alpha");
1900        let name1_rec = string_record("beta");
1901        let name0_rel = cfg1_rel + cfg_layout.root_size;
1902        let name1_rel = name0_rel + name0_rec.len();
1903        let in_len = name1_rel + name1_rec.len();
1904
1905        let name_fo = cfg_layout.fields.iter().find(|f| f.name == "name").unwrap();
1906        let port_fo = cfg_layout.fields.iter().find(|f| f.name == "port").unwrap();
1907
1908        // We don't yet know in_start; build with a placeholder then patch.
1909        // To keep it simple, compute in_start from arena_with_regions by
1910        // building a zero in-buffer of the right length first.
1911        let in_placeholder = vec![0u8; in_len];
1912        let out_placeholder = vec![0u8; outer_layout.root_size];
1913        let (mut arena, multi, in_start, out_start) =
1914            arena_with_regions(&in_placeholder, &out_placeholder);
1915
1916        // Now fill `in` with absolute offsets.
1917        let put_u32 = |arena: &mut [u8], abs: usize, v: u32| {
1918            arena[abs..abs + 4].copy_from_slice(&v.to_le_bytes());
1919        };
1920        let put_i64 = |arena: &mut [u8], abs: usize, v: i64| {
1921            arena[abs..abs + 8].copy_from_slice(&v.to_le_bytes());
1922        };
1923        // list header len
1924        put_u32(&mut arena, in_start + header_rel, two);
1925        // entry pointers (absolute) -> cfg fixed areas
1926        put_u32(
1927            &mut arena,
1928            in_start + entries_rel,
1929            (in_start + cfg0_rel) as u32,
1930        );
1931        put_u32(
1932            &mut arena,
1933            in_start + entries_rel + 4,
1934            (in_start + cfg1_rel) as u32,
1935        );
1936        // cfg_0: name -> name0 (absolute), port inline
1937        put_u32(
1938            &mut arena,
1939            in_start + cfg0_rel + name_fo.offset,
1940            (in_start + name0_rel) as u32,
1941        );
1942        put_i64(&mut arena, in_start + cfg0_rel + port_fo.offset, 1);
1943        // cfg_1: name -> name1 (absolute), port inline
1944        put_u32(
1945            &mut arena,
1946            in_start + cfg1_rel + name_fo.offset,
1947            (in_start + name1_rel) as u32,
1948        );
1949        put_i64(&mut arena, in_start + cfg1_rel + port_fo.offset, 2);
1950        // name strings
1951        arena[in_start + name0_rel..in_start + name0_rel + name0_rec.len()]
1952            .copy_from_slice(&name0_rec);
1953        arena[in_start + name1_rel..in_start + name1_rel + name1_rec.len()]
1954            .copy_from_slice(&name1_rec);
1955
1956        // --- fill the `out` object head ----------------------------------
1957        let servers_fo = outer_layout
1958            .fields
1959            .iter()
1960            .find(|f| f.name == "servers")
1961            .unwrap();
1962        let n_fo = outer_layout.fields.iter().find(|f| f.name == "n").unwrap();
1963        // `servers` slot -> the List<Cfg> header in `in` (cross-region).
1964        put_u32(
1965            &mut arena,
1966            out_start + servers_fo.offset,
1967            (in_start + header_rel) as u32,
1968        );
1969        put_i64(&mut arena, out_start + n_fo.offset, 42);
1970
1971        (arena, multi, outer_layout, outer, out_start)
1972    }
1973
1974    #[test]
1975    fn multi_region_dict_of_list_cfg_verifies_clean() {
1976        // The full F1 cross-region object: object head in `out`, a
1977        // `List<Cfg>` field whose header, entries, sub-records, and every
1978        // String field all live in `in`. The multi-region verifier must
1979        // certify the whole graph clean.
1980        let (arena, multi, layout, schema, base) = cross_region_dict_of_list_cfg();
1981        verify_record_multi(&arena, &layout, &schema.fields, base, multi).expect(
1982            "a legal cross-region Dict { servers: List<Cfg>, n: Int } must verify multi-region",
1983        );
1984    }
1985
1986    #[test]
1987    fn multi_region_dict_of_list_cfg_corrupt_subrecord_field_rejected() {
1988        // Smash one sub-record's `name` field pointer (deep in `in`) so it
1989        // points at no region; the multi-region verifier must follow the
1990        // cross-region link all the way into the sub-record field layer
1991        // and reject loudly. The most easily-missed depth.
1992        let (mut arena, multi, layout, schema, base) = cross_region_dict_of_list_cfg();
1993        // Re-derive the absolute offset of cfg_0's name slot the same way
1994        // the builder did, then smash it.
1995        let cfg = cross_region_cfg();
1996        let cfg_layout = SchemaLayout::offsets_for(&cfg).expect("cfg layout");
1997        let name_fo = cfg_layout.fields.iter().find(|f| f.name == "name").unwrap();
1998        let in_start = 16usize;
1999        let entries_rel = 4usize;
2000        let cfg0_rel = entries_rel + 8;
2001        let name_slot = in_start + cfg0_rel + name_fo.offset;
2002        // Point it far past the arena so it fits no region for sure
2003        // (recomputing the exact inter-region gap here is brittle; the
2004        // "fits no region" catch is what we assert).
2005        let bogus = (arena.len() as u32) + 4096;
2006        arena[name_slot..name_slot + 4].copy_from_slice(&bogus.to_le_bytes());
2007        let err = verify_record_multi(&arena, &layout, &schema.fields, base, multi)
2008            .expect_err("a corrupt cross-region sub-record field pointer must be rejected");
2009        assert!(matches!(err, VerifyError::NoRegion { .. }), "got {err:?}");
2010    }
2011
2012    #[test]
2013    fn multi_region_buffer_shorter_than_regions_rejected() {
2014        // A `multi` whose largest region end exceeds the slice length must
2015        // be rejected up front, never indexed past.
2016        let multi = MultiRegion::new((0, 8), (8, 16), (16, 64), (64, 80)).expect("multi");
2017        let schema = cross_region_cfg();
2018        let layout = SchemaLayout::offsets_for(&schema).expect("layout");
2019        let short = vec![0u8; 16];
2020        let err = verify_record_multi(&short, &layout, &schema.fields, 16, multi)
2021            .expect_err("a slice shorter than the region span must be rejected");
2022        assert!(
2023            matches!(
2024                err,
2025                VerifyError::BufferShorterThanRegion { have: 16, end: 80 }
2026            ),
2027            "got {err:?}"
2028        );
2029    }
2030}