Skip to main content

hara_native/
direct_wasm.rs

1//! Target-neutral discovery for the deliberately small direct `core.v1` ABI.
2
3use 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}