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