Skip to main content

ic_host_tools/wasm/
mod.rs

1//! Borrowed structural facts from core WebAssembly artifacts.
2//!
3//! `wasmparser` owns framing, section ordering, names and vector decoding.
4//! Inspection checks the function/data/export structures it reports, but is not
5//! instruction validation, type checking, feature admission, or IC install policy.
6//! Facts borrow source bytes; no metadata or export names are copied.
7
8use std::{collections::BTreeMap, fmt};
9use wasmparser::{Encoding, ExternalKind, Parser, Payload};
10
11#[cfg(test)]
12mod tests;
13
14/// Consumer-selected resource bounds; zero allows only an empty collection.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub struct InspectionLimits {
17    /// Maximum complete module size in bytes.
18    pub module_bytes: usize,
19    /// Maximum number of top-level sections, including custom sections.
20    pub sections: usize,
21    /// Maximum number of export entries.
22    pub exports: u32,
23    /// Maximum number of custom sections retained as borrowed views.
24    pub custom_sections: usize,
25}
26
27/// Which consumer-supplied bound rejected an artifact.
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum InspectionResource {
30    /// Complete source byte length.
31    ModuleBytes,
32    /// Number of top-level sections.
33    Sections,
34    /// Number of exported items.
35    Exports,
36    /// Number of custom metadata sections.
37    CustomSections,
38}
39
40/// A structural inspection failed without yielding partial facts.
41#[derive(Debug)]
42pub enum InspectionError {
43    /// A caller-supplied resource bound was exceeded.
44    LimitExceeded {
45        /// Resource being counted.
46        resource: InspectionResource,
47        /// Observed count or byte length.
48        actual: u64,
49        /// Caller-selected maximum.
50        limit: u64,
51    },
52    /// A component was supplied instead of a core module.
53    UnsupportedEncoding,
54    /// Framing, section ordering, or an inspected vector is malformed.
55    Parse(wasmparser::BinaryReaderError),
56    /// Export names must be unique regardless of export kind.
57    DuplicateExport {
58        /// Offset of the duplicate entry in the original source.
59        offset: usize,
60    },
61    /// An unknown core section cannot be interpreted as artifact evidence.
62    UnknownSection {
63        /// Binary section identifier.
64        id: u8,
65        /// Offset of the section payload.
66        offset: usize,
67    },
68}
69
70impl fmt::Display for InspectionError {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match self {
73            Self::LimitExceeded {
74                resource,
75                actual,
76                limit,
77            } => write!(f, "Wasm {resource:?} count {actual} exceeds {limit}"),
78            Self::UnsupportedEncoding => {
79                f.write_str("expected a core Wasm module, received a component")
80            }
81            Self::Parse(source) => write!(f, "malformed Wasm structure: {source}"),
82            Self::DuplicateExport { offset } => write!(f, "duplicate Wasm export at byte {offset}"),
83            Self::UnknownSection { id, offset } => {
84                write!(f, "unknown Wasm section {id} at byte {offset}")
85            }
86        }
87    }
88}
89impl std::error::Error for InspectionError {
90    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
91        match self {
92            Self::Parse(source) => Some(source),
93            _ => None,
94        }
95    }
96}
97impl From<wasmparser::BinaryReaderError> for InspectionError {
98    fn from(source: wasmparser::BinaryReaderError) -> Self {
99        Self::Parse(source)
100    }
101}
102
103/// The exported item's core Wasm kind, independent of application method policy.
104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub enum ExportKind {
106    /// Function export.
107    Function,
108    /// Table export.
109    Table,
110    /// Linear memory export.
111    Memory,
112    /// Global export.
113    Global,
114    /// Exception tag export.
115    Tag,
116}
117
118/// Export identity within its kind's index space.
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120pub struct Export {
121    /// Item kind.
122    pub kind: ExportKind,
123    /// Index within that kind's index space; not type-validated here.
124    pub index: u32,
125}
126
127/// Borrowed custom metadata, preserving duplicates and encounter order.
128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub struct CustomSection<'a> {
130    /// Decoded UTF-8 section name.
131    pub name: &'a str,
132    /// Exact bytes after the name, with no normalization.
133    pub data: &'a [u8],
134}
135
136/// Core Wasm facts for consumer reports and transform-contract comparisons.
137#[derive(Clone, Debug, Eq, PartialEq)]
138pub struct WasmFacts<'a> {
139    /// Complete raw artifact byte length.
140    pub raw_bytes: usize,
141    /// Code section payload bytes, including its vector count and body lengths.
142    pub code_section_bytes: usize,
143    /// Data section payload bytes, including its vector count and framing.
144    pub data_section_bytes: usize,
145    /// Defined functions, excluding imports; function/code counts must agree.
146    pub defined_functions: u32,
147    /// Number of encoded data segments.
148    pub data_segments: u32,
149    /// All exports keyed by exact name; callers select IC/application methods.
150    pub exports: BTreeMap<&'a str, Export>,
151    /// All custom metadata; callers select Candid sections and acceptance rules.
152    pub custom_sections: Vec<CustomSection<'a>>,
153}
154
155/// Inspect bounded core Wasm framing and the vectors used by artifact reports.
156///
157/// Preserves input bytes and borrows names/metadata. Storage is bounded by the
158/// explicit export/custom-section limits; traversal is bounded by module bytes.
159/// Unreported core section contents and function instructions are not validated.
160/// Run the consumer's Wasm validator before admitting a deployable artifact.
161///
162/// # Errors
163/// Rejects resource overflow, unsupported encoding, malformed framing or
164/// inspected vectors, duplicate exports, unknown sections, and unequal function
165/// and code counts. No partial facts are returned.
166pub fn inspect(bytes: &[u8], limits: InspectionLimits) -> Result<WasmFacts<'_>, InspectionError> {
167    enforce(
168        InspectionResource::ModuleBytes,
169        bytes.len() as u64,
170        limits.module_bytes as u64,
171    )?;
172    let mut facts = WasmFacts {
173        raw_bytes: bytes.len(),
174        code_section_bytes: 0,
175        data_section_bytes: 0,
176        defined_functions: 0,
177        data_segments: 0,
178        exports: BTreeMap::new(),
179        custom_sections: Vec::new(),
180    };
181    let mut sections = 0;
182    for payload in Parser::new(0).parse_all(bytes) {
183        let payload = payload?;
184        if payload.as_section().is_some() {
185            sections += 1;
186            enforce(
187                InspectionResource::Sections,
188                sections,
189                limits.sections as u64,
190            )?;
191        }
192        match payload {
193            Payload::Version { encoding, .. } if encoding != Encoding::Module => {
194                return Err(InspectionError::UnsupportedEncoding);
195            }
196            Payload::FunctionSection(reader) => {
197                facts.defined_functions = reader.count();
198                // Exhaust the iterator to reject count/payload mismatch, rather
199                // than trusting the count prefix as the hand-written readers did.
200                for index in reader {
201                    index?;
202                }
203            }
204            Payload::CodeSectionStart { range, .. } => {
205                facts.code_section_bytes = range.len();
206            }
207            Payload::DataSection(reader) => {
208                facts.data_section_bytes = reader.range().len();
209                facts.data_segments = reader.count();
210                for segment in reader {
211                    segment?;
212                }
213            }
214            Payload::ExportSection(reader) => {
215                enforce(
216                    InspectionResource::Exports,
217                    u64::from(reader.count()),
218                    u64::from(limits.exports),
219                )?;
220                for entry in reader.into_iter_with_offsets() {
221                    let (offset, entry) = entry?;
222                    let kind = match entry.kind {
223                        ExternalKind::Func | ExternalKind::FuncExact => ExportKind::Function,
224                        ExternalKind::Table => ExportKind::Table,
225                        ExternalKind::Memory => ExportKind::Memory,
226                        ExternalKind::Global => ExportKind::Global,
227                        ExternalKind::Tag => ExportKind::Tag,
228                    };
229                    if facts
230                        .exports
231                        .insert(
232                            entry.name,
233                            Export {
234                                kind,
235                                index: entry.index,
236                            },
237                        )
238                        .is_some()
239                    {
240                        return Err(InspectionError::DuplicateExport { offset });
241                    }
242                }
243            }
244            Payload::CustomSection(reader) => {
245                enforce(
246                    InspectionResource::CustomSections,
247                    facts.custom_sections.len() as u64 + 1,
248                    limits.custom_sections as u64,
249                )?;
250                facts.custom_sections.push(CustomSection {
251                    name: reader.name(),
252                    data: reader.data(),
253                });
254            }
255            Payload::UnknownSection { id, range, .. } => {
256                return Err(InspectionError::UnknownSection {
257                    id,
258                    offset: range.start,
259                });
260            }
261            _ => {}
262        }
263    }
264    Ok(facts)
265}
266
267const fn enforce(
268    resource: InspectionResource,
269    actual: u64,
270    limit: u64,
271) -> Result<(), InspectionError> {
272    if actual > limit {
273        return Err(InspectionError::LimitExceeded {
274            resource,
275            actual,
276            limit,
277        });
278    }
279    Ok(())
280}