Skip to main content

gasm_host/
wasi.rs

1//! The WASI preview1 subset gasm runners provide: enough for wasi-libc's stdio,
2//! malloc, clocks and random numbers, and nothing that reaches the host's files,
3//! environment or arguments. runners/web/gasm-host.js `wasiImports` implements
4//! exactly the same functions with the same results (see spec/ABI.md):
5//!
6//! - `fd_write` to fd 1 and 2 goes to the runner's log (stderr here), never to
7//!   stdout (that carries the hash lines of headless runs)
8//! - `clock_time_get` / `clock_res_get`: the virtual time in headless runs
9//! - `random_get`: OS randomness, or a fixed sequence in headless runs
10//! - `fd_prestat_get` answers EBADF (no preopened directories)
11//! - anything else the guest imports from `wasi_snapshot_preview1` returns ENOSYS
12//! - `proc_exit` ends the game
13
14use std::io::Write;
15
16use wasmtime::{Caller, Linker, Module, Val};
17
18use crate::host::{Host, guest_slice, guest_slice_mut, memory};
19
20pub const MODULE: &str = "wasi_snapshot_preview1";
21
22const SUCCESS: i32 = 0;
23const EBADF: i32 = 8;
24const EINVAL: i32 = 28;
25const ENOSYS: i32 = 52;
26const ESPIPE: i32 = 70;
27
28/// The guest called `proc_exit(code)`.
29#[derive(Debug)]
30pub struct Exit(pub i32);
31
32impl std::fmt::Display for Exit {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(f, "guest called proc_exit({})", self.0)
35    }
36}
37
38impl std::error::Error for Exit {}
39
40/// Deterministic `random_get` for headless runs: splitmix64 from a fixed seed,
41/// little-endian (same sequence as gasm-host.js).
42#[derive(Clone, Copy)]
43pub struct Splitmix(pub u64);
44
45impl Splitmix {
46    pub fn next_u64(&mut self) -> u64 {
47        self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
48        let mut z = self.0;
49        z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
50        z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
51        z ^ (z >> 31)
52    }
53
54    pub fn fill(&mut self, out: &mut [u8]) {
55        for chunk in out.chunks_mut(8) {
56            let v = self.next_u64().to_le_bytes();
57            chunk.copy_from_slice(&v[..chunk.len()]);
58        }
59    }
60}
61
62fn write_u32(caller: &mut Caller<'_, Host>, ptr: u32, v: u32) -> wasmtime::Result<()> {
63    let mem = memory(caller)?;
64    guest_slice_mut(mem.data_mut(caller), ptr, 4)?.copy_from_slice(&v.to_le_bytes());
65    Ok(())
66}
67
68fn write_u64(caller: &mut Caller<'_, Host>, ptr: u32, v: u64) -> wasmtime::Result<()> {
69    let mem = memory(caller)?;
70    guest_slice_mut(mem.data_mut(caller), ptr, 8)?.copy_from_slice(&v.to_le_bytes());
71    Ok(())
72}
73
74/// Nanoseconds for a WASI clock: virtual time in headless runs (realtime counts
75/// from the epoch, 1970), else the system clock / time since start.
76fn clock_ns(host: &Host, id: u32) -> u64 {
77    match host.virtual_time_ms {
78        Some(ms) => (ms * 1e6).round() as u64,
79        None if id == 0 => std::time::SystemTime::now()
80            .duration_since(std::time::UNIX_EPOCH)
81            .map_or(0, |d| d.as_nanos() as u64),
82        None => host.started().elapsed().as_nanos() as u64,
83    }
84}
85
86/// The functions implemented above (the rest answer ENOSYS).
87pub const IMPLEMENTED: [&str; 14] = [
88    "fd_write", "fd_close", "fd_seek", "fd_prestat_get", "fd_fdstat_get", "clock_res_get", "clock_time_get",
89    "random_get", "args_sizes_get", "environ_sizes_get", "args_get", "environ_get", "sched_yield", "proc_exit",
90];
91
92pub fn add_to_linker(linker: &mut Linker<Host>, module: &Module) -> wasmtime::Result<()> {
93    linker.func_wrap(MODULE, "fd_write", |mut caller: Caller<'_, Host>, fd: i32, iovs: u32, iovs_len: u32, nwritten: u32| -> wasmtime::Result<i32> {
94        if fd != 1 && fd != 2 {
95            return Ok(EBADF);
96        }
97        let mem = memory(&caller)?;
98        let data = mem.data(&caller);
99        let list = guest_slice(data, iovs, iovs_len as u64 * 8)?;
100        let mut total = 0u32;
101        let mut err = std::io::stderr().lock();
102        for iov in list.chunks_exact(8) {
103            let ptr = u32::from_le_bytes([iov[0], iov[1], iov[2], iov[3]]);
104            let len = u32::from_le_bytes([iov[4], iov[5], iov[6], iov[7]]);
105            let _ = err.write_all(guest_slice(data, ptr, len as u64)?);
106            total = total.wrapping_add(len);
107        }
108        drop(err);
109        write_u32(&mut caller, nwritten, total)?;
110        Ok(SUCCESS)
111    })?;
112    linker.func_wrap(MODULE, "fd_close", |_: Caller<'_, Host>, _fd: i32| -> i32 { SUCCESS })?;
113    linker.func_wrap(MODULE, "fd_seek", |_: Caller<'_, Host>, _fd: i32, _off: i64, _whence: i32, _out: u32| -> i32 { ESPIPE })?;
114    linker.func_wrap(MODULE, "fd_prestat_get", |_: Caller<'_, Host>, _fd: i32, _out: u32| -> i32 { EBADF })?;
115    linker.func_wrap(MODULE, "fd_fdstat_get", |mut caller: Caller<'_, Host>, fd: i32, out: u32| -> wasmtime::Result<i32> {
116        if !(0..=2).contains(&fd) {
117            return Ok(EBADF);
118        }
119        let mem = memory(&caller)?;
120        let stat = guest_slice_mut(mem.data_mut(&mut caller), out, 24)?;
121        stat.fill(0);
122        stat[0] = 2; // filetype: character device
123        Ok(SUCCESS)
124    })?;
125    linker.func_wrap(MODULE, "clock_res_get", |mut caller: Caller<'_, Host>, id: u32, out: u32| -> wasmtime::Result<i32> {
126        if id > 3 {
127            return Ok(EINVAL);
128        }
129        write_u64(&mut caller, out, 1000)?;
130        Ok(SUCCESS)
131    })?;
132    linker.func_wrap(MODULE, "clock_time_get", |mut caller: Caller<'_, Host>, id: u32, _precision: i64, out: u32| -> wasmtime::Result<i32> {
133        if id > 3 {
134            return Ok(EINVAL);
135        }
136        let ns = clock_ns(caller.data(), id);
137        write_u64(&mut caller, out, ns)?;
138        Ok(SUCCESS)
139    })?;
140    linker.func_wrap(MODULE, "random_get", |mut caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
141        let mem = memory(&caller)?;
142        let (data, host) = mem.data_and_store_mut(&mut caller);
143        let buf = guest_slice_mut(data, ptr, len as u64)?;
144        match &mut host.random {
145            Some(rng) => rng.fill(buf),
146            None => getrandom::fill(buf).map_err(|e| wasmtime::format_err!("random_get: {e}"))?,
147        }
148        Ok(SUCCESS)
149    })?;
150    for name in ["args_sizes_get", "environ_sizes_get"] {
151        linker.func_wrap(MODULE, name, |mut caller: Caller<'_, Host>, count: u32, buf_size: u32| -> wasmtime::Result<i32> {
152            write_u32(&mut caller, count, 0)?;
153            write_u32(&mut caller, buf_size, 0)?;
154            Ok(SUCCESS)
155        })?;
156    }
157    linker.func_wrap(MODULE, "args_get", |_: Caller<'_, Host>, _a: u32, _b: u32| -> i32 { SUCCESS })?;
158    linker.func_wrap(MODULE, "environ_get", |_: Caller<'_, Host>, _a: u32, _b: u32| -> i32 { SUCCESS })?;
159    linker.func_wrap(MODULE, "sched_yield", |_: Caller<'_, Host>| -> i32 { SUCCESS })?;
160    linker.func_wrap(MODULE, "proc_exit", |_: Caller<'_, Host>, code: i32| -> wasmtime::Result<()> {
161        Err(wasmtime::Error::new(Exit(code)))
162    })?;
163
164    // Everything else the guest imports from WASI exists and answers ENOSYS.
165    for import in module.imports().filter(|i| i.module() == MODULE && !IMPLEMENTED.contains(&i.name())) {
166        let Some(ty) = import.ty().func().cloned() else { continue };
167        let returns_errno = ty.results().len() == 1 && ty.results().next().is_some_and(|t| t.is_i32());
168        let name = import.name().to_owned();
169        let what = format!("unsupported WASI function {MODULE}.{name}");
170        linker.func_new(MODULE, &name, ty, move |_, _params, results| {
171            if returns_errno {
172                results[0] = Val::I32(ENOSYS);
173                Ok(())
174            } else {
175                Err(wasmtime::format_err!("{what}"))
176            }
177        })?;
178    }
179    Ok(())
180}