From a0ba58a8e8869744094cda23f52ba497d7900b78 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 30 May 2026 14:50:28 +0100 Subject: [PATCH] feat(verify): land verify_capabilities + verify_access_sites passes (proposal 0001 + 0002 Gate 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes proposal 0002 §Gate 4 (`verify_access_sites` cross-section pass) and adds the equivalent for proposal 0001's capabilities carrier (§"What the proposal adds" #2). Both are now thoroughly tested cross-section verifier passes. ## New public API `crates/typed-wasm-verify/src/lib.rs`: - `CAPABILITIES_SECTION_NAME` = "typedwasm.capabilities" (gated `unstable-l15`) - `ACCESS_SITES_SECTION_NAME` = "typedwasm.access-sites" (gated `unstable-l2`) - `CapabilitiesError` enum (gated `unstable-l15`): `FuncIdxOutOfRange`, `CapabilityIdxOutOfRange` - `AccessSiteError` enum (gated `unstable-l2`): `MissingDependentRegions`, `FuncIdxOutOfRange`, `RegionIdOutOfRange`, `FieldIdOutOfRange` - `verify_capabilities_from_module(wasm_bytes)` — entry point - `verify_access_sites_from_module(wasm_bytes)` — entry point ## New impls `crates/typed-wasm-verify/src/verify.rs`: - `function_count(wasm_bytes)` — small helper: scans Import + Function sections to get total `import_count + locally_defined_count`. Shared by both new passes. - `verify_capabilities_from_module` — iterates the capabilities section, checks each `func_idx` against `function_count()` and each per-function `required` index against `capability_count`. - `verify_access_sites_from_module` — iterates the access-sites section + regions section together (single pass). Checks: 1. `MissingDependentCarrier` (proposal 0002 §"Producer obligations" #2): access-sites present without regions is a hard error. Treated identically if regions present but unparseable (version mismatch). 2. `func_idx` in range against `function_count()`. 3. `region_id` in range against regions table. 4. `field_id` in range against target region's field table (skipped if region_id was out of range — can't cross-reference a missing row). `DistinctCaps` (strictly-increasing per-function required list) is NOT re-checked because the codec parser already normalises required indices to sorted+deduped form. Verifying that would require parsing raw wire bytes pre-normalisation; defer as a producer-side property test. `AccessSiteMisalignment` (instruction_byte_offset doesn't land on a typed access opcode) is NOT checked — would require parsing function bodies to verify the offset. Proposal 0002 explicitly defers this to a follow-up. ## Tests (11 new) `capabilities_verifier_tests` (5, gated `unstable-l15`): - module without section verifies trivially - well-formed capabilities verifies clean - out-of-bounds func_idx is flagged - out-of-bounds capability index is flagged - imports count toward function_count (validates Import section is consulted in addition to Function section) `access_sites_verifier_tests` (6, gated `unstable-l2`): - module without access-sites section verifies trivially - access-sites without regions = MissingDependentRegions - well-formed access-sites verifies clean - out-of-bounds func_idx is flagged - out-of-bounds region_id is flagged + skips field check - out-of-bounds field_id is flagged All 4 feature combos green: | features | tests passed | |--------------------------------|--------------| | (default) | 43 | | unstable-l2 | 65 | | unstable-l15 | 55 | | unstable-l2,unstable-l15 | 77 | ## What this leaves for [accepted] For proposal 0001: - §Criterion 2: wire-vs-Idris2-spec review — could land as a Coq-style proof-doc OR an Idris2 `Schema ≡ wire_layout` property test in `src/abi/`. Not real impl, but a deliberate write-up step. For proposal 0002: - §Gate 6: producer codegen issues for access-sites (affinescript + ephapax). Filed under #97-adjacent producer-readiness checklist. Both proposals are now ~95% through their acceptance criteria. Refs hyperpolymath/typed-wasm#34 hyperpolymath/typed-wasm#76 hyperpolymath/typed-wasm#78 Refs hyperpolymath/typed-wasm#86 hyperpolymath/typed-wasm#106 Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/typed-wasm-verify/src/lib.rs | 109 ++++++ crates/typed-wasm-verify/src/verify.rs | 517 +++++++++++++++++++++++++ 2 files changed, 626 insertions(+) diff --git a/crates/typed-wasm-verify/src/lib.rs b/crates/typed-wasm-verify/src/lib.rs index eef1b42..123d790 100644 --- a/crates/typed-wasm-verify/src/lib.rs +++ b/crates/typed-wasm-verify/src/lib.rs @@ -138,6 +138,71 @@ pub const OWNERSHIP_SECTION_NAME: &str = "typedwasm.ownership"; #[cfg(feature = "unstable-l2")] pub const REGIONS_SECTION_NAME: &str = "typedwasm.regions"; +/// Custom-section name carrying L15 capability lattice (proposal 0001). +/// UNSTABLE. +#[cfg(feature = "unstable-l15")] +pub const CAPABILITIES_SECTION_NAME: &str = "typedwasm.capabilities"; + +/// Custom-section name carrying per-instruction `(region_id, field_id)` +/// mapping (proposal 0002, typed-wasm#86). UNSTABLE. +#[cfg(feature = "unstable-l2")] +pub const ACCESS_SITES_SECTION_NAME: &str = "typedwasm.access-sites"; + +/// L15 capability-section violation (parsing succeeded, content invalid). +#[cfg(feature = "unstable-l15")] +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum CapabilitiesError { + #[error("Level 15 violation: function index {func_idx} (entry {entry_idx}) is out of bounds for wasm function section (function_count = {function_count})")] + FuncIdxOutOfRange { + entry_idx: u32, + func_idx: u32, + function_count: u32, + }, + + #[error("Level 15 violation: capability index {cap_idx} in function entry {entry_idx} (func_idx = {func_idx}) is out of bounds for capability table (capability_count = {capability_count})")] + CapabilityIdxOutOfRange { + entry_idx: u32, + func_idx: u32, + cap_idx: u32, + capability_count: u32, + }, +} + +/// L2 access-site-section violation. +#[cfg(feature = "unstable-l2")] +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum AccessSiteError { + /// Hard error per proposal 0002 §"Producer obligations" #2: a module + /// with a `typedwasm.access-sites` section must also have a + /// `typedwasm.regions` section — the access-site entries reference + /// `region_id` + `field_id` keys with nothing to resolve them + /// against otherwise. + #[error("Level 2 violation: typedwasm.access-sites section emitted without companion typedwasm.regions section (MissingDependentCarrier)")] + MissingDependentRegions, + + #[error("Level 2 violation: access-site entry {entry_idx}: func_idx {func_idx} is out of bounds for wasm function section (function_count = {function_count})")] + FuncIdxOutOfRange { + entry_idx: u32, + func_idx: u32, + function_count: u32, + }, + + #[error("Level 2 violation: access-site entry {entry_idx}: region_id {region_id} is out of bounds for typedwasm.regions table (region_count = {region_count})")] + RegionIdOutOfRange { + entry_idx: u32, + region_id: u32, + region_count: u32, + }, + + #[error("Level 2 violation: access-site entry {entry_idx}: field_id {field_id} is out of bounds for region {region_id}'s field table (field_count = {field_count})")] + FieldIdOutOfRange { + entry_idx: u32, + region_id: u32, + field_id: u32, + field_count: u32, + }, +} + // ---------------------------------------------------------------------- // Public entry points (stubbed in C1; implementations land in C2-C4). // ---------------------------------------------------------------------- @@ -151,6 +216,50 @@ pub fn verify_from_module(wasm_bytes: &[u8]) -> Result<(), VerifyError> { verify::verify_from_module(wasm_bytes) } +/// Verify the L15 capability constraints on a wasm module by reading +/// its embedded `typedwasm.capabilities` custom section. Modules without +/// the section verify trivially. Checks: +/// +/// 1. Every per-function `func_idx` is within the module's function +/// section bounds. +/// 2. Every per-function `required` capability index is within the +/// section's capability table bounds. +/// +/// `DistinctCaps` (strictly-increasing per-function required list) is +/// not re-checked here because the codec parser already normalises +/// `required` to sorted+deduped form on read — verifying it would +/// require parsing the raw wire bytes pre-normalisation. +#[cfg(feature = "unstable-l15")] +pub fn verify_capabilities_from_module( + wasm_bytes: &[u8], +) -> Result, VerifyError> { + verify::verify_capabilities_from_module(wasm_bytes) +} + +/// Verify the L2 access-site constraints on a wasm module by reading +/// its embedded `typedwasm.access-sites` + `typedwasm.regions` custom +/// sections. Returns `Ok(vec![])` when no violations are found; modules +/// without the access-sites section verify trivially. Checks: +/// +/// 1. `MissingDependentCarrier`: access-sites present without regions +/// is a hard error (per proposal 0002 §"Producer obligations" #2). +/// 2. Every entry's `func_idx` is within the module's function section +/// bounds. +/// 3. Every entry's `region_id` is within the regions table. +/// 4. Every entry's `field_id` is within the target region's field +/// table. +/// +/// Does NOT check `instruction_byte_offset` validity (would require +/// parsing function bodies to verify the offset lands on a typed +/// access opcode — proposal 0002 calls this `AccessSiteMisalignment` +/// and defers it to a follow-up). +#[cfg(feature = "unstable-l2")] +pub fn verify_access_sites_from_module( + wasm_bytes: &[u8], +) -> Result, VerifyError> { + verify::verify_access_sites_from_module(wasm_bytes) +} + /// Ownership-annotated signature for one exported function. /// Mirrors OCaml `Tw_interface.func_interface`. #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/typed-wasm-verify/src/verify.rs b/crates/typed-wasm-verify/src/verify.rs index da4da06..c2b6587 100644 --- a/crates/typed-wasm-verify/src/verify.rs +++ b/crates/typed-wasm-verify/src/verify.rs @@ -823,3 +823,520 @@ mod tests { assert!(verify_from_module(&module.finish()).is_ok()); } } + +// ---------------------------------------------------------------------- +// L15 capabilities verifier pass (proposal 0001) +// ---------------------------------------------------------------------- + +#[cfg(feature = "unstable-l15")] +use crate::section::parse_capabilities_section_payload; +#[cfg(feature = "unstable-l15")] +use crate::{CapabilitiesError, CAPABILITIES_SECTION_NAME}; + +/// Pre-scan the module to discover `(import_count, locally_defined_count)` +/// pairs so cross-section verifiers can compute the total wasm function +/// count without re-parsing the whole module per check. +fn function_count(wasm_bytes: &[u8]) -> Result { + let parser = Parser::new(0); + let mut import_count: u32 = 0; + let mut local_count: u32 = 0; + for payload in parser.parse_all(wasm_bytes) { + match payload? { + Payload::ImportSection(reader) => { + for import in reader.into_imports() { + let import = import?; + if matches!(import.ty, wasmparser::TypeRef::Func(_)) { + import_count += 1; + } + } + } + Payload::FunctionSection(reader) => { + local_count = reader.count(); + } + _ => {} + } + } + Ok(import_count + local_count) +} + +#[cfg(feature = "unstable-l15")] +pub fn verify_capabilities_from_module( + wasm_bytes: &[u8], +) -> Result, VerifyError> { + // Locate the capabilities custom section. + let parser = Parser::new(0); + let mut payload_bytes: Option> = None; + for payload in parser.parse_all(wasm_bytes) { + if let Payload::CustomSection(reader) = payload? { + if reader.name() == CAPABILITIES_SECTION_NAME { + payload_bytes = Some(reader.data().to_vec()); + break; + } + } + } + let Some(payload) = payload_bytes else { + // No capabilities section: nothing constrained, verify trivially. + return Ok(vec![]); + }; + let Some((capabilities, functions)) = parse_capabilities_section_payload(&payload) else { + // Unsupported version: lenient — return no errors. Producers + // emitting a version we don't know are not our problem; they + // should bump the verifier first. + return Ok(vec![]); + }; + + let fn_count = function_count(wasm_bytes)?; + let cap_count = capabilities.len() as u32; + let mut errors = Vec::new(); + + for (entry_idx, fc) in functions.iter().enumerate() { + let entry_idx = entry_idx as u32; + if fc.func_idx >= fn_count { + errors.push(CapabilitiesError::FuncIdxOutOfRange { + entry_idx, + func_idx: fc.func_idx, + function_count: fn_count, + }); + } + for &cap_idx in &fc.required { + if cap_idx >= cap_count { + errors.push(CapabilitiesError::CapabilityIdxOutOfRange { + entry_idx, + func_idx: fc.func_idx, + cap_idx, + capability_count: cap_count, + }); + } + } + } + Ok(errors) +} + +// ---------------------------------------------------------------------- +// L2 access-sites verifier pass (proposal 0002) +// ---------------------------------------------------------------------- + +#[cfg(feature = "unstable-l2")] +use crate::section::{parse_access_sites_section_payload, parse_regions_section_payload}; +#[cfg(feature = "unstable-l2")] +use crate::{AccessSiteError, ACCESS_SITES_SECTION_NAME, REGIONS_SECTION_NAME}; + +#[cfg(feature = "unstable-l2")] +pub fn verify_access_sites_from_module( + wasm_bytes: &[u8], +) -> Result, VerifyError> { + // Collect both companion sections in a single pass. + let parser = Parser::new(0); + let mut access_sites_payload: Option> = None; + let mut regions_payload: Option> = None; + for payload in parser.parse_all(wasm_bytes) { + if let Payload::CustomSection(reader) = payload? { + match reader.name() { + ACCESS_SITES_SECTION_NAME => { + access_sites_payload = Some(reader.data().to_vec()); + } + REGIONS_SECTION_NAME => { + regions_payload = Some(reader.data().to_vec()); + } + _ => {} + } + } + } + let Some(access_payload) = access_sites_payload else { + // No access-sites section: trivially verified. Note that absence + // of the section means "no claim made about L2 enforcement," + // not "claim of compliance" — separate concern. + return Ok(vec![]); + }; + // MissingDependentCarrier check (proposal 0002 §"Producer obligations" #2): + // access-sites without regions is a hard error. + let Some(regions_bytes) = regions_payload else { + return Ok(vec![AccessSiteError::MissingDependentRegions]); + }; + let Some(regions) = parse_regions_section_payload(®ions_bytes) else { + // Regions section present but unparseable (version mismatch). + // Treat as missing for MissingDependentCarrier purposes — + // we can't validate against a table we can't read. + return Ok(vec![AccessSiteError::MissingDependentRegions]); + }; + let Some(entries) = parse_access_sites_section_payload(&access_payload) else { + // Unsupported access-sites version: lenient, no errors. + return Ok(vec![]); + }; + + let fn_count = function_count(wasm_bytes)?; + let region_count = regions.len() as u32; + let mut errors = Vec::new(); + + for (entry_idx, e) in entries.iter().enumerate() { + let entry_idx = entry_idx as u32; + if e.func_idx >= fn_count { + errors.push(AccessSiteError::FuncIdxOutOfRange { + entry_idx, + func_idx: e.func_idx, + function_count: fn_count, + }); + } + if e.region_id >= region_count { + errors.push(AccessSiteError::RegionIdOutOfRange { + entry_idx, + region_id: e.region_id, + region_count, + }); + // If region_id is out of bounds we cannot meaningfully + // check field_id — skip to next entry. + continue; + } + let field_count = regions[e.region_id as usize].fields.len() as u32; + if e.field_id >= field_count { + errors.push(AccessSiteError::FieldIdOutOfRange { + entry_idx, + region_id: e.region_id, + field_id: e.field_id, + field_count, + }); + } + } + Ok(errors) +} + +// ---------------------------------------------------------------------- +// Tests — capabilities + access-sites verifier passes +// ---------------------------------------------------------------------- + +#[cfg(all(test, feature = "unstable-l15"))] +mod capabilities_verifier_tests { + use super::*; + use crate::section::{ + build_capabilities_section_payload, CapabilityEntry, FunctionCapabilities, + }; + use wasm_encoder::{ + CodeSection, CustomSection, Function, FunctionSection, Instruction, Module, TypeSection, + ValType, + }; + + /// Build a valid wasm module with `n_locals` empty `() -> ()` + /// functions. wasm validation requires that FunctionSection and + /// CodeSection have matching counts; this helper enforces that + /// invariant so tests can focus on the section we're verifying. + fn module_with_n_funcs(n_locals: u32) -> Module { + let mut module = Module::new(); + let mut types = TypeSection::new(); + types + .ty() + .function(Vec::::new(), Vec::::new()); + module.section(&types); + let mut funcs = FunctionSection::new(); + for _ in 0..n_locals { + funcs.function(0); + } + module.section(&funcs); + let mut code = CodeSection::new(); + for _ in 0..n_locals { + let mut f = Function::new([]); + f.instruction(&Instruction::End); + code.function(&f); + } + module.section(&code); + module + } + + fn empty_module_with_function_section(n_locals: u32) -> Vec { + module_with_n_funcs(n_locals).finish() + } + + fn module_with_capabilities( + n_locals: u32, + caps: Vec, + funs: Vec, + ) -> Vec { + let mut module = module_with_n_funcs(n_locals); + let payload = build_capabilities_section_payload(&caps, &funs); + module.section(&CustomSection { + name: CAPABILITIES_SECTION_NAME.into(), + data: (&payload[..]).into(), + }); + module.finish() + } + + #[test] + fn module_without_section_verifies_trivially() { + let bytes = empty_module_with_function_section(2); + assert_eq!(verify_capabilities_from_module(&bytes).unwrap(), vec![]); + } + + #[test] + fn well_formed_capabilities_verifies_clean() { + let bytes = module_with_capabilities( + 3, + vec![ + CapabilityEntry { name: "net".into() }, + CapabilityEntry { name: "fs".into() }, + ], + vec![FunctionCapabilities { + func_idx: 1, + required: vec![0], + }], + ); + assert_eq!(verify_capabilities_from_module(&bytes).unwrap(), vec![]); + } + + #[test] + fn out_of_bounds_func_idx_is_flagged() { + let bytes = module_with_capabilities( + 2, + vec![CapabilityEntry { name: "net".into() }], + vec![FunctionCapabilities { + func_idx: 99, // module has only 2 functions + required: vec![0], + }], + ); + let errors = verify_capabilities_from_module(&bytes).unwrap(); + assert_eq!(errors.len(), 1); + assert!(matches!( + errors[0], + CapabilitiesError::FuncIdxOutOfRange { + entry_idx: 0, + func_idx: 99, + function_count: 2, + } + )); + } + + #[test] + fn out_of_bounds_capability_index_is_flagged() { + let bytes = module_with_capabilities( + 2, + vec![CapabilityEntry { name: "net".into() }], // 1 capability + vec![FunctionCapabilities { + func_idx: 0, + required: vec![0, 5], // 5 is out of bounds + }], + ); + let errors = verify_capabilities_from_module(&bytes).unwrap(); + assert_eq!(errors.len(), 1); + assert!(matches!( + errors[0], + CapabilitiesError::CapabilityIdxOutOfRange { + entry_idx: 0, + func_idx: 0, + cap_idx: 5, + capability_count: 1, + } + )); + } + + #[test] + fn imports_count_toward_function_count() { + use wasm_encoder::{EntityType, ImportSection}; + let mut module = Module::new(); + let mut types = TypeSection::new(); + types + .ty() + .function(Vec::::new(), Vec::::new()); + module.section(&types); + let mut imports = ImportSection::new(); + imports.import("env", "host", EntityType::Function(0)); + module.section(&imports); + let mut funcs = FunctionSection::new(); + funcs.function(0); // local func at index 1 (after the 1 import) + module.section(&funcs); + let mut code = CodeSection::new(); + let mut f = Function::new([]); + f.instruction(&Instruction::End); + code.function(&f); + module.section(&code); + let caps = vec![CapabilityEntry { name: "x".into() }]; + let funs = vec![FunctionCapabilities { + func_idx: 1, // valid: imported = 1, local = 1, total = 2 + required: vec![0], + }]; + let payload = build_capabilities_section_payload(&caps, &funs); + module.section(&CustomSection { + name: CAPABILITIES_SECTION_NAME.into(), + data: (&payload[..]).into(), + }); + assert_eq!( + verify_capabilities_from_module(&module.finish()).unwrap(), + vec![] + ); + } +} + +#[cfg(all(test, feature = "unstable-l2"))] +mod access_sites_verifier_tests { + use super::*; + use crate::section::{ + build_access_sites_section_payload, build_regions_section_payload, AccessSiteEntry, + FieldEntry, FieldKind, Nullability, RegionEntry, WasmTy, NO_TARGET_REGION, + }; + use wasm_encoder::{ + CodeSection, CustomSection, Function, FunctionSection, Instruction, Module, TypeSection, + ValType, + }; + + fn scalar_field(name: &str, ty: WasmTy) -> FieldEntry { + FieldEntry { + name: name.into(), + kind: FieldKind::Scalar, + wasm_ty: ty, + target_region: NO_TARGET_REGION, + nullability: Nullability::NonNull, + cardinality: 1, + } + } + + fn module_with_sections( + n_locals: u32, + regions: Option>, + entries: Option>, + ) -> Vec { + let mut module = Module::new(); + let mut types = TypeSection::new(); + types + .ty() + .function(Vec::::new(), Vec::::new()); + module.section(&types); + let mut funcs = FunctionSection::new(); + for _ in 0..n_locals { + funcs.function(0); + } + module.section(&funcs); + let mut code = CodeSection::new(); + for _ in 0..n_locals { + let mut f = Function::new([]); + f.instruction(&Instruction::End); + code.function(&f); + } + module.section(&code); + if let Some(regions) = regions { + let bytes = build_regions_section_payload(®ions); + module.section(&CustomSection { + name: REGIONS_SECTION_NAME.into(), + data: (&bytes[..]).into(), + }); + } + if let Some(entries) = entries { + let bytes = build_access_sites_section_payload(&entries); + module.section(&CustomSection { + name: ACCESS_SITES_SECTION_NAME.into(), + data: (&bytes[..]).into(), + }); + } + module.finish() + } + + #[test] + fn module_without_access_sites_section_verifies_trivially() { + let bytes = module_with_sections(2, None, None); + assert_eq!(verify_access_sites_from_module(&bytes).unwrap(), vec![]); + } + + #[test] + fn access_sites_without_regions_is_missing_dependent_carrier() { + let entries = vec![AccessSiteEntry { + func_idx: 0, + instruction_byte_offset: 7, + region_id: 0, + field_id: 0, + }]; + let bytes = module_with_sections(2, None, Some(entries)); + let errors = verify_access_sites_from_module(&bytes).unwrap(); + assert_eq!(errors, vec![AccessSiteError::MissingDependentRegions]); + } + + #[test] + fn well_formed_access_sites_verifies_clean() { + let regions = vec![RegionEntry { + name: "R".into(), + fields: vec![scalar_field("f", WasmTy::I32), scalar_field("g", WasmTy::F64)], + region_byte_size: 12, + }]; + let entries = vec![AccessSiteEntry { + func_idx: 0, + instruction_byte_offset: 7, + region_id: 0, + field_id: 1, + }]; + let bytes = module_with_sections(2, Some(regions), Some(entries)); + assert_eq!(verify_access_sites_from_module(&bytes).unwrap(), vec![]); + } + + #[test] + fn out_of_bounds_func_idx_is_flagged() { + let regions = vec![RegionEntry { + name: "R".into(), + fields: vec![scalar_field("f", WasmTy::I32)], + region_byte_size: 4, + }]; + let entries = vec![AccessSiteEntry { + func_idx: 42, // module has 2 funcs + instruction_byte_offset: 0, + region_id: 0, + field_id: 0, + }]; + let bytes = module_with_sections(2, Some(regions), Some(entries)); + let errors = verify_access_sites_from_module(&bytes).unwrap(); + assert!(matches!( + errors.as_slice(), + [AccessSiteError::FuncIdxOutOfRange { + func_idx: 42, + function_count: 2, + .. + }] + )); + } + + #[test] + fn out_of_bounds_region_id_is_flagged_and_skips_field_check() { + let regions = vec![RegionEntry { + name: "R".into(), + fields: vec![scalar_field("f", WasmTy::I32)], + region_byte_size: 4, + }]; + let entries = vec![AccessSiteEntry { + func_idx: 0, + instruction_byte_offset: 0, + region_id: 99, // module has 1 region + field_id: 99, // would-be out of bounds, but skipped + }]; + let bytes = module_with_sections(2, Some(regions), Some(entries)); + let errors = verify_access_sites_from_module(&bytes).unwrap(); + assert_eq!(errors.len(), 1); + assert!(matches!( + errors[0], + AccessSiteError::RegionIdOutOfRange { + region_id: 99, + region_count: 1, + .. + } + )); + } + + #[test] + fn out_of_bounds_field_id_is_flagged() { + let regions = vec![RegionEntry { + name: "R".into(), + fields: vec![scalar_field("f", WasmTy::I32)], + region_byte_size: 4, + }]; + let entries = vec![AccessSiteEntry { + func_idx: 0, + instruction_byte_offset: 0, + region_id: 0, + field_id: 7, // region R has 1 field + }]; + let bytes = module_with_sections(2, Some(regions), Some(entries)); + let errors = verify_access_sites_from_module(&bytes).unwrap(); + assert_eq!(errors.len(), 1); + assert!(matches!( + errors[0], + AccessSiteError::FieldIdOutOfRange { + region_id: 0, + field_id: 7, + field_count: 1, + .. + } + )); + } +}