Skip to main content

miden_note_schema/
codec_structure.rs

1//! Wasm feature and structural policy for note codec components.
2//!
3//! Parsing, compilation, and instantiation cost work before any fuel budget applies, so a byte
4//! cap alone does not bound that work. A small binary can declare thousands of tiny functions,
5//! thousands of globals, or a component tree whose instantiations expand exponentially.
6//!
7//! The rules are fixed policy. The producer applies them when it attaches a codec, and every
8//! consumer applies them when it loads one. [`validate_note_codec_component`] runs the whole
9//! policy in one place. It checks that:
10//!
11//! - the component fits in the caller's byte budget;
12//! - the component validates under [`NOTE_CODEC_WASM_FEATURES`];
13//! - the component declares no component-level start function;
14//! - each core module stays under its own counts, each of its function types stays under the
15//!   parameter and result caps, and its code section keeps a plausible average function size;
16//! - the component tree stays under its budgets for core modules and nesting depth;
17//! - one instantiation of any component in the tree stays under its budgets for core
18//!   instantiations, component instantiations, linear memories, and tables;
19//! - each kind of component-level entry stays under its cap for the whole tree.
20//!
21//! The walk counts what an instantiation creates, not what the tree declares. A core module that
22//! is instantiated twice counts twice, and a nested component contributes what one instantiation
23//! of it creates, once per instantiation. A consumer store admits the same counts, so a component
24//! that passes the walk instantiates inside the store limits.
25//!
26//! The walk rejects only a component-level start function. A core module keeps its own start
27//! function, which runs at instantiation under the consumer fuel budget and store limits.
28//!
29//! Nothing else is checked here. The producer checks the exported codec interface, and a
30//! consumer bounds the run time of a codec call with fuel and store limits.
31
32use wasmparser::{
33    ComponentAlias, ComponentExternalKind, ComponentInstance, ComponentOuterAliasKind,
34    ComponentTypeRef, Encoding, FuncValidatorAllocations, Instance, Parser, Payload, TypeRef,
35    ValidPayload, Validator, WasmFeatures,
36};
37
38use crate::{CodecFailure, Error, Result};
39
40/// Wasm proposals a note codec component may use.
41///
42/// The producer and every consumer validate against this constant, so the accepted set does not
43/// depend on the Cargo features a host compiled its engine with. The set holds the proposals the
44/// `wasm32-wasip2` target emits, the component model, and floating point. Every other proposal is
45/// off, including SIMD, threads, garbage collection, typed function references, and the
46/// asynchronous component-model additions.
47///
48/// The value is written bit by bit instead of subtracting from `WasmFeatures::default()` or
49/// `WasmFeatures::all()`, so a wasmparser upgrade cannot widen the policy.
50pub const NOTE_CODEC_WASM_FEATURES: WasmFeatures = WasmFeatures::COMPONENT_MODEL
51    .union(WasmFeatures::FLOATS)
52    .union(WasmFeatures::MUTABLE_GLOBAL)
53    .union(WasmFeatures::SATURATING_FLOAT_TO_INT)
54    .union(WasmFeatures::SIGN_EXTENSION)
55    .union(WasmFeatures::REFERENCE_TYPES)
56    .union(WasmFeatures::MULTI_VALUE)
57    .union(WasmFeatures::BULK_MEMORY);
58
59/// Maximum functions in one core module, imported and defined.
60const MAX_MODULE_FUNCTIONS: usize = 10_000;
61
62/// Maximum globals in one core module, imported and defined.
63const MAX_MODULE_GLOBALS: usize = 1_000;
64
65/// Maximum types in one core module.
66const MAX_MODULE_TYPES: usize = 1_000;
67
68/// Maximum tables in one core module, imported and defined.
69///
70/// A module that is instantiated reaches the tighter tree-wide instantiated-table budget first.
71/// This cap constrains imported tables, and the tables of a module that is never instantiated.
72const MAX_MODULE_TABLES: usize = 100;
73
74/// Maximum linear memories in one core module, imported and defined.
75const MAX_MODULE_MEMORIES: usize = 1;
76
77/// Maximum element segments in one core module.
78const MAX_MODULE_ELEMENT_SEGMENTS: usize = 1_000;
79
80/// Maximum data segments in one core module.
81const MAX_MODULE_DATA_SEGMENTS: usize = 1_000;
82
83/// Maximum imports in one core module.
84const MAX_MODULE_IMPORTS: usize = 1_024;
85
86/// Maximum exports in one core module.
87const MAX_MODULE_EXPORTS: usize = 1_024;
88
89/// Maximum parameters in one core function type.
90const MAX_FUNCTION_PARAMS: usize = 32;
91
92/// Maximum results in one core function type.
93const MAX_FUNCTION_RESULTS: usize = 32;
94
95/// Code section size, in bytes, above which the average function size applies.
96///
97/// Small modules stay below it, so a short helper module is never rejected for its shape.
98const MIN_METERED_CODE_SECTION_BYTES: u32 = 1_000;
99
100/// Minimum average bytes per function body in a metered code section.
101///
102/// Compiled Wasm averages far above this value. A module below it is a compilation bomb:
103/// many tiny functions that each cost a fixed amount of host work.
104const MIN_AVERAGE_FUNCTION_BYTES: u32 = 40;
105
106/// Maximum core modules in one component, at any nesting level.
107const MAX_CORE_MODULES: usize = 16;
108
109/// Maximum component nesting depth.
110const MAX_COMPONENT_DEPTH: usize = 4;
111
112/// Maximum entries of one kind of component-level item in the whole component tree.
113///
114/// A component may split one kind over many sections, so the cap counts each kind over the
115/// whole tree: core types, component types, aliases, canonical functions, imports, and exports.
116const MAX_COMPONENT_SECTION_ITEMS: usize = 256;
117
118/// Maximum core instances one instantiation of a component creates.
119///
120/// The consumer store admits the same number of instances.
121pub(crate) const MAX_CORE_INSTANCES: usize = 32;
122
123/// Maximum nested component instances one instantiation of a component creates.
124///
125/// A `wasm32-wasip2` codec builds one nested component instance per exported interface. The
126/// budget has no consumer store counterpart: a component instance holds no core instance,
127/// memory, or table of its own, and what it creates is counted through those budgets.
128const MAX_COMPONENT_INSTANCES: usize = 8;
129
130/// Maximum tables one instantiation of a component creates.
131///
132/// The consumer store admits the same number of tables.
133pub(crate) const MAX_INSTANTIATED_TABLES: usize = 32;
134
135/// Maximum linear memories one instantiation of a component creates.
136///
137/// The consumer store admits the same number of memories.
138pub(crate) const MAX_INSTANTIATED_MEMORIES: usize = 1;
139
140/// Applies the whole note codec component policy: the byte cap, the Wasm feature set, and the
141/// structural limits.
142///
143/// `max_bytes` is the caller's byte budget. The producer passes
144/// [`MAX_NOTE_CODEC_COMPONENT_BYTES`](crate::MAX_NOTE_CODEC_COMPONENT_BYTES), and a consumer
145/// passes the cap in its own limits.
146pub fn validate_note_codec_component(component: &[u8], max_bytes: usize) -> Result<()> {
147    ensure_component_byte_limit(component.len(), max_bytes)?;
148    validate_note_codec_structure(component)
149}
150
151/// Rejects a note codec component whose Wasm features or structure fall outside the policy.
152///
153/// [`validate_note_codec_component`] is the entry point both sides call. This function is public
154/// for a caller that bounds the byte length itself.
155pub fn validate_note_codec_structure(component: &[u8]) -> Result<()> {
156    let mut validator = Validator::new_with_features(NOTE_CODEC_WASM_FEATURES);
157    // Function bodies are validated with one reusable allocation, not one per function.
158    let mut allocations = FuncValidatorAllocations::default();
159    let mut walk = StructureWalk::default();
160    for payload in Parser::new(0).parse_all(component) {
161        let payload = payload.map_err(malformed)?;
162        // Reject a start function ahead of the validator. A start function runs guest code when
163        // the component is instantiated, before the export call the limits are built around, and
164        // the validator reports only that component values are disabled.
165        if matches!(payload, Payload::ComponentStartSection { .. }) {
166            return Err(policy_rejection(
167                "note codec component declares a start function; the policy rejects a component \
168                 that runs code when it is instantiated",
169            ));
170        }
171        if let ValidPayload::Func(function, body) =
172            validator.payload(&payload).map_err(rejected_feature)?
173        {
174            let mut function = function.into_validator(allocations);
175            function.validate(&body).map_err(rejected_feature)?;
176            allocations = function.into_allocations();
177        }
178        walk.visit(payload)?;
179    }
180    Ok(())
181}
182
183/// Rejects component bytes over the caller's budget, before any parsing or compilation.
184fn ensure_component_byte_limit(byte_len: usize, limit: usize) -> Result<()> {
185    if byte_len <= limit {
186        return Ok(());
187    }
188    Err(policy_rejection(format!(
189        "note codec component is {byte_len} bytes; the pre-compilation limit is {limit}"
190    )))
191}
192
193/// The state carried while the parser walks one component.
194#[derive(Default)]
195struct StructureWalk {
196    /// One frame per core module or component the walk entered, innermost last.
197    frames: Vec<Frame>,
198    /// Core modules seen anywhere in the component.
199    core_modules: usize,
200    /// Component-level items declared anywhere in the component, one count per kind.
201    component_items: ComponentItemCounts,
202}
203
204/// Component-level items counted over the whole component tree.
205#[derive(Default)]
206struct ComponentItemCounts {
207    core_types: usize,
208    types: usize,
209    aliases: usize,
210    canonical_functions: usize,
211    imports: usize,
212    exports: usize,
213}
214
215/// One nesting level of the walk.
216enum Frame {
217    /// A core module, with the counters checked when the module ends.
218    Module(ModuleCounts),
219    /// A component, with its index spaces and what one instantiation of it creates.
220    Component(ComponentFrame),
221}
222
223/// One component nesting level.
224///
225/// An index-space entry is `None` for a core module or a component the walk cannot read, such as
226/// an imported or an aliased one. An instantiation of such an entry is rejected.
227#[derive(Default)]
228struct ComponentFrame {
229    /// The core module index space of this component.
230    core_modules: Vec<Option<ModuleRuntimeCounts>>,
231    /// The component index space of this component.
232    components: Vec<Option<CreatedCounts>>,
233    /// What one instantiation of this component creates.
234    created: CreatedCounts,
235}
236
237/// What one core module creates every time it is instantiated.
238#[derive(Clone, Copy, Default)]
239struct ModuleRuntimeCounts {
240    memories: usize,
241    tables: usize,
242}
243
244/// What one instantiation of a component creates.
245#[derive(Clone, Copy, Default)]
246struct CreatedCounts {
247    core_instances: usize,
248    component_instances: usize,
249    memories: usize,
250    tables: usize,
251}
252
253impl CreatedCounts {
254    /// Adds what one nested instantiation creates.
255    fn add(&mut self, other: Self) {
256        self.core_instances = self.core_instances.saturating_add(other.core_instances);
257        self.component_instances =
258            self.component_instances.saturating_add(other.component_instances);
259        self.memories = self.memories.saturating_add(other.memories);
260        self.tables = self.tables.saturating_add(other.tables);
261    }
262
263    /// Checks every budget that bounds one instantiation.
264    fn check(&self) -> Result<()> {
265        ensure_created_cap("core instances", self.core_instances, MAX_CORE_INSTANCES)?;
266        ensure_created_cap(
267            "component instances",
268            self.component_instances,
269            MAX_COMPONENT_INSTANCES,
270        )?;
271        ensure_created_cap("linear memories", self.memories, MAX_INSTANTIATED_MEMORIES)?;
272        ensure_created_cap("tables", self.tables, MAX_INSTANTIATED_TABLES)
273    }
274}
275
276/// Counters collected for one core module.
277#[derive(Default)]
278struct ModuleCounts {
279    types: usize,
280    functions: usize,
281    globals: usize,
282    tables: usize,
283    memories: usize,
284    element_segments: usize,
285    data_segments: usize,
286    imports: usize,
287    exports: usize,
288    /// The memories and tables one instantiation of this module creates.
289    runtime: ModuleRuntimeCounts,
290}
291
292impl ModuleCounts {
293    /// Checks every per-module cap once the module ends.
294    fn check(&self) -> Result<()> {
295        ensure_module_cap("types", self.types, MAX_MODULE_TYPES)?;
296        ensure_module_cap("functions", self.functions, MAX_MODULE_FUNCTIONS)?;
297        ensure_module_cap("globals", self.globals, MAX_MODULE_GLOBALS)?;
298        ensure_module_cap("tables", self.tables, MAX_MODULE_TABLES)?;
299        ensure_module_cap("memories", self.memories, MAX_MODULE_MEMORIES)?;
300        ensure_module_cap("element segments", self.element_segments, MAX_MODULE_ELEMENT_SEGMENTS)?;
301        ensure_module_cap("data segments", self.data_segments, MAX_MODULE_DATA_SEGMENTS)?;
302        ensure_module_cap("imports", self.imports, MAX_MODULE_IMPORTS)?;
303        ensure_module_cap("exports", self.exports, MAX_MODULE_EXPORTS)
304    }
305}
306
307impl StructureWalk {
308    /// Applies one parser payload to the current nesting level.
309    fn visit(&mut self, payload: Payload<'_>) -> Result<()> {
310        match payload {
311            Payload::Version { encoding, .. } => self.enter(encoding)?,
312            Payload::End(_) => self.leave()?,
313            Payload::TypeSection(reader) => {
314                self.module_counts()?.types += reader.count() as usize;
315                // The feature validator rejects a GC type before the walk sees the section, so
316                // only a core function type reaches this loop.
317                for ty in reader.into_iter_err_on_gc_types() {
318                    let ty = ty.map_err(rejected_feature)?;
319                    ensure_signature_cap("parameters", ty.params().len(), MAX_FUNCTION_PARAMS)?;
320                    ensure_signature_cap("results", ty.results().len(), MAX_FUNCTION_RESULTS)?;
321                }
322            }
323            Payload::ImportSection(reader) => {
324                for import in reader.into_imports() {
325                    let import = import.map_err(malformed)?;
326                    let counts = self.module_counts()?;
327                    counts.imports += 1;
328                    match import.ty {
329                        TypeRef::Func(_) | TypeRef::FuncExact(_) => counts.functions += 1,
330                        TypeRef::Global(_) => counts.globals += 1,
331                        TypeRef::Table(_) => counts.tables += 1,
332                        TypeRef::Memory(_) => counts.memories += 1,
333                        TypeRef::Tag(_) => {}
334                    }
335                }
336            }
337            Payload::FunctionSection(reader) => {
338                self.module_counts()?.functions += reader.count() as usize;
339            }
340            Payload::GlobalSection(reader) => {
341                self.module_counts()?.globals += reader.count() as usize;
342            }
343            Payload::TableSection(reader) => {
344                let count = reader.count() as usize;
345                let counts = self.module_counts()?;
346                counts.tables += count;
347                counts.runtime.tables += count;
348            }
349            Payload::MemorySection(reader) => {
350                let count = reader.count() as usize;
351                let counts = self.module_counts()?;
352                counts.memories += count;
353                counts.runtime.memories += count;
354            }
355            Payload::ElementSection(reader) => {
356                self.module_counts()?.element_segments += reader.count() as usize;
357            }
358            Payload::DataSection(reader) => {
359                self.module_counts()?.data_segments += reader.count() as usize;
360            }
361            Payload::ExportSection(reader) => {
362                self.module_counts()?.exports += reader.count() as usize;
363            }
364            Payload::CodeSectionStart { count, size, .. } => {
365                ensure_average_function_size(count, size)?;
366            }
367            Payload::InstanceSection(reader) => {
368                for instance in reader {
369                    self.visit_core_instance(instance.map_err(malformed)?)?;
370                }
371            }
372            Payload::ComponentInstanceSection(reader) => {
373                for instance in reader {
374                    self.visit_component_instance(instance.map_err(malformed)?)?;
375                }
376            }
377            Payload::CoreTypeSection(reader) => {
378                self.component_items.core_types += reader.count() as usize;
379                ensure_component_item_cap("core types", self.component_items.core_types)?;
380            }
381            Payload::ComponentTypeSection(reader) => {
382                self.component_items.types += reader.count() as usize;
383                ensure_component_item_cap("types", self.component_items.types)?;
384            }
385            Payload::ComponentAliasSection(reader) => {
386                for alias in reader {
387                    let alias = alias.map_err(malformed)?;
388                    self.component_items.aliases += 1;
389                    ensure_component_item_cap("aliases", self.component_items.aliases)?;
390                    self.declare_aliased_item(&alias)?;
391                }
392            }
393            Payload::ComponentCanonicalSection(reader) => {
394                self.component_items.canonical_functions += reader.count() as usize;
395                ensure_component_item_cap(
396                    "canonical functions",
397                    self.component_items.canonical_functions,
398                )?;
399            }
400            Payload::ComponentImportSection(reader) => {
401                for import in reader {
402                    let import = import.map_err(malformed)?;
403                    self.component_items.imports += 1;
404                    ensure_component_item_cap("imports", self.component_items.imports)?;
405                    match import.ty {
406                        ComponentTypeRef::Module(_) => self.declare_core_module(None)?,
407                        ComponentTypeRef::Component(_) => self.declare_component(None)?,
408                        _ => {}
409                    }
410                }
411            }
412            Payload::ComponentExportSection(reader) => {
413                self.component_items.exports += reader.count() as usize;
414                ensure_component_item_cap("exports", self.component_items.exports)?;
415            }
416            _ => {}
417        }
418        Ok(())
419    }
420
421    /// Opens one nesting level and checks the component-wide caps.
422    fn enter(&mut self, encoding: Encoding) -> Result<()> {
423        match encoding {
424            Encoding::Module => {
425                self.core_modules += 1;
426                if self.core_modules > MAX_CORE_MODULES {
427                    return Err(policy_rejection(format!(
428                        "note codec component has {} core modules; the limit is {MAX_CORE_MODULES}",
429                        self.core_modules
430                    )));
431                }
432                self.frames.push(Frame::Module(ModuleCounts::default()));
433            }
434            Encoding::Component => {
435                self.frames.push(Frame::Component(ComponentFrame::default()));
436                let depth =
437                    self.frames.iter().filter(|frame| matches!(frame, Frame::Component(_))).count();
438                if depth > MAX_COMPONENT_DEPTH {
439                    return Err(policy_rejection(format!(
440                        "note codec component nests components {depth} deep; the limit is \
441                         {MAX_COMPONENT_DEPTH}"
442                    )));
443                }
444            }
445        }
446        Ok(())
447    }
448
449    /// Closes one nesting level and records what it creates in the component that declares it.
450    fn leave(&mut self) -> Result<()> {
451        match self.frames.pop() {
452            Some(Frame::Module(counts)) => {
453                counts.check()?;
454                // The module now holds an index in the core module index space of the component
455                // that declares it.
456                self.declare_core_module(Some(counts.runtime))
457            }
458            Some(Frame::Component(frame)) => self.declare_component(Some(frame.created)),
459            // An unbalanced end cannot happen: the parser reports one `End` for every header it
460            // accepted.
461            None => Ok(()),
462        }
463    }
464
465    /// Counts one core instantiation and what it creates.
466    fn visit_core_instance(&mut self, instance: Instance<'_>) -> Result<()> {
467        // An instance built from exports names items other instances already created.
468        let created = match instance {
469            Instance::Instantiate { module_index, .. } => {
470                let module = self.instantiated_core_module(module_index)?;
471                CreatedCounts {
472                    core_instances: 1,
473                    component_instances: 0,
474                    memories: module.memories,
475                    tables: module.tables,
476                }
477            }
478            Instance::FromExports(_) => CreatedCounts {
479                core_instances: 1,
480                ..CreatedCounts::default()
481            },
482        };
483        self.record_created(created)
484    }
485
486    /// Counts one component instantiation and what it creates.
487    fn visit_component_instance(&mut self, instance: ComponentInstance<'_>) -> Result<()> {
488        // An instance built from exports names items other instances already created.
489        let ComponentInstance::Instantiate {
490            component_index, ..
491        } = instance
492        else {
493            return Ok(());
494        };
495        let mut created = self.instantiated_component(component_index)?;
496        created.component_instances = created.component_instances.saturating_add(1);
497        self.record_created(created)
498    }
499
500    /// Adds what one instantiation creates to the component the walk is inside.
501    fn record_created(&mut self, created: CreatedCounts) -> Result<()> {
502        let frame = self.component_frame()?;
503        frame.created.add(created);
504        frame.created.check()
505    }
506
507    /// Returns what one instantiation of a core module of the current component creates.
508    fn instantiated_core_module(&mut self, module_index: u32) -> Result<ModuleRuntimeCounts> {
509        let frame = self.component_frame()?;
510        frame.core_modules.get(module_index as usize).copied().flatten().ok_or_else(|| {
511            policy_rejection(format!(
512                "note codec component instantiates core module {module_index}, which the policy \
513                 cannot read; a codec instantiates only the core modules it defines"
514            ))
515        })
516    }
517
518    /// Returns what one instantiation of a nested component of the current component creates.
519    fn instantiated_component(&mut self, component_index: u32) -> Result<CreatedCounts> {
520        let frame = self.component_frame()?;
521        frame
522            .components
523            .get(component_index as usize)
524            .copied()
525            .flatten()
526            .ok_or_else(|| {
527                policy_rejection(format!(
528                    "note codec component instantiates component {component_index}, which the \
529                     policy cannot read; a codec instantiates only the components it defines"
530                ))
531            })
532    }
533
534    /// Adds one core module to the index space of the component the walk is inside.
535    ///
536    /// A core module at the top level belongs to no component index space, so it is dropped.
537    fn declare_core_module(&mut self, created: Option<ModuleRuntimeCounts>) -> Result<()> {
538        if let Some(Frame::Component(frame)) = self.frames.last_mut() {
539            frame.core_modules.push(created);
540        }
541        Ok(())
542    }
543
544    /// Adds one component to the index space of the component the walk is inside.
545    ///
546    /// The root component belongs to no component index space, so it is dropped.
547    fn declare_component(&mut self, created: Option<CreatedCounts>) -> Result<()> {
548        if let Some(Frame::Component(frame)) = self.frames.last_mut() {
549            frame.components.push(created);
550        }
551        Ok(())
552    }
553
554    /// Adds one aliased core module or component to the current index spaces.
555    fn declare_aliased_item(&mut self, alias: &ComponentAlias<'_>) -> Result<()> {
556        match aliased_item_kind(alias) {
557            Some(AliasedItem::CoreModule) => self.declare_core_module(None),
558            Some(AliasedItem::Component) => self.declare_component(None),
559            None => Ok(()),
560        }
561    }
562
563    /// Returns the frame of the component the walk is inside.
564    ///
565    /// Component sections appear only inside a component. A component section anywhere else is a
566    /// malformed layout, and the walk fails closed instead of guessing a frame for it.
567    fn component_frame(&mut self) -> Result<&mut ComponentFrame> {
568        match self.frames.last_mut() {
569            Some(Frame::Component(frame)) => Ok(frame),
570            _ => Err(policy_rejection(
571                "note codec component is malformed: a component section appears outside a \
572                 component",
573            )),
574        }
575    }
576
577    /// Returns the counters of the core module the walk is inside.
578    ///
579    /// Core sections appear only inside a core module. A core section anywhere else is a
580    /// malformed layout, and the walk fails closed instead of guessing a frame for it.
581    fn module_counts(&mut self) -> Result<&mut ModuleCounts> {
582        match self.frames.last_mut() {
583            Some(Frame::Module(counts)) => Ok(counts),
584            _ => Err(policy_rejection(
585                "note codec component is malformed: a core section appears outside a core module",
586            )),
587        }
588    }
589}
590
591/// An index space one alias adds an item to.
592enum AliasedItem {
593    CoreModule,
594    Component,
595}
596
597/// Returns the index space one alias adds an item to, if the walk tracks that space.
598fn aliased_item_kind(alias: &ComponentAlias<'_>) -> Option<AliasedItem> {
599    match alias {
600        ComponentAlias::InstanceExport {
601            kind: ComponentExternalKind::Module,
602            ..
603        }
604        | ComponentAlias::Outer {
605            kind: ComponentOuterAliasKind::CoreModule,
606            ..
607        } => Some(AliasedItem::CoreModule),
608        ComponentAlias::InstanceExport {
609            kind: ComponentExternalKind::Component,
610            ..
611        }
612        | ComponentAlias::Outer {
613            kind: ComponentOuterAliasKind::Component,
614            ..
615        } => Some(AliasedItem::Component),
616        _ => None,
617    }
618}
619
620/// Reports a core module that is over one of its caps.
621fn ensure_module_cap(kind: &str, observed: usize, limit: usize) -> Result<()> {
622    if observed > limit {
623        return Err(policy_rejection(format!(
624            "note codec component has a core module with {observed} {kind}; the limit is {limit}"
625        )));
626    }
627    Ok(())
628}
629
630/// Reports a component that creates too much when it is instantiated.
631fn ensure_created_cap(kind: &str, observed: usize, limit: usize) -> Result<()> {
632    if observed > limit {
633        return Err(policy_rejection(format!(
634            "note codec component creates {observed} {kind} when it is instantiated; the limit is \
635             {limit}"
636        )));
637    }
638    Ok(())
639}
640
641/// Reports a core function type that is over its parameter or result cap.
642fn ensure_signature_cap(kind: &str, observed: usize, limit: usize) -> Result<()> {
643    if observed > limit {
644        return Err(policy_rejection(format!(
645            "note codec component has a function type with {observed} {kind}; the limit is {limit}"
646        )));
647    }
648    Ok(())
649}
650
651/// Reports a component tree that is over the cap for one kind of component-level item.
652fn ensure_component_item_cap(kind: &str, observed: usize) -> Result<()> {
653    if observed > MAX_COMPONENT_SECTION_ITEMS {
654        return Err(policy_rejection(format!(
655            "note codec component has {observed} component {kind}; the limit is \
656             {MAX_COMPONENT_SECTION_ITEMS}"
657        )));
658    }
659    Ok(())
660}
661
662/// Reports a code section built from many tiny functions.
663fn ensure_average_function_size(count: u32, size: u32) -> Result<()> {
664    if size < MIN_METERED_CODE_SECTION_BYTES || count == 0 {
665        return Ok(());
666    }
667    let average = size / count;
668    if average < MIN_AVERAGE_FUNCTION_BYTES {
669        return Err(policy_rejection(format!(
670            "note codec component has a core module with {count} functions in {size} bytes of \
671             code, an average of {average} bytes; the limit is {MIN_AVERAGE_FUNCTION_BYTES} bytes \
672             per function"
673        )));
674    }
675    Ok(())
676}
677
678/// Reports bytes that do not parse as a component.
679fn malformed(error: wasmparser::BinaryReaderError) -> Error {
680    policy_rejection(format!("note codec component is malformed: {error}"))
681}
682
683/// Reports a component that does not validate under [`NOTE_CODEC_WASM_FEATURES`].
684fn rejected_feature(error: wasmparser::BinaryReaderError) -> Error {
685    policy_rejection(format!(
686        "note codec component uses a Wasm feature the policy rejects: {error}"
687    ))
688}
689
690/// Creates an error for a component the structural load policy does not admit.
691///
692/// Every rejection carries one class, so a consumer reports a codec the policy refused the same
693/// way it reports a codec that ran past a host limit.
694fn policy_rejection(message: impl Into<String>) -> Error {
695    Error::codec(CodecFailure::LimitExceeded, message)
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701
702    /// Wraps core module text in a component and assembles it.
703    fn component(core_module: &str) -> Vec<u8> {
704        wat::parse_str(format!("(component (core module {core_module}))")).unwrap()
705    }
706
707    /// Validates one component with a byte budget that never trips.
708    fn validate(component: &[u8]) -> Result<()> {
709        validate_note_codec_component(component, usize::MAX)
710    }
711
712    #[test]
713    fn minimal_component_passes() {
714        validate(&component("(func)")).unwrap();
715    }
716
717    #[test]
718    fn the_wasm_feature_policy_is_exact() {
719        // Destructuring is exhaustive on purpose: a wasmparser upgrade that adds a proposal
720        // fails to compile here, so the policy cannot widen without a decision.
721        let wasmparser::WasmFeaturesInflated {
722            mutable_global,
723            saturating_float_to_int,
724            sign_extension,
725            reference_types,
726            multi_value,
727            bulk_memory,
728            simd,
729            relaxed_simd,
730            threads,
731            shared_everything_threads,
732            tail_call,
733            floats,
734            multi_memory,
735            exceptions,
736            memory64,
737            extended_const,
738            component_model,
739            function_references,
740            memory_control,
741            gc,
742            custom_page_sizes,
743            legacy_exceptions,
744            gc_types,
745            stack_switching,
746            wide_arithmetic,
747            cm_values,
748            cm_nested_names,
749            cm_async,
750            cm_async_stackful,
751            cm_more_async_builtins,
752            cm_threading,
753            cm_error_context,
754            cm_fixed_length_lists,
755            cm_gc,
756            call_indirect_overlong,
757            bulk_memory_opt,
758            custom_descriptors,
759            compact_imports,
760            cm_map,
761            cm64,
762        } = NOTE_CODEC_WASM_FEATURES.inflate();
763
764        for (name, required) in [
765            ("component model", component_model),
766            ("floats", floats),
767            ("mutable globals", mutable_global),
768            ("saturating float to int", saturating_float_to_int),
769            ("sign extension", sign_extension),
770            ("reference types", reference_types),
771            ("call-indirect overlong", call_indirect_overlong),
772            ("multi value", multi_value),
773            ("bulk memory", bulk_memory),
774            ("bulk memory opt", bulk_memory_opt),
775        ] {
776            assert!(required, "the policy must accept {name}");
777        }
778
779        for (name, rejected) in [
780            ("simd", simd),
781            ("relaxed simd", relaxed_simd),
782            ("threads", threads),
783            ("shared everything threads", shared_everything_threads),
784            ("tail call", tail_call),
785            ("multi memory", multi_memory),
786            ("exceptions", exceptions),
787            ("legacy exceptions", legacy_exceptions),
788            ("memory64", memory64),
789            ("extended const", extended_const),
790            ("function references", function_references),
791            ("memory control", memory_control),
792            ("gc", gc),
793            ("gc types", gc_types),
794            ("custom page sizes", custom_page_sizes),
795            ("stack switching", stack_switching),
796            ("wide arithmetic", wide_arithmetic),
797            ("component model values", cm_values),
798            ("component model nested names", cm_nested_names),
799            ("component model async", cm_async),
800            ("component model stackful async", cm_async_stackful),
801            ("component model async builtins", cm_more_async_builtins),
802            ("component model threading", cm_threading),
803            ("component model error context", cm_error_context),
804            ("component model fixed length lists", cm_fixed_length_lists),
805            ("component model gc", cm_gc),
806            ("component model maps", cm_map),
807            ("component model 64-bit contexts", cm64),
808            ("custom descriptors", custom_descriptors),
809            ("compact imports", compact_imports),
810        ] {
811            assert!(!rejected, "the policy must reject {name}");
812        }
813    }
814
815    #[test]
816    fn oversized_components_are_rejected_before_parsing() {
817        let error = validate_note_codec_component(&[0; 8], 4).unwrap_err().to_string();
818
819        assert!(error.contains("is 8 bytes"), "unexpected error: {error}");
820        assert!(error.contains("the pre-compilation limit is 4"), "unexpected error: {error}");
821    }
822
823    #[test]
824    fn too_many_globals_are_rejected() {
825        let globals = "(global i32 (i32.const 0))".repeat(MAX_MODULE_GLOBALS + 1);
826        let error = validate(&component(&globals)).unwrap_err().to_string();
827
828        assert!(error.contains("1001 globals"), "unexpected error: {error}");
829        assert!(error.contains("the limit is 1000"), "unexpected error: {error}");
830    }
831
832    #[test]
833    fn oversized_function_signatures_are_rejected() {
834        let params = "i32 ".repeat(MAX_FUNCTION_PARAMS + 1);
835        let module = format!("(type (func (param {params})))");
836        let error = validate(&component(&module)).unwrap_err().to_string();
837
838        assert!(error.contains("33 parameters"), "unexpected error: {error}");
839        assert!(error.contains("the limit is 32"), "unexpected error: {error}");
840    }
841
842    #[test]
843    fn many_tiny_functions_are_rejected() {
844        let error = validate(&component(&"(func)".repeat(600))).unwrap_err().to_string();
845
846        assert!(error.contains("600 functions"), "unexpected error: {error}");
847        assert!(error.contains("bytes per function"), "unexpected error: {error}");
848    }
849
850    #[test]
851    fn malformed_bytes_are_rejected() {
852        let error = validate(b"not a component").unwrap_err().to_string();
853
854        assert!(error.contains("note codec component is malformed"), "unexpected error: {error}");
855    }
856
857    #[test]
858    fn what_a_nested_component_creates_is_counted_once_per_instantiation() {
859        // One instantiation of the nested component creates one memory, so two instantiations
860        // reach the memory budget. A nested component that is never instantiated creates
861        // nothing.
862        let text = "(component
863                (component $inner
864                    (core module $m (memory 1))
865                    (core instance (instantiate $m)))
866                (instance (instantiate $inner))
867                (instance (instantiate $inner)))";
868        let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string();
869
870        assert!(
871            error.contains("creates 2 linear memories when it is instantiated"),
872            "unexpected error: {error}"
873        );
874        assert!(
875            error.contains(&format!("the limit is {MAX_INSTANTIATED_MEMORIES}")),
876            "unexpected error: {error}"
877        );
878    }
879
880    #[test]
881    fn a_component_that_is_never_instantiated_creates_nothing() {
882        let text = "(component
883                (component $unused
884                    (core module $m (memory 1))
885                    (core instance (instantiate $m)))
886                (core module $m (memory 1))
887                (core instance (instantiate $m)))";
888
889        validate(&wat::parse_str(text).unwrap()).unwrap();
890    }
891
892    #[test]
893    fn too_many_component_instances_are_rejected() {
894        let instantiate = "(instance (instantiate $inner))".repeat(MAX_COMPONENT_INSTANCES + 1);
895        let text = format!("(component (component $inner) {instantiate})");
896        let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string();
897
898        assert!(
899            error.contains(&format!("creates {} component instances", MAX_COMPONENT_INSTANCES + 1)),
900            "unexpected error: {error}"
901        );
902    }
903
904    #[test]
905    fn instantiated_memories_are_counted_across_core_modules() {
906        let text = "(component
907                (core module $a (memory 1))
908                (core module $b (memory 1))
909                (core instance (instantiate $a))
910                (core instance (instantiate $b)))";
911        let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string();
912
913        assert!(
914            error.contains("creates 2 linear memories when it is instantiated"),
915            "unexpected error: {error}"
916        );
917        assert!(
918            error.contains(&format!("the limit is {MAX_INSTANTIATED_MEMORIES}")),
919            "unexpected error: {error}"
920        );
921    }
922
923    #[test]
924    fn one_module_instantiated_twice_is_counted_twice() {
925        // The budgets count what instantiation creates, not what the tree declares, so one
926        // memory-defining module reaches the memory budget when it is instantiated twice.
927        let text = r#"(component
928                (core module $m (memory 1) (func (export "f")))
929                (core instance $a (instantiate $m))
930                (core instance $b (instantiate $m)))"#;
931        let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string();
932
933        assert!(
934            error.contains("creates 2 linear memories when it is instantiated"),
935            "unexpected error: {error}"
936        );
937    }
938
939    #[test]
940    fn a_core_module_the_walk_cannot_read_is_not_instantiable() {
941        let text = r#"(component
942                (import "m" (core module $m))
943                (core instance (instantiate $m)))"#;
944        let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string();
945
946        assert!(error.contains("which the policy cannot read"), "unexpected error: {error}");
947    }
948
949    #[test]
950    fn component_items_of_one_kind_are_capped_across_sections() {
951        // Component sections repeat, so the cap counts one kind over the whole tree. A core
952        // module between the two type sections keeps them apart in the encoding.
953        let types = |count: usize| "(type u8)".repeat(count);
954        let text = format!(
955            "(component {first} (core module) {second})",
956            first = types(MAX_COMPONENT_SECTION_ITEMS),
957            second = types(1),
958        );
959        let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string();
960
961        assert!(
962            error.contains(&format!("{} component types", MAX_COMPONENT_SECTION_ITEMS + 1)),
963            "unexpected error: {error}"
964        );
965        assert!(
966            error.contains(&format!("the limit is {MAX_COMPONENT_SECTION_ITEMS}")),
967            "unexpected error: {error}"
968        );
969    }
970
971    #[test]
972    fn too_many_module_types_are_rejected() {
973        let types = "(type (func (param i32)))".repeat(MAX_MODULE_TYPES + 1);
974        let error = validate(&component(&types)).unwrap_err().to_string();
975
976        assert!(
977            error.contains(&format!("{} types", MAX_MODULE_TYPES + 1)),
978            "unexpected error: {error}"
979        );
980    }
981
982    #[test]
983    fn every_structural_rejection_carries_a_class() {
984        let error = validate(b"not a component").unwrap_err();
985
986        assert_eq!(error.codec_failure(), Some(crate::CodecFailure::LimitExceeded));
987    }
988
989    #[test]
990    fn component_start_functions_are_rejected() {
991        let text = r#"(component
992            (core module $m (func (export "f")))
993            (core instance $i (instantiate $m))
994            (func $f (canon lift (core func $i "f")))
995            (start $f))"#;
996        let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string();
997
998        assert!(error.contains("declares a start function"), "unexpected error: {error}");
999    }
1000
1001    #[cfg(feature = "codec-component")]
1002    #[test]
1003    fn fixture_component_passes() {
1004        if !midenc_integration_test_support::wasm_target_is_installed() {
1005            eprintln!("skipping the structural limit fixture test: wasm32-wasip2 is not installed");
1006            return;
1007        }
1008
1009        validate(&crate::codec_component::tests::build_fixture_component()).unwrap();
1010    }
1011}