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#[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 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 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 pub fn stdin(&mut self, stdin: impl StdinStream + 'static) {
468 self.ctx.stdin(stdin);
469 }
470
471 pub fn stdin_pipe(&mut self, r: impl Read + Send + Sync + Unpin + 'static) {
473 self.stdin(PipeReadStream::new(r));
474 }
475
476 pub fn stdout(&mut self, stdout: impl StdoutStream + 'static) {
478 self.ctx.stdout(stdout);
479 }
480
481 pub fn stdout_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
483 self.stdout(PipedWriteStream::new(w));
484 }
485
486 pub fn stderr(&mut self, stderr: impl StdoutStream + 'static) {
488 self.ctx.stderr(stderr);
489 }
490
491 pub fn stderr_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
493 self.stderr(PipedWriteStream::new(w));
494 }
495
496 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 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 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 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}