1use std::collections::HashSet;
4
5use crate::extension::ExtensionExport;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum DirectWasmImportKind {
9 Function,
10 Table,
11 Memory,
12 Global,
13 Tag,
14}
15
16impl DirectWasmImportKind {
17 pub fn as_keyword(self) -> &'static str {
18 match self {
19 Self::Function => "function",
20 Self::Table => "table",
21 Self::Memory => "memory",
22 Self::Global => "global",
23 Self::Tag => "tag",
24 }
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct DirectWasmImport {
30 pub module: String,
31 pub name: String,
32 pub kind: DirectWasmImportKind,
33 pub signature: Option<ExtensionExport>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct DirectWasmMemory {
38 pub imported: bool,
39 pub minimum_pages: u32,
40 pub maximum_pages: Option<u32>,
41 pub shared: bool,
42 pub export_names: Vec<String>,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct DirectWasmFunctionExport {
47 pub name: String,
48 pub signature: ExtensionExport,
49 pub imported: bool,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct DirectWasmInspection {
54 pub imports: Vec<DirectWasmImport>,
55 pub memories: Vec<DirectWasmMemory>,
56 pub exports: Vec<DirectWasmFunctionExport>,
57 pub start: Option<u32>,
58}
59
60impl DirectWasmInspection {
61 pub fn direct_exports(&self) -> Result<Vec<(String, ExtensionExport)>, String> {
62 if !self.imports.is_empty() {
63 return Err("native/module-import-denied: direct WASM must be import-free".into());
64 }
65 validate_direct_memories(&self.memories)?;
66 self.exports
67 .iter()
68 .map(|export| {
69 if export.imported {
70 return Err(
71 "native/module-import-denied: direct WASM must be import-free".into(),
72 );
73 }
74 Ok((export.name.clone(), export.signature.clone()))
75 })
76 .collect()
77 }
78}
79
80pub fn inspect(bytes: &[u8]) -> Result<DirectWasmInspection, String> {
81 if bytes.get(..8) != Some(b"\0asm\x01\0\0\0") {
82 return Err("native/module-invalid: invalid WebAssembly header".into());
83 }
84
85 let mut cursor = 8;
86 let mut types = Vec::new();
87 let mut imported_functions = Vec::new();
88 let mut functions = Vec::new();
89 let mut imports = Vec::new();
90 let mut memories = Vec::new();
91 let mut exported_functions = Vec::new();
92 let mut exported_memories = Vec::new();
93 let mut export_names = HashSet::new();
94 let mut start = None;
95
96 while cursor < bytes.len() {
97 let id = byte(bytes, &mut cursor)?;
98 let size = unsigned(bytes, &mut cursor)? as usize;
99 let end = cursor
100 .checked_add(size)
101 .filter(|end| *end <= bytes.len())
102 .ok_or("native/module-invalid: section exceeds module")?;
103 let section = &bytes[cursor..end];
104 cursor = end;
105 let mut at = 0;
106
107 match id {
108 1 => parse_types(section, &mut at, &mut types)?,
109 2 => parse_imports(
110 section,
111 &mut at,
112 &types,
113 &mut imported_functions,
114 &mut imports,
115 &mut memories,
116 )?,
117 3 => parse_functions(section, &mut at, &mut functions)?,
118 5 => parse_memories(section, &mut at, &mut memories)?,
119 7 => parse_exports(
120 section,
121 &mut at,
122 &mut export_names,
123 &mut exported_functions,
124 &mut exported_memories,
125 )?,
126 8 => {
127 start = Some(unsigned(section, &mut at)? as u32);
128 }
129 _ => {}
130 }
131
132 if matches!(id, 1 | 2 | 3 | 5 | 7 | 8) && at != section.len() {
133 return Err(format!(
134 "native/module-invalid: trailing bytes in section {id}"
135 ));
136 }
137 }
138
139 let exports = exported_functions
140 .into_iter()
141 .map(|(name, index)| {
142 let (type_index, imported) = if index < imported_functions.len() {
143 (imported_functions[index], true)
144 } else {
145 let defined = index - imported_functions.len();
146 (
147 *functions.get(defined).ok_or_else(|| {
148 format!("native/module-invalid: bad function export {name}")
149 })?,
150 false,
151 )
152 };
153 let signature = types
154 .get(type_index)
155 .cloned()
156 .ok_or_else(|| format!("native/module-invalid: bad type for export {name}"))?;
157 Ok(DirectWasmFunctionExport {
158 name,
159 signature,
160 imported,
161 })
162 })
163 .collect::<Result<Vec<_>, String>>()?;
164
165 for (name, index) in exported_memories {
166 let memory = memories
167 .get_mut(index)
168 .ok_or_else(|| format!("native/module-invalid: bad memory export {name}"))?;
169 memory.export_names.push(name);
170 }
171 for memory in &mut memories {
172 memory.export_names.sort();
173 }
174
175 Ok(DirectWasmInspection {
176 imports,
177 memories,
178 exports,
179 start,
180 })
181}
182
183pub(crate) fn exports(bytes: &[u8]) -> Result<Vec<(String, ExtensionExport)>, String> {
184 inspect(bytes)?.direct_exports()
185}
186
187fn parse_types(
188 bytes: &[u8],
189 at: &mut usize,
190 types: &mut Vec<ExtensionExport>,
191) -> Result<(), String> {
192 for _ in 0..unsigned(bytes, at)? {
193 if byte(bytes, at)? != 0x60 {
194 return Err("native/abi-type-unsupported: non-function type".into());
195 }
196 let arguments = value_types(bytes, at)?;
197 let results = value_types(bytes, at)?;
198 if results.len() > 1 {
199 return Err("native/abi-type-unsupported: multiple results".into());
200 }
201 types.push(ExtensionExport {
202 arguments: arguments.into_iter().map(str::to_owned).collect(),
203 returns: results.into_iter().next().unwrap_or("void").to_owned(),
204 asynchronous: false,
205 raw_export: None,
206 });
207 }
208 Ok(())
209}
210
211fn parse_imports(
212 bytes: &[u8],
213 at: &mut usize,
214 types: &[ExtensionExport],
215 imported_functions: &mut Vec<usize>,
216 imports: &mut Vec<DirectWasmImport>,
217 memories: &mut Vec<DirectWasmMemory>,
218) -> Result<(), String> {
219 for _ in 0..unsigned(bytes, at)? {
220 let module = name(bytes, at)?;
221 let name = name(bytes, at)?;
222 let descriptor = byte(bytes, at)?;
223 let (kind, signature) = match descriptor {
224 0 => {
225 let type_index = unsigned(bytes, at)? as usize;
226 let signature = types.get(type_index).cloned().ok_or_else(|| {
227 format!("native/module-invalid: bad type for import {module}/{name}")
228 })?;
229 imported_functions.push(type_index);
230 (DirectWasmImportKind::Function, Some(signature))
231 }
232 1 => {
233 table_type(bytes, at)?;
234 (DirectWasmImportKind::Table, None)
235 }
236 2 => {
237 memories.push(memory_type(bytes, at, true)?);
238 (DirectWasmImportKind::Memory, None)
239 }
240 3 => {
241 global_type(bytes, at)?;
242 (DirectWasmImportKind::Global, None)
243 }
244 4 => {
245 tag_type(bytes, at, types.len())?;
246 (DirectWasmImportKind::Tag, None)
247 }
248 value => {
249 return Err(format!(
250 "native/module-invalid: unknown import kind 0x{value:02x}"
251 ))
252 }
253 };
254 imports.push(DirectWasmImport {
255 module,
256 name,
257 kind,
258 signature,
259 });
260 }
261 Ok(())
262}
263
264fn parse_functions(bytes: &[u8], at: &mut usize, functions: &mut Vec<usize>) -> Result<(), String> {
265 for _ in 0..unsigned(bytes, at)? {
266 functions.push(unsigned(bytes, at)? as usize);
267 }
268 Ok(())
269}
270
271fn parse_memories(
272 bytes: &[u8],
273 at: &mut usize,
274 memories: &mut Vec<DirectWasmMemory>,
275) -> Result<(), String> {
276 for _ in 0..unsigned(bytes, at)? {
277 memories.push(memory_type(bytes, at, false)?);
278 }
279 Ok(())
280}
281
282fn parse_exports(
283 bytes: &[u8],
284 at: &mut usize,
285 names: &mut HashSet<String>,
286 functions: &mut Vec<(String, usize)>,
287 memories: &mut Vec<(String, usize)>,
288) -> Result<(), String> {
289 for _ in 0..unsigned(bytes, at)? {
290 let name = name(bytes, at)?;
291 if !names.insert(name.clone()) {
292 return Err(format!(
293 "native/module-invalid: duplicate export name {name}"
294 ));
295 }
296 let kind = byte(bytes, at)?;
297 let index = unsigned(bytes, at)? as usize;
298 match kind {
299 0 => functions.push((name, index)),
300 2 => memories.push((name, index)),
301 1 | 3 | 4 => {}
302 value => {
303 return Err(format!(
304 "native/module-invalid: unknown export kind 0x{value:02x}"
305 ))
306 }
307 }
308 }
309 Ok(())
310}
311
312fn memory_type(bytes: &[u8], at: &mut usize, imported: bool) -> Result<DirectWasmMemory, String> {
313 let (minimum_pages, maximum_pages, shared) = limits(bytes, at, "memory")?;
314 Ok(DirectWasmMemory {
315 imported,
316 minimum_pages,
317 maximum_pages,
318 shared,
319 export_names: Vec::new(),
320 })
321}
322
323fn table_type(bytes: &[u8], at: &mut usize) -> Result<(), String> {
324 match byte(bytes, at)? {
325 0x70 | 0x6f => {}
326 value => {
327 return Err(format!(
328 "native/abi-type-unsupported: table reference type 0x{value:02x}"
329 ))
330 }
331 }
332 limits(bytes, at, "table")?;
333 Ok(())
334}
335
336fn global_type(bytes: &[u8], at: &mut usize) -> Result<(), String> {
337 match byte(bytes, at)? {
338 0x7f | 0x7e | 0x7d | 0x7c | 0x7b | 0x70 | 0x6f => {}
339 value => {
340 return Err(format!(
341 "native/abi-type-unsupported: global value type 0x{value:02x}"
342 ))
343 }
344 }
345 match byte(bytes, at)? {
346 0 | 1 => Ok(()),
347 value => Err(format!(
348 "native/module-invalid: global mutability 0x{value:02x}"
349 )),
350 }
351}
352
353fn tag_type(bytes: &[u8], at: &mut usize, type_count: usize) -> Result<(), String> {
354 if byte(bytes, at)? != 0 {
355 return Err("native/module-invalid: unsupported tag attribute".into());
356 }
357 let type_index = unsigned(bytes, at)? as usize;
358 if type_index >= type_count {
359 return Err("native/module-invalid: bad tag type".into());
360 }
361 Ok(())
362}
363
364fn limits(bytes: &[u8], at: &mut usize, subject: &str) -> Result<(u32, Option<u32>, bool), String> {
365 let flags = unsigned(bytes, at)?;
366 if flags & !0x03 != 0 {
367 return Err(format!("native/abi-type-unsupported: {subject}64 limits"));
368 }
369 let minimum = unsigned(bytes, at)?;
370 let maximum = if flags & 1 != 0 {
371 Some(unsigned(bytes, at)?)
372 } else {
373 None
374 };
375 let shared = flags & 2 != 0;
376 if shared && maximum.is_none() {
377 return Err(format!(
378 "native/module-invalid: shared {subject} requires a maximum"
379 ));
380 }
381 if maximum.is_some_and(|value| value < minimum) {
382 return Err(format!(
383 "native/module-invalid: {subject} maximum is below minimum"
384 ));
385 }
386 Ok((minimum, maximum, shared))
387}
388
389fn validate_direct_memories(memories: &[DirectWasmMemory]) -> Result<(), String> {
390 if memories.len() > 1 {
391 return Err("native/resource-limit: at most one memory is allowed".into());
392 }
393 for memory in memories {
394 if memory.shared {
395 return Err("native/resource-limit: shared memories are unsupported".into());
396 }
397 if memory.minimum_pages > 1024
398 || memory.maximum_pages.is_none()
399 || memory.maximum_pages.is_some_and(|value| value > 1024)
400 {
401 return Err("native/resource-limit: memory must be bounded to 64 MiB".into());
402 }
403 }
404 Ok(())
405}
406
407fn value_types(bytes: &[u8], at: &mut usize) -> Result<Vec<&'static str>, String> {
408 (0..unsigned(bytes, at)?)
409 .map(|_| match byte(bytes, at)? {
410 0x7f => Ok("i32"),
411 0x7e => Ok("i64"),
412 0x7d => Ok("f32"),
413 0x7c => Ok("f64"),
414 value => Err(format!("native/abi-type-unsupported: 0x{value:02x}")),
415 })
416 .collect()
417}
418
419fn name(bytes: &[u8], at: &mut usize) -> Result<String, String> {
420 let size = unsigned(bytes, at)? as usize;
421 let end = at
422 .checked_add(size)
423 .filter(|end| *end <= bytes.len())
424 .ok_or("native/module-invalid: name exceeds section")?;
425 let value = std::str::from_utf8(&bytes[*at..end])
426 .map_err(|_| "native/module-invalid: name is not UTF-8")?
427 .to_owned();
428 *at = end;
429 Ok(value)
430}
431
432fn byte(bytes: &[u8], at: &mut usize) -> Result<u8, String> {
433 let value = *bytes
434 .get(*at)
435 .ok_or("native/module-invalid: unexpected end of module")?;
436 *at += 1;
437 Ok(value)
438}
439
440fn unsigned(bytes: &[u8], at: &mut usize) -> Result<u32, String> {
441 let mut value = 0u32;
442 for shift in (0..35).step_by(7) {
443 let byte = byte(bytes, at)?;
444 if shift == 28 && byte & 0xf0 != 0 {
445 return Err("native/module-invalid: integer overflow".into());
446 }
447 value |= u32::from(byte & 0x7f) << shift;
448 if byte & 0x80 == 0 {
449 return Ok(value);
450 }
451 }
452 Err("native/module-invalid: invalid integer".into())
453}
454
455#[cfg(test)]
456mod tests {
457 use super::{exports, inspect, DirectWasmImportKind};
458
459 const ADD: &[u8] = b"\0asm\x01\0\0\0\x01\x07\x01\x60\x02\x7e\x7e\x01\x7e\x03\x02\x01\0\x07\x07\x01\x03add\0\0\x0a\x09\x01\x07\0\x20\0\x20\x01\x7c\x0b";
460 const IMPORT: &[u8] =
461 b"\0asm\x01\0\0\0\x01\x05\x01\x60\x01\x7f\0\x02\x0b\x01\x03env\x03log\0\0";
462
463 #[test]
464 fn discovers_scalar_exports_without_a_host_engine() {
465 let found = exports(ADD).unwrap();
466 assert_eq!(found[0].0, "add");
467 assert_eq!(found[0].1.arguments, ["i64", "i64"]);
468 assert_eq!(found[0].1.returns, "i64");
469 }
470
471 #[test]
472 fn reports_imports_before_direct_policy_rejects_them() {
473 let found = inspect(IMPORT).unwrap();
474 assert_eq!(found.imports.len(), 1);
475 assert_eq!(found.imports[0].module, "env");
476 assert_eq!(found.imports[0].name, "log");
477 assert_eq!(found.imports[0].kind, DirectWasmImportKind::Function);
478 assert_eq!(
479 found.imports[0].signature.as_ref().unwrap().arguments,
480 ["i32"]
481 );
482 assert!(found.direct_exports().unwrap_err().contains("import-free"));
483 }
484
485 #[test]
486 fn reports_bounded_exported_memory() {
487 let memory = b"\0asm\x01\0\0\0\x05\x04\x01\x01\x01\x02\x07\x0a\x01\x06memory\x02\0";
488 let found = inspect(memory).unwrap();
489 assert_eq!(found.memories.len(), 1);
490 assert_eq!(found.memories[0].minimum_pages, 1);
491 assert_eq!(found.memories[0].maximum_pages, Some(2));
492 assert_eq!(found.memories[0].export_names, ["memory"]);
493 assert!(found.direct_exports().is_ok());
494 }
495}