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}