From db8d92bd5ce7db86682b88de50c0490ee89976b8 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Tue, 21 Jan 2025 15:49:31 +0100 Subject: [PATCH 01/10] ci: add subdir support Signed-off-by: Roman Volosatovs --- ci/vendor-wit.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ci/vendor-wit.sh b/ci/vendor-wit.sh index e607777d4509..54a276e479c8 100755 --- a/ci/vendor-wit.sh +++ b/ci/vendor-wit.sh @@ -19,18 +19,18 @@ make_vendor() { mkdir -p $path for package in $packages; do - IFS='@' read -r repo tag <<< "$package" - mkdir -p $path/$repo + IFS='@' read -r repo tag subdir <<< "$package" + mkdir -p "$path/$repo" cached_extracted_dir="$cache_dir/$repo-$tag" if [[ ! -d $cached_extracted_dir ]]; then mkdir -p $cached_extracted_dir curl -sL https://github.com/WebAssembly/wasi-$repo/archive/$tag.tar.gz | \ tar xzf - --strip-components=1 -C $cached_extracted_dir - rm -rf $cached_extracted_dir/wit/deps* + rm -rf $cached_extracted_dir/${subdir:-"wit"}/deps* fi - cp -r $cached_extracted_dir/wit/* $path/$repo + cp -r $cached_extracted_dir/${subdir:-"wit"}/* $path/$repo done } From f2de7e76cb91f382409d8aaf0049bddb33e1d656 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Fri, 11 Jul 2025 15:19:36 +0200 Subject: [PATCH 02/10] feat: extract common `WasiCtxBuilder` Signed-off-by: Roman Volosatovs --- crates/wasi/src/ctx.rs | 301 ++++++++++++++++++++++++++++++++++++++ crates/wasi/src/lib.rs | 2 + crates/wasi/src/p2/ctx.rs | 167 ++++++--------------- 3 files changed, 351 insertions(+), 119 deletions(-) create mode 100644 crates/wasi/src/ctx.rs diff --git a/crates/wasi/src/ctx.rs b/crates/wasi/src/ctx.rs new file mode 100644 index 000000000000..fa05a901b134 --- /dev/null +++ b/crates/wasi/src/ctx.rs @@ -0,0 +1,301 @@ +use crate::clocks::host::{monotonic_clock, wall_clock}; +use crate::clocks::{HostMonotonicClock, HostWallClock}; +use crate::net::{SocketAddrCheck, SocketAddrUse}; +use crate::random; +use cap_rand::{Rng, RngCore, SeedableRng}; +use std::future::Future; +use std::net::SocketAddr; +use std::pin::Pin; +use std::sync::Arc; + +/// Builder-style structure used to create a WASI context. +/// +/// This type is used to create a WASI context that is considered per-[`Store`] +/// state. +/// This is a low-level abstraction, users of this crate are expected to use it via +/// builders specific to WASI version used, for example, +/// [p2::WasiCtxBuilder](crate::p2::WasiCtxBuilder) +/// +/// [`Store`]: wasmtime::Store +pub(crate) struct WasiCtxBuilder { + pub(crate) env: Vec<(String, String)>, + pub(crate) args: Vec, + pub(crate) socket_addr_check: SocketAddrCheck, + pub(crate) random: Box, + pub(crate) insecure_random: Box, + pub(crate) insecure_random_seed: u128, + pub(crate) wall_clock: Box, + pub(crate) monotonic_clock: Box, + pub(crate) allowed_network_uses: AllowedNetworkUses, + pub(crate) allow_blocking_current_thread: bool, +} + +impl WasiCtxBuilder { + /// Creates a builder for a new context with default parameters set. + /// + /// The current defaults are: + /// + /// * stdin is closed + /// * stdout and stderr eat all input and it doesn't go anywhere + /// * no env vars + /// * no arguments + /// * no preopens + /// * clocks use the host implementation of wall/monotonic clocks + /// * RNGs are all initialized with random state and suitable generator + /// quality to satisfy the requirements of WASI APIs. + /// * TCP/UDP are allowed but all addresses are denied by default. + /// * IP name lookup is denied by default. + /// + /// These defaults can all be updated via the various builder configuration + /// methods below. + pub(crate) fn new() -> Self { + // For the insecure random API, use `SmallRng`, which is fast. It's + // also insecure, but that's the deal here. + let insecure_random = Box::new( + cap_rand::rngs::SmallRng::from_rng(cap_rand::thread_rng(cap_rand::ambient_authority())) + .unwrap(), + ); + + // For the insecure random seed, use a `u128` generated from + // `thread_rng()`, so that it's not guessable from the insecure_random + // API. + let insecure_random_seed = + cap_rand::thread_rng(cap_rand::ambient_authority()).r#gen::(); + Self { + env: Vec::new(), + args: Vec::new(), + socket_addr_check: SocketAddrCheck::default(), + random: random::thread_rng(), + insecure_random, + insecure_random_seed, + wall_clock: wall_clock(), + monotonic_clock: monotonic_clock(), + allowed_network_uses: AllowedNetworkUses::default(), + allow_blocking_current_thread: false, + } + } + + /// Configures whether or not blocking operations made through this + /// `WasiCtx` are allowed to block the current thread. + /// + /// WASI is currently implemented on top of the Rust + /// [Tokio](https://tokio.rs/) library. While most WASI APIs are + /// non-blocking some are instead blocking from the perspective of + /// WebAssembly. For example opening a file is a blocking operation with + /// respect to WebAssembly but it's implemented as an asynchronous operation + /// on the host. This is currently done with Tokio's + /// [`spawn_blocking`](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html). + /// + /// When WebAssembly is used in a synchronous context, for example when + /// [`Config::async_support`] is disabled, then this asynchronous operation + /// is quickly turned back into a synchronous operation with a `block_on` in + /// Rust. This switching back-and-forth between a blocking a non-blocking + /// context can have overhead, and this option exists to help alleviate this + /// overhead. + /// + /// This option indicates that for WASI functions that are blocking from the + /// perspective of WebAssembly it's ok to block the native thread as well. + /// This means that this back-and-forth between async and sync won't happen + /// and instead blocking operations are performed on-thread (such as opening + /// a file). This can improve the performance of WASI operations when async + /// support is disabled. + /// + /// [`Config::async_support`]: https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#method.async_support + pub fn allow_blocking_current_thread(&mut self, enable: bool) -> &mut Self { + self.allow_blocking_current_thread = enable; + self + } + + /// Appends multiple environment variables at once for this builder. + /// + /// All environment variables are appended to the list of environment + /// variables that this builder will configure. + /// + /// At this time environment variables are not deduplicated and if the same + /// key is set twice then the guest will see two entries for the same key. + pub fn envs(&mut self, env: &[(impl AsRef, impl AsRef)]) -> &mut Self { + self.env.extend( + env.iter() + .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().to_owned())), + ); + self + } + + /// Appends a single environment variable for this builder. + /// + /// At this time environment variables are not deduplicated and if the same + /// key is set twice then the guest will see two entries for the same key. + pub fn env(&mut self, k: impl AsRef, v: impl AsRef) -> &mut Self { + self.env + .push((k.as_ref().to_owned(), v.as_ref().to_owned())); + self + } + + /// Configures all environment variables to be inherited from the calling + /// process into this configuration. + /// + /// This will use [`envs`](WasiCtxBuilder::envs) to append all host-defined + /// environment variables. + pub fn inherit_env(&mut self) -> &mut Self { + self.envs(&std::env::vars().collect::>()) + } + + /// Appends a list of arguments to the argument array to pass to wasm. + pub fn args(&mut self, args: &[impl AsRef]) -> &mut Self { + self.args.extend(args.iter().map(|a| a.as_ref().to_owned())); + self + } + + /// Appends a single argument to get passed to wasm. + pub fn arg(&mut self, arg: impl AsRef) -> &mut Self { + self.args.push(arg.as_ref().to_owned()); + self + } + + /// Appends all host process arguments to the list of arguments to get + /// passed to wasm. + pub fn inherit_args(&mut self) -> &mut Self { + self.args(&std::env::args().collect::>()) + } + + /// Set the generator for the `wasi:random/random` number generator to the + /// custom generator specified. + /// + /// Note that contexts have a default RNG configured which is a suitable + /// generator for WASI and is configured with a random seed per-context. + /// + /// Guest code may rely on this random number generator to produce fresh + /// unpredictable random data in order to maintain its security invariants, + /// and ideally should use the insecure random API otherwise, so using any + /// prerecorded or otherwise predictable data may compromise security. + pub fn secure_random(&mut self, random: impl RngCore + Send + 'static) -> &mut Self { + self.random = Box::new(random); + self + } + + /// Configures the generator for `wasi:random/insecure`. + /// + /// The `insecure_random` generator provided will be used for all randomness + /// requested by the `wasi:random/insecure` interface. + pub fn insecure_random(&mut self, insecure_random: impl RngCore + Send + 'static) -> &mut Self { + self.insecure_random = Box::new(insecure_random); + self + } + + /// Configures the seed to be returned from `wasi:random/insecure-seed` to + /// the specified custom value. + /// + /// By default this number is randomly generated when a builder is created. + pub fn insecure_random_seed(&mut self, insecure_random_seed: u128) -> &mut Self { + self.insecure_random_seed = insecure_random_seed; + self + } + + /// Configures `wasi:clocks/wall-clock` to use the `clock` specified. + /// + /// By default the host's wall clock is used. + pub fn wall_clock(&mut self, clock: impl HostWallClock + 'static) -> &mut Self { + self.wall_clock = Box::new(clock); + self + } + + /// Configures `wasi:clocks/monotonic-clock` to use the `clock` specified. + /// + /// By default the host's monotonic clock is used. + pub fn monotonic_clock(&mut self, clock: impl HostMonotonicClock + 'static) -> &mut Self { + self.monotonic_clock = Box::new(clock); + self + } + + /// Allow all network addresses accessible to the host. + /// + /// This method will inherit all network addresses meaning that any address + /// can be bound by the guest or connected to by the guest using any + /// protocol. + /// + /// See also [`WasiCtxBuilder::socket_addr_check`]. + pub fn inherit_network(&mut self) -> &mut Self { + self.socket_addr_check(|_, _| Box::pin(async { true })) + } + + /// A check that will be called for each socket address that is used. + /// + /// Returning `true` will permit socket connections to the `SocketAddr`, + /// while returning `false` will reject the connection. + pub fn socket_addr_check(&mut self, check: F) -> &mut Self + where + F: Fn(SocketAddr, SocketAddrUse) -> Pin + Send + Sync>> + + Send + + Sync + + 'static, + { + self.socket_addr_check = SocketAddrCheck(Arc::new(check)); + self + } + + /// Allow usage of `wasi:sockets/ip-name-lookup` + /// + /// By default this is disabled. + pub fn allow_ip_name_lookup(&mut self, enable: bool) -> &mut Self { + self.allowed_network_uses.ip_name_lookup = enable; + self + } + + /// Allow usage of UDP. + /// + /// This is enabled by default, but can be disabled if UDP should be blanket + /// disabled. + pub fn allow_udp(&mut self, enable: bool) -> &mut Self { + self.allowed_network_uses.udp = enable; + self + } + + /// Allow usage of TCP + /// + /// This is enabled by default, but can be disabled if TCP should be blanket + /// disabled. + pub fn allow_tcp(&mut self, enable: bool) -> &mut Self { + self.allowed_network_uses.tcp = enable; + self + } +} + +pub struct AllowedNetworkUses { + pub ip_name_lookup: bool, + pub udp: bool, + pub tcp: bool, +} + +impl Default for AllowedNetworkUses { + fn default() -> Self { + Self { + ip_name_lookup: false, + udp: true, + tcp: true, + } + } +} + +impl AllowedNetworkUses { + pub(crate) fn check_allowed_udp(&self) -> std::io::Result<()> { + if !self.udp { + return Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "UDP is not allowed", + )); + } + + Ok(()) + } + + pub(crate) fn check_allowed_tcp(&self) -> std::io::Result<()> { + if !self.tcp { + return Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "TCP is not allowed", + )); + } + + Ok(()) + } +} diff --git a/crates/wasi/src/lib.rs b/crates/wasi/src/lib.rs index cf48830d2801..44ebc1eca07f 100644 --- a/crates/wasi/src/lib.rs +++ b/crates/wasi/src/lib.rs @@ -11,6 +11,7 @@ //! For WASIp1 and core modules, see the [`preview1`] module documentation. mod clocks; +mod ctx; mod error; mod fs; mod net; @@ -23,6 +24,7 @@ mod random; pub mod runtime; pub use self::clocks::{HostMonotonicClock, HostWallClock}; +pub(crate) use self::ctx::WasiCtxBuilder; pub use self::error::{I32Exit, TrappableError}; pub use self::fs::{DirPerms, FilePerms, OpenMode}; pub use self::net::{Network, SocketAddrUse}; diff --git a/crates/wasi/src/p2/ctx.rs b/crates/wasi/src/p2/ctx.rs index 428f63b3eec0..63b6ac1f28a4 100644 --- a/crates/wasi/src/p2/ctx.rs +++ b/crates/wasi/src/p2/ctx.rs @@ -1,21 +1,18 @@ -use crate::clocks::{ - HostMonotonicClock, HostWallClock, - host::{monotonic_clock, wall_clock}, -}; +use crate::clocks::{HostMonotonicClock, HostWallClock}; +use crate::ctx::AllowedNetworkUses; use crate::net::{SocketAddrCheck, SocketAddrUse}; -use crate::p2::{ - filesystem::Dir, - pipe, stdio, - stdio::{StdinStream, StdoutStream}, -}; -use crate::{DirPerms, FilePerms, OpenMode, random}; +use crate::p2::filesystem::Dir; +use crate::p2::pipe; +use crate::p2::stdio::{self, StdinStream, StdoutStream}; +use crate::{DirPerms, FilePerms, OpenMode}; use anyhow::Result; -use cap_rand::{Rng, RngCore, SeedableRng}; +use cap_rand::RngCore; use cap_std::ambient_authority; +use std::future::Future; +use std::mem; +use std::net::SocketAddr; use std::path::Path; -use std::sync::Arc; -use std::{future::Future, pin::Pin}; -use std::{mem, net::SocketAddr}; +use std::pin::Pin; /// Builder-style structure used to create a [`WasiCtx`]. /// @@ -38,20 +35,11 @@ use std::{mem, net::SocketAddr}; /// /// [`Store`]: wasmtime::Store pub struct WasiCtxBuilder { + common: crate::WasiCtxBuilder, stdin: Box, stdout: Box, stderr: Box, - env: Vec<(String, String)>, - args: Vec, preopens: Vec<(Dir, String)>, - socket_addr_check: SocketAddrCheck, - random: Box, - insecure_random: Box, - insecure_random_seed: u128, - wall_clock: Box, - monotonic_clock: Box, - allowed_network_uses: AllowedNetworkUses, - allow_blocking_current_thread: bool, built: bool, } @@ -69,38 +57,17 @@ impl WasiCtxBuilder { /// * RNGs are all initialized with random state and suitable generator /// quality to satisfy the requirements of WASI APIs. /// * TCP/UDP are allowed but all addresses are denied by default. - /// * `wasi:network/ip-name-lookup` is denied by default. + /// * IP name lookup is denied by default. /// /// These defaults can all be updated via the various builder configuration /// methods below. pub fn new() -> Self { - // For the insecure random API, use `SmallRng`, which is fast. It's - // also insecure, but that's the deal here. - let insecure_random = Box::new( - cap_rand::rngs::SmallRng::from_rng(cap_rand::thread_rng(cap_rand::ambient_authority())) - .unwrap(), - ); - - // For the insecure random seed, use a `u128` generated from - // `thread_rng()`, so that it's not guessable from the insecure_random - // API. - let insecure_random_seed = - cap_rand::thread_rng(cap_rand::ambient_authority()).r#gen::(); Self { + common: crate::WasiCtxBuilder::new(), stdin: Box::new(pipe::ClosedInputStream), stdout: Box::new(pipe::SinkOutputStream), stderr: Box::new(pipe::SinkOutputStream), - env: Vec::new(), - args: Vec::new(), preopens: Vec::new(), - socket_addr_check: SocketAddrCheck::default(), - random: random::thread_rng(), - insecure_random, - insecure_random_seed, - wall_clock: wall_clock(), - monotonic_clock: monotonic_clock(), - allowed_network_uses: AllowedNetworkUses::default(), - allow_blocking_current_thread: false, built: false, } } @@ -201,7 +168,7 @@ impl WasiCtxBuilder { /// /// [`Config::async_support`]: https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#method.async_support pub fn allow_blocking_current_thread(&mut self, enable: bool) -> &mut Self { - self.allow_blocking_current_thread = enable; + self.common.allow_blocking_current_thread(enable); self } @@ -225,10 +192,7 @@ impl WasiCtxBuilder { /// ]); /// ``` pub fn envs(&mut self, env: &[(impl AsRef, impl AsRef)]) -> &mut Self { - self.env.extend( - env.iter() - .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().to_owned())), - ); + self.common.envs(env); self } @@ -246,8 +210,7 @@ impl WasiCtxBuilder { /// wasi.env("FOO", "bar"); /// ``` pub fn env(&mut self, k: impl AsRef, v: impl AsRef) -> &mut Self { - self.env - .push((k.as_ref().to_owned(), v.as_ref().to_owned())); + self.common.env(k, v); self } @@ -257,25 +220,27 @@ impl WasiCtxBuilder { /// This will use [`envs`](WasiCtxBuilder::envs) to append all host-defined /// environment variables. pub fn inherit_env(&mut self) -> &mut Self { - self.envs(&std::env::vars().collect::>()) + self.common.inherit_env(); + self } /// Appends a list of arguments to the argument array to pass to wasm. pub fn args(&mut self, args: &[impl AsRef]) -> &mut Self { - self.args.extend(args.iter().map(|a| a.as_ref().to_owned())); + self.common.args(args); self } /// Appends a single argument to get passed to wasm. pub fn arg(&mut self, arg: impl AsRef) -> &mut Self { - self.args.push(arg.as_ref().to_owned()); + self.common.arg(arg); self } /// Appends all host process arguments to the list of arguments to get /// passed to wasm. pub fn inherit_args(&mut self) -> &mut Self { - self.args(&std::env::args().collect::>()) + self.common.inherit_args(); + self } /// Configures a "preopened directory" to be available to WebAssembly. @@ -345,7 +310,7 @@ impl WasiCtxBuilder { dir_perms, file_perms, open_mode, - self.allow_blocking_current_thread, + self.common.allow_blocking_current_thread, ), guest_path.as_ref().to_owned(), )); @@ -363,7 +328,7 @@ impl WasiCtxBuilder { /// and ideally should use the insecure random API otherwise, so using any /// prerecorded or otherwise predictable data may compromise security. pub fn secure_random(&mut self, random: impl RngCore + Send + 'static) -> &mut Self { - self.random = Box::new(random); + self.common.secure_random(random); self } @@ -372,7 +337,7 @@ impl WasiCtxBuilder { /// The `insecure_random` generator provided will be used for all randomness /// requested by the `wasi:random/insecure` interface. pub fn insecure_random(&mut self, insecure_random: impl RngCore + Send + 'static) -> &mut Self { - self.insecure_random = Box::new(insecure_random); + self.common.insecure_random(insecure_random); self } @@ -381,7 +346,7 @@ impl WasiCtxBuilder { /// /// By default this number is randomly generated when a builder is created. pub fn insecure_random_seed(&mut self, insecure_random_seed: u128) -> &mut Self { - self.insecure_random_seed = insecure_random_seed; + self.common.insecure_random_seed(insecure_random_seed); self } @@ -389,7 +354,7 @@ impl WasiCtxBuilder { /// /// By default the host's wall clock is used. pub fn wall_clock(&mut self, clock: impl HostWallClock + 'static) -> &mut Self { - self.wall_clock = Box::new(clock); + self.common.wall_clock(clock); self } @@ -397,7 +362,7 @@ impl WasiCtxBuilder { /// /// By default the host's monotonic clock is used. pub fn monotonic_clock(&mut self, clock: impl HostMonotonicClock + 'static) -> &mut Self { - self.monotonic_clock = Box::new(clock); + self.common.monotonic_clock(clock); self } @@ -409,7 +374,8 @@ impl WasiCtxBuilder { /// /// See also [`WasiCtxBuilder::socket_addr_check`]. pub fn inherit_network(&mut self) -> &mut Self { - self.socket_addr_check(|_, _| Box::pin(async { true })) + self.common.inherit_network(); + self } /// A check that will be called for each socket address that is used. @@ -423,7 +389,7 @@ impl WasiCtxBuilder { + Sync + 'static, { - self.socket_addr_check = SocketAddrCheck(Arc::new(check)); + self.common.socket_addr_check(check); self } @@ -431,7 +397,7 @@ impl WasiCtxBuilder { /// /// By default this is disabled. pub fn allow_ip_name_lookup(&mut self, enable: bool) -> &mut Self { - self.allowed_network_uses.ip_name_lookup = enable; + self.common.allow_ip_name_lookup(enable); self } @@ -440,7 +406,7 @@ impl WasiCtxBuilder { /// This is enabled by default, but can be disabled if UDP should be blanket /// disabled. pub fn allow_udp(&mut self, enable: bool) -> &mut Self { - self.allowed_network_uses.udp = enable; + self.common.allow_udp(enable); self } @@ -449,7 +415,7 @@ impl WasiCtxBuilder { /// This is enabled by default, but can be disabled if TCP should be blanket /// disabled. pub fn allow_tcp(&mut self, enable: bool) -> &mut Self { - self.allowed_network_uses.tcp = enable; + self.common.allow_tcp(enable); self } @@ -467,20 +433,23 @@ impl WasiCtxBuilder { assert!(!self.built); let Self { + common: + crate::WasiCtxBuilder { + env, + args, + socket_addr_check, + random, + insecure_random, + insecure_random_seed, + wall_clock, + monotonic_clock, + allowed_network_uses, + allow_blocking_current_thread, + }, stdin, stdout, stderr, - env, - args, preopens, - socket_addr_check, - random, - insecure_random, - insecure_random_seed, - wall_clock, - monotonic_clock, - allowed_network_uses, - allow_blocking_current_thread, built: _, } = mem::replace(self, Self::new()); self.built = true; @@ -593,43 +562,3 @@ impl WasiCtx { WasiCtxBuilder::new() } } - -pub struct AllowedNetworkUses { - pub ip_name_lookup: bool, - pub udp: bool, - pub tcp: bool, -} - -impl Default for AllowedNetworkUses { - fn default() -> Self { - Self { - ip_name_lookup: false, - udp: true, - tcp: true, - } - } -} - -impl AllowedNetworkUses { - pub(crate) fn check_allowed_udp(&self) -> std::io::Result<()> { - if !self.udp { - return Err(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - "UDP is not allowed", - )); - } - - Ok(()) - } - - pub(crate) fn check_allowed_tcp(&self) -> std::io::Result<()> { - if !self.tcp { - return Err(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - "TCP is not allowed", - )); - } - - Ok(()) - } -} From ef81b5d138b22f48201027981704f2e2038ebafd Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Fri, 11 Jul 2025 15:29:13 +0200 Subject: [PATCH 03/10] chore: vendor p3 WIT Signed-off-by: Roman Volosatovs --- ci/vendor-wit.sh | 8 + crates/wasi/src/p3/wit/deps/cli/command.wit | 10 + .../wasi/src/p3/wit/deps/cli/environment.wit | 22 + crates/wasi/src/p3/wit/deps/cli/exit.wit | 17 + crates/wasi/src/p3/wit/deps/cli/imports.wit | 34 + crates/wasi/src/p3/wit/deps/cli/run.wit | 6 + crates/wasi/src/p3/wit/deps/cli/stdio.wit | 17 + crates/wasi/src/p3/wit/deps/cli/terminal.wit | 62 ++ .../p3/wit/deps/clocks/monotonic-clock.wit | 45 ++ .../wasi/src/p3/wit/deps/clocks/timezone.wit | 55 ++ .../src/p3/wit/deps/clocks/wall-clock.wit | 46 ++ crates/wasi/src/p3/wit/deps/clocks/world.wit | 11 + .../src/p3/wit/deps/filesystem/preopens.wit | 11 + .../wasi/src/p3/wit/deps/filesystem/types.wit | 629 +++++++++++++++ .../wasi/src/p3/wit/deps/filesystem/world.wit | 9 + .../src/p3/wit/deps/random/insecure-seed.wit | 27 + .../wasi/src/p3/wit/deps/random/insecure.wit | 25 + crates/wasi/src/p3/wit/deps/random/random.wit | 29 + crates/wasi/src/p3/wit/deps/random/world.wit | 13 + .../p3/wit/deps/sockets/ip-name-lookup.wit | 62 ++ crates/wasi/src/p3/wit/deps/sockets/types.wit | 725 ++++++++++++++++++ crates/wasi/src/p3/wit/deps/sockets/world.wit | 9 + crates/wasi/src/p3/wit/package.wit | 2 + 23 files changed, 1874 insertions(+) create mode 100644 crates/wasi/src/p3/wit/deps/cli/command.wit create mode 100644 crates/wasi/src/p3/wit/deps/cli/environment.wit create mode 100644 crates/wasi/src/p3/wit/deps/cli/exit.wit create mode 100644 crates/wasi/src/p3/wit/deps/cli/imports.wit create mode 100644 crates/wasi/src/p3/wit/deps/cli/run.wit create mode 100644 crates/wasi/src/p3/wit/deps/cli/stdio.wit create mode 100644 crates/wasi/src/p3/wit/deps/cli/terminal.wit create mode 100644 crates/wasi/src/p3/wit/deps/clocks/monotonic-clock.wit create mode 100644 crates/wasi/src/p3/wit/deps/clocks/timezone.wit create mode 100644 crates/wasi/src/p3/wit/deps/clocks/wall-clock.wit create mode 100644 crates/wasi/src/p3/wit/deps/clocks/world.wit create mode 100644 crates/wasi/src/p3/wit/deps/filesystem/preopens.wit create mode 100644 crates/wasi/src/p3/wit/deps/filesystem/types.wit create mode 100644 crates/wasi/src/p3/wit/deps/filesystem/world.wit create mode 100644 crates/wasi/src/p3/wit/deps/random/insecure-seed.wit create mode 100644 crates/wasi/src/p3/wit/deps/random/insecure.wit create mode 100644 crates/wasi/src/p3/wit/deps/random/random.wit create mode 100644 crates/wasi/src/p3/wit/deps/random/world.wit create mode 100644 crates/wasi/src/p3/wit/deps/sockets/ip-name-lookup.wit create mode 100644 crates/wasi/src/p3/wit/deps/sockets/types.wit create mode 100644 crates/wasi/src/p3/wit/deps/sockets/world.wit create mode 100644 crates/wasi/src/p3/wit/package.wit diff --git a/ci/vendor-wit.sh b/ci/vendor-wit.sh index 54a276e479c8..0c98410245cd 100755 --- a/ci/vendor-wit.sh +++ b/ci/vendor-wit.sh @@ -68,6 +68,14 @@ make_vendor "wasi-config" "config@f4d699b" make_vendor "wasi-keyvalue" "keyvalue@219ea36" +make_vendor "wasi/src/p3" " + cli@939bd6d@wit-0.3.0-draft + clocks@13d1c82@wit-0.3.0-draft + filesystem@e2a2ddc@wit-0.3.0-draft + random@4e94663@wit-0.3.0-draft + sockets@bb247e2@wit-0.3.0-draft +" + rm -rf $cache_dir # Separately (for now), vendor the `wasi-nn` WIT files since their retrieval is diff --git a/crates/wasi/src/p3/wit/deps/cli/command.wit b/crates/wasi/src/p3/wit/deps/cli/command.wit new file mode 100644 index 000000000000..0310e515148b --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/command.wit @@ -0,0 +1,10 @@ +package wasi:cli@0.3.0; + +@since(version = 0.3.0) +world command { + @since(version = 0.3.0) + include imports; + + @since(version = 0.3.0) + export run; +} diff --git a/crates/wasi/src/p3/wit/deps/cli/environment.wit b/crates/wasi/src/p3/wit/deps/cli/environment.wit new file mode 100644 index 000000000000..d99dcc0ae395 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/environment.wit @@ -0,0 +1,22 @@ +@since(version = 0.3.0) +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + @since(version = 0.3.0) + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + @since(version = 0.3.0) + get-arguments: func() -> list; + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + @since(version = 0.3.0) + initial-cwd: func() -> option; +} diff --git a/crates/wasi/src/p3/wit/deps/cli/exit.wit b/crates/wasi/src/p3/wit/deps/cli/exit.wit new file mode 100644 index 000000000000..e799a95a26e1 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/exit.wit @@ -0,0 +1,17 @@ +@since(version = 0.3.0) +interface exit { + /// Exit the current instance and any linked instances. + @since(version = 0.3.0) + exit: func(status: result); + + /// Exit the current instance and any linked instances, reporting the + /// specified status code to the host. + /// + /// The meaning of the code depends on the context, with 0 usually meaning + /// "success", and other values indicating various types of failure. + /// + /// This function does not return; the effect is analogous to a trap, but + /// without the connotation that something bad has happened. + @unstable(feature = cli-exit-with-code) + exit-with-code: func(status-code: u8); +} diff --git a/crates/wasi/src/p3/wit/deps/cli/imports.wit b/crates/wasi/src/p3/wit/deps/cli/imports.wit new file mode 100644 index 000000000000..5dbc2ede8de6 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/imports.wit @@ -0,0 +1,34 @@ +package wasi:cli@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + include wasi:clocks/imports@0.3.0; + @since(version = 0.3.0) + include wasi:filesystem/imports@0.3.0; + @since(version = 0.3.0) + include wasi:sockets/imports@0.3.0; + @since(version = 0.3.0) + include wasi:random/imports@0.3.0; + + @since(version = 0.3.0) + import environment; + @since(version = 0.3.0) + import exit; + @since(version = 0.3.0) + import stdin; + @since(version = 0.3.0) + import stdout; + @since(version = 0.3.0) + import stderr; + @since(version = 0.3.0) + import terminal-input; + @since(version = 0.3.0) + import terminal-output; + @since(version = 0.3.0) + import terminal-stdin; + @since(version = 0.3.0) + import terminal-stdout; + @since(version = 0.3.0) + import terminal-stderr; +} diff --git a/crates/wasi/src/p3/wit/deps/cli/run.wit b/crates/wasi/src/p3/wit/deps/cli/run.wit new file mode 100644 index 000000000000..6dd8b6879e74 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/run.wit @@ -0,0 +1,6 @@ +@since(version = 0.3.0) +interface run { + /// Run the program. + @since(version = 0.3.0) + run: func() -> result; +} diff --git a/crates/wasi/src/p3/wit/deps/cli/stdio.wit b/crates/wasi/src/p3/wit/deps/cli/stdio.wit new file mode 100644 index 000000000000..6a1208fad777 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/stdio.wit @@ -0,0 +1,17 @@ +@since(version = 0.3.0) +interface stdin { + @since(version = 0.3.0) + get-stdin: func() -> stream; +} + +@since(version = 0.3.0) +interface stdout { + @since(version = 0.3.0) + set-stdout: func(data: stream); +} + +@since(version = 0.3.0) +interface stderr { + @since(version = 0.3.0) + set-stderr: func(data: stream); +} diff --git a/crates/wasi/src/p3/wit/deps/cli/terminal.wit b/crates/wasi/src/p3/wit/deps/cli/terminal.wit new file mode 100644 index 000000000000..c37184f4c72a --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/cli/terminal.wit @@ -0,0 +1,62 @@ +/// Terminal input. +/// +/// In the future, this may include functions for disabling echoing, +/// disabling input buffering so that keyboard events are sent through +/// immediately, querying supported features, and so on. +@since(version = 0.3.0) +interface terminal-input { + /// The input side of a terminal. + @since(version = 0.3.0) + resource terminal-input; +} + +/// Terminal output. +/// +/// In the future, this may include functions for querying the terminal +/// size, being notified of terminal size changes, querying supported +/// features, and so on. +@since(version = 0.3.0) +interface terminal-output { + /// The output side of a terminal. + @since(version = 0.3.0) + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +@since(version = 0.3.0) +interface terminal-stdin { + @since(version = 0.3.0) + use terminal-input.{terminal-input}; + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + @since(version = 0.3.0) + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +@since(version = 0.3.0) +interface terminal-stdout { + @since(version = 0.3.0) + use terminal-output.{terminal-output}; + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.3.0) + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +@since(version = 0.3.0) +interface terminal-stderr { + @since(version = 0.3.0) + use terminal-output.{terminal-output}; + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.3.0) + get-terminal-stderr: func() -> option; +} diff --git a/crates/wasi/src/p3/wit/deps/clocks/monotonic-clock.wit b/crates/wasi/src/p3/wit/deps/clocks/monotonic-clock.wit new file mode 100644 index 000000000000..809fc446ea9a --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/clocks/monotonic-clock.wit @@ -0,0 +1,45 @@ +package wasi:clocks@0.3.0; +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +@since(version = 0.3.0) +interface monotonic-clock { + /// An instant in time, in nanoseconds. An instant is relative to an + /// unspecified initial value, and can only be compared to instances from + /// the same monotonic-clock. + @since(version = 0.3.0) + type instant = u64; + + /// A duration of time, in nanoseconds. + @since(version = 0.3.0) + type duration = u64; + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + @since(version = 0.3.0) + now: func() -> instant; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + @since(version = 0.3.0) + resolution: func() -> duration; + + /// Wait until the specified instant has occurred. + @since(version = 0.3.0) + wait-until: async func( + when: instant, + ); + + /// Wait for the specified duration has elapsed. + @since(version = 0.3.0) + wait-for: async func( + how-long: duration, + ); +} diff --git a/crates/wasi/src/p3/wit/deps/clocks/timezone.wit b/crates/wasi/src/p3/wit/deps/clocks/timezone.wit new file mode 100644 index 000000000000..ac9146834f80 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/clocks/timezone.wit @@ -0,0 +1,55 @@ +package wasi:clocks@0.3.0; + +@unstable(feature = clocks-timezone) +interface timezone { + @unstable(feature = clocks-timezone) + use wall-clock.{datetime}; + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + @unstable(feature = clocks-timezone) + display: func(when: datetime) -> timezone-display; + + /// The same as `display`, but only return the UTC offset. + @unstable(feature = clocks-timezone) + utc-offset: func(when: datetime) -> s32; + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + @unstable(feature = clocks-timezone) + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } +} diff --git a/crates/wasi/src/p3/wit/deps/clocks/wall-clock.wit b/crates/wasi/src/p3/wit/deps/clocks/wall-clock.wit new file mode 100644 index 000000000000..b7a85ab35636 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/clocks/wall-clock.wit @@ -0,0 +1,46 @@ +package wasi:clocks@0.3.0; +/// WASI Wall Clock is a clock API intended to let users query the current +/// time. The name "wall" makes an analogy to a "clock on the wall", which +/// is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A wall clock is a clock which measures the date and time according to +/// some external reference. +/// +/// External references may be reset, so this clock is not necessarily +/// monotonic, making it unsuitable for measuring elapsed time. +/// +/// It is intended for reporting the current date and time for humans. +@since(version = 0.3.0) +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + @since(version = 0.3.0) + record datetime { + seconds: u64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The returned timestamps represent the number of seconds since + /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + /// also known as [Unix Time]. + /// + /// The nanoseconds field of the output is always less than 1000000000. + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + @since(version = 0.3.0) + now: func() -> datetime; + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + @since(version = 0.3.0) + resolution: func() -> datetime; +} diff --git a/crates/wasi/src/p3/wit/deps/clocks/world.wit b/crates/wasi/src/p3/wit/deps/clocks/world.wit new file mode 100644 index 000000000000..f97bcfef13b2 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/clocks/world.wit @@ -0,0 +1,11 @@ +package wasi:clocks@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + import monotonic-clock; + @since(version = 0.3.0) + import wall-clock; + @unstable(feature = clocks-timezone) + import timezone; +} diff --git a/crates/wasi/src/p3/wit/deps/filesystem/preopens.wit b/crates/wasi/src/p3/wit/deps/filesystem/preopens.wit new file mode 100644 index 000000000000..0b29aae33448 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/filesystem/preopens.wit @@ -0,0 +1,11 @@ +package wasi:filesystem@0.3.0; + +@since(version = 0.3.0) +interface preopens { + @since(version = 0.3.0) + use types.{descriptor}; + + /// Return the set of preopened directories, and their paths. + @since(version = 0.3.0) + get-directories: func() -> list>; +} diff --git a/crates/wasi/src/p3/wit/deps/filesystem/types.wit b/crates/wasi/src/p3/wit/deps/filesystem/types.wit new file mode 100644 index 000000000000..af3cb254cc76 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/filesystem/types.wit @@ -0,0 +1,629 @@ +package wasi:filesystem@0.3.0; +/// WASI filesystem is a filesystem API primarily intended to let users run WASI +/// programs that access their files on their existing filesystems, without +/// significant overhead. +/// +/// It is intended to be roughly portable between Unix-family platforms and +/// Windows, though it does not hide many of the major differences. +/// +/// Paths are passed as interface-type `string`s, meaning they must consist of +/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain +/// paths which are not accessible by this API. +/// +/// The directory separator in WASI is always the forward-slash (`/`). +/// +/// All paths in WASI are relative paths, and are interpreted relative to a +/// `descriptor` referring to a base directory. If a `path` argument to any WASI +/// function starts with `/`, or if any step of resolving a `path`, including +/// `..` and symbolic link steps, reaches a directory outside of the base +/// directory, or reaches a symlink to an absolute or rooted path in the +/// underlying filesystem, the function fails with `error-code::not-permitted`. +/// +/// For more information about WASI path resolution and sandboxing, see +/// [WASI filesystem path resolution]. +/// +/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +@since(version = 0.3.0) +interface types { + @since(version = 0.3.0) + use wasi:clocks/wall-clock@0.3.0.{datetime}; + + /// File size or length of a region within a file. + @since(version = 0.3.0) + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + @since(version = 0.3.0) + enum descriptor-type { + /// The type of the descriptor or file is unknown or is different from + /// any of the other types specified. + unknown, + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + @since(version = 0.3.0) + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrity + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + @since(version = 0.3.0) + record descriptor-stat { + /// File type. + %type: descriptor-type, + /// Number of hard links to the file. + link-count: link-count, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + size: filesize, + /// Last data access timestamp. + /// + /// If the `option` is none, the platform doesn't maintain an access + /// timestamp for this file. + data-access-timestamp: option, + /// Last data modification timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// modification timestamp for this file. + data-modification-timestamp: option, + /// Last file status-change timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// status-change timestamp for this file. + status-change-timestamp: option, + } + + /// Flags determining the method of how paths are resolved. + @since(version = 0.3.0) + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + @since(version = 0.3.0) + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Number of hard links to an inode. + @since(version = 0.3.0) + type link-count = u64; + + /// When setting a timestamp, this gives the value to set it to. + @since(version = 0.3.0) + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(datetime), + } + + /// A directory entry. + record directory-entry { + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// Not all of these error codes are returned by the functions provided by this + /// API; some are used in higher-level library layers, and others are provided + /// merely for alignment with POSIX. + enum error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + } + + /// File or memory access pattern advisory information. + @since(version = 0.3.0) + enum advice { + /// The application has no advice to give on its behavior with respect + /// to the specified data. + normal, + /// The application expects to access the specified data sequentially + /// from lower offsets to higher offsets. + sequential, + /// The application expects to access the specified data in a random + /// order. + random, + /// The application expects to access the specified data in the near + /// future. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A 128-bit hash value, split into parts because wasm doesn't have a + /// 128-bit integer type. + @since(version = 0.3.0) + record metadata-hash-value { + /// 64 bits of a 128-bit hash value. + lower: u64, + /// Another 64 bits of a 128-bit hash value. + upper: u64, + } + + /// A descriptor is a reference to a filesystem object, which may be a file, + /// directory, named pipe, special file, or other object on which filesystem + /// calls may be made. + @since(version = 0.3.0) + resource descriptor { + /// Return a stream for reading from a file. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// This function returns a future, which will resolve to an error code if + /// reading full contents of the file fails. + /// + /// Note: This is similar to `pread` in POSIX. + @since(version = 0.3.0) + read-via-stream: func( + /// The offset within the file at which to start reading. + offset: filesize, + ) -> tuple, future>>; + + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `pwrite` in POSIX. + @since(version = 0.3.0) + write-via-stream: func( + /// Data to write + data: stream, + /// The offset within the file at which to start writing. + offset: filesize, + ) -> result<_, error-code>; + + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `write` with `O_APPEND` in POSIX. + @since(version = 0.3.0) + append-via-stream: func(data: stream) -> result<_, error-code>; + + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + @since(version = 0.3.0) + advise: func( + /// The offset within the file to which the advisory applies. + offset: filesize, + /// The length of the region to which the advisory applies. + length: filesize, + /// The advice. + advice: advice + ) -> result<_, error-code>; + + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + @since(version = 0.3.0) + sync-data: func() -> result<_, error-code>; + + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.3.0) + get-flags: func() -> result; + + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.3.0) + get-type: func() -> result; + + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + @since(version = 0.3.0) + set-size: func(size: filesize) -> result<_, error-code>; + + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + @since(version = 0.3.0) + set-times: func( + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + /// + /// This function returns a future, which will resolve to an error code if + /// reading full contents of the directory fails. + @since(version = 0.3.0) + read-directory: func() -> tuple, future>>; + + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + @since(version = 0.3.0) + sync: func() -> result<_, error-code>; + + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + @since(version = 0.3.0) + create-directory-at: func( + /// The relative path at which to create the directory. + path: string, + ) -> result<_, error-code>; + + /// Return the attributes of an open file or directory. + /// + /// Note: This is similar to `fstat` in POSIX, except that it does not return + /// device and inode information. For testing whether two descriptors refer to + /// the same underlying filesystem object, use `is-same-object`. To obtain + /// additional data that can be used do determine whether a file has been + /// modified, use `metadata-hash`. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + @since(version = 0.3.0) + stat: func() -> result; + + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX, except that it does not + /// return device and inode information. See the `stat` description for a + /// discussion of alternatives. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + @since(version = 0.3.0) + stat-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + @since(version = 0.3.0) + set-times-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to operate on. + path: string, + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Create a hard link. + /// + /// Fails with `error-code::no-entry` if the old path does not exist, + /// with `error-code::exist` if the new path already exists, and + /// `error-code::not-permitted` if the old path is not a file. + /// + /// Note: This is similar to `linkat` in POSIX. + @since(version = 0.3.0) + link-at: func( + /// Flags determining the method of how the path is resolved. + old-path-flags: path-flags, + /// The relative source path from which to link. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path at which to create the hard link. + new-path: string, + ) -> result<_, error-code>; + + /// Open a file or directory. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + @since(version = 0.3.0) + open-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the object to open. + path: string, + /// The method by which to open the file. + open-flags: open-flags, + /// Flags to use for the resulting descriptor. + %flags: descriptor-flags, + ) -> result; + + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + @since(version = 0.3.0) + readlink-at: func( + /// The relative path of the symbolic link from which to read. + path: string, + ) -> result; + + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + @since(version = 0.3.0) + remove-directory-at: func( + /// The relative path to a directory to remove. + path: string, + ) -> result<_, error-code>; + + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + @since(version = 0.3.0) + rename-at: func( + /// The relative source path of the file or directory to rename. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path to which to rename the file or directory. + new-path: string, + ) -> result<_, error-code>; + + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + @since(version = 0.3.0) + symlink-at: func( + /// The contents of the symbolic link. + old-path: string, + /// The relative destination path at which to create the symbolic link. + new-path: string, + ) -> result<_, error-code>; + + /// Unlink a filesystem object that is not a directory. + /// + /// Return `error-code::is-directory` if the path refers to a directory. + /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + @since(version = 0.3.0) + unlink-file-at: func( + /// The relative path to a file to unlink. + path: string, + ) -> result<_, error-code>; + + /// Test whether two descriptors refer to the same filesystem object. + /// + /// In POSIX, this corresponds to testing whether the two descriptors have the + /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. + /// wasi-filesystem does not expose device and inode numbers, so this function + /// may be used instead. + @since(version = 0.3.0) + is-same-object: func(other: borrow) -> bool; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encouraged to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + @since(version = 0.3.0) + metadata-hash: func() -> result; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a directory descriptor and a relative path. + /// + /// This performs the same hash computation as `metadata-hash`. + @since(version = 0.3.0) + metadata-hash-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + } +} diff --git a/crates/wasi/src/p3/wit/deps/filesystem/world.wit b/crates/wasi/src/p3/wit/deps/filesystem/world.wit new file mode 100644 index 000000000000..c0ab32afe295 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/filesystem/world.wit @@ -0,0 +1,9 @@ +package wasi:filesystem@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + import types; + @since(version = 0.3.0) + import preopens; +} diff --git a/crates/wasi/src/p3/wit/deps/random/insecure-seed.wit b/crates/wasi/src/p3/wit/deps/random/insecure-seed.wit new file mode 100644 index 000000000000..4708d9049359 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/random/insecure-seed.wit @@ -0,0 +1,27 @@ +package wasi:random@0.3.0; +/// The insecure-seed interface for seeding hash-map DoS resistance. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0) +interface insecure-seed { + /// Return a 128-bit value that may contain a pseudo-random value. + /// + /// The returned value is not required to be computed from a CSPRNG, and may + /// even be entirely deterministic. Host implementations are encouraged to + /// provide pseudo-random values to any program exposed to + /// attacker-controlled content, to enable DoS protection built into many + /// languages' hash-map implementations. + /// + /// This function is intended to only be called once, by a source language + /// to initialize Denial Of Service (DoS) protection in its hash-map + /// implementation. + /// + /// # Expected future evolution + /// + /// This will likely be changed to a value import, to prevent it from being + /// called multiple times and potentially used for purposes other than DoS + /// protection. + @since(version = 0.3.0) + insecure-seed: func() -> tuple; +} diff --git a/crates/wasi/src/p3/wit/deps/random/insecure.wit b/crates/wasi/src/p3/wit/deps/random/insecure.wit new file mode 100644 index 000000000000..4ea5e581fd2d --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/random/insecure.wit @@ -0,0 +1,25 @@ +package wasi:random@0.3.0; +/// The insecure interface for insecure pseudo-random numbers. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0) +interface insecure { + /// Return `len` insecure pseudo-random bytes. + /// + /// This function is not cryptographically secure. Do not use it for + /// anything related to security. + /// + /// There are no requirements on the values of the returned bytes, however + /// implementations are encouraged to return evenly distributed values with + /// a long period. + @since(version = 0.3.0) + get-insecure-random-bytes: func(len: u64) -> list; + + /// Return an insecure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-insecure-random-bytes`, represented as a `u64`. + @since(version = 0.3.0) + get-insecure-random-u64: func() -> u64; +} diff --git a/crates/wasi/src/p3/wit/deps/random/random.wit b/crates/wasi/src/p3/wit/deps/random/random.wit new file mode 100644 index 000000000000..786ef25f68cf --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/random/random.wit @@ -0,0 +1,29 @@ +package wasi:random@0.3.0; +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0) +interface random { + /// Return `len` cryptographically-secure random or pseudo-random bytes. + /// + /// This function must produce data at least as cryptographically secure and + /// fast as an adequately seeded cryptographically-secure pseudo-random + /// number generator (CSPRNG). It must not block, from the perspective of + /// the calling program, under any circumstances, including on the first + /// request and on requests for numbers of bytes. The returned data must + /// always be unpredictable. + /// + /// This function must always return fresh data. Deterministic environments + /// must omit this function, rather than implementing it with deterministic + /// data. + @since(version = 0.3.0) + get-random-bytes: func(len: u64) -> list; + + /// Return a cryptographically-secure random or pseudo-random `u64` value. + /// + /// This function returns the same type of data as `get-random-bytes`, + /// represented as a `u64`. + @since(version = 0.3.0) + get-random-u64: func() -> u64; +} diff --git a/crates/wasi/src/p3/wit/deps/random/world.wit b/crates/wasi/src/p3/wit/deps/random/world.wit new file mode 100644 index 000000000000..838d38023cfe --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/random/world.wit @@ -0,0 +1,13 @@ +package wasi:random@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + import random; + + @since(version = 0.3.0) + import insecure; + + @since(version = 0.3.0) + import insecure-seed; +} diff --git a/crates/wasi/src/p3/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi/src/p3/wit/deps/sockets/ip-name-lookup.wit new file mode 100644 index 000000000000..7cc8b03e35f2 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/sockets/ip-name-lookup.wit @@ -0,0 +1,62 @@ +@since(version = 0.3.0) +interface ip-name-lookup { + @since(version = 0.3.0) + use types.{ip-address}; + + /// Lookup error codes. + @since(version = 0.3.0) + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// `name` is a syntactically invalid domain name or IP address. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Name does not exist or has no suitable associated IP addresses. + /// + /// POSIX equivalent: EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY + name-unresolvable, + + /// A temporary failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_AGAIN + temporary-resolver-failure, + + /// A permanent failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_FAIL + permanent-resolver-failure, + } + + /// Resolve an internet host name to a list of IP addresses. + /// + /// Unicode domain names are automatically converted to ASCII using IDNA encoding. + /// If the input is an IP address string, the address is parsed and returned + /// as-is without making any external requests. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// The results are returned in connection order preference. + /// + /// This function never succeeds with 0 results. It either fails or succeeds + /// with at least one address. Additionally, this function never returns + /// IPv4-mapped IPv6 addresses. + /// + /// The returned future will resolve to an error code in case of failure. + /// It will resolve to success once the returned stream is exhausted. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + resolve-addresses: func(name: string) -> result, error-code>; +} diff --git a/crates/wasi/src/p3/wit/deps/sockets/types.wit b/crates/wasi/src/p3/wit/deps/sockets/types.wit new file mode 100644 index 000000000000..156cc502675c --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/sockets/types.wit @@ -0,0 +1,725 @@ +@since(version = 0.3.0) +interface types { + @since(version = 0.3.0) + use wasi:clocks/monotonic-clock@0.3.0.{duration}; + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `unknown` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + @since(version = 0.3.0) + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP + not-supported, + + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY + out-of-memory, + + /// The operation timed out before it could finish completely. + timeout, + + /// The operation is not valid in the socket's current state. + invalid-state, + + /// A bind operation failed because the provided address is not an address that the `network` can bind to. + address-not-bindable, + + /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. + address-in-use, + + /// The remote address is not reachable + remote-unreachable, + + + /// The TCP connection was forcefully rejected + connection-refused, + + /// The TCP connection was reset. + connection-reset, + + /// A TCP connection was aborted. + connection-aborted, + + + /// The size of a datagram sent to a UDP socket exceeded the maximum + /// supported size. + datagram-too-large, + } + + @since(version = 0.3.0) + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + @since(version = 0.3.0) + type ipv4-address = tuple; + @since(version = 0.3.0) + type ipv6-address = tuple; + + @since(version = 0.3.0) + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + @since(version = 0.3.0) + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + @since(version = 0.3.0) + record ipv6-socket-address { + /// sin6_port + port: u16, + /// sin6_flowinfo + flow-info: u32, + /// sin6_addr + address: ipv6-address, + /// sin6_scope_id + scope-id: u32, + } + + @since(version = 0.3.0) + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bound` (See note below) + /// - `listening` + /// - `connecting` + /// - `connected` + /// - `closed` + /// See + /// for more information. + /// + /// Note: Except where explicitly mentioned, whenever this documentation uses + /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. + /// (i.e. `bound`, `listening`, `connecting` or `connected`) + /// + /// In addition to the general error codes documented on the + /// `types::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + @since(version = 0.3.0) + resource tcp-socket { + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + constructor(address-family: ip-address-family); + + /// Bind the socket to the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// Bind can be attempted multiple times on the same socket, even with + /// different arguments on each iteration. But never concurrently and + /// only as long as the previous bind failed. Once a bind succeeds, the + /// binding can't be changed anymore. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that can be bound to. (EADDRNOTAVAIL) + /// + /// # Implementors note + /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT + /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR + /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior + /// and SO_REUSEADDR performs something different entirely. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + + /// Connect to a remote endpoint. + /// + /// On success, the socket is transitioned into the `connected` state and this function returns a connection resource. + /// + /// After a failed connection attempt, the socket will be in the `closed` + /// state and the only valid action left is to `drop` the socket. A single + /// socket can not be used to connect more than once. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-state`: The socket is already in the `connecting` state. (EALREADY) + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + connect: func(remote-address: ip-socket-address) -> result<_, error-code>; + + /// Start listening return a stream of new inbound connections. + /// + /// Transitions the socket into the `listening` state. This can be called + /// at most once per socket. + /// + /// If the socket is not already explicitly bound, this function will + /// implicitly bind the socket to a random free port. + /// + /// Normally, the returned sockets are bound, in the `connected` state + /// and immediately ready for I/O. Though, depending on exact timing and + /// circumstances, a newly accepted connection may already be `closed` + /// by the time the server attempts to perform its first I/O on it. This + /// is true regardless of whether the WASI implementation uses + /// "synthesized" sockets or not (see Implementors Notes below). + /// + /// The following properties are inherited from the listener socket: + /// - `address-family` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + /// # Typical errors + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the `listening` state. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// + /// # Implementors note + /// This method returns a single perpetual stream that should only close + /// on fatal errors (if any). Yet, the POSIX' `accept` function may also + /// return transient errors (e.g. ECONNABORTED). The exact details differ + /// per operation system. For example, the Linux manual mentions: + /// + /// > Linux accept() passes already-pending network errors on the new + /// > socket as an error code from accept(). This behavior differs from + /// > other BSD socket implementations. For reliable operation the + /// > application should detect the network errors defined for the + /// > protocol after accept() and treat them like EAGAIN by retrying. + /// > In the case of TCP/IP, these are ENETDOWN, EPROTO, ENOPROTOOPT, + /// > EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP, and ENETUNREACH. + /// Source: https://man7.org/linux/man-pages/man2/accept.2.html + /// + /// WASI implementations have two options to handle this: + /// - Optionally log it and then skip over non-fatal errors returned by + /// `accept`. Guest code never gets to see these failures. Or: + /// - Synthesize a `tcp-socket` resource that exposes the error when + /// attempting to send or receive on it. Guest code then sees these + /// failures as regular I/O errors. + /// + /// In either case, the stream returned by this `listen` method remains + /// operational. + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + listen: func() -> result, error-code>; + + /// Transmit data to peer. + /// + /// The caller should close the stream when it has no more data to send + /// to the peer. Under normal circumstances this will cause a FIN packet + /// to be sent out. Closing the stream is equivalent to calling + /// `shutdown(SHUT_WR)` in POSIX. + /// + /// This function may be called at most once and returns once the full + /// contents of the stream are transmitted or an error is encountered. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + send: func(data: stream) -> result<_, error-code>; + + /// Read data from peer. + /// + /// This function returns a `stream` which provides the data received from the + /// socket, and a `future` providing additional error information in case the + /// socket is closed abnormally. + /// + /// If the socket is closed normally, `stream.read` on the `stream` will return + /// `read-status::closed` with no `error-context` and the future resolves to + /// the value `ok`. If the socket is closed abnormally, `stream.read` on the + /// `stream` returns `read-status::closed` with an `error-context` and the future + /// resolves to `err` with an `error-code`. + /// + /// `receive` is meant to be called only once per socket. If it is called more + /// than once, the subsequent calls return a new `stream` that fails as if it + /// were closed abnormally. + /// + /// If the caller is not expecting to receive any data from the peer, + /// they may drop the stream. Any data still in the receive queue + /// will be discarded. This is equivalent to calling `shutdown(SHUT_RD)` + /// in POSIX. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + receive: func() -> tuple, future>>; + + /// Get the bound local address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + local-address: func() -> result; + + /// Get the remote address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + remote-address: func() -> result; + + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + @since(version = 0.3.0) + is-listening: func() -> bool; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0) + address-family: func() -> ip-address-family; + + /// Hints the desired listen queue size. Implementations are free to ignore this. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// + /// # Typical errors + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `invalid-argument`: (set) The provided value was 0. + /// - `invalid-state`: (set) The socket is in the `connecting` or `connected` state. + @since(version = 0.3.0) + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + + /// Enables or disables keepalive. + /// + /// The keepalive behavior can be adjusted using: + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. + /// + /// Equivalent to the SO_KEEPALIVE socket option. + @since(version = 0.3.0) + keep-alive-enabled: func() -> result; + @since(version = 0.3.0) + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + + /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + keep-alive-idle-time: func() -> result; + @since(version = 0.3.0) + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + + /// The time between keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPINTVL socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + keep-alive-interval: func() -> result; + @since(version = 0.3.0) + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + + /// The maximum amount of keepalive packets TCP should send before aborting the connection. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPCNT socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + keep-alive-count: func() -> result; + @since(version = 0.3.0) + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.3.0) + hop-limit: func() -> result; + @since(version = 0.3.0) + set-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + receive-buffer-size: func() -> result; + @since(version = 0.3.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0) + send-buffer-size: func() -> result; + @since(version = 0.3.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } + + /// A UDP socket handle. + @since(version = 0.3.0) + resource udp-socket { + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + constructor(address-family: ip-address-family); + + /// Bind the socket to the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the port is zero, the socket will be bound to a random free port. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that can be bound to. (EADDRNOTAVAIL) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + + /// Associate this socket with a specific peer address. + /// + /// On success, the `remote-address` of the socket is updated. + /// The `local-address` may be updated as well, based on the best network + /// path to `remote-address`. If the socket was not already explicitly + /// bound, this function will implicitly bind the socket to a random + /// free port. + /// + /// When a UDP socket is "connected", the `send` and `receive` methods + /// are limited to communicating with that peer only: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// The name "connect" was kept to align with the existing POSIX + /// terminology. Other than that, this function only changes the local + /// socket configuration and does not generate any network traffic. + /// The peer is not aware of this "connection". + /// + /// This method may be called multiple times on the same socket to change + /// its association, but only the most recent one will be effective. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// + /// # Implementors note + /// If the socket is already connected, some platforms (e.g. Linux) + /// require a disconnect before connecting to a different peer address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + connect: func(remote-address: ip-socket-address) -> result<_, error-code>; + + /// Dissociate this socket from its peer address. + /// + /// After calling this method, `send` & `receive` are free to communicate + /// with any address again. + /// + /// The POSIX equivalent of this is calling `connect` with an `AF_UNSPEC` address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + disconnect: func() -> result<_, error-code>; + + /// Send a message on the socket to a particular peer. + /// + /// If the socket is connected, the peer address may be left empty. In + /// that case this is equivalent to `send` in POSIX. Otherwise it is + /// equivalent to `sendto`. + /// + /// Additionally, if the socket is connected, a `remote-address` argument + /// _may_ be provided but then it must be identical to the address + /// passed to `connect`. + /// + /// Implementations may trap if the `data` length exceeds 64 KiB. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `connect`. (EISCONN) + /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + send: func(data: list, remote-address: option) -> result<_, error-code>; + + /// Receive a message on the socket. + /// + /// On success, the return value contains a tuple of the received data + /// and the address of the sender. Theoretical maximum length of the + /// data is 64 KiB. Though in practice, it will typically be less than + /// 1500 bytes. + /// + /// If the socket is connected, the sender address is guaranteed to + /// match the remote address passed to `connect`. + /// + /// # Typical errors + /// - `invalid-state`: The socket has not been bound yet. + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + receive: func() -> result, ip-socket-address>, error-code>; + + /// Get the current bound address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + local-address: func() -> result; + + /// Get the address the socket is currently "connected" to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not "connected" to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + remote-address: func() -> result; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0) + address-family: func() -> ip-address-family; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.3.0) + unicast-hop-limit: func() -> result; + @since(version = 0.3.0) + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + receive-buffer-size: func() -> result; + @since(version = 0.3.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0) + send-buffer-size: func() -> result; + @since(version = 0.3.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } +} diff --git a/crates/wasi/src/p3/wit/deps/sockets/world.wit b/crates/wasi/src/p3/wit/deps/sockets/world.wit new file mode 100644 index 000000000000..6c9951d1c652 --- /dev/null +++ b/crates/wasi/src/p3/wit/deps/sockets/world.wit @@ -0,0 +1,9 @@ +package wasi:sockets@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + import types; + @since(version = 0.3.0) + import ip-name-lookup; +} diff --git a/crates/wasi/src/p3/wit/package.wit b/crates/wasi/src/p3/wit/package.wit new file mode 100644 index 000000000000..74db4ae942bd --- /dev/null +++ b/crates/wasi/src/p3/wit/package.wit @@ -0,0 +1,2 @@ +// We actually don't use this; it's just to let bindgen! find the corresponding world in wit/deps. +package wasmtime:wasi; From 5f347a0fc444668528c3ac7e3ea36ecd60733465 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Fri, 11 Jul 2025 18:40:09 +0200 Subject: [PATCH 04/10] feat: begin wasip3 implementation Signed-off-by: Roman Volosatovs --- Cargo.lock | 11 +- crates/test-programs/artifacts/build.rs | 1 + .../test-programs/src/bin/p3_clocks_sleep.rs | 51 ++ .../src/bin/p3_random_imports.rs | 44 ++ crates/test-programs/src/lib.rs | 1 + crates/test-programs/src/p3/mod.rs | 19 + crates/wasi/Cargo.toml | 5 +- crates/wasi/src/clocks.rs | 125 ++++- crates/wasi/src/clocks/host.rs | 73 --- crates/wasi/src/ctx.rs | 45 +- crates/wasi/src/lib.rs | 8 +- crates/wasi/src/p2/ctx.rs | 25 +- crates/wasi/src/p3/bindings.rs | 306 +++++++++++ crates/wasi/src/p3/clocks/host.rs | 114 ++++ crates/wasi/src/p3/clocks/mod.rs | 71 +++ crates/wasi/src/p3/ctx.rs | 493 ++++++++++++++++++ crates/wasi/src/p3/filesystem/mod.rs | 17 + crates/wasi/src/p3/mod.rs | 90 ++++ crates/wasi/src/p3/random/host.rs | 47 ++ crates/wasi/src/p3/random/mod.rs | 78 +++ crates/wasi/src/p3/view.rs | 64 +++ crates/wasi/src/random.rs | 54 +- crates/wasi/tests/all/main.rs | 2 + crates/wasi/tests/all/p3/mod.rs | 99 ++++ 24 files changed, 1714 insertions(+), 129 deletions(-) create mode 100644 crates/test-programs/src/bin/p3_clocks_sleep.rs create mode 100644 crates/test-programs/src/bin/p3_random_imports.rs create mode 100644 crates/test-programs/src/p3/mod.rs delete mode 100644 crates/wasi/src/clocks/host.rs create mode 100644 crates/wasi/src/p3/bindings.rs create mode 100644 crates/wasi/src/p3/clocks/host.rs create mode 100644 crates/wasi/src/p3/clocks/mod.rs create mode 100644 crates/wasi/src/p3/ctx.rs create mode 100644 crates/wasi/src/p3/filesystem/mod.rs create mode 100644 crates/wasi/src/p3/mod.rs create mode 100644 crates/wasi/src/p3/random/host.rs create mode 100644 crates/wasi/src/p3/random/mod.rs create mode 100644 crates/wasi/src/p3/view.rs create mode 100644 crates/wasi/tests/all/p3/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 3794317ec8f0..0f243840061b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,7 +211,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -2107,15 +2107,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" diff --git a/crates/test-programs/artifacts/build.rs b/crates/test-programs/artifacts/build.rs index a7a88cb3c733..c921fc16be56 100644 --- a/crates/test-programs/artifacts/build.rs +++ b/crates/test-programs/artifacts/build.rs @@ -104,6 +104,7 @@ impl Artifacts { } let adapter = match test.name.as_str() { "reactor" => &reactor_adapter, + s if s.starts_with("p3_") => &reactor_adapter, s if s.starts_with("api_proxy") => &proxy_adapter, _ => &command_adapter, }; diff --git a/crates/test-programs/src/bin/p3_clocks_sleep.rs b/crates/test-programs/src/bin/p3_clocks_sleep.rs new file mode 100644 index 000000000000..e667d53ac2a1 --- /dev/null +++ b/crates/test-programs/src/bin/p3_clocks_sleep.rs @@ -0,0 +1,51 @@ +use core::future::Future as _; +use core::pin::pin; +use core::task::{Context, Poll, Waker}; + +use test_programs::p3::wasi::clocks::monotonic_clock; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + sleep_10ms().await; + sleep_0ms(); + sleep_backwards_in_time(); + Ok(()) + } +} + +async fn sleep_10ms() { + let dur = 10_000_000; + monotonic_clock::wait_until(monotonic_clock::now() + dur).await; + monotonic_clock::wait_for(dur).await; +} + +fn sleep_0ms() { + let mut cx = Context::from_waker(Waker::noop()); + + assert_eq!( + pin!(monotonic_clock::wait_until(monotonic_clock::now())).poll(&mut cx), + Poll::Ready(()), + "waiting until now() is ready immediately", + ); + assert_eq!( + pin!(monotonic_clock::wait_for(0)).poll(&mut cx), + Poll::Ready(()), + "waiting for 0 is ready immediately", + ); +} + +fn sleep_backwards_in_time() { + let mut cx = Context::from_waker(Waker::noop()); + + assert_eq!( + pin!(monotonic_clock::wait_until(monotonic_clock::now() - 1)).poll(&mut cx), + Poll::Ready(()), + "waiting until instant which has passed is ready immediately", + ); +} + +fn main() {} diff --git a/crates/test-programs/src/bin/p3_random_imports.rs b/crates/test-programs/src/bin/p3_random_imports.rs new file mode 100644 index 000000000000..223d5e44dcaa --- /dev/null +++ b/crates/test-programs/src/bin/p3_random_imports.rs @@ -0,0 +1,44 @@ +use test_programs::p3::wasi::random; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + // Acquired random bytes should be of the expected length. + let array = random::random::get_random_bytes(100); + assert_eq!(array.len(), 100); + + // It shouldn't take 100+ tries to get a nonzero random integer. + for i in 0.. { + if random::random::get_random_u64() == 0 { + continue; + } + assert!(i < 100); + break; + } + + // The `insecure_seed` API should return the same result each time. + let (a1, b1) = random::insecure_seed::insecure_seed(); + let (a2, b2) = random::insecure_seed::insecure_seed(); + assert_eq!(a1, a2); + assert_eq!(b1, b2); + + // Acquired random bytes should be of the expected length. + let array = random::insecure::get_insecure_random_bytes(100); + assert_eq!(array.len(), 100); + + // It shouldn't take 100+ tries to get a nonzero random integer. + for i in 0.. { + if random::insecure::get_insecure_random_u64() == 0 { + continue; + } + assert!(i < 100); + break; + } + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/lib.rs b/crates/test-programs/src/lib.rs index 65bfb7069a0e..ef06aef0590f 100644 --- a/crates/test-programs/src/lib.rs +++ b/crates/test-programs/src/lib.rs @@ -1,6 +1,7 @@ pub mod async_; pub mod http; pub mod nn; +pub mod p3; pub mod preview1; pub mod sockets; pub mod tls; diff --git a/crates/test-programs/src/p3/mod.rs b/crates/test-programs/src/p3/mod.rs new file mode 100644 index 000000000000..09b94a1498af --- /dev/null +++ b/crates/test-programs/src/p3/mod.rs @@ -0,0 +1,19 @@ +wit_bindgen::generate!({ + inline: " + package wasmtime:test; + + world testp3 { + include wasi:cli/imports@0.3.0; + + export wasi:cli/run@0.3.0; + } + ", + path: "../wasi/src/p3/wit", + world: "wasmtime:test/testp3", + default_bindings_module: "test_programs::p3", + pub_export_macro: true, + async: [ + "wasi:cli/run@0.3.0#run", + ], + generate_all +}); diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 8c4c83f465f5..0e7bd464644e 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -55,10 +55,13 @@ windows-sys = { workspace = true } rustix = { workspace = true, features = ["event", "net"] } [features] -default = [ "preview1"] +default = ["preview1"] preview1 = [ "dep:wiggle", ] +p3 = [ + "wasmtime/component-model-async", +] [[test]] name = "process_stdin" diff --git a/crates/wasi/src/clocks.rs b/crates/wasi/src/clocks.rs index 175ac1990ffc..2618655424a9 100644 --- a/crates/wasi/src/clocks.rs +++ b/crates/wasi/src/clocks.rs @@ -1,5 +1,45 @@ -pub mod host; -use cap_std::time::Duration; +use cap_std::time::{Duration, Instant, SystemClock}; +use cap_std::{AmbientAuthority, ambient_authority}; +use cap_time_ext::{MonotonicClockExt as _, SystemClockExt as _}; + +#[repr(transparent)] +pub struct WasiClocksImpl(pub T); + +impl WasiClocksView for &mut T { + fn clocks(&mut self) -> &WasiClocksCtx { + (**self).clocks() + } +} + +impl WasiClocksView for WasiClocksImpl { + fn clocks(&mut self) -> &WasiClocksCtx { + self.0.clocks() + } +} + +impl WasiClocksView for WasiClocksCtx { + fn clocks(&mut self) -> &WasiClocksCtx { + self + } +} + +pub trait WasiClocksView: Send { + fn clocks(&mut self) -> &WasiClocksCtx; +} + +pub struct WasiClocksCtx { + pub wall_clock: Box, + pub monotonic_clock: Box, +} + +impl Default for WasiClocksCtx { + fn default() -> Self { + Self { + wall_clock: wall_clock(), + monotonic_clock: monotonic_clock(), + } + } +} pub trait HostWallClock: Send { fn resolution(&self) -> Duration; @@ -10,3 +50,84 @@ pub trait HostMonotonicClock: Send { fn resolution(&self) -> u64; fn now(&self) -> u64; } + +pub struct WallClock { + /// The underlying system clock. + clock: cap_std::time::SystemClock, +} + +impl Default for WallClock { + fn default() -> Self { + Self::new(ambient_authority()) + } +} + +impl WallClock { + pub fn new(ambient_authority: AmbientAuthority) -> Self { + Self { + clock: cap_std::time::SystemClock::new(ambient_authority), + } + } +} + +impl HostWallClock for WallClock { + fn resolution(&self) -> Duration { + self.clock.resolution() + } + + fn now(&self) -> Duration { + // WASI defines wall clocks to return "Unix time". + self.clock + .now() + .duration_since(SystemClock::UNIX_EPOCH) + .unwrap() + } +} + +pub struct MonotonicClock { + /// The underlying system clock. + clock: cap_std::time::MonotonicClock, + + /// The `Instant` this clock was created. All returned times are + /// durations since that time. + initial: Instant, +} + +impl Default for MonotonicClock { + fn default() -> Self { + Self::new(ambient_authority()) + } +} + +impl MonotonicClock { + pub fn new(ambient_authority: AmbientAuthority) -> Self { + let clock = cap_std::time::MonotonicClock::new(ambient_authority); + let initial = clock.now(); + Self { clock, initial } + } +} + +impl HostMonotonicClock for MonotonicClock { + fn resolution(&self) -> u64 { + self.clock.resolution().as_nanos().try_into().unwrap() + } + + fn now(&self) -> u64 { + // Unwrap here and in `resolution` above; a `u64` is wide enough to + // hold over 584 years of nanoseconds. + self.clock + .now() + .duration_since(self.initial) + .as_nanos() + .try_into() + .unwrap() + } +} + +pub fn monotonic_clock() -> Box { + Box::new(MonotonicClock::default()) +} + +pub fn wall_clock() -> Box { + Box::new(WallClock::default()) +} diff --git a/crates/wasi/src/clocks/host.rs b/crates/wasi/src/clocks/host.rs deleted file mode 100644 index 51ed26d3cfe0..000000000000 --- a/crates/wasi/src/clocks/host.rs +++ /dev/null @@ -1,73 +0,0 @@ -use super::{HostMonotonicClock, HostWallClock}; -use cap_std::time::{Duration, Instant, SystemClock}; -use cap_std::{AmbientAuthority, ambient_authority}; -use cap_time_ext::{MonotonicClockExt, SystemClockExt}; - -pub struct WallClock { - /// The underlying system clock. - clock: cap_std::time::SystemClock, -} - -impl WallClock { - pub fn new(ambient_authority: AmbientAuthority) -> Self { - Self { - clock: cap_std::time::SystemClock::new(ambient_authority), - } - } -} - -impl HostWallClock for WallClock { - fn resolution(&self) -> Duration { - self.clock.resolution() - } - - fn now(&self) -> Duration { - // WASI defines wall clocks to return "Unix time". - self.clock - .now() - .duration_since(SystemClock::UNIX_EPOCH) - .unwrap() - } -} - -pub struct MonotonicClock { - /// The underlying system clock. - clock: cap_std::time::MonotonicClock, - - /// The `Instant` this clock was created. All returned times are - /// durations since that time. - initial: Instant, -} - -impl MonotonicClock { - pub fn new(ambient_authority: AmbientAuthority) -> Self { - let clock = cap_std::time::MonotonicClock::new(ambient_authority); - let initial = clock.now(); - Self { clock, initial } - } -} - -impl HostMonotonicClock for MonotonicClock { - fn resolution(&self) -> u64 { - self.clock.resolution().as_nanos().try_into().unwrap() - } - - fn now(&self) -> u64 { - // Unwrap here and in `resolution` above; a `u64` is wide enough to - // hold over 584 years of nanoseconds. - self.clock - .now() - .duration_since(self.initial) - .as_nanos() - .try_into() - .unwrap() - } -} - -pub fn monotonic_clock() -> Box { - Box::new(MonotonicClock::new(ambient_authority())) -} - -pub fn wall_clock() -> Box { - Box::new(WallClock::new(ambient_authority())) -} diff --git a/crates/wasi/src/ctx.rs b/crates/wasi/src/ctx.rs index fa05a901b134..4e057996bf02 100644 --- a/crates/wasi/src/ctx.rs +++ b/crates/wasi/src/ctx.rs @@ -1,8 +1,7 @@ -use crate::clocks::host::{monotonic_clock, wall_clock}; -use crate::clocks::{HostMonotonicClock, HostWallClock}; +use crate::clocks::{HostMonotonicClock, HostWallClock, WasiClocksCtx}; use crate::net::{SocketAddrCheck, SocketAddrUse}; -use crate::random; -use cap_rand::{Rng, RngCore, SeedableRng}; +use crate::random::WasiRandomCtx; +use cap_rand::RngCore; use std::future::Future; use std::net::SocketAddr; use std::pin::Pin; @@ -20,12 +19,9 @@ use std::sync::Arc; pub(crate) struct WasiCtxBuilder { pub(crate) env: Vec<(String, String)>, pub(crate) args: Vec, + pub(crate) random: WasiRandomCtx, + pub(crate) clocks: WasiClocksCtx, pub(crate) socket_addr_check: SocketAddrCheck, - pub(crate) random: Box, - pub(crate) insecure_random: Box, - pub(crate) insecure_random_seed: u128, - pub(crate) wall_clock: Box, - pub(crate) monotonic_clock: Box, pub(crate) allowed_network_uses: AllowedNetworkUses, pub(crate) allow_blocking_current_thread: bool, } @@ -49,27 +45,14 @@ impl WasiCtxBuilder { /// These defaults can all be updated via the various builder configuration /// methods below. pub(crate) fn new() -> Self { - // For the insecure random API, use `SmallRng`, which is fast. It's - // also insecure, but that's the deal here. - let insecure_random = Box::new( - cap_rand::rngs::SmallRng::from_rng(cap_rand::thread_rng(cap_rand::ambient_authority())) - .unwrap(), - ); - - // For the insecure random seed, use a `u128` generated from - // `thread_rng()`, so that it's not guessable from the insecure_random - // API. - let insecure_random_seed = - cap_rand::thread_rng(cap_rand::ambient_authority()).r#gen::(); + let random = WasiRandomCtx::default(); + let clocks = WasiClocksCtx::default(); Self { env: Vec::new(), args: Vec::new(), + random, + clocks, socket_addr_check: SocketAddrCheck::default(), - random: random::thread_rng(), - insecure_random, - insecure_random_seed, - wall_clock: wall_clock(), - monotonic_clock: monotonic_clock(), allowed_network_uses: AllowedNetworkUses::default(), allow_blocking_current_thread: false, } @@ -169,7 +152,7 @@ impl WasiCtxBuilder { /// and ideally should use the insecure random API otherwise, so using any /// prerecorded or otherwise predictable data may compromise security. pub fn secure_random(&mut self, random: impl RngCore + Send + 'static) -> &mut Self { - self.random = Box::new(random); + self.random.random = Box::new(random); self } @@ -178,7 +161,7 @@ impl WasiCtxBuilder { /// The `insecure_random` generator provided will be used for all randomness /// requested by the `wasi:random/insecure` interface. pub fn insecure_random(&mut self, insecure_random: impl RngCore + Send + 'static) -> &mut Self { - self.insecure_random = Box::new(insecure_random); + self.random.insecure_random = Box::new(insecure_random); self } @@ -187,7 +170,7 @@ impl WasiCtxBuilder { /// /// By default this number is randomly generated when a builder is created. pub fn insecure_random_seed(&mut self, insecure_random_seed: u128) -> &mut Self { - self.insecure_random_seed = insecure_random_seed; + self.random.insecure_random_seed = insecure_random_seed; self } @@ -195,7 +178,7 @@ impl WasiCtxBuilder { /// /// By default the host's wall clock is used. pub fn wall_clock(&mut self, clock: impl HostWallClock + 'static) -> &mut Self { - self.wall_clock = Box::new(clock); + self.clocks.wall_clock = Box::new(clock); self } @@ -203,7 +186,7 @@ impl WasiCtxBuilder { /// /// By default the host's monotonic clock is used. pub fn monotonic_clock(&mut self, clock: impl HostMonotonicClock + 'static) -> &mut Self { - self.monotonic_clock = Box::new(clock); + self.clocks.monotonic_clock = Box::new(clock); self } diff --git a/crates/wasi/src/lib.rs b/crates/wasi/src/lib.rs index 44ebc1eca07f..0fc8e6580d3d 100644 --- a/crates/wasi/src/lib.rs +++ b/crates/wasi/src/lib.rs @@ -9,18 +9,22 @@ //! //! For components and WASIp2, see [`p2`]. //! For WASIp1 and core modules, see the [`preview1`] module documentation. +//! +//! For WASIp3, see [`p3`]. WASIp3 support is experimental, unstable and incomplete. -mod clocks; +pub mod clocks; mod ctx; mod error; mod fs; mod net; pub mod p2; +#[cfg(feature = "p3")] +pub mod p3; #[cfg(feature = "preview1")] pub mod preview0; #[cfg(feature = "preview1")] pub mod preview1; -mod random; +pub mod random; pub mod runtime; pub use self::clocks::{HostMonotonicClock, HostWallClock}; diff --git a/crates/wasi/src/p2/ctx.rs b/crates/wasi/src/p2/ctx.rs index 63b6ac1f28a4..11a12f57705a 100644 --- a/crates/wasi/src/p2/ctx.rs +++ b/crates/wasi/src/p2/ctx.rs @@ -1,9 +1,10 @@ -use crate::clocks::{HostMonotonicClock, HostWallClock}; +use crate::clocks::{HostMonotonicClock, HostWallClock, WasiClocksCtx}; use crate::ctx::AllowedNetworkUses; use crate::net::{SocketAddrCheck, SocketAddrUse}; use crate::p2::filesystem::Dir; use crate::p2::pipe; use crate::p2::stdio::{self, StdinStream, StdoutStream}; +use crate::random::WasiRandomCtx; use crate::{DirPerms, FilePerms, OpenMode}; use anyhow::Result; use cap_rand::RngCore; @@ -57,7 +58,7 @@ impl WasiCtxBuilder { /// * RNGs are all initialized with random state and suitable generator /// quality to satisfy the requirements of WASI APIs. /// * TCP/UDP are allowed but all addresses are denied by default. - /// * IP name lookup is denied by default. + /// * `wasi:sockets/ip-name-lookup` is denied by default. /// /// These defaults can all be updated via the various builder configuration /// methods below. @@ -183,7 +184,7 @@ impl WasiCtxBuilder { /// # Examples /// /// ``` - /// use wasmtime_wasi::p2::{stdin, WasiCtxBuilder}; + /// use wasmtime_wasi::p2::WasiCtxBuilder; /// /// let mut wasi = WasiCtxBuilder::new(); /// wasi.envs(&[ @@ -204,7 +205,7 @@ impl WasiCtxBuilder { /// # Examples /// /// ``` - /// use wasmtime_wasi::p2::{stdin, WasiCtxBuilder}; + /// use wasmtime_wasi::p2::WasiCtxBuilder; /// /// let mut wasi = WasiCtxBuilder::new(); /// wasi.env("FOO", "bar"); @@ -437,12 +438,18 @@ impl WasiCtxBuilder { crate::WasiCtxBuilder { env, args, + random: + WasiRandomCtx { + random, + insecure_random, + insecure_random_seed, + }, + clocks: + WasiClocksCtx { + wall_clock, + monotonic_clock, + }, socket_addr_check, - random, - insecure_random, - insecure_random_seed, - wall_clock, - monotonic_clock, allowed_network_uses, allow_blocking_current_thread, }, diff --git a/crates/wasi/src/p3/bindings.rs b/crates/wasi/src/p3/bindings.rs new file mode 100644 index 000000000000..2bedc0476b72 --- /dev/null +++ b/crates/wasi/src/p3/bindings.rs @@ -0,0 +1,306 @@ +//! Auto-generated bindings for WASI interfaces. +//! +//! This module contains the output of the [`bindgen!`] macro when run over +//! the `wasi:cli/imports` world. +//! +//! [`bindgen!`]: https://docs.rs/wasmtime/latest/wasmtime/component/macro.bindgen.html +//! +//! # Examples +//! +//! If you have a WIT world which refers to WASI interfaces you probably want to +//! use this modules's bindings rather than generate fresh bindings. That can be +//! done using the `with` option to [`bindgen!`]: +//! +//! ```rust +//! use wasmtime_wasi::p3::{WasiCtx, WasiView}; +//! use wasmtime::{Result, Engine, Config}; +//! use wasmtime::component::{Linker, HasSelf}; +//! +//! wasmtime::component::bindgen!({ +//! inline: " +//! package example:wasi; +//! +//! // An example of extending the `wasi:cli/command` world with a +//! // custom host interface. +//! world my-world { +//! include wasi:cli/command@0.3.0; +//! +//! import custom-host; +//! } +//! +//! interface custom-host { +//! my-custom-function: func(); +//! } +//! ", +//! path: "src/p3/wit", +//! with: { +//! "wasi": wasmtime_wasi::p3::bindings, +//! }, +//! concurrent_exports: true, +//! concurrent_imports: true, +//! async: { +//! only_imports: [ +//! "wasi:cli/stdin@0.3.0#get-stdin", +//! "wasi:cli/stdout@0.3.0#set-stdout", +//! "wasi:cli/stderr@0.3.0#set-stderr", +//! "wasi:clocks/monotonic-clock@0.3.0#[async]wait-for", +//! "wasi:clocks/monotonic-clock@0.3.0#[async]wait-until", +//! "wasi:filesystem/types@0.3.0#[method]descriptor.read-via-stream", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.write-via-stream", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.append-via-stream", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.advise", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.sync-data", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.get-flags", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.get-type", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.set-size", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.set-times", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.read-directory", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.sync", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.create-directory-at", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.stat", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.stat-at", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.set-times-at", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.link-at", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.open-at", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.readlink-at", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.remove-directory-at", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.rename-at", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.symlink-at", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.unlink-file-at", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.is-same-object", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.metadata-hash", +//! "wasi:filesystem/types@0.3.0#[async method]descriptor.metadata-hash-at", +//! "wasi:sockets/ip-name-lookup@0.3.0#[async]resolve-addresses", +//! "wasi:sockets/types@0.3.0#[async method]tcp-socket.connect", +//! "wasi:sockets/types@0.3.0#[async method]tcp-socket.send", +//! "wasi:sockets/types@0.3.0#[async method]udp-socket.receive", +//! "wasi:sockets/types@0.3.0#[async method]udp-socket.send", +//! "wasi:sockets/types@0.3.0#[method]tcp-socket.bind", +//! "wasi:sockets/types@0.3.0#[method]tcp-socket.listen", +//! "wasi:sockets/types@0.3.0#[method]tcp-socket.receive", +//! "wasi:sockets/types@0.3.0#[method]udp-socket.bind", +//! "wasi:sockets/types@0.3.0#[method]udp-socket.connect", +//! ], +//! }, +//! }); +//! +//! struct MyState { +//! ctx: WasiCtx, +//! } +//! +//! impl example::wasi::custom_host::Host for MyState { +//! fn my_custom_function(&mut self) { +//! // .. +//! } +//! } +//! +//! impl WasiView for MyState { +//! fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +//! } +//! +//! fn main() -> Result<()> { +//! let mut config = Config::default(); +//! config.async_support(true); +//! config.wasm_component_model_async(true); +//! let engine = Engine::new(&config)?; +//! let mut linker: Linker = Linker::new(&engine); +//! wasmtime_wasi::p3::add_to_linker(&mut linker)?; +//! example::wasi::custom_host::add_to_linker::<_, HasSelf<_>>(&mut linker, |state| state)?; +//! +//! // .. use `Linker` to instantiate component ... +//! +//! Ok(()) +//! } +//! ``` + +mod generated { + wasmtime::component::bindgen!({ + path: "src/p3/wit", + world: "wasi:cli/command", + //tracing: true, // TODO: Enable once fixed + trappable_imports: true, + concurrent_exports: true, + concurrent_imports: true, + async: { + only_imports: [ + "wasi:cli/stdin@0.3.0#get-stdin", + "wasi:cli/stdout@0.3.0#set-stdout", + "wasi:cli/stderr@0.3.0#set-stderr", + "wasi:clocks/monotonic-clock@0.3.0#[async]wait-for", + "wasi:clocks/monotonic-clock@0.3.0#[async]wait-until", + "wasi:filesystem/types@0.3.0#[method]descriptor.read-via-stream", + "wasi:filesystem/types@0.3.0#[async method]descriptor.write-via-stream", + "wasi:filesystem/types@0.3.0#[async method]descriptor.append-via-stream", + "wasi:filesystem/types@0.3.0#[async method]descriptor.advise", + "wasi:filesystem/types@0.3.0#[async method]descriptor.sync-data", + "wasi:filesystem/types@0.3.0#[async method]descriptor.get-flags", + "wasi:filesystem/types@0.3.0#[async method]descriptor.get-type", + "wasi:filesystem/types@0.3.0#[async method]descriptor.set-size", + "wasi:filesystem/types@0.3.0#[async method]descriptor.set-times", + "wasi:filesystem/types@0.3.0#[async method]descriptor.read-directory", + "wasi:filesystem/types@0.3.0#[async method]descriptor.sync", + "wasi:filesystem/types@0.3.0#[async method]descriptor.create-directory-at", + "wasi:filesystem/types@0.3.0#[async method]descriptor.stat", + "wasi:filesystem/types@0.3.0#[async method]descriptor.stat-at", + "wasi:filesystem/types@0.3.0#[async method]descriptor.set-times-at", + "wasi:filesystem/types@0.3.0#[async method]descriptor.link-at", + "wasi:filesystem/types@0.3.0#[async method]descriptor.open-at", + "wasi:filesystem/types@0.3.0#[async method]descriptor.readlink-at", + "wasi:filesystem/types@0.3.0#[async method]descriptor.remove-directory-at", + "wasi:filesystem/types@0.3.0#[async method]descriptor.rename-at", + "wasi:filesystem/types@0.3.0#[async method]descriptor.symlink-at", + "wasi:filesystem/types@0.3.0#[async method]descriptor.unlink-file-at", + "wasi:filesystem/types@0.3.0#[async method]descriptor.is-same-object", + "wasi:filesystem/types@0.3.0#[async method]descriptor.metadata-hash", + "wasi:filesystem/types@0.3.0#[async method]descriptor.metadata-hash-at", + "wasi:sockets/ip-name-lookup@0.3.0#[async]resolve-addresses", + "wasi:sockets/types@0.3.0#[async method]tcp-socket.connect", + "wasi:sockets/types@0.3.0#[async method]tcp-socket.send", + "wasi:sockets/types@0.3.0#[async method]udp-socket.receive", + "wasi:sockets/types@0.3.0#[async method]udp-socket.send", + "wasi:sockets/types@0.3.0#[method]tcp-socket.bind", + "wasi:sockets/types@0.3.0#[method]tcp-socket.listen", + "wasi:sockets/types@0.3.0#[method]tcp-socket.receive", + "wasi:sockets/types@0.3.0#[method]udp-socket.bind", + "wasi:sockets/types@0.3.0#[method]udp-socket.connect", + ], + }, + }); +} +pub use self::generated::LinkOptions; +pub use self::generated::exports; +pub use self::generated::wasi::*; + +/// Bindings to execute and run a `wasi:cli/command`. +/// +/// This structure is automatically generated by `bindgen!`. +/// +/// This can be used for a more "typed" view of executing a command component +/// through the [`Command::wasi_cli_run`] method plus +/// [`Guest::call_run`](exports::wasi::cli::run::Guest::call_run). +/// +/// # Examples +/// +/// ```no_run +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{Component, Linker}; +/// use wasmtime_wasi::p3::{WasiCtx, WasiView, WasiCtxBuilder}; +/// use wasmtime_wasi::p3::bindings::Command; +/// +/// // This example is an example shim of executing a component based on the +/// // command line arguments provided to this program. +/// #[tokio::main] +/// async fn main() -> Result<()> { +/// let args = std::env::args().skip(1).collect::>(); +/// +/// // Configure and create `Engine` +/// let mut config = Config::new(); +/// config.async_support(true); +/// config.wasm_component_model_async(true); +/// let engine = Engine::new(&config)?; +/// +/// // Configure a `Linker` with WASI, compile a component based on +/// // command line arguments, and then pre-instantiate it. +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi::p3::add_to_linker(&mut linker)?; +/// let component = Component::from_file(&engine, &args[0])?; +/// +/// +/// // Configure a `WasiCtx` based on this program's environment. Then +/// // build a `Store` to instantiate into. +/// let mut builder = WasiCtxBuilder::new(); +/// builder.inherit_stdio().inherit_env().args(&args); +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// ctx: builder.build(), +/// }, +/// ); +/// +/// // Instantiate the component and we're off to the races. +/// let command = Command::instantiate_async(&mut store, &component, &linker).await?; +/// let program_result = command.wasi_cli_run().call_run(&mut store).await?; +/// match program_result { +/// Ok(()) => Ok(()), +/// Err(()) => std::process::exit(1), +/// } +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +/// } +/// ``` +/// +/// --- +pub use self::generated::Command; + +/// Pre-instantiated analog of [`Command`] +/// +/// This can be used to front-load work such as export lookup before +/// instantiation. +/// +/// # Examples +/// +/// ```no_run +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{Linker, Component}; +/// use wasmtime_wasi::p3::{WasiCtx, WasiView, WasiCtxBuilder}; +/// use wasmtime_wasi::p3::bindings::CommandPre; +/// +/// // This example is an example shim of executing a component based on the +/// // command line arguments provided to this program. +/// #[tokio::main] +/// async fn main() -> Result<()> { +/// let args = std::env::args().skip(1).collect::>(); +/// +/// // Configure and create `Engine` +/// let mut config = Config::new(); +/// config.async_support(true); +/// config.wasm_component_model_async(true); +/// let engine = Engine::new(&config)?; +/// +/// // Configure a `Linker` with WASI, compile a component based on +/// // command line arguments, and then pre-instantiate it. +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi::p3::add_to_linker(&mut linker)?; +/// let component = Component::from_file(&engine, &args[0])?; +/// let pre = CommandPre::new(linker.instantiate_pre(&component)?)?; +/// +/// +/// // Configure a `WasiCtx` based on this program's environment. Then +/// // build a `Store` to instantiate into. +/// let mut builder = WasiCtxBuilder::new(); +/// builder.inherit_stdio().inherit_env().args(&args); +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// ctx: builder.build(), +/// }, +/// ); +/// +/// // Instantiate the component and we're off to the races. +/// let command = pre.instantiate_async(&mut store).await?; +/// let program_result = command.wasi_cli_run().call_run(&mut store).await?; +/// match program_result { +/// Ok(()) => Ok(()), +/// Err(()) => std::process::exit(1), +/// } +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +/// } +/// ``` +/// +/// --- +pub use self::generated::CommandPre; + +pub use self::generated::CommandIndices; diff --git a/crates/wasi/src/p3/clocks/host.rs b/crates/wasi/src/p3/clocks/host.rs new file mode 100644 index 000000000000..5ce87c07321f --- /dev/null +++ b/crates/wasi/src/p3/clocks/host.rs @@ -0,0 +1,114 @@ +use core::time::Duration; + +use cap_std::time::SystemTime; +use tokio::time::sleep; +use wasmtime::component::Accessor; + +use crate::p3::bindings::clocks::{monotonic_clock, wall_clock}; +use crate::p3::clocks::{WasiClocks, WasiClocksImpl, WasiClocksView}; + +// TODO: Remove once `Accessor::with` is implemented +trait AccessorWith +where + D: wasmtime::component::HasData, +{ + fn with( + &mut self, + _fun: impl FnOnce(wasmtime::component::Access<'_, T, D>) -> R, + ) -> R { + todo!() + } +} + +// TODO: Remove once `Accessor::with` is implemented +impl AccessorWith for Accessor where D: wasmtime::component::HasData {} + +// TODO: Remove once `Access::get` is implemented +trait AccessGet +where + D: wasmtime::component::HasData, +{ + fn get(&mut self) -> D::Data<'_> { + todo!() + } +} + +// TODO: Remove once `Access::get` is implemented +impl AccessGet for wasmtime::component::Access<'_, T, D> where + D: wasmtime::component::HasData +{ +} + +impl TryFrom for wall_clock::Datetime { + type Error = wasmtime::Error; + + fn try_from(time: SystemTime) -> Result { + let duration = + time.duration_since(SystemTime::from_std(std::time::SystemTime::UNIX_EPOCH))?; + + Ok(Self { + seconds: duration.as_secs(), + nanoseconds: duration.subsec_nanos(), + }) + } +} + +impl wall_clock::Host for WasiClocksImpl +where + T: WasiClocksView, +{ + fn now(&mut self) -> wasmtime::Result { + let now = self.clocks().wall_clock.now(); + Ok(wall_clock::Datetime { + seconds: now.as_secs(), + nanoseconds: now.subsec_nanos(), + }) + } + + fn resolution(&mut self) -> wasmtime::Result { + let res = self.clocks().wall_clock.resolution(); + Ok(wall_clock::Datetime { + seconds: res.as_secs(), + nanoseconds: res.subsec_nanos(), + }) + } +} + +impl monotonic_clock::HostConcurrent for WasiClocks +where + T: WasiClocksView + 'static, +{ + async fn wait_until( + store: &mut Accessor, + when: monotonic_clock::Instant, + ) -> wasmtime::Result<()> { + let clock_now = store.with(|mut view| view.get().clocks().monotonic_clock.now()); + if when > clock_now { + sleep(Duration::from_nanos(when - clock_now)).await; + }; + Ok(()) + } + + async fn wait_for( + _store: &mut Accessor, + duration: monotonic_clock::Duration, + ) -> wasmtime::Result<()> { + if duration > 0 { + sleep(Duration::from_nanos(duration)).await; + } + Ok(()) + } +} + +impl monotonic_clock::Host for WasiClocksImpl +where + T: WasiClocksView, +{ + fn now(&mut self) -> wasmtime::Result { + Ok(self.clocks().monotonic_clock.now()) + } + + fn resolution(&mut self) -> wasmtime::Result { + Ok(self.clocks().monotonic_clock.resolution()) + } +} diff --git a/crates/wasi/src/p3/clocks/mod.rs b/crates/wasi/src/p3/clocks/mod.rs new file mode 100644 index 000000000000..2f542f69b0b5 --- /dev/null +++ b/crates/wasi/src/p3/clocks/mod.rs @@ -0,0 +1,71 @@ +mod host; + +use crate::clocks::{WasiClocksImpl, WasiClocksView}; +use crate::p3::bindings::clocks; +use wasmtime::component::{HasData, Linker}; + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add all interfaces implemented by this module to the +/// [`Linker`], which corresponds to the `wasi:clocks/imports` world supported by +/// this module. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::Linker; +/// use wasmtime_wasi::clocks::{WasiClocksView, WasiClocksCtx}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// config.wasm_component_model_async(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi::p3::clocks::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// clocks: WasiClocksCtx::default(), +/// }, +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// clocks: WasiClocksCtx, +/// } +/// +/// impl WasiClocksView for MyState { +/// fn clocks(&mut self) -> &WasiClocksCtx { &self.clocks } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> { + add_to_linker_impl(linker, |x| WasiClocksImpl(x)) +} + +pub(crate) fn add_to_linker_impl( + linker: &mut Linker, + host_getter: fn(&mut T) -> WasiClocksImpl<&mut U>, +) -> wasmtime::Result<()> +where + T: Send, + U: WasiClocksView + 'static, +{ + clocks::monotonic_clock::add_to_linker::<_, WasiClocks>(linker, host_getter)?; + clocks::wall_clock::add_to_linker::<_, WasiClocks>(linker, host_getter)?; + Ok(()) +} + +struct WasiClocks(T); + +impl HasData for WasiClocks { + type Data<'a> = WasiClocksImpl<&'a mut T>; +} diff --git a/crates/wasi/src/p3/ctx.rs b/crates/wasi/src/p3/ctx.rs new file mode 100644 index 000000000000..8e10e394fdfb --- /dev/null +++ b/crates/wasi/src/p3/ctx.rs @@ -0,0 +1,493 @@ +use crate::clocks::{HostMonotonicClock, HostWallClock, WasiClocksCtx}; +use crate::net::SocketAddrUse; +use crate::p3::filesystem::Dir; +use crate::random::WasiRandomCtx; +use crate::{DirPerms, FilePerms, OpenMode}; +use anyhow::Result; +use cap_rand::RngCore; +use cap_std::ambient_authority; +use std::future::Future; +use std::mem; +use std::net::SocketAddr; +use std::path::Path; +use std::pin::Pin; + +/// Builder-style structure used to create a [`WasiCtx`]. +/// +/// This type is used to create a [`WasiCtx`] that is considered per-[`Store`] +/// state. The [`build`][WasiCtxBuilder::build] method is used to finish the +/// building process and produce a finalized [`WasiCtx`]. +/// +/// # Examples +/// +/// ``` +/// use wasmtime_wasi::p3::{WasiCtxBuilder, WasiCtx}; +/// +/// let mut wasi = WasiCtxBuilder::new(); +/// wasi.arg("./foo.wasm"); +/// wasi.arg("--help"); +/// wasi.env("FOO", "bar"); +/// +/// let wasi: WasiCtx = wasi.build(); +/// ``` +/// +/// [`Store`]: wasmtime::Store +pub struct WasiCtxBuilder { + common: crate::WasiCtxBuilder, + // TODO: implement CLI and filesystem + stdin: Box<()>, + stdout: Box<()>, + stderr: Box<()>, + preopens: Vec<(Dir, String)>, + built: bool, +} + +impl WasiCtxBuilder { + /// Creates a builder for a new context with default parameters set. + /// + /// The current defaults are: + /// + /// * stdin is closed + /// * stdout and stderr eat all input and it doesn't go anywhere + /// * no env vars + /// * no arguments + /// * no preopens + /// * clocks use the host implementation of wall/monotonic clocks + /// * RNGs are all initialized with random state and suitable generator + /// quality to satisfy the requirements of WASI APIs. + /// * TCP/UDP are allowed but all addresses are denied by default. + /// * `wasi:sockets/ip-name-lookup` is denied by default. + /// + /// These defaults can all be updated via the various builder configuration + /// methods below. + pub fn new() -> Self { + Self { + common: crate::WasiCtxBuilder::new(), + stdin: Box::default(), + stdout: Box::default(), + stderr: Box::default(), + preopens: Vec::default(), + built: false, + } + } + + /// Provides a custom implementation of stdin to use. + /// + /// By default stdin is closed. + /// + /// Note that inheriting the process's stdin can also be done through + /// [`inherit_stdin`](WasiCtxBuilder::inherit_stdin). + // TODO: implement + pub fn stdin(&mut self, stdin: ()) -> &mut Self { + self.stdin = Box::new(stdin); + self + } + + /// Same as [`stdin`](WasiCtxBuilder::stdin), but for stdout. + // TODO: implement + pub fn stdout(&mut self, stdout: ()) -> &mut Self { + self.stdout = Box::new(stdout); + self + } + + /// Same as [`stdin`](WasiCtxBuilder::stdin), but for stderr. + // TODO: implement + pub fn stderr(&mut self, stderr: ()) -> &mut Self { + self.stderr = Box::new(stderr); + self + } + + /// Configures this context's stdin stream to read the host process's + /// stdin. + /// + /// Note that concurrent reads of stdin can produce surprising results so + /// when using this it's typically best to have a single wasm instance in + /// the process using this. + pub fn inherit_stdin(&mut self) -> &mut Self { + // TODO: implement + self.stdin(()) + } + + /// Configures this context's stdout stream to write to the host process's + /// stdout. + /// + /// Note that unlike [`inherit_stdin`](WasiCtxBuilder::inherit_stdin) + /// multiple instances printing to stdout works well. + pub fn inherit_stdout(&mut self) -> &mut Self { + // TODO: implement + self.stdout(()) + } + + /// Configures this context's stderr stream to write to the host process's + /// stderr. + /// + /// Note that unlike [`inherit_stdin`](WasiCtxBuilder::inherit_stdin) + /// multiple instances printing to stderr works well. + pub fn inherit_stderr(&mut self) -> &mut Self { + // TODO: implement + self.stderr(()) + } + + /// Configures all of stdin, stdout, and stderr to be inherited from the + /// host process. + /// + /// See [`inherit_stdin`](WasiCtxBuilder::inherit_stdin) for some rationale + /// on why this should only be done in situations of + /// one-instance-per-process. + pub fn inherit_stdio(&mut self) -> &mut Self { + self.inherit_stdin().inherit_stdout().inherit_stderr() + } + + /// Configures whether or not blocking operations made through this + /// `WasiCtx` are allowed to block the current thread. + /// + /// WASI is currently implemented on top of the Rust + /// [Tokio](https://tokio.rs/) library. While most WASI APIs are + /// non-blocking some are instead blocking from the perspective of + /// WebAssembly. For example opening a file is a blocking operation with + /// respect to WebAssembly but it's implemented as an asynchronous operation + /// on the host. This is currently done with Tokio's + /// [`spawn_blocking`](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html). + /// + /// When WebAssembly is used in a synchronous context, for example when + /// [`Config::async_support`] is disabled, then this asynchronous operation + /// is quickly turned back into a synchronous operation with a `block_on` in + /// Rust. This switching back-and-forth between a blocking a non-blocking + /// context can have overhead, and this option exists to help alleviate this + /// overhead. + /// + /// This option indicates that for WASI functions that are blocking from the + /// perspective of WebAssembly it's ok to block the native thread as well. + /// This means that this back-and-forth between async and sync won't happen + /// and instead blocking operations are performed on-thread (such as opening + /// a file). This can improve the performance of WASI operations when async + /// support is disabled. + /// + /// [`Config::async_support`]: https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#method.async_support + pub fn allow_blocking_current_thread(&mut self, enable: bool) -> &mut Self { + self.common.allow_blocking_current_thread(enable); + self + } + + /// Appends multiple environment variables at once for this builder. + /// + /// All environment variables are appended to the list of environment + /// variables that this builder will configure. + /// + /// At this time environment variables are not deduplicated and if the same + /// key is set twice then the guest will see two entries for the same key. + /// + /// # Examples + /// + /// ``` + /// use wasmtime_wasi::p3::WasiCtxBuilder; + /// + /// let mut wasi = WasiCtxBuilder::new(); + /// wasi.envs(&[ + /// ("FOO", "bar"), + /// ("HOME", "/somewhere"), + /// ]); + /// ``` + pub fn envs(&mut self, env: &[(impl AsRef, impl AsRef)]) -> &mut Self { + self.common.envs(env); + self + } + + /// Appends a single environment variable for this builder. + /// + /// At this time environment variables are not deduplicated and if the same + /// key is set twice then the guest will see two entries for the same key. + /// + /// # Examples + /// + /// ``` + /// use wasmtime_wasi::p3::WasiCtxBuilder; + /// + /// let mut wasi = WasiCtxBuilder::new(); + /// wasi.env("FOO", "bar"); + /// ``` + pub fn env(&mut self, k: impl AsRef, v: impl AsRef) -> &mut Self { + self.common.env(k, v); + self + } + + /// Configures all environment variables to be inherited from the calling + /// process into this configuration. + /// + /// This will use [`envs`](WasiCtxBuilder::envs) to append all host-defined + /// environment variables. + pub fn inherit_env(&mut self) -> &mut Self { + self.common.inherit_env(); + self + } + + /// Appends a list of arguments to the argument array to pass to wasm. + pub fn args(&mut self, args: &[impl AsRef]) -> &mut Self { + self.common.args(args); + self + } + + /// Appends a single argument to get passed to wasm. + pub fn arg(&mut self, arg: impl AsRef) -> &mut Self { + self.common.arg(arg); + self + } + + /// Appends all host process arguments to the list of arguments to get + /// passed to wasm. + pub fn inherit_args(&mut self) -> &mut Self { + self.common.inherit_args(); + self + } + + /// Configures a "preopened directory" to be available to WebAssembly. + /// + /// By default WebAssembly does not have access to the filesystem because + /// there are no preopened directories. All filesystem operations, such as + /// opening a file, are done through a preexisting handle. This means that + /// to provide WebAssembly access to a directory it must be configured + /// through this API. + /// + /// WASI will also prevent access outside of files provided here. For + /// example `..` can't be used to traverse up from the `host_path` provided here + /// to the containing directory. + /// + /// * `host_path` - a path to a directory on the host to open and make + /// accessible to WebAssembly. Note that the name of this directory in the + /// guest is configured with `guest_path` below. + /// * `guest_path` - the name of the preopened directory from WebAssembly's + /// perspective. Note that this does not need to match the host's name for + /// the directory. + /// * `dir_perms` - this is the permissions that wasm will have to operate on + /// `guest_path`. This can be used, for example, to provide readonly access to a + /// directory. + /// * `file_perms` - similar to `dir_perms` but corresponds to the maximum set + /// of permissions that can be used for any file in this directory. + /// + /// # Errors + /// + /// This method will return an error if `host_path` cannot be opened. + /// + /// # Examples + /// + /// ``` + /// use wasmtime_wasi::p3::WasiCtxBuilder; + /// use wasmtime_wasi::{DirPerms, FilePerms}; + /// + /// # fn main() {} + /// # fn foo() -> wasmtime::Result<()> { + /// let mut wasi = WasiCtxBuilder::new(); + /// + /// // Make `./host-directory` available in the guest as `.` + /// wasi.preopened_dir("./host-directory", ".", DirPerms::all(), FilePerms::all()); + /// + /// // Make `./readonly` available in the guest as `./ro` + /// wasi.preopened_dir("./readonly", "./ro", DirPerms::READ, FilePerms::READ); + /// # Ok(()) + /// # } + /// ``` + pub fn preopened_dir( + &mut self, + host_path: impl AsRef, + guest_path: impl AsRef, + dir_perms: DirPerms, + file_perms: FilePerms, + ) -> Result<&mut Self> { + let dir = cap_std::fs::Dir::open_ambient_dir(host_path.as_ref(), ambient_authority())?; + let mut open_mode = OpenMode::empty(); + if dir_perms.contains(DirPerms::READ) { + open_mode |= OpenMode::READ; + } + if dir_perms.contains(DirPerms::MUTATE) { + open_mode |= OpenMode::WRITE; + } + self.preopens.push(( + Dir::new( + dir, + dir_perms, + file_perms, + open_mode, + self.common.allow_blocking_current_thread, + ), + guest_path.as_ref().to_owned(), + )); + Ok(self) + } + + /// Set the generator for the `wasi:random/random` number generator to the + /// custom generator specified. + /// + /// Note that contexts have a default RNG configured which is a suitable + /// generator for WASI and is configured with a random seed per-context. + /// + /// Guest code may rely on this random number generator to produce fresh + /// unpredictable random data in order to maintain its security invariants, + /// and ideally should use the insecure random API otherwise, so using any + /// prerecorded or otherwise predictable data may compromise security. + pub fn secure_random(&mut self, random: impl RngCore + Send + 'static) -> &mut Self { + self.common.secure_random(random); + self + } + + /// Configures the generator for `wasi:random/insecure`. + /// + /// The `insecure_random` generator provided will be used for all randomness + /// requested by the `wasi:random/insecure` interface. + pub fn insecure_random(&mut self, insecure_random: impl RngCore + Send + 'static) -> &mut Self { + self.common.insecure_random(insecure_random); + self + } + + /// Configures the seed to be returned from `wasi:random/insecure-seed` to + /// the specified custom value. + /// + /// By default this number is randomly generated when a builder is created. + pub fn insecure_random_seed(&mut self, insecure_random_seed: u128) -> &mut Self { + self.common.insecure_random_seed(insecure_random_seed); + self + } + + /// Configures `wasi:clocks/wall-clock` to use the `clock` specified. + /// + /// By default the host's wall clock is used. + pub fn wall_clock(&mut self, clock: impl HostWallClock + 'static) -> &mut Self { + self.common.wall_clock(clock); + self + } + + /// Configures `wasi:clocks/monotonic-clock` to use the `clock` specified. + /// + /// By default the host's monotonic clock is used. + pub fn monotonic_clock(&mut self, clock: impl HostMonotonicClock + 'static) -> &mut Self { + self.common.monotonic_clock(clock); + self + } + + /// Allow all network addresses accessible to the host. + /// + /// This method will inherit all network addresses meaning that any address + /// can be bound by the guest or connected to by the guest using any + /// protocol. + /// + /// See also [`WasiCtxBuilder::socket_addr_check`]. + pub fn inherit_network(&mut self) -> &mut Self { + self.common.inherit_network(); + self + } + + /// A check that will be called for each socket address that is used. + /// + /// Returning `true` will permit socket connections to the `SocketAddr`, + /// while returning `false` will reject the connection. + pub fn socket_addr_check(&mut self, check: F) -> &mut Self + where + F: Fn(SocketAddr, SocketAddrUse) -> Pin + Send + Sync>> + + Send + + Sync + + 'static, + { + self.common.socket_addr_check(check); + self + } + + /// Allow usage of `wasi:sockets/ip-name-lookup` + /// + /// By default this is disabled. + pub fn allow_ip_name_lookup(&mut self, enable: bool) -> &mut Self { + self.common.allow_ip_name_lookup(enable); + self + } + + /// Allow usage of UDP. + /// + /// This is enabled by default, but can be disabled if UDP should be blanket + /// disabled. + pub fn allow_udp(&mut self, enable: bool) -> &mut Self { + self.common.allow_udp(enable); + self + } + + /// Allow usage of TCP + /// + /// This is enabled by default, but can be disabled if TCP should be blanket + /// disabled. + pub fn allow_tcp(&mut self, enable: bool) -> &mut Self { + self.common.allow_tcp(enable); + self + } + + /// Uses the configured context so far to construct the final [`WasiCtx`]. + /// + /// Note that each `WasiCtxBuilder` can only be used to "build" once, and + /// calling this method twice will panic. + /// + /// # Panics + /// + /// Panics if this method is called twice. Each [`WasiCtxBuilder`] can be + /// used to create only a single [`WasiCtx`]. Repeated usage of this method + /// is not allowed and should use a second builder instead. + pub fn build(&mut self) -> WasiCtx { + assert!(!self.built); + + let Self { + common: crate::WasiCtxBuilder { random, clocks, .. }, + built: _, + .. + } = mem::replace(self, Self::new()); + self.built = true; + + WasiCtx { random, clocks } + } +} + +/// Per-[`Store`] state which holds state necessary to implement WASI from this +/// crate. +/// +/// This structure is created through [`WasiCtxBuilder`] and is stored within +/// the `T` of [`Store`][`Store`]. Access to the structure is provided +/// through the [`WasiView`](crate::p3::WasiView) trait as an implementation on `T`. +/// +/// Note that this structure itself does not have any accessors, it's here for +/// internal use within the `wasmtime-wasi` crate's implementation of +/// bindgen-generated traits. +/// +/// [`Store`]: wasmtime::Store +/// +/// # Example +/// +/// ``` +/// use wasmtime_wasi::p3::{WasiCtx, WasiView, WasiCtxBuilder}; +/// +/// struct MyState { +/// ctx: WasiCtx, +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +/// } +/// +/// impl MyState { +/// fn new() -> MyState { +/// let mut wasi = WasiCtxBuilder::new(); +/// wasi.arg("./foo.wasm"); +/// wasi.arg("--help"); +/// wasi.env("FOO", "bar"); +/// +/// MyState { +/// ctx: wasi.build(), +/// } +/// } +/// } +/// ``` +#[derive(Default)] +pub struct WasiCtx { + pub random: WasiRandomCtx, + pub clocks: WasiClocksCtx, +} + +impl WasiCtx { + /// Convenience function for calling [`WasiCtxBuilder::new`]. + pub fn builder() -> WasiCtxBuilder { + WasiCtxBuilder::new() + } +} diff --git a/crates/wasi/src/p3/filesystem/mod.rs b/crates/wasi/src/p3/filesystem/mod.rs new file mode 100644 index 000000000000..5b88035d9e8c --- /dev/null +++ b/crates/wasi/src/p3/filesystem/mod.rs @@ -0,0 +1,17 @@ +use crate::{DirPerms, FilePerms, OpenMode}; + +// TODO: implement +pub struct Dir; + +impl Dir { + #[allow(unused)] + pub fn new( + dir: cap_std::fs::Dir, + perms: DirPerms, + file_perms: FilePerms, + open_mode: OpenMode, + allow_blocking_current_thread: bool, + ) -> Self { + Self + } +} diff --git a/crates/wasi/src/p3/mod.rs b/crates/wasi/src/p3/mod.rs new file mode 100644 index 000000000000..35d84d979277 --- /dev/null +++ b/crates/wasi/src/p3/mod.rs @@ -0,0 +1,90 @@ +//! Experimental, unstable and incomplete implementation of wasip3 version of WASI. +//! +//! This module is under heavy development. +//! It is not compliant with semver and is not ready +//! for production use. +//! +//! Bug and security fixes limited to wasip3 will not be given patch releases. +//! +//! Documentation of this module may be incorrect or out-of-sync with the implementation. + +pub mod bindings; +pub mod clocks; +mod ctx; +pub mod filesystem; +pub mod random; +mod view; + +use wasmtime::component::Linker; + +use crate::clocks::WasiClocksImpl; +use crate::p3::bindings::LinkOptions; +use crate::random::WasiRandomImpl; + +pub use self::ctx::{WasiCtx, WasiCtxBuilder}; +pub use self::view::{WasiImpl, WasiView}; + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add all interfaces implemented by this module to the +/// [`Linker`], which corresponds to the `wasi:cli/imports` world supported by +/// this module. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::Linker; +/// use wasmtime_wasi::p3::{WasiCtx, WasiView}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// config.wasm_component_model_async(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi::p3::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState::default(), +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// #[derive(Default)] +/// struct MyState { +/// ctx: WasiCtx, +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> +where + T: WasiView + 'static, +{ + let options = LinkOptions::default(); + add_to_linker_with_options(linker, &options) +} + +/// Similar to [`add_to_linker`], but with the ability to enable unstable features. +pub fn add_to_linker_with_options( + linker: &mut Linker, + options: &LinkOptions, +) -> anyhow::Result<()> +where + T: WasiView + 'static, +{ + // TODO: use options + _ = options; + clocks::add_to_linker_impl(linker, |x| WasiClocksImpl(&mut x.ctx().clocks))?; + random::add_to_linker_impl(linker, |x| WasiRandomImpl(&mut x.ctx().random))?; + Ok(()) +} diff --git a/crates/wasi/src/p3/random/host.rs b/crates/wasi/src/p3/random/host.rs new file mode 100644 index 000000000000..c23904ca4464 --- /dev/null +++ b/crates/wasi/src/p3/random/host.rs @@ -0,0 +1,47 @@ +use cap_rand::Rng; +use cap_rand::distributions::Standard; + +use crate::p3::bindings::random::{insecure, insecure_seed, random}; +use crate::p3::random::{WasiRandomImpl, WasiRandomView}; + +impl random::Host for WasiRandomImpl +where + T: WasiRandomView, +{ + fn get_random_bytes(&mut self, len: u64) -> wasmtime::Result> { + Ok((&mut self.random().random) + .sample_iter(Standard) + .take(len as usize) + .collect()) + } + + fn get_random_u64(&mut self) -> wasmtime::Result { + Ok(self.random().random.sample(Standard)) + } +} + +impl insecure::Host for WasiRandomImpl +where + T: WasiRandomView, +{ + fn get_insecure_random_bytes(&mut self, len: u64) -> wasmtime::Result> { + Ok((&mut self.random().insecure_random) + .sample_iter(Standard) + .take(len as usize) + .collect()) + } + + fn get_insecure_random_u64(&mut self) -> wasmtime::Result { + Ok(self.random().insecure_random.sample(Standard)) + } +} + +impl insecure_seed::Host for WasiRandomImpl +where + T: WasiRandomView, +{ + fn insecure_seed(&mut self) -> wasmtime::Result<(u64, u64)> { + let seed: u128 = self.random().insecure_random_seed; + Ok((seed as u64, (seed >> 64) as u64)) + } +} diff --git a/crates/wasi/src/p3/random/mod.rs b/crates/wasi/src/p3/random/mod.rs new file mode 100644 index 000000000000..d9078bf79a28 --- /dev/null +++ b/crates/wasi/src/p3/random/mod.rs @@ -0,0 +1,78 @@ +mod host; + +use crate::p3::bindings::random; +use crate::random::{WasiRandomImpl, WasiRandomView}; +use wasmtime::component::{HasData, Linker}; + +/// Add all WASI interfaces from this module into the `linker` provided. +/// +/// This function will add the `async` variant of all interfaces into the +/// [`Linker`] provided. By `async` this means that this function is only +/// compatible with [`Config::async_support(true)`][async]. For embeddings with +/// async support disabled see [`add_to_linker_sync`] instead. +/// +/// This function will add all interfaces implemented by this crate to the +/// [`Linker`], which corresponds to the `wasi:random/imports` world supported by +/// this crate. +/// +/// [async]: wasmtime::Config::async_support +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime_wasi::random::{WasiRandomView, WasiRandomCtx}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi::p3::random::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState { +/// random: WasiRandomCtx::default(), +/// }, +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// random: WasiRandomCtx, +/// } +/// +/// impl WasiRandomView for MyState { +/// fn random(&mut self) -> &mut WasiRandomCtx { &mut self.random } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> { + add_to_linker_impl(linker, |x| WasiRandomImpl(x)) +} + +pub(crate) fn add_to_linker_impl( + linker: &mut Linker, + host_getter: fn(&mut T) -> WasiRandomImpl<&mut U>, +) -> wasmtime::Result<()> +where + T: Send, + U: WasiRandomView + 'static, +{ + random::random::add_to_linker::<_, WasiRandom>(linker, host_getter)?; + random::insecure::add_to_linker::<_, WasiRandom>(linker, host_getter)?; + random::insecure_seed::add_to_linker::<_, WasiRandom>(linker, host_getter)?; + Ok(()) +} + +struct WasiRandom(T); + +impl HasData for WasiRandom { + type Data<'a> = WasiRandomImpl<&'a mut T>; +} diff --git a/crates/wasi/src/p3/view.rs b/crates/wasi/src/p3/view.rs new file mode 100644 index 000000000000..c455eae7dd77 --- /dev/null +++ b/crates/wasi/src/p3/view.rs @@ -0,0 +1,64 @@ +use crate::p3::ctx::WasiCtx; + +/// A trait which provides access to the [`WasiCtx`] inside the embedder's `T` +/// of [`Store`][`Store`]. +/// +/// This crate's WASI Host implementations depend on the contents of +/// [`WasiCtx`]. The `T` type [`Store`][`Store`] is defined in each +/// embedding of Wasmtime. These implementations are connected to the +/// [`Linker`][`Linker`] by the +/// [`add_to_linker`](crate::p3::add_to_linker) function. +/// +/// # Example +/// +/// ``` +/// use wasmtime_wasi::p3::{WasiCtx, WasiView, WasiCtxBuilder}; +/// +/// struct MyState { +/// ctx: WasiCtx, +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +/// } +/// ``` +/// [`Store`]: wasmtime::Store +/// [`Linker`]: wasmtime::component::Linker +/// +pub trait WasiView: Send { + /// Yields mutable access to the [`WasiCtx`] configuration used for this + /// context. + fn ctx(&mut self) -> &mut WasiCtx; +} + +impl WasiView for &mut T { + fn ctx(&mut self) -> &mut WasiCtx { + T::ctx(self) + } +} + +impl WasiView for Box { + fn ctx(&mut self) -> &mut WasiCtx { + T::ctx(self) + } +} + +/// A small newtype wrapper which serves as the basis for implementations of +/// `Host` WASI traits in this crate. +/// +/// This type is used as the basis for the implementation of all `Host` traits +/// generated by `bindgen!` for WASI interfaces. This is used automatically with +/// [`add_to_linker`](crate::p3::add_to_linker_sync). +/// +/// This type is otherwise provided if you're calling the `add_to_linker` +/// functions generated by `bindgen!` from the [`bindings` +/// module](crate::p3::bindings). In this situation you'll want to create a value of +/// this type in the closures added to a `Linker`. +#[repr(transparent)] +pub struct WasiImpl(pub T); + +impl WasiView for WasiImpl { + fn ctx(&mut self) -> &mut WasiCtx { + T::ctx(&mut self.0) + } +} diff --git a/crates/wasi/src/random.rs b/crates/wasi/src/random.rs index a609c5384f75..f3df9c6af695 100644 --- a/crates/wasi/src/random.rs +++ b/crates/wasi/src/random.rs @@ -1,4 +1,56 @@ -use cap_rand::RngCore; +use cap_rand::{Rng as _, RngCore, SeedableRng as _}; + +#[repr(transparent)] +pub struct WasiRandomImpl(pub T); + +impl WasiRandomView for &mut T { + fn random(&mut self) -> &mut WasiRandomCtx { + (**self).random() + } +} + +impl WasiRandomView for WasiRandomImpl { + fn random(&mut self) -> &mut WasiRandomCtx { + self.0.random() + } +} + +impl WasiRandomView for WasiRandomCtx { + fn random(&mut self) -> &mut WasiRandomCtx { + self + } +} + +pub trait WasiRandomView: Send { + fn random(&mut self) -> &mut WasiRandomCtx; +} + +pub struct WasiRandomCtx { + pub random: Box, + pub insecure_random: Box, + pub insecure_random_seed: u128, +} + +impl Default for WasiRandomCtx { + fn default() -> Self { + // For the insecure random API, use `SmallRng`, which is fast. It's + // also insecure, but that's the deal here. + let insecure_random = Box::new( + cap_rand::rngs::SmallRng::from_rng(cap_rand::thread_rng(cap_rand::ambient_authority())) + .unwrap(), + ); + // For the insecure random seed, use a `u128` generated from + // `thread_rng()`, so that it's not guessable from the insecure_random + // API. + let insecure_random_seed = + cap_rand::thread_rng(cap_rand::ambient_authority()).r#gen::(); + Self { + random: thread_rng(), + insecure_random, + insecure_random_seed, + } + } +} /// Implement `insecure-random` using a deterministic cycle of bytes. pub struct Deterministic { diff --git a/crates/wasi/tests/all/main.rs b/crates/wasi/tests/all/main.rs index ee9f28d290c2..d859c6afb5f1 100644 --- a/crates/wasi/tests/all/main.rs +++ b/crates/wasi/tests/all/main.rs @@ -1 +1,3 @@ mod p2; +#[cfg(feature = "p3")] +mod p3; diff --git a/crates/wasi/tests/all/p3/mod.rs b/crates/wasi/tests/all/p3/mod.rs new file mode 100644 index 000000000000..828becd0e4fc --- /dev/null +++ b/crates/wasi/tests/all/p3/mod.rs @@ -0,0 +1,99 @@ +#![cfg(feature = "p3")] + +use std::path::Path; + +use anyhow::{Context as _, anyhow}; +use wasmtime::Store; +use wasmtime::component::{Component, Linker, ResourceTable}; +use wasmtime_wasi::p3::bindings::Command; +use wasmtime_wasi::p3::{WasiCtx, WasiCtxBuilder, WasiView}; +use wasmtime_wasi::{DirPerms, FilePerms}; + +use test_programs_artifacts::*; + +macro_rules! assert_test_exists { + ($name:ident) => { + #[expect(unused_imports, reason = "just here to assert it exists")] + use self::$name as _; + }; +} + +struct Ctx { + ctx: WasiCtx, +} + +impl WasiView for Ctx { + fn ctx(&mut self) -> &mut WasiCtx { + &mut self.ctx + } +} + +// TODO: Remove once test components are not built for `wasm32-wasip1` +impl wasmtime_wasi::p2::WasiView for Ctx { + fn ctx(&mut self) -> &mut wasmtime_wasi::p2::WasiCtx { + panic!("should not use wasip2") + } +} + +// TODO: Remove once test components are not built for `wasm32-wasip1` +impl wasmtime_wasi::p2::IoView for Ctx { + fn table(&mut self) -> &mut ResourceTable { + panic!("should not use wasip2") + } +} + +async fn run(path: &str) -> anyhow::Result<()> { + let path = Path::new(path); + let engine = test_programs_artifacts::engine(|config| { + config.async_support(true); + config.wasm_component_model_async(true); + }); + let component = Component::from_file(&engine, path).context("failed to compile component")?; + + let mut linker = Linker::new(&engine); + // TODO: Remove once test components are not built for `wasm32-wasip1` + wasmtime_wasi::p2::add_to_linker_async(&mut linker) + .context("failed to link `wasi:cli@0.2.x`")?; + wasmtime_wasi::p3::add_to_linker(&mut linker).context("failed to link `wasi:cli@0.3.x`")?; + + let mut builder = WasiCtxBuilder::new(); + let name = path.file_stem().unwrap().to_str().unwrap(); + let tempdir = tempfile::Builder::new() + .prefix(&format!("wasi_components_{name}_",)) + .tempdir()?; + builder + .args(&[name, "."]) + .inherit_network() + .allow_ip_name_lookup(true); + println!("preopen: {tempdir:?}"); + builder.preopened_dir(tempdir.path(), ".", DirPerms::all(), FilePerms::all())?; + for (var, val) in test_programs_artifacts::wasi_tests_environment() { + builder.env(var, val); + } + let ctx = builder.build(); + let mut store = Store::new(&engine, Ctx { ctx }); + let instance = linker.instantiate_async(&mut store, &component).await?; + let command = + Command::new(&mut store, &instance).context("failed to instantiate `wasi:cli/command`")?; + let run = command.wasi_cli_run().call_run(&mut store); + instance + .run(&mut store, run) + .await + .context("failed to call `wasi:cli/run#run`")? + .context("guest trapped")? + .map_err(|()| anyhow!("`wasi:cli/run#run` failed")) +} + +foreach_p3!(assert_test_exists); + +#[ignore = "not yet implemented"] +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_clocks_sleep() -> anyhow::Result<()> { + run(P3_CLOCKS_SLEEP_COMPONENT).await +} + +#[ignore = "not yet implemented"] +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_random_imports() -> anyhow::Result<()> { + run(P3_RANDOM_IMPORTS_COMPONENT).await +} From b5fcbf572c2274c975d8de456b4359fc2769717d Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 14 Jul 2025 16:17:05 +0200 Subject: [PATCH 05/10] chore(wasip3): remove now-redundant async stubs Signed-off-by: Roman Volosatovs --- crates/wasi/src/p3/clocks/host.rs | 32 ------------------------------- 1 file changed, 32 deletions(-) diff --git a/crates/wasi/src/p3/clocks/host.rs b/crates/wasi/src/p3/clocks/host.rs index 5ce87c07321f..13dc83c5ee72 100644 --- a/crates/wasi/src/p3/clocks/host.rs +++ b/crates/wasi/src/p3/clocks/host.rs @@ -7,38 +7,6 @@ use wasmtime::component::Accessor; use crate::p3::bindings::clocks::{monotonic_clock, wall_clock}; use crate::p3::clocks::{WasiClocks, WasiClocksImpl, WasiClocksView}; -// TODO: Remove once `Accessor::with` is implemented -trait AccessorWith -where - D: wasmtime::component::HasData, -{ - fn with( - &mut self, - _fun: impl FnOnce(wasmtime::component::Access<'_, T, D>) -> R, - ) -> R { - todo!() - } -} - -// TODO: Remove once `Accessor::with` is implemented -impl AccessorWith for Accessor where D: wasmtime::component::HasData {} - -// TODO: Remove once `Access::get` is implemented -trait AccessGet -where - D: wasmtime::component::HasData, -{ - fn get(&mut self) -> D::Data<'_> { - todo!() - } -} - -// TODO: Remove once `Access::get` is implemented -impl AccessGet for wasmtime::component::Access<'_, T, D> where - D: wasmtime::component::HasData -{ -} - impl TryFrom for wall_clock::Datetime { type Error = wasmtime::Error; From 046e0269da6278e738c441c053833dedabab6491 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 14 Jul 2025 16:17:29 +0200 Subject: [PATCH 06/10] test(wasip3): link wasip2 Signed-off-by: Roman Volosatovs --- crates/wasi/tests/all/p3/mod.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/wasi/tests/all/p3/mod.rs b/crates/wasi/tests/all/p3/mod.rs index 828becd0e4fc..813dfad6ec88 100644 --- a/crates/wasi/tests/all/p3/mod.rs +++ b/crates/wasi/tests/all/p3/mod.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "p3")] - use std::path::Path; use anyhow::{Context as _, anyhow}; @@ -19,26 +17,28 @@ macro_rules! assert_test_exists { } struct Ctx { - ctx: WasiCtx, + table: ResourceTable, + p2: wasmtime_wasi::p2::WasiCtx, + p3: WasiCtx, } impl WasiView for Ctx { fn ctx(&mut self) -> &mut WasiCtx { - &mut self.ctx + &mut self.p3 } } // TODO: Remove once test components are not built for `wasm32-wasip1` impl wasmtime_wasi::p2::WasiView for Ctx { fn ctx(&mut self) -> &mut wasmtime_wasi::p2::WasiCtx { - panic!("should not use wasip2") + &mut self.p2 } } // TODO: Remove once test components are not built for `wasm32-wasip1` impl wasmtime_wasi::p2::IoView for Ctx { fn table(&mut self) -> &mut ResourceTable { - panic!("should not use wasip2") + &mut self.table } } @@ -70,8 +70,10 @@ async fn run(path: &str) -> anyhow::Result<()> { for (var, val) in test_programs_artifacts::wasi_tests_environment() { builder.env(var, val); } - let ctx = builder.build(); - let mut store = Store::new(&engine, Ctx { ctx }); + let table = ResourceTable::default(); + let p2 = wasmtime_wasi::p2::WasiCtx::builder().build(); + let p3 = builder.build(); + let mut store = Store::new(&engine, Ctx { table, p2, p3 }); let instance = linker.instantiate_async(&mut store, &component).await?; let command = Command::new(&mut store, &instance).context("failed to instantiate `wasi:cli/command`")?; @@ -86,13 +88,11 @@ async fn run(path: &str) -> anyhow::Result<()> { foreach_p3!(assert_test_exists); -#[ignore = "not yet implemented"] #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn p3_clocks_sleep() -> anyhow::Result<()> { run(P3_CLOCKS_SLEEP_COMPONENT).await } -#[ignore = "not yet implemented"] #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn p3_random_imports() -> anyhow::Result<()> { run(P3_RANDOM_IMPORTS_COMPONENT).await From 78cb76a6ada89e4505fe314aa7e57fd36fb6e09d Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 14 Jul 2025 16:19:31 +0200 Subject: [PATCH 07/10] refactor: `allow` -> `expect` Signed-off-by: Roman Volosatovs --- crates/wasi/src/p3/filesystem/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wasi/src/p3/filesystem/mod.rs b/crates/wasi/src/p3/filesystem/mod.rs index 5b88035d9e8c..c2fe1c30bf5e 100644 --- a/crates/wasi/src/p3/filesystem/mod.rs +++ b/crates/wasi/src/p3/filesystem/mod.rs @@ -4,7 +4,7 @@ use crate::{DirPerms, FilePerms, OpenMode}; pub struct Dir; impl Dir { - #[allow(unused)] + #[expect(unused)] pub fn new( dir: cap_std::fs::Dir, perms: DirPerms, From 0cfdb2e02da6a0b9e43d307a8caf2fd13ea4cca7 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Tue, 15 Jul 2025 17:29:26 +0200 Subject: [PATCH 08/10] chore: add bindgen `tracing` issue ref ref https://github.com/bytecodealliance/wasmtime/issues/11245 Signed-off-by: Roman Volosatovs --- crates/wasi/src/p3/bindings.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/wasi/src/p3/bindings.rs b/crates/wasi/src/p3/bindings.rs index 2bedc0476b72..864d1eb8a41e 100644 --- a/crates/wasi/src/p3/bindings.rs +++ b/crates/wasi/src/p3/bindings.rs @@ -117,7 +117,9 @@ mod generated { wasmtime::component::bindgen!({ path: "src/p3/wit", world: "wasi:cli/command", - //tracing: true, // TODO: Enable once fixed + // TODO: Enable `tracing` once fixed: + // https://github.com/bytecodealliance/wasmtime/issues/11245 + //tracing: true, trappable_imports: true, concurrent_exports: true, concurrent_imports: true, From ae814931a3444a3e306f6ae71b0f511e40202a3d Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Tue, 15 Jul 2025 18:27:40 +0200 Subject: [PATCH 09/10] chore: adapt to `Accessor` API changes - hide `CommandPre`, since it is currently unusable https://github.com/bytecodealliance/wasmtime/issues/11249 - use `Command::new` directly in examples, since `instantiate_async` does not provide a way to call an export Signed-off-by: Roman Volosatovs --- crates/wasi/src/p3/bindings.rs | 16 +++++++++++++--- crates/wasi/src/p3/clocks/host.rs | 4 ++-- crates/wasi/tests/all/p3/mod.rs | 5 +++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/wasi/src/p3/bindings.rs b/crates/wasi/src/p3/bindings.rs index 864d1eb8a41e..4bd790ec0301 100644 --- a/crates/wasi/src/p3/bindings.rs +++ b/crates/wasi/src/p3/bindings.rs @@ -220,8 +220,11 @@ pub use self::generated::wasi::*; /// ); /// /// // Instantiate the component and we're off to the races. -/// let command = Command::instantiate_async(&mut store, &component, &linker).await?; -/// let program_result = command.wasi_cli_run().call_run(&mut store).await?; +/// let instance = linker.instantiate_async(&mut store, &component).await?; +/// let command = Command::new(&mut store, &instance)?; +/// let program_result = instance.run_with(&mut store, async move |store| { +/// command.wasi_cli_run().call_run(store).await +/// }).await??; /// match program_result { /// Ok(()) => Ok(()), /// Err(()) => std::process::exit(1), @@ -286,7 +289,10 @@ pub use self::generated::Command; /// /// // Instantiate the component and we're off to the races. /// let command = pre.instantiate_async(&mut store).await?; -/// let program_result = command.wasi_cli_run().call_run(&mut store).await?; +/// // TODO: Construct an accessor from `store` to call `run` +/// // https://github.com/bytecodealliance/wasmtime/issues/11249 +/// //let program_result = command.wasi_cli_run().call_run(&mut store).await?; +/// let program_result = todo!(); /// match program_result { /// Ok(()) => Ok(()), /// Err(()) => std::process::exit(1), @@ -303,6 +309,10 @@ pub use self::generated::Command; /// ``` /// /// --- +// TODO: Make this public, once `CommandPre` can be used for +// calling exports +// https://github.com/bytecodealliance/wasmtime/issues/11249 +#[doc(hidden)] pub use self::generated::CommandPre; pub use self::generated::CommandIndices; diff --git a/crates/wasi/src/p3/clocks/host.rs b/crates/wasi/src/p3/clocks/host.rs index 13dc83c5ee72..5fb024a1d521 100644 --- a/crates/wasi/src/p3/clocks/host.rs +++ b/crates/wasi/src/p3/clocks/host.rs @@ -47,7 +47,7 @@ where T: WasiClocksView + 'static, { async fn wait_until( - store: &mut Accessor, + store: &Accessor, when: monotonic_clock::Instant, ) -> wasmtime::Result<()> { let clock_now = store.with(|mut view| view.get().clocks().monotonic_clock.now()); @@ -58,7 +58,7 @@ where } async fn wait_for( - _store: &mut Accessor, + _store: &Accessor, duration: monotonic_clock::Duration, ) -> wasmtime::Result<()> { if duration > 0 { diff --git a/crates/wasi/tests/all/p3/mod.rs b/crates/wasi/tests/all/p3/mod.rs index 813dfad6ec88..fd2183ff0325 100644 --- a/crates/wasi/tests/all/p3/mod.rs +++ b/crates/wasi/tests/all/p3/mod.rs @@ -77,9 +77,10 @@ async fn run(path: &str) -> anyhow::Result<()> { let instance = linker.instantiate_async(&mut store, &component).await?; let command = Command::new(&mut store, &instance).context("failed to instantiate `wasi:cli/command`")?; - let run = command.wasi_cli_run().call_run(&mut store); instance - .run(&mut store, run) + .run_with(&mut store, async move |store| { + command.wasi_cli_run().call_run(store).await + }) .await .context("failed to call `wasi:cli/run#run`")? .context("guest trapped")? From 63a9517097e63d4f31ec7fbe0e5a3f48f75a799a Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Wed, 16 Jul 2025 12:41:24 +0200 Subject: [PATCH 10/10] doc: add a link to p3 `add_to_linker` Signed-off-by: Roman Volosatovs --- crates/wasi/src/p3/clocks/mod.rs | 4 ++++ crates/wasi/src/p3/random/mod.rs | 12 +++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/wasi/src/p3/clocks/mod.rs b/crates/wasi/src/p3/clocks/mod.rs index 2f542f69b0b5..6d6167a5508b 100644 --- a/crates/wasi/src/p3/clocks/mod.rs +++ b/crates/wasi/src/p3/clocks/mod.rs @@ -10,6 +10,10 @@ use wasmtime::component::{HasData, Linker}; /// [`Linker`], which corresponds to the `wasi:clocks/imports` world supported by /// this module. /// +/// This is low-level API for advanced use cases, +/// [`wasmtime_wasi::p3::add_to_linker`](crate::p3::add_to_linker) can be used instead +/// to add *all* wasip3 interfaces (including the ones from this module) to the `linker`. +/// /// # Example /// /// ``` diff --git a/crates/wasi/src/p3/random/mod.rs b/crates/wasi/src/p3/random/mod.rs index d9078bf79a28..8f41008e2352 100644 --- a/crates/wasi/src/p3/random/mod.rs +++ b/crates/wasi/src/p3/random/mod.rs @@ -6,16 +6,14 @@ use wasmtime::component::{HasData, Linker}; /// Add all WASI interfaces from this module into the `linker` provided. /// -/// This function will add the `async` variant of all interfaces into the -/// [`Linker`] provided. By `async` this means that this function is only -/// compatible with [`Config::async_support(true)`][async]. For embeddings with -/// async support disabled see [`add_to_linker_sync`] instead. -/// -/// This function will add all interfaces implemented by this crate to the +/// This function will add all interfaces implemented by this module to the /// [`Linker`], which corresponds to the `wasi:random/imports` world supported by /// this crate. /// -/// [async]: wasmtime::Config::async_support +/// This is low-level API for advanced use cases, +/// [`wasmtime_wasi::p3::add_to_linker`](crate::p3::add_to_linker) can be used instead +/// to add *all* wasip3 interfaces (including the ones from this module) to the `linker`. +/// /// /// # Example ///