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 malformed Wasm structure, independent of the parser dependency's API.
41///
42/// The parser and its diagnostic metadata remain private. Use [`Self::offset`]
43/// for the source position and [`Self::message`] for a human-readable diagnostic.
44#[derive(Debug)]
45pub struct ParseError {
46    source: wasmparser::BinaryReaderError,
47}
48
49impl ParseError {
50    /// Byte offset in the original Wasm input where parsing failed.
51    #[must_use]
52    pub fn offset(&self) -> u64 {
53        self.source.offset()
54    }
55
56    /// Human-readable parser diagnostic, without the formatted offset.
57    ///
58    /// Diagnostic wording may change; it is not a machine-readable category.
59    #[must_use]
60    pub fn message(&self) -> &str {
61        self.source.message()
62    }
63}
64
65impl fmt::Display for ParseError {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        fmt::Display::fmt(&self.source, f)
68    }
69}
70
71impl std::error::Error for ParseError {}
72
73/// A structural inspection failed without yielding partial facts.
74#[derive(Debug)]
75pub enum InspectionError {
76    /// A caller-supplied resource bound was exceeded.
77    LimitExceeded {
78        /// Resource being counted.
79        resource: InspectionResource,
80        /// Observed count or byte length.
81        actual: u64,
82        /// Caller-selected maximum.
83        limit: u64,
84    },
85    /// A component was supplied instead of a core module.
86    UnsupportedEncoding,
87    /// Framing, section ordering, or an inspected vector is malformed.
88    Parse(ParseError),
89    /// Export names must be unique regardless of export kind.
90    DuplicateExport {
91        /// Offset of the duplicate entry in the original source.
92        offset: usize,
93    },
94    /// An unknown core section cannot be interpreted as artifact evidence.
95    UnknownSection {
96        /// Binary section identifier.
97        id: u8,
98        /// Offset of the section payload.
99        offset: usize,
100    },
101}
102
103impl fmt::Display for InspectionError {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        match self {
106            Self::LimitExceeded {
107                resource,
108                actual,
109                limit,
110            } => write!(f, "Wasm {resource:?} count {actual} exceeds {limit}"),
111            Self::UnsupportedEncoding => {
112                f.write_str("expected a core Wasm module, received a component")
113            }
114            Self::Parse(source) => write!(f, "malformed Wasm structure: {source}"),
115            Self::DuplicateExport { offset } => write!(f, "duplicate Wasm export at byte {offset}"),
116            Self::UnknownSection { id, offset } => {
117                write!(f, "unknown Wasm section {id} at byte {offset}")
118            }
119        }
120    }
121}
122impl std::error::Error for InspectionError {
123    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
124        match self {
125            Self::Parse(source) => Some(source),
126            _ => None,
127        }
128    }
129}
130const fn parse_error(source: wasmparser::BinaryReaderError) -> InspectionError {
131    InspectionError::Parse(ParseError { source })
132}
133
134/// The exported item's core Wasm kind, independent of application method policy.
135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136pub enum ExportKind {
137    /// Function export.
138    Function,
139    /// Table export.
140    Table,
141    /// Linear memory export.
142    Memory,
143    /// Global export.
144    Global,
145    /// Exception tag export.
146    Tag,
147}
148
149/// Export identity within its kind's index space.
150#[derive(Clone, Copy, Debug, Eq, PartialEq)]
151pub struct Export {
152    /// Item kind.
153    pub kind: ExportKind,
154    /// Index within that kind's index space; not type-validated here.
155    pub index: u32,
156}
157
158/// Borrowed custom metadata, preserving duplicates and encounter order.
159#[derive(Clone, Copy, Debug, Eq, PartialEq)]
160pub struct CustomSection<'a> {
161    /// Decoded UTF-8 section name.
162    pub name: &'a str,
163    /// Exact bytes after the name, with no normalization.
164    pub data: &'a [u8],
165}
166
167/// Core Wasm facts for consumer reports and transform-contract comparisons.
168#[derive(Clone, Debug, Eq, PartialEq)]
169pub struct WasmFacts<'a> {
170    /// Complete raw artifact byte length.
171    pub raw_bytes: usize,
172    /// Code section payload bytes, including its vector count and body lengths.
173    pub code_section_bytes: usize,
174    /// Data section payload bytes, including its vector count and framing.
175    pub data_section_bytes: usize,
176    /// Defined functions, excluding imports; function/code counts must agree.
177    pub defined_functions: u32,
178    /// Number of encoded data segments.
179    pub data_segments: u32,
180    /// All exports keyed by exact name; callers select IC/application methods.
181    pub exports: BTreeMap<&'a str, Export>,
182    /// All custom metadata; callers select Candid sections and acceptance rules.
183    pub custom_sections: Vec<CustomSection<'a>>,
184}
185
186/// Inspect bounded core Wasm framing and the vectors used by artifact reports.
187///
188/// Preserves input bytes and borrows names/metadata. Storage is bounded by the
189/// explicit export/custom-section limits; traversal is bounded by module bytes.
190/// Unreported core section contents and function instructions are not validated.
191/// Run the consumer's Wasm validator before admitting a deployable artifact.
192///
193/// # Errors
194/// Rejects resource overflow, unsupported encoding, malformed framing or
195/// inspected vectors, duplicate exports, unknown sections, and unequal function
196/// and code counts. No partial facts are returned.
197pub fn inspect(bytes: &[u8], limits: InspectionLimits) -> Result<WasmFacts<'_>, InspectionError> {
198    enforce(
199        InspectionResource::ModuleBytes,
200        bytes.len() as u64,
201        limits.module_bytes as u64,
202    )?;
203    let mut facts = WasmFacts {
204        raw_bytes: bytes.len(),
205        code_section_bytes: 0,
206        data_section_bytes: 0,
207        defined_functions: 0,
208        data_segments: 0,
209        exports: BTreeMap::new(),
210        custom_sections: Vec::new(),
211    };
212    let mut sections = 0;
213    for payload in Parser::new(0).parse_all(bytes) {
214        let payload = payload.map_err(parse_error)?;
215        if payload.as_section().is_some() {
216            sections += 1;
217            enforce(
218                InspectionResource::Sections,
219                sections,
220                limits.sections as u64,
221            )?;
222        }
223        match payload {
224            Payload::Version { encoding, .. } if encoding != Encoding::Module => {
225                return Err(InspectionError::UnsupportedEncoding);
226            }
227            Payload::FunctionSection(reader) => {
228                facts.defined_functions = reader.count();
229                // Exhaust the iterator to reject count/payload mismatch, rather
230                // than trusting the count prefix as the hand-written readers did.
231                for index in reader {
232                    index.map_err(parse_error)?;
233                }
234            }
235            Payload::CodeSectionStart { range, .. } => {
236                facts.code_section_bytes = host_size(range.end - range.start)?;
237            }
238            Payload::DataSection(reader) => {
239                let range = reader.range();
240                facts.data_section_bytes = host_size(range.end - range.start)?;
241                facts.data_segments = reader.count();
242                for segment in reader {
243                    segment.map_err(parse_error)?;
244                }
245            }
246            Payload::ExportSection(reader) => {
247                enforce(
248                    InspectionResource::Exports,
249                    u64::from(reader.count()),
250                    u64::from(limits.exports),
251                )?;
252                for entry in reader.into_iter_with_offsets() {
253                    let (offset, entry) = entry.map_err(parse_error)?;
254                    let kind = match entry.kind {
255                        ExternalKind::Func | ExternalKind::FuncExact => ExportKind::Function,
256                        ExternalKind::Table => ExportKind::Table,
257                        ExternalKind::Memory => ExportKind::Memory,
258                        ExternalKind::Global => ExportKind::Global,
259                        ExternalKind::Tag => ExportKind::Tag,
260                    };
261                    if facts
262                        .exports
263                        .insert(
264                            entry.name,
265                            Export {
266                                kind,
267                                index: entry.index,
268                            },
269                        )
270                        .is_some()
271                    {
272                        return Err(InspectionError::DuplicateExport {
273                            offset: host_size(offset)?,
274                        });
275                    }
276                }
277            }
278            Payload::CustomSection(reader) => {
279                enforce(
280                    InspectionResource::CustomSections,
281                    facts.custom_sections.len() as u64 + 1,
282                    limits.custom_sections as u64,
283                )?;
284                facts.custom_sections.push(CustomSection {
285                    name: reader.name(),
286                    data: reader.data(),
287                });
288            }
289            Payload::UnknownSection { id, range, .. } => {
290                return Err(InspectionError::UnknownSection {
291                    id,
292                    offset: host_size(range.start)?,
293                });
294            }
295            _ => {}
296        }
297    }
298    Ok(facts)
299}
300
301// The parser carries u64 offsets; borrowed artifact facts use host-sized values.
302// No offset or extent can require more than the host's entire addressable input.
303fn host_size(value: u64) -> Result<usize, InspectionError> {
304    usize::try_from(value).map_err(|_| InspectionError::LimitExceeded {
305        resource: InspectionResource::ModuleBytes,
306        actual: value,
307        limit: usize::MAX as u64,
308    })
309}
310
311const fn enforce(
312    resource: InspectionResource,
313    actual: u64,
314    limit: u64,
315) -> Result<(), InspectionError> {
316    if actual > limit {
317        return Err(InspectionError::LimitExceeded {
318            resource,
319            actual,
320            limit,
321        });
322    }
323    Ok(())
324}