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::{DirPerms, FilePerms, 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 (dir_perms, file_perms) = if writable {
450            (DirPerms::all(), FilePerms::all())
451        } else {
452            (DirPerms::READ, FilePerms::READ)
453        };
454        self.ctx
455            .preopened_dir(host_path, guest_path, dir_perms, file_perms)?;
456        Ok(())
457    }
458}
459
460pub struct InstanceBuilder {
461    ctx: WasiCtxBuilder,
462    socket_permit_state: Option<Arc<SocketPermitState>>,
463}
464
465impl InstanceBuilder {
466    /// Sets the WASI `stdin` descriptor to the given [`StdinStream`].
467    pub fn stdin(&mut self, stdin: impl StdinStream + 'static) {
468        self.ctx.stdin(stdin);
469    }
470
471    /// Sets the WASI `stdin` descriptor to the given [`Read`]er.
472    pub fn stdin_pipe(&mut self, r: impl Read + Send + Sync + Unpin + 'static) {
473        self.stdin(PipeReadStream::new(r));
474    }
475
476    /// Sets the WASI `stdout` descriptor to the given [`StdoutStream`].
477    pub fn stdout(&mut self, stdout: impl StdoutStream + 'static) {
478        self.ctx.stdout(stdout);
479    }
480
481    /// Sets the WASI `stdout` descriptor to the given [`Write`]r.
482    pub fn stdout_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
483        self.stdout(PipedWriteStream::new(w));
484    }
485
486    /// Sets the WASI `stderr` descriptor to the given [`StdoutStream`].
487    pub fn stderr(&mut self, stderr: impl StdoutStream + 'static) {
488        self.ctx.stderr(stderr);
489    }
490
491    /// Sets the WASI `stderr` descriptor to the given [`Write`]r.
492    pub fn stderr_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
493        self.stderr(PipedWriteStream::new(w));
494    }
495
496    /// Appends the given strings to the WASI 'args'.
497    pub fn args(&mut self, args: impl IntoIterator<Item = impl AsRef<str>>) {
498        for arg in args {
499            self.ctx.arg(arg);
500        }
501    }
502
503    /// Sets the given key/value string entries on the WASI 'env'.
504    pub fn env(&mut self, vars: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>) {
505        for (k, v) in vars {
506            self.ctx.env(k, v);
507        }
508    }
509
510    /// "Mounts" the given `host_path` into the WASI filesystem at the given
511    /// `guest_path`.
512    pub fn preopened_dir(
513        &mut self,
514        host_path: impl AsRef<Path>,
515        guest_path: impl AsRef<str>,
516        writable: bool,
517    ) -> anyhow::Result<()> {
518        let (dir_perms, file_perms) = if writable {
519            (DirPerms::all(), FilePerms::all())
520        } else {
521            (DirPerms::READ, FilePerms::READ)
522        };
523        self.ctx
524            .preopened_dir(host_path, guest_path, dir_perms, file_perms)?;
525        Ok(())
526    }
527}
528
529impl FactorInstanceBuilder for InstanceBuilder {
530    type InstanceState = InstanceState;
531
532    fn build(self) -> anyhow::Result<Self::InstanceState> {
533        let InstanceBuilder {
534            ctx: mut wasi_ctx,
535            socket_permit_state,
536        } = self;
537        Ok(InstanceState {
538            ctx: wasi_ctx.build(),
539            socket_permit_state,
540        })
541    }
542}
543
544impl InstanceBuilder {
545    /// Sets the socket permit state for per-connection quota tracking.
546    pub fn set_socket_permit_state(&mut self, state: Arc<SocketPermitState>) {
547        self.socket_permit_state = Some(state);
548    }
549
550    pub fn outbound_socket_addr_check<F, Fut>(&mut self, check: F)
551    where
552        F: Fn(SocketAddr, SocketAddrUse) -> Fut + Send + Sync + Clone + 'static,
553        Fut: Future<Output = bool> + Send + Sync,
554    {
555        self.ctx.socket_addr_check(move |addr, addr_use| {
556            let check = check.clone();
557            Box::pin(async move {
558                match addr_use {
559                    SocketAddrUse::TcpBind => false,
560                    SocketAddrUse::TcpConnect
561                    | SocketAddrUse::UdpBind
562                    | SocketAddrUse::UdpConnect
563                    | SocketAddrUse::UdpOutgoingDatagram => check(addr, addr_use).await,
564                }
565            })
566        });
567    }
568}
569
570pub struct InstanceState {
571    ctx: WasiCtx,
572    socket_permit_state: Option<Arc<SocketPermitState>>,
573}
574
575impl InstanceState {
576    pub fn ctx(&mut self) -> &mut WasiCtx {
577        &mut self.ctx
578    }
579}