Skip to main content

zwasm_sdk/
wasi.rs

1use crate::error;
2use crate::utils;
3use zwasm_sys as sys;
4
5/* ================================================================
6 * WASI configuration
7 * ================================================================ */
8
9/// WASI (WebAssembly System Interface) configuration for zwasm modules.
10///
11/// Supports WASI Preview 1 and 2, full syscalls, stdio overrides, preopened directories,
12/// and environment/argv configuration. Used to provide system interface capabilities to
13/// Wasm modules running in the zwasm runtime.
14pub struct WasiConfig {
15    pub(crate) ptr: *mut sys::zwasm_wasi_config_t,
16    argv: Vec<std::ffi::CString>,
17    argv_ptrs: Vec<*const std::os::raw::c_char>,
18    env_keys: Vec<std::ffi::CString>,
19    env_key_lens: Vec<usize>,
20    env_key_ptrs: Vec<*const std::os::raw::c_char>,
21    env_vals: Vec<std::ffi::CString>,
22    env_val_lens: Vec<usize>,
23    env_val_ptrs: Vec<*const std::os::raw::c_char>,
24    preopens: Vec<(std::ffi::CString, std::ffi::CString)>,
25    preopen_fd_guest_paths: Vec<std::ffi::CString>,
26    _not_send_sync: std::marker::PhantomData<std::rc::Rc<()>>,
27}
28
29impl WasiConfig {
30    /// Creates a new WASI configuration for zwasm modules.
31    ///
32    /// Supports both WASI Preview 1 and 2. Use this to provide system interface capabilities to Wasm code.
33    pub fn new() -> Result<Self, error::ZwasmError> {
34        let ptr = unsafe { sys::zwasm_wasi_config_new() };
35
36        if ptr.is_null() {
37            Err(error::last_error()
38                .unwrap_or_else(|| error::ZwasmError("Unknown error".to_string())))
39        } else {
40            Ok(WasiConfig {
41                ptr,
42                argv: Vec::new(),
43                argv_ptrs: Vec::new(),
44                env_keys: Vec::new(),
45                env_key_lens: Vec::new(),
46                env_key_ptrs: Vec::new(),
47                env_vals: Vec::new(),
48                env_val_lens: Vec::new(),
49                env_val_ptrs: Vec::new(),
50                preopens: Vec::new(),
51                preopen_fd_guest_paths: Vec::new(),
52                _not_send_sync: std::marker::PhantomData,
53            })
54        }
55    }
56
57    /// Sets the argv (command-line arguments) for the guest process.
58    ///
59    /// These will be visible to the Wasm module via WASI syscalls.
60    pub fn set_argv(&mut self, argv: &[&str]) -> Result<(), error::ZwasmError> {
61        self.argv = argv
62            .iter()
63            .map(|s| {
64                std::ffi::CString::new(*s)
65                    .map_err(|_| error::ZwasmError("argument contains NUL byte".into()))
66            })
67            .collect::<Result<Vec<_>, _>>()?;
68
69        self.argv_ptrs = self.argv.iter().map(|s| s.as_ptr()).collect::<Vec<_>>();
70        let argc = utils::to_u32_len(self.argv_ptrs.len())?;
71
72        unsafe {
73            sys::zwasm_wasi_config_set_argv(
74                self.ptr,
75                argc,
76                if self.argv_ptrs.is_empty() {
77                    std::ptr::null()
78                } else {
79                    self.argv_ptrs.as_ptr()
80                },
81            )
82        };
83
84        Ok(())
85    }
86
87    /// Sets environment variables for the guest process.
88    ///
89    /// These will be visible to the Wasm module via WASI syscalls.
90    pub fn set_env(&mut self, env: &[(&str, &str)]) -> Result<(), error::ZwasmError> {
91        let c_keys = env
92            .iter()
93            .map(|(key, _)| {
94                std::ffi::CString::new(*key).map_err(|_| {
95                    error::ZwasmError("environment variable key contains NUL byte".into())
96                })
97            })
98            .collect::<Result<Vec<_>, _>>()?;
99        let c_key_lens = c_keys
100            .iter()
101            .map(|s| s.as_bytes().len())
102            .collect::<Vec<_>>();
103        let c_key_ptrs = c_keys.iter().map(|s| s.as_ptr()).collect::<Vec<_>>();
104        let c_vals = env
105            .iter()
106            .map(|(_, val)| {
107                std::ffi::CString::new(*val).map_err(|_| {
108                    error::ZwasmError("environment variable value contains NUL byte".into())
109                })
110            })
111            .collect::<Result<Vec<_>, _>>()?;
112        let c_val_lens = c_vals
113            .iter()
114            .map(|s| s.as_bytes().len())
115            .collect::<Vec<_>>();
116        let c_val_ptrs = c_vals.iter().map(|s| s.as_ptr()).collect::<Vec<_>>();
117        let count = utils::to_u32_len(c_key_ptrs.len())?;
118
119        self.env_keys = c_keys;
120        self.env_key_lens = c_key_lens;
121        self.env_key_ptrs = c_key_ptrs;
122        self.env_vals = c_vals;
123        self.env_val_lens = c_val_lens;
124        self.env_val_ptrs = c_val_ptrs;
125
126        unsafe {
127            sys::zwasm_wasi_config_set_env(
128                self.ptr,
129                count,
130                if self.env_key_ptrs.is_empty() {
131                    std::ptr::null()
132                } else {
133                    self.env_key_ptrs.as_ptr()
134                },
135                if self.env_key_lens.is_empty() {
136                    std::ptr::null()
137                } else {
138                    self.env_key_lens.as_ptr()
139                },
140                if self.env_val_ptrs.is_empty() {
141                    std::ptr::null()
142                } else {
143                    self.env_val_ptrs.as_ptr()
144                },
145                if self.env_val_lens.is_empty() {
146                    std::ptr::null()
147                } else {
148                    self.env_val_lens.as_ptr()
149                },
150            )
151        };
152
153        Ok(())
154    }
155
156    /// Preopens a host directory at a guest-visible path for the Wasm module.
157    ///
158    /// Grants the guest access to the specified host directory under the given guest path.
159    ///
160    /// # Safety
161    /// This method grants the guest access to host filesystem resources. Callers must ensure
162    /// the mapped host path is intended to be exposed to untrusted Wasm code.
163    pub fn preopen_dir(
164        &mut self,
165        host_path: &str,
166        guest_path: &str,
167    ) -> Result<(), error::ZwasmError> {
168        let c_host_path = std::ffi::CString::new(host_path)
169            .map_err(|_| error::ZwasmError("host path contains NUL byte".into()))?;
170        let c_guest_path = std::ffi::CString::new(guest_path)
171            .map_err(|_| error::ZwasmError("guest path contains NUL byte".into()))?;
172
173        self.preopens.push((c_host_path, c_guest_path));
174        let (c_host_path, c_guest_path) = &self.preopens[self.preopens.len() - 1];
175        let c_host_path_len = c_host_path.as_bytes().len();
176        let c_guest_path_len = c_guest_path.as_bytes().len();
177
178        unsafe {
179            sys::zwasm_wasi_config_preopen_dir(
180                self.ptr,
181                c_host_path.as_ptr(),
182                c_host_path_len,
183                c_guest_path.as_ptr(),
184                c_guest_path_len,
185            )
186        };
187
188        Ok(())
189    }
190
191    /// Preopens an existing host file descriptor at a guest-visible path for the Wasm module.
192    ///
193    /// Useful for passing already-opened files or sockets into the guest.
194    ///
195    /// # Safety
196    /// This method transfers or borrows host FD capabilities into the guest, depending on
197    /// `ownership`. Callers must ensure FD lifetime/ownership policy matches the supplied flag
198    /// and that exposing the FD to guest code is acceptable.
199    pub fn preopen_fd(
200        &mut self,
201        host_fd: isize,
202        guest_path: &str,
203        kind: u8,
204        ownership: u8,
205    ) -> Result<(), error::ZwasmError> {
206        let c_guest_path = std::ffi::CString::new(guest_path)
207            .map_err(|_| error::ZwasmError("guest path contains NUL byte".into()))?;
208
209        // Keep the guest path alive for the lifetime of the WASI config.
210        self.preopen_fd_guest_paths.push(c_guest_path);
211        let c_guest_path = self.preopen_fd_guest_paths.last().ok_or_else(|| {
212            error::ZwasmError("internal error: failed to retain preopen fd guest path".into())
213        })?;
214        let c_guest_path_len = c_guest_path.as_bytes().len();
215
216        unsafe {
217            sys::zwasm_wasi_config_preopen_fd(
218                self.ptr,
219                host_fd,
220                c_guest_path.as_ptr(),
221                c_guest_path_len,
222                kind,
223                ownership,
224            )
225        };
226
227        Ok(())
228    }
229
230    /// Overrides WASI stdio file descriptor mapping (stdin, stdout, stderr).
231    ///
232    /// Allows redirecting guest stdio to custom host file descriptors.
233    ///
234    /// # Safety
235    /// The runtime may close or retain the supplied `host_fd` based on `ownership`. Callers
236    /// must ensure ownership mode is correct to avoid double-close or leaked descriptors.
237    pub fn set_stdio_fd(
238        &mut self,
239        wasi_fd: u32,
240        host_fd: isize,
241        ownership: u8,
242    ) -> Result<(), error::ZwasmError> {
243        unsafe {
244            sys::zwasm_wasi_config_set_stdio_fd(self.ptr, wasi_fd, host_fd, ownership);
245        }
246        Ok(())
247    }
248}
249
250impl Drop for WasiConfig {
251    fn drop(&mut self) {
252        unsafe {
253            sys::zwasm_wasi_config_delete(self.ptr);
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use std::os::fd::AsRawFd;
261
262    use super::*;
263
264    #[test]
265    fn test_wasi_fd_api() {
266        let mut wc = WasiConfig::new().expect("Failed to create WasiConfig");
267
268        /* Set stdio overrides (use pipe fds) */
269        let (stdout_read, stdout_write) =
270            nix::unistd::pipe().expect("Failed to create pipe for stdout");
271
272        /* Override stdout (fd 1) with write end of pipe, borrow mode */
273        wc.set_stdio_fd(1, stdout_write.as_raw_fd() as isize, 0 /* borrow */)
274            .expect("Failed to set stdio fd for stdout");
275
276        /* Override stderr (fd 2) with write end as well, borrow mode */
277        wc.set_stdio_fd(2, stdout_write.as_raw_fd() as isize, 0 /* borrow */)
278            .expect("Failed to set stdio fd for stderr");
279
280        /* Invalid fd index (>=3) should be silently ignored */
281        wc.set_stdio_fd(5, stdout_read.as_raw_fd() as isize, 0)
282            .expect("Failed to set stdio fd for invalid index");
283
284        /* Add an FD-based preopen (borrow mode) */
285        let dir_fd = nix::fcntl::open(
286            ".",
287            nix::fcntl::OFlag::O_RDONLY,
288            nix::sys::stat::Mode::empty(),
289        )
290        .expect("Failed to open current directory for preopen fd");
291        wc.preopen_fd(
292            dir_fd.as_raw_fd() as isize,
293            "/sandbox",
294            1, /* dir */
295            0, /* borrow */
296        )
297        .expect("Failed to add preopen fd");
298
299        drop(wc);
300
301        let written = nix::unistd::write(&stdout_write, b"ok")
302            .expect("Failed to write to borrowed stdout pipe");
303        assert_eq!(written, 2, "borrowed stdout pipe still writable");
304
305        let _stat = nix::sys::stat::fstat(&dir_fd)
306            .expect("borrowed dir fd still valid after WasiConfig drop");
307    }
308}