1mod io;
2pub mod spin;
3mod wasi_2023_10_18;
4mod wasi_2023_11_10;
5
6use std::{
7 future::Future,
8 io::{Read, Write},
9 net::SocketAddr,
10 path::Path,
11};
12
13use io::{PipeReadStream, PipedWriteStream};
14use spin_factors::{
15 anyhow, AppComponent, Factor, FactorInstanceBuilder, InitContext, PrepareContext,
16 RuntimeFactors, RuntimeFactorsInstanceState,
17};
18use wasmtime::component::HasData;
19use wasmtime_wasi::cli::{StdinStream, StdoutStream, WasiCli, WasiCliCtxView};
20use wasmtime_wasi::clocks::{WasiClocks, WasiClocksCtxView};
21use wasmtime_wasi::filesystem::{WasiFilesystem, WasiFilesystemCtxView};
22use wasmtime_wasi::random::{WasiRandom, WasiRandomCtx};
23use wasmtime_wasi::sockets::{WasiSockets, WasiSocketsCtxView};
24use wasmtime_wasi::{DirPerms, FilePerms, ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView};
25
26pub use wasmtime_wasi::sockets::SocketAddrUse;
27
28pub struct WasiFactor {
29 files_mounter: Box<dyn FilesMounter>,
30}
31
32impl WasiFactor {
33 pub fn new(files_mounter: impl FilesMounter + 'static) -> Self {
34 Self {
35 files_mounter: Box::new(files_mounter),
36 }
37 }
38
39 pub fn get_wasi_impl(
40 runtime_instance_state: &mut impl RuntimeFactorsInstanceState,
41 ) -> Option<WasiCtxView<'_>> {
42 let (state, table) = runtime_instance_state.get_with_table::<WasiFactor>()?;
43 Some(WasiCtxView {
44 ctx: &mut state.ctx,
45 table,
46 })
47 }
48
49 pub fn get_cli_impl(
50 runtime_instance_state: &mut impl RuntimeFactorsInstanceState,
51 ) -> Option<WasiCliCtxView<'_>> {
52 let (state, table) = runtime_instance_state.get_with_table::<WasiFactor>()?;
53 Some(WasiCliCtxView {
54 ctx: state.ctx.cli(),
55 table,
56 })
57 }
58
59 pub fn get_sockets_impl(
60 runtime_instance_state: &mut impl RuntimeFactorsInstanceState,
61 ) -> Option<WasiSocketsCtxView<'_>> {
62 let (state, table) = runtime_instance_state.get_with_table::<WasiFactor>()?;
63 Some(WasiSocketsCtxView {
64 ctx: state.ctx.sockets(),
65 table,
66 })
67 }
68}
69
70#[allow(clippy::type_complexity, reason = "sorry, blame alex")]
74trait InitContextExt: InitContext<WasiFactor> {
75 fn get_table(data: &mut Self::StoreData) -> &mut ResourceTable {
76 let (_state, table) = Self::get_data_with_table(data);
77 table
78 }
79
80 fn get_clocks(data: &mut Self::StoreData) -> WasiClocksCtxView<'_> {
81 let (state, table) = Self::get_data_with_table(data);
82 WasiClocksCtxView {
83 ctx: state.ctx.clocks(),
84 table,
85 }
86 }
87
88 fn get_random(data: &mut Self::StoreData) -> &mut WasiRandomCtx {
89 let (state, _) = Self::get_data_with_table(data);
90 state.ctx.random()
91 }
92
93 fn link_clocks_bindings(
94 &mut self,
95 add_to_linker: fn(
96 &mut wasmtime::component::Linker<Self::StoreData>,
97 fn(&mut Self::StoreData) -> WasiClocksCtxView<'_>,
98 ) -> wasmtime::Result<()>,
99 ) -> wasmtime::Result<()> {
100 add_to_linker(self.linker(), Self::get_clocks)
101 }
102
103 fn get_cli(data: &mut Self::StoreData) -> WasiCliCtxView<'_> {
104 let (state, table) = Self::get_data_with_table(data);
105 WasiCliCtxView {
106 ctx: state.ctx.cli(),
107 table,
108 }
109 }
110
111 fn link_cli_bindings(
112 &mut self,
113 add_to_linker: fn(
114 &mut wasmtime::component::Linker<Self::StoreData>,
115 fn(&mut Self::StoreData) -> WasiCliCtxView<'_>,
116 ) -> wasmtime::Result<()>,
117 ) -> wasmtime::Result<()> {
118 add_to_linker(self.linker(), Self::get_cli)
119 }
120
121 fn link_cli_default_bindings<O: Default>(
122 &mut self,
123 add_to_linker: fn(
124 &mut wasmtime::component::Linker<Self::StoreData>,
125 &O,
126 fn(&mut Self::StoreData) -> WasiCliCtxView<'_>,
127 ) -> wasmtime::Result<()>,
128 ) -> wasmtime::Result<()> {
129 add_to_linker(self.linker(), &O::default(), 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 link_io_bindings(
180 &mut self,
181 add_to_linker: fn(
182 &mut wasmtime::component::Linker<Self::StoreData>,
183 fn(&mut Self::StoreData) -> &mut ResourceTable,
184 ) -> wasmtime::Result<()>,
185 ) -> wasmtime::Result<()> {
186 add_to_linker(self.linker(), Self::get_table)
187 }
188
189 fn link_random_bindings(
190 &mut self,
191 add_to_linker: fn(
192 &mut wasmtime::component::Linker<Self::StoreData>,
193 fn(&mut Self::StoreData) -> &mut WasiRandomCtx,
194 ) -> wasmtime::Result<()>,
195 ) -> wasmtime::Result<()> {
196 add_to_linker(self.linker(), |data| {
197 let (state, _table) = Self::get_data_with_table(data);
198 state.ctx.random()
199 })
200 }
201
202 fn link_all_bindings(
203 &mut self,
204 add_to_linker: fn(
205 &mut wasmtime::component::Linker<Self::StoreData>,
206 fn(&mut Self::StoreData) -> &mut ResourceTable,
207 fn(&mut Self::StoreData) -> &mut WasiRandomCtx,
208 fn(&mut Self::StoreData) -> WasiClocksCtxView<'_>,
209 fn(&mut Self::StoreData) -> WasiCliCtxView<'_>,
210 fn(&mut Self::StoreData) -> WasiFilesystemCtxView<'_>,
211 fn(&mut Self::StoreData) -> WasiSocketsCtxView<'_>,
212 ) -> anyhow::Result<()>,
213 ) -> anyhow::Result<()> {
214 add_to_linker(
215 self.linker(),
216 Self::get_table,
217 Self::get_random,
218 Self::get_clocks,
219 Self::get_cli,
220 Self::get_filesystem,
221 Self::get_sockets,
222 )
223 }
224}
225
226impl<T> InitContextExt for T where T: InitContext<WasiFactor> {}
227
228struct HasIo;
229
230impl HasData for HasIo {
231 type Data<'a> = &'a mut ResourceTable;
232}
233
234impl Factor for WasiFactor {
235 type RuntimeConfig = ();
236 type AppState = ();
237 type InstanceBuilder = InstanceBuilder;
238
239 fn init(&mut self, ctx: &mut impl InitContext<Self>) -> anyhow::Result<()> {
240 use wasmtime_wasi::{p2, p3};
241
242 ctx.link_clocks_bindings(p2::bindings::clocks::wall_clock::add_to_linker::<_, WasiClocks>)?;
243 ctx.link_clocks_bindings(
244 p3::bindings::clocks::system_clock::add_to_linker::<_, WasiClocks>,
245 )?;
246 ctx.link_clocks_bindings(
247 p2::bindings::clocks::monotonic_clock::add_to_linker::<_, WasiClocks>,
248 )?;
249 ctx.link_clocks_bindings(
250 p3::bindings::clocks::monotonic_clock::add_to_linker::<_, WasiClocks>,
251 )?;
252 ctx.link_filesystem_bindings(
253 p2::bindings::filesystem::types::add_to_linker::<_, WasiFilesystem>,
254 )?;
255 ctx.link_filesystem_bindings(
256 p3::bindings::filesystem::types::add_to_linker::<_, WasiFilesystem>,
257 )?;
258 ctx.link_filesystem_bindings(
259 p2::bindings::filesystem::preopens::add_to_linker::<_, WasiFilesystem>,
260 )?;
261 ctx.link_filesystem_bindings(
262 p3::bindings::filesystem::preopens::add_to_linker::<_, WasiFilesystem>,
263 )?;
264 ctx.link_io_bindings(p2::bindings::io::error::add_to_linker::<_, HasIo>)?;
265 ctx.link_io_bindings(p2::bindings::io::poll::add_to_linker::<_, HasIo>)?;
266 ctx.link_io_bindings(p2::bindings::io::streams::add_to_linker::<_, HasIo>)?;
267 ctx.link_random_bindings(p2::bindings::random::random::add_to_linker::<_, WasiRandom>)?;
268 ctx.link_random_bindings(p3::bindings::random::random::add_to_linker::<_, WasiRandom>)?;
269 ctx.link_random_bindings(p2::bindings::random::insecure::add_to_linker::<_, WasiRandom>)?;
270 ctx.link_random_bindings(p3::bindings::random::insecure::add_to_linker::<_, WasiRandom>)?;
271 ctx.link_random_bindings(
272 p2::bindings::random::insecure_seed::add_to_linker::<_, WasiRandom>,
273 )?;
274 ctx.link_random_bindings(
275 p3::bindings::random::insecure_seed::add_to_linker::<_, WasiRandom>,
276 )?;
277 ctx.link_cli_default_bindings(p2::bindings::cli::exit::add_to_linker::<_, WasiCli>)?;
278 ctx.link_cli_default_bindings(p3::bindings::cli::exit::add_to_linker::<_, WasiCli>)?;
279 ctx.link_cli_bindings(p2::bindings::cli::environment::add_to_linker::<_, WasiCli>)?;
280 ctx.link_cli_bindings(p3::bindings::cli::environment::add_to_linker::<_, WasiCli>)?;
281 ctx.link_cli_bindings(p2::bindings::cli::stdin::add_to_linker::<_, WasiCli>)?;
282 ctx.link_cli_bindings(p3::bindings::cli::stdin::add_to_linker::<_, WasiCli>)?;
283 ctx.link_cli_bindings(p2::bindings::cli::stdout::add_to_linker::<_, WasiCli>)?;
284 ctx.link_cli_bindings(p3::bindings::cli::stdout::add_to_linker::<_, WasiCli>)?;
285 ctx.link_cli_bindings(p2::bindings::cli::stderr::add_to_linker::<_, WasiCli>)?;
286 ctx.link_cli_bindings(p3::bindings::cli::stderr::add_to_linker::<_, WasiCli>)?;
287 ctx.link_cli_bindings(p2::bindings::cli::terminal_input::add_to_linker::<_, WasiCli>)?;
288 ctx.link_cli_bindings(p3::bindings::cli::terminal_input::add_to_linker::<_, WasiCli>)?;
289 ctx.link_cli_bindings(p2::bindings::cli::terminal_output::add_to_linker::<_, WasiCli>)?;
290 ctx.link_cli_bindings(p3::bindings::cli::terminal_output::add_to_linker::<_, WasiCli>)?;
291 ctx.link_cli_bindings(p2::bindings::cli::terminal_stdin::add_to_linker::<_, WasiCli>)?;
292 ctx.link_cli_bindings(p3::bindings::cli::terminal_stdin::add_to_linker::<_, WasiCli>)?;
293 ctx.link_cli_bindings(p2::bindings::cli::terminal_stdout::add_to_linker::<_, WasiCli>)?;
294 ctx.link_cli_bindings(p3::bindings::cli::terminal_stdout::add_to_linker::<_, WasiCli>)?;
295 ctx.link_cli_bindings(p2::bindings::cli::terminal_stderr::add_to_linker::<_, WasiCli>)?;
296 ctx.link_cli_bindings(p3::bindings::cli::terminal_stderr::add_to_linker::<_, WasiCli>)?;
297 ctx.link_sockets_bindings(p2::bindings::sockets::tcp::add_to_linker::<_, WasiSockets>)?;
298 ctx.link_sockets_bindings(
299 p2::bindings::sockets::tcp_create_socket::add_to_linker::<_, WasiSockets>,
300 )?;
301 ctx.link_sockets_bindings(p2::bindings::sockets::udp::add_to_linker::<_, WasiSockets>)?;
302 ctx.link_sockets_bindings(
303 p2::bindings::sockets::udp_create_socket::add_to_linker::<_, WasiSockets>,
304 )?;
305 ctx.link_sockets_bindings(
306 p2::bindings::sockets::instance_network::add_to_linker::<_, WasiSockets>,
307 )?;
308 ctx.link_sockets_default_bindings(
309 p2::bindings::sockets::network::add_to_linker::<_, WasiSockets>,
310 )?;
311 ctx.link_sockets_bindings(
312 p2::bindings::sockets::ip_name_lookup::add_to_linker::<_, WasiSockets>,
313 )?;
314 ctx.link_sockets_bindings(
315 p3::bindings::sockets::ip_name_lookup::add_to_linker::<_, WasiSockets>,
316 )?;
317 ctx.link_sockets_bindings(p3::bindings::sockets::types::add_to_linker::<_, WasiSockets>)?;
318
319 ctx.link_all_bindings(wasi_2023_10_18::add_to_linker)?;
320 ctx.link_all_bindings(wasi_2023_11_10::add_to_linker)?;
321 Ok(())
322 }
323
324 fn configure_app<T: RuntimeFactors>(
325 &self,
326 _ctx: spin_factors::ConfigureAppContext<T, Self>,
327 ) -> anyhow::Result<Self::AppState> {
328 Ok(())
329 }
330
331 fn prepare<T: RuntimeFactors>(
332 &self,
333 ctx: PrepareContext<T, Self>,
334 ) -> anyhow::Result<InstanceBuilder> {
335 let mut wasi_ctx = WasiCtxBuilder::new();
336
337 let mount_ctx = MountFilesContext { ctx: &mut wasi_ctx };
339 self.files_mounter
340 .mount_files(ctx.app_component(), mount_ctx)?;
341
342 let mut builder = InstanceBuilder { ctx: wasi_ctx };
343
344 builder.env(ctx.app_component().environment());
346
347 Ok(builder)
348 }
349}
350
351pub trait FilesMounter: Send + Sync {
352 fn mount_files(
353 &self,
354 app_component: &AppComponent,
355 ctx: MountFilesContext,
356 ) -> anyhow::Result<()>;
357}
358
359pub struct DummyFilesMounter;
360
361impl FilesMounter for DummyFilesMounter {
362 fn mount_files(
363 &self,
364 app_component: &AppComponent,
365 _ctx: MountFilesContext,
366 ) -> anyhow::Result<()> {
367 anyhow::ensure!(
368 app_component.files().next().is_none(),
369 "DummyFilesMounter can't actually mount files"
370 );
371 Ok(())
372 }
373}
374
375pub struct MountFilesContext<'a> {
376 ctx: &'a mut WasiCtxBuilder,
377}
378
379impl MountFilesContext<'_> {
380 pub fn preopened_dir(
381 &mut self,
382 host_path: impl AsRef<Path>,
383 guest_path: impl AsRef<str>,
384 writable: bool,
385 ) -> anyhow::Result<()> {
386 let (dir_perms, file_perms) = if writable {
387 (DirPerms::all(), FilePerms::all())
388 } else {
389 (DirPerms::READ, FilePerms::READ)
390 };
391 self.ctx
392 .preopened_dir(host_path, guest_path, dir_perms, file_perms)?;
393 Ok(())
394 }
395}
396
397pub struct InstanceBuilder {
398 ctx: WasiCtxBuilder,
399}
400
401impl InstanceBuilder {
402 pub fn stdin(&mut self, stdin: impl StdinStream + 'static) {
404 self.ctx.stdin(stdin);
405 }
406
407 pub fn stdin_pipe(&mut self, r: impl Read + Send + Sync + Unpin + 'static) {
409 self.stdin(PipeReadStream::new(r));
410 }
411
412 pub fn stdout(&mut self, stdout: impl StdoutStream + 'static) {
414 self.ctx.stdout(stdout);
415 }
416
417 pub fn stdout_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
419 self.stdout(PipedWriteStream::new(w));
420 }
421
422 pub fn stderr(&mut self, stderr: impl StdoutStream + 'static) {
424 self.ctx.stderr(stderr);
425 }
426
427 pub fn stderr_pipe(&mut self, w: impl Write + Send + Sync + Unpin + 'static) {
429 self.stderr(PipedWriteStream::new(w));
430 }
431
432 pub fn args(&mut self, args: impl IntoIterator<Item = impl AsRef<str>>) {
434 for arg in args {
435 self.ctx.arg(arg);
436 }
437 }
438
439 pub fn env(&mut self, vars: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>) {
441 for (k, v) in vars {
442 self.ctx.env(k, v);
443 }
444 }
445
446 pub fn preopened_dir(
449 &mut self,
450 host_path: impl AsRef<Path>,
451 guest_path: impl AsRef<str>,
452 writable: bool,
453 ) -> anyhow::Result<()> {
454 let (dir_perms, file_perms) = if writable {
455 (DirPerms::all(), FilePerms::all())
456 } else {
457 (DirPerms::READ, FilePerms::READ)
458 };
459 self.ctx
460 .preopened_dir(host_path, guest_path, dir_perms, file_perms)?;
461 Ok(())
462 }
463}
464
465impl FactorInstanceBuilder for InstanceBuilder {
466 type InstanceState = InstanceState;
467
468 fn build(self) -> anyhow::Result<Self::InstanceState> {
469 let InstanceBuilder { ctx: mut wasi_ctx } = self;
470 Ok(InstanceState {
471 ctx: wasi_ctx.build(),
472 })
473 }
474}
475
476impl InstanceBuilder {
477 pub fn outbound_socket_addr_check<F, Fut>(&mut self, check: F)
478 where
479 F: Fn(SocketAddr, SocketAddrUse) -> Fut + Send + Sync + Clone + 'static,
480 Fut: Future<Output = bool> + Send + Sync,
481 {
482 self.ctx.socket_addr_check(move |addr, addr_use| {
483 let check = check.clone();
484 Box::pin(async move {
485 match addr_use {
486 SocketAddrUse::TcpBind => false,
487 SocketAddrUse::TcpConnect
488 | SocketAddrUse::UdpBind
489 | SocketAddrUse::UdpConnect
490 | SocketAddrUse::UdpOutgoingDatagram => check(addr, addr_use).await,
491 }
492 })
493 });
494 }
495}
496
497pub struct InstanceState {
498 ctx: WasiCtx,
499}