Skip to main content

spin_factor_wasi/
lib.rs

1mod io;
2pub mod sockets;
3pub mod spin;
4mod wasi_2023_10_18;
5mod wasi_2023_11_10;
6
7use std::{
8    future::Future,
9    io::{Read, Write},
10    net::SocketAddr,
11    path::Path,
12    sync::Arc,
13};
14
15use io::{PipeReadStream, PipedWriteStream};
16use spin_factors::{
17    AppComponent, Factor, FactorInstanceBuilder, InitContext, PrepareContext, RuntimeFactors,
18    RuntimeFactorsInstanceState, anyhow,
19};
20use wasmtime::component::HasData;
21use wasmtime_wasi::cli::{StdinStream, StdoutStream, WasiCli, WasiCliCtxView};
22use wasmtime_wasi::clocks::{WasiClocks, WasiClocksCtxView};
23use wasmtime_wasi::filesystem::{WasiFilesystem, WasiFilesystemCtxView};
24use wasmtime_wasi::random::{WasiRandom, WasiRandomCtx};
25use wasmtime_wasi::sockets::{WasiSockets, WasiSocketsCtxView};
26use wasmtime_wasi::{DirPerms, FilePerms, ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView};
27
28pub use sockets::{SocketPermitState, SpinSockets, SpinSocketsView};
29pub use wasmtime_wasi::sockets::SocketAddrUse;
30
31pub struct WasiFactor {
32    files_mounter: Box<dyn FilesMounter>,
33}
34
35impl WasiFactor {
36    pub fn new(files_mounter: impl FilesMounter + 'static) -> Self {
37        Self {
38            files_mounter: Box::new(files_mounter),
39        }
40    }
41
42    pub fn get_wasi_impl(
43        runtime_instance_state: &mut impl RuntimeFactorsInstanceState,
44    ) -> Option<WasiCtxView<'_>> {
45        let (state, table) = runtime_instance_state.get_with_table::<WasiFactor>()?;
46        Some(WasiCtxView {
47            ctx: &mut state.ctx,
48            table,
49        })
50    }
51
52    pub fn get_cli_impl(
53        runtime_instance_state: &mut impl RuntimeFactorsInstanceState,
54    ) -> Option<WasiCliCtxView<'_>> {
55        let (state, table) = runtime_instance_state.get_with_table::<WasiFactor>()?;
56        Some(WasiCliCtxView {
57            ctx: state.ctx.cli(),
58            table,
59        })
60    }
61
62    pub fn get_sockets_impl(
63        runtime_instance_state: &mut impl RuntimeFactorsInstanceState,
64    ) -> Option<SpinSocketsView<'_>> {
65        let (state, table) = runtime_instance_state.get_with_table::<WasiFactor>()?;
66        Some(SpinSocketsView {
67            inner: WasiSocketsCtxView {
68                ctx: state.ctx.sockets(),
69                table,
70            },
71            permit_state: state.socket_permit_state.clone(),
72        })
73    }
74}
75
76/// Helper trait to extend `InitContext` with some more `link_*_bindings`
77/// methods related to `wasmtime-wasi` and `wasmtime-wasi-io`-specific
78/// signatures.
79#[allow(clippy::type_complexity, reason = "sorry, blame alex")]
80trait InitContextExt: InitContext<WasiFactor> {
81    fn get_table(data: &mut Self::StoreData) -> &mut ResourceTable {
82        let (_state, table) = Self::get_data_with_table(data);
83        table
84    }
85
86    fn get_clocks(data: &mut Self::StoreData) -> WasiClocksCtxView<'_> {
87        let (state, table) = Self::get_data_with_table(data);
88        WasiClocksCtxView {
89            ctx: state.ctx.clocks(),
90            table,
91        }
92    }
93
94    fn get_random(data: &mut Self::StoreData) -> &mut WasiRandomCtx {
95        let (state, _) = Self::get_data_with_table(data);
96        state.ctx.random()
97    }
98
99    fn link_clocks_bindings(
100        &mut self,
101        add_to_linker: fn(
102            &mut wasmtime::component::Linker<Self::StoreData>,
103            fn(&mut Self::StoreData) -> WasiClocksCtxView<'_>,
104        ) -> wasmtime::Result<()>,
105    ) -> wasmtime::Result<()> {
106        add_to_linker(self.linker(), Self::get_clocks)
107    }
108
109    fn get_cli(data: &mut Self::StoreData) -> WasiCliCtxView<'_> {
110        let (state, table) = Self::get_data_with_table(data);
111        WasiCliCtxView {
112            ctx: state.ctx.cli(),
113            table,
114        }
115    }
116
117    fn link_cli_bindings(
118        &mut self,
119        add_to_linker: fn(
120            &mut wasmtime::component::Linker<Self::StoreData>,
121            fn(&mut Self::StoreData) -> WasiCliCtxView<'_>,
122        ) -> wasmtime::Result<()>,
123    ) -> wasmtime::Result<()> {
124        add_to_linker(self.linker(), Self::get_cli)
125    }
126
127    fn link_cli_default_bindings<O: Default>(
128        &mut self,
129        add_to_linker: fn(
130            &mut wasmtime::component::Linker<Self::StoreData>,
131            &O,
132            fn(&mut Self::StoreData) -> WasiCliCtxView<'_>,
133        ) -> wasmtime::Result<()>,
134    ) -> wasmtime::Result<()> {
135        add_to_linker(self.linker(), &O::default(), Self::get_cli)
136    }
137
138    fn get_filesystem(data: &mut Self::StoreData) -> WasiFilesystemCtxView<'_> {
139        let (state, table) = Self::get_data_with_table(data);
140        WasiFilesystemCtxView {
141            ctx: state.ctx.filesystem(),
142            table,
143        }
144    }
145
146    fn link_filesystem_bindings(
147        &mut self,
148        add_to_linker: fn(
149            &mut wasmtime::component::Linker<Self::StoreData>,
150            fn(&mut Self::StoreData) -> WasiFilesystemCtxView<'_>,
151        ) -> wasmtime::Result<()>,
152    ) -> wasmtime::Result<()> {
153        add_to_linker(self.linker(), Self::get_filesystem)
154    }
155
156    fn get_sockets(data: &mut Self::StoreData) -> WasiSocketsCtxView<'_> {
157        let (state, table) = Self::get_data_with_table(data);
158        WasiSocketsCtxView {
159            ctx: state.ctx.sockets(),
160            table,
161        }
162    }
163
164    fn link_sockets_bindings(
165        &mut self,
166        add_to_linker: fn(
167            &mut wasmtime::component::Linker<Self::StoreData>,
168            fn(&mut Self::StoreData) -> WasiSocketsCtxView<'_>,
169        ) -> wasmtime::Result<()>,
170    ) -> wasmtime::Result<()> {
171        add_to_linker(self.linker(), Self::get_sockets)
172    }
173
174    fn link_sockets_default_bindings<O: Default>(
175        &mut self,
176        add_to_linker: fn(
177            &mut wasmtime::component::Linker<Self::StoreData>,
178            &O,
179            fn(&mut Self::StoreData) -> WasiSocketsCtxView<'_>,
180        ) -> wasmtime::Result<()>,
181    ) -> wasmtime::Result<()> {
182        add_to_linker(self.linker(), &O::default(), Self::get_sockets)
183    }
184
185    fn get_spin_sockets(data: &mut Self::StoreData) -> SpinSocketsView<'_> {
186        let (state, table) = Self::get_data_with_table(data);
187        SpinSocketsView {
188            inner: WasiSocketsCtxView {
189                ctx: state.ctx.sockets(),
190                table,
191            },
192            permit_state: state.socket_permit_state.clone(),
193        }
194    }
195
196    fn link_spin_sockets_bindings(
197        &mut self,
198        add_to_linker: fn(
199            &mut wasmtime::component::Linker<Self::StoreData>,
200            fn(&mut Self::StoreData) -> SpinSocketsView<'_>,
201        ) -> wasmtime::Result<()>,
202    ) -> wasmtime::Result<()> {
203        add_to_linker(self.linker(), Self::get_spin_sockets)
204    }
205
206    fn link_io_bindings(
207        &mut self,
208        add_to_linker: fn(
209            &mut wasmtime::component::Linker<Self::StoreData>,
210            fn(&mut Self::StoreData) -> &mut ResourceTable,
211        ) -> wasmtime::Result<()>,
212    ) -> wasmtime::Result<()> {
213        add_to_linker(self.linker(), Self::get_table)
214    }
215
216    fn link_random_bindings(
217        &mut self,
218        add_to_linker: fn(
219            &mut wasmtime::component::Linker<Self::StoreData>,
220            fn(&mut Self::StoreData) -> &mut WasiRandomCtx,
221        ) -> wasmtime::Result<()>,
222    ) -> wasmtime::Result<()> {
223        add_to_linker(self.linker(), |data| {
224            let (state, _table) = Self::get_data_with_table(data);
225            state.ctx.random()
226        })
227    }
228
229    fn link_all_bindings(
230        &mut self,
231        add_to_linker: fn(
232            &mut wasmtime::component::Linker<Self::StoreData>,
233            fn(&mut Self::StoreData) -> &mut ResourceTable,
234            fn(&mut Self::StoreData) -> &mut WasiRandomCtx,
235            fn(&mut Self::StoreData) -> WasiClocksCtxView<'_>,
236            fn(&mut Self::StoreData) -> WasiCliCtxView<'_>,
237            fn(&mut Self::StoreData) -> WasiFilesystemCtxView<'_>,
238            fn(&mut Self::StoreData) -> SpinSocketsView<'_>,
239        ) -> anyhow::Result<()>,
240    ) -> anyhow::Result<()> {
241        add_to_linker(
242            self.linker(),
243            Self::get_table,
244            Self::get_random,
245            Self::get_clocks,
246            Self::get_cli,
247            Self::get_filesystem,
248            Self::get_spin_sockets,
249        )
250    }
251}
252
253impl<T> InitContextExt for T where T: InitContext<WasiFactor> {}
254
255struct HasIo;
256
257impl HasData for HasIo {
258    type Data<'a> = &'a mut ResourceTable;
259}
260
261impl Factor for WasiFactor {
262    type RuntimeConfig = ();
263    type AppState = ();
264    type InstanceBuilder = InstanceBuilder;
265
266    fn init(&mut self, ctx: &mut impl InitContext<Self>) -> anyhow::Result<()> {
267        use wasmtime_wasi::{p2, p3};
268
269        ctx.link_clocks_bindings(p2::bindings::clocks::wall_clock::add_to_linker::<_, WasiClocks>)?;
270        ctx.link_clocks_bindings(
271            p3::bindings::clocks::system_clock::add_to_linker::<_, WasiClocks>,
272        )?;
273        ctx.link_clocks_bindings(
274            p2::bindings::clocks::monotonic_clock::add_to_linker::<_, WasiClocks>,
275        )?;
276        ctx.link_clocks_bindings(
277            p3::bindings::clocks::monotonic_clock::add_to_linker::<_, WasiClocks>,
278        )?;
279        ctx.link_filesystem_bindings(
280            p2::bindings::filesystem::types::add_to_linker::<_, WasiFilesystem>,
281        )?;
282        ctx.link_filesystem_bindings(
283            p3::bindings::filesystem::types::add_to_linker::<_, WasiFilesystem>,
284        )?;
285        ctx.link_filesystem_bindings(
286            p2::bindings::filesystem::preopens::add_to_linker::<_, WasiFilesystem>,
287        )?;
288        ctx.link_filesystem_bindings(
289            p3::bindings::filesystem::preopens::add_to_linker::<_, WasiFilesystem>,
290        )?;
291        ctx.link_io_bindings(p2::bindings::io::error::add_to_linker::<_, HasIo>)?;
292        ctx.link_io_bindings(p2::bindings::io::poll::add_to_linker::<_, HasIo>)?;
293        ctx.link_io_bindings(p2::bindings::io::streams::add_to_linker::<_, HasIo>)?;
294        ctx.link_random_bindings(p2::bindings::random::random::add_to_linker::<_, WasiRandom>)?;
295        ctx.link_random_bindings(p3::bindings::random::random::add_to_linker::<_, WasiRandom>)?;
296        ctx.link_random_bindings(p2::bindings::random::insecure::add_to_linker::<_, WasiRandom>)?;
297        ctx.link_random_bindings(p3::bindings::random::insecure::add_to_linker::<_, WasiRandom>)?;
298        ctx.link_random_bindings(
299            p2::bindings::random::insecure_seed::add_to_linker::<_, WasiRandom>,
300        )?;
301        ctx.link_random_bindings(
302            p3::bindings::random::insecure_seed::add_to_linker::<_, WasiRandom>,
303        )?;
304        ctx.link_cli_default_bindings(p2::bindings::cli::exit::add_to_linker::<_, WasiCli>)?;
305        ctx.link_cli_default_bindings(p3::bindings::cli::exit::add_to_linker::<_, WasiCli>)?;
306        ctx.link_cli_bindings(p2::bindings::cli::environment::add_to_linker::<_, WasiCli>)?;
307        ctx.link_cli_bindings(p3::bindings::cli::environment::add_to_linker::<_, WasiCli>)?;
308        ctx.link_cli_bindings(p2::bindings::cli::stdin::add_to_linker::<_, WasiCli>)?;
309        ctx.link_cli_bindings(p3::bindings::cli::stdin::add_to_linker::<_, WasiCli>)?;
310        ctx.link_cli_bindings(p2::bindings::cli::stdout::add_to_linker::<_, WasiCli>)?;
311        ctx.link_cli_bindings(p3::bindings::cli::stdout::add_to_linker::<_, WasiCli>)?;
312        ctx.link_cli_bindings(p2::bindings::cli::stderr::add_to_linker::<_, WasiCli>)?;
313        ctx.link_cli_bindings(p3::bindings::cli::stderr::add_to_linker::<_, WasiCli>)?;
314        ctx.link_cli_bindings(p2::bindings::cli::terminal_input::add_to_linker::<_, WasiCli>)?;
315        ctx.link_cli_bindings(p3::bindings::cli::terminal_input::add_to_linker::<_, WasiCli>)?;
316        ctx.link_cli_bindings(p2::bindings::cli::terminal_output::add_to_linker::<_, WasiCli>)?;
317        ctx.link_cli_bindings(p3::bindings::cli::terminal_output::add_to_linker::<_, WasiCli>)?;
318        ctx.link_cli_bindings(p2::bindings::cli::terminal_stdin::add_to_linker::<_, WasiCli>)?;
319        ctx.link_cli_bindings(p3::bindings::cli::terminal_stdin::add_to_linker::<_, WasiCli>)?;
320        ctx.link_cli_bindings(p2::bindings::cli::terminal_stdout::add_to_linker::<_, WasiCli>)?;
321        ctx.link_cli_bindings(p3::bindings::cli::terminal_stdout::add_to_linker::<_, WasiCli>)?;
322        ctx.link_cli_bindings(p2::bindings::cli::terminal_stderr::add_to_linker::<_, WasiCli>)?;
323        ctx.link_cli_bindings(p3::bindings::cli::terminal_stderr::add_to_linker::<_, WasiCli>)?;
324        ctx.link_spin_sockets_bindings(
325            p2::bindings::sockets::tcp::add_to_linker::<_, SpinSockets>,
326        )?;
327        ctx.link_spin_sockets_bindings(
328            p2::bindings::sockets::tcp_create_socket::add_to_linker::<_, SpinSockets>,
329        )?;
330        ctx.link_spin_sockets_bindings(
331            p2::bindings::sockets::udp::add_to_linker::<_, SpinSockets>,
332        )?;
333        ctx.link_spin_sockets_bindings(
334            p2::bindings::sockets::udp_create_socket::add_to_linker::<_, SpinSockets>,
335        )?;
336        ctx.link_sockets_bindings(
337            p2::bindings::sockets::instance_network::add_to_linker::<_, WasiSockets>,
338        )?;
339        ctx.link_sockets_default_bindings(
340            p2::bindings::sockets::network::add_to_linker::<_, WasiSockets>,
341        )?;
342        ctx.link_sockets_bindings(
343            p2::bindings::sockets::ip_name_lookup::add_to_linker::<_, WasiSockets>,
344        )?;
345        ctx.link_sockets_bindings(
346            p3::bindings::sockets::ip_name_lookup::add_to_linker::<_, WasiSockets>,
347        )?;
348        // TODO(rylev): switch to SpinSockets once possible
349        ctx.link_sockets_bindings(p3::bindings::sockets::types::add_to_linker::<_, WasiSockets>)?;
350
351        ctx.link_all_bindings(wasi_2023_10_18::add_to_linker)?;
352        ctx.link_all_bindings(wasi_2023_11_10::add_to_linker)?;
353        Ok(())
354    }
355
356    fn configure_app<T: RuntimeFactors>(
357        &self,
358        _ctx: spin_factors::ConfigureAppContext<T, Self>,
359    ) -> anyhow::Result<Self::AppState> {
360        Ok(())
361    }
362
363    fn prepare<T: RuntimeFactors>(
364        &self,
365        ctx: PrepareContext<T, Self>,
366    ) -> anyhow::Result<InstanceBuilder> {
367        let mut wasi_ctx = WasiCtxBuilder::new();
368
369        // Mount files
370        let mount_ctx = MountFilesContext { ctx: &mut wasi_ctx };
371        self.files_mounter
372            .mount_files(ctx.app_component(), mount_ctx)?;
373
374        let mut builder = InstanceBuilder {
375            ctx: wasi_ctx,
376            socket_permit_state: None,
377        };
378
379        // Apply environment variables
380        builder.env(ctx.app_component().environment());
381
382        Ok(builder)
383    }
384}
385
386pub trait FilesMounter: Send + Sync {
387    fn mount_files(
388        &self,
389        app_component: &AppComponent,
390        ctx: MountFilesContext,
391    ) -> anyhow::Result<()>;
392}
393
394pub struct DummyFilesMounter;
395
396impl FilesMounter for DummyFilesMounter {
397    fn mount_files(
398        &self,
399        app_component: &AppComponent,
400        _ctx: MountFilesContext,
401    ) -> anyhow::Result<()> {
402        anyhow::ensure!(
403            app_component.files().next().is_none(),
404            "DummyFilesMounter can't actually mount files"
405        );
406        Ok(())
407    }
408}
409
410pub struct MountFilesContext<'a> {
411    ctx: &'a mut WasiCtxBuilder,
412}
413
414impl MountFilesContext<'_> {
415    pub fn preopened_dir(
416        &mut self,
417        host_path: impl AsRef<Path>,
418        guest_path: impl AsRef<str>,
419        writable: bool,
420    ) -> anyhow::Result<()> {
421        let (dir_perms, file_perms) = if writable {
422            (DirPerms::all(), FilePerms::all())
423        } else {
424            (DirPerms::READ, FilePerms::READ)
425        };
426        self.ctx
427            .preopened_dir(host_path, guest_path, dir_perms, file_perms)?;
428        Ok(())
429    }
430}
431
432pub struct InstanceBuilder {
433    ctx: WasiCtxBuilder,
434    socket_permit_state: Option<Arc<SocketPermitState>>,
435}
436
437impl InstanceBuilder {
438    /// Sets the WASI `stdin` descriptor to the given [`StdinStream`].
439    pub fn stdin(&mut self, stdin: impl StdinStream + 'static) {
440        self.ctx.stdin(stdin);
441    }
442
443    /// Sets the WASI `stdin` descriptor to the given [`Read`]er.
444    pub fn stdin_pipe(&mut self, r: impl Read + Send + Sync + Unpin + 'static) {
445        self.stdin(PipeReadStream::new(r));
446    }
447
448    /// Sets the WASI `stdout` descriptor to the given [`StdoutStream`].
449    pub fn stdout(&mut self, stdout: impl StdoutStream + 'static) {
450        self.ctx.stdout(stdout);
451    }
452
453    /// Sets the WASI `stdout` descriptor to the given [`Write`]r.
454    pub fn stdout_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
455        self.stdout(PipedWriteStream::new(w));
456    }
457
458    /// Sets the WASI `stderr` descriptor to the given [`StdoutStream`].
459    pub fn stderr(&mut self, stderr: impl StdoutStream + 'static) {
460        self.ctx.stderr(stderr);
461    }
462
463    /// Sets the WASI `stderr` descriptor to the given [`Write`]r.
464    pub fn stderr_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
465        self.stderr(PipedWriteStream::new(w));
466    }
467
468    /// Appends the given strings to the WASI 'args'.
469    pub fn args(&mut self, args: impl IntoIterator<Item = impl AsRef<str>>) {
470        for arg in args {
471            self.ctx.arg(arg);
472        }
473    }
474
475    /// Sets the given key/value string entries on the WASI 'env'.
476    pub fn env(&mut self, vars: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>) {
477        for (k, v) in vars {
478            self.ctx.env(k, v);
479        }
480    }
481
482    /// "Mounts" the given `host_path` into the WASI filesystem at the given
483    /// `guest_path`.
484    pub fn preopened_dir(
485        &mut self,
486        host_path: impl AsRef<Path>,
487        guest_path: impl AsRef<str>,
488        writable: bool,
489    ) -> anyhow::Result<()> {
490        let (dir_perms, file_perms) = if writable {
491            (DirPerms::all(), FilePerms::all())
492        } else {
493            (DirPerms::READ, FilePerms::READ)
494        };
495        self.ctx
496            .preopened_dir(host_path, guest_path, dir_perms, file_perms)?;
497        Ok(())
498    }
499}
500
501impl FactorInstanceBuilder for InstanceBuilder {
502    type InstanceState = InstanceState;
503
504    fn build(self) -> anyhow::Result<Self::InstanceState> {
505        let InstanceBuilder {
506            ctx: mut wasi_ctx,
507            socket_permit_state,
508        } = self;
509        Ok(InstanceState {
510            ctx: wasi_ctx.build(),
511            socket_permit_state,
512        })
513    }
514}
515
516impl InstanceBuilder {
517    /// Sets the socket permit state for per-connection quota tracking.
518    pub fn set_socket_permit_state(&mut self, state: Arc<SocketPermitState>) {
519        self.socket_permit_state = Some(state);
520    }
521
522    pub fn outbound_socket_addr_check<F, Fut>(&mut self, check: F)
523    where
524        F: Fn(SocketAddr, SocketAddrUse) -> Fut + Send + Sync + Clone + 'static,
525        Fut: Future<Output = bool> + Send + Sync,
526    {
527        self.ctx.socket_addr_check(move |addr, addr_use| {
528            let check = check.clone();
529            Box::pin(async move {
530                match addr_use {
531                    SocketAddrUse::TcpBind => false,
532                    SocketAddrUse::TcpConnect
533                    | SocketAddrUse::UdpBind
534                    | SocketAddrUse::UdpConnect
535                    | SocketAddrUse::UdpOutgoingDatagram => check(addr, addr_use).await,
536                }
537            })
538        });
539    }
540}
541
542pub struct InstanceState {
543    ctx: WasiCtx,
544    socket_permit_state: Option<Arc<SocketPermitState>>,
545}