Merge branch 'main' of github.com:wasmerio/wasmer

This commit is contained in:
Syrus Akbary
2024-06-10 11:17:25 +02:00
28 changed files with 778 additions and 334 deletions

60
Cargo.lock generated
View File

@@ -923,9 +923,9 @@ dependencies = [
[[package]]
name = "crc32fast"
version = "1.4.0"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if 1.0.0",
]
@@ -976,7 +976,7 @@ version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
dependencies = [
"crossbeam-utils 0.8.19",
"crossbeam-utils 0.8.20",
]
[[package]]
@@ -997,7 +997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
dependencies = [
"crossbeam-epoch 0.9.18",
"crossbeam-utils 0.8.19",
"crossbeam-utils 0.8.20",
]
[[package]]
@@ -1021,7 +1021,7 @@ version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils 0.8.19",
"crossbeam-utils 0.8.20",
]
[[package]]
@@ -1041,7 +1041,7 @@ version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
dependencies = [
"crossbeam-utils 0.8.19",
"crossbeam-utils 0.8.20",
]
[[package]]
@@ -1057,9 +1057,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.19"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crossterm"
@@ -2778,6 +2778,12 @@ dependencies = [
"scopeguard",
]
[[package]]
name = "lockfree-object-pool"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]]
name = "log"
version = "0.3.9"
@@ -3806,7 +3812,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque 0.8.5",
"crossbeam-utils 0.8.19",
"crossbeam-utils 0.8.20",
]
[[package]]
@@ -4680,6 +4686,12 @@ dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simdutf8"
version = "0.1.4"
@@ -4999,18 +5011,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.60"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.60"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
@@ -7686,15 +7698,31 @@ checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
[[package]]
name = "zip"
version = "1.2.3"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c700ea425e148de30c29c580c1f9508b93ca57ad31c9f4e96b83c194c37a7a8f"
checksum = "775a2b471036342aa69bc5a602bc889cb0a06cda00477d0c69566757d5553d39"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils 0.8.19",
"crossbeam-utils 0.8.20",
"displaydoc",
"flate2",
"indexmap 2.2.6",
"memchr",
"thiserror",
"zopfli",
]
[[package]]
name = "zopfli"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
dependencies = [
"bumpalo",
"crc32fast",
"lockfree-object-pool",
"log 0.4.21",
"once_cell",
"simd-adler32",
]

View File

@@ -146,3 +146,6 @@ in line as the events are generated
Filters out a specific set of log events and drops the rest, this
capturer can be useful for restoring to a previous call point but
retaining the memory changes (e.g. WCGI runner).
## Improvements
https://codesandbox.io/blog/how-we-scale-our-microvm-infrastructure-using-low-latency-memory-decompression

View File

@@ -164,7 +164,15 @@ pub async fn tag_package_release(
.map(|r| r.tag_package_release)
}
/// Get the currently logged in used, together with all accessible namespaces.
/// Get the currently logged in user.
pub async fn current_user(client: &WasmerClient) -> Result<Option<types::User>, anyhow::Error> {
client
.run_graphql(types::GetCurrentUser::build(()))
.await
.map(|x| x.viewer)
}
/// Get the currently logged in user, together with all accessible namespaces.
///
/// You can optionally filter the namespaces by the user role.
pub async fn current_user_with_namespaces(
@@ -172,9 +180,9 @@ pub async fn current_user_with_namespaces(
namespace_role: Option<types::GrapheneRole>,
) -> Result<types::UserWithNamespaces, anyhow::Error> {
client
.run_graphql(types::GetCurrentUser::build(types::GetCurrentUserVars {
namespace_role,
}))
.run_graphql(types::GetCurrentUserWithNamespaces::build(
types::GetCurrentUserWithNamespacesVars { namespace_role },
))
.await?
.viewer
.context("not logged in")
@@ -544,9 +552,11 @@ pub async fn user_accessible_apps(
// Get all aps in user-accessible namespaces.
let namespace_res = client
.run_graphql(types::GetCurrentUser::build(types::GetCurrentUserVars {
namespace_role: None,
}))
.run_graphql(types::GetCurrentUserWithNamespaces::build(
types::GetCurrentUserWithNamespacesVars {
namespace_role: None,
},
))
.await?;
let active_user = namespace_res.viewer.context("not logged in")?;
let namespace_names = active_user
@@ -655,9 +665,11 @@ pub async fn user_namespaces(
client: &WasmerClient,
) -> Result<Vec<types::Namespace>, anyhow::Error> {
let user = client
.run_graphql(types::GetCurrentUser::build(types::GetCurrentUserVars {
namespace_role: None,
}))
.run_graphql(types::GetCurrentUserWithNamespaces::build(
types::GetCurrentUserWithNamespacesVars {
namespace_role: None,
},
))
.await?
.viewer
.context("not logged in")?;

View File

@@ -41,14 +41,20 @@ mod queries {
Viewer,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query")]
pub struct GetCurrentUser {
pub viewer: Option<User>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetCurrentUserVars {
pub struct GetCurrentUserWithNamespacesVars {
pub namespace_role: Option<GrapheneRole>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetCurrentUserVars")]
pub struct GetCurrentUser {
#[cynic(graphql_type = "Query", variables = "GetCurrentUserWithNamespacesVars")]
pub struct GetCurrentUserWithNamespaces {
pub viewer: Option<UserWithNamespaces>,
}
@@ -447,7 +453,7 @@ mod queries {
}
#[derive(cynic::QueryFragment, Debug, Clone, Serialize)]
#[cynic(graphql_type = "User", variables = "GetCurrentUserVars")]
#[cynic(graphql_type = "User", variables = "GetCurrentUserWithNamespacesVars")]
pub struct UserWithNamespaces {
pub id: cynic::Id,
pub username: String,

View File

@@ -229,7 +229,7 @@ tun-tap = { version = "0.1.3", features = ["tokio"], optional = true }
clap_complete = "4.5.2"
clap_mangen = "0.2.20"
zip = { version = "1.2.3", default-features = false, features = ["deflate"] }
zip = { version = "2.1.3", default-features = false, features = ["deflate"] }
# NOTE: Must use different features for clap because the "color" feature does not
# work on wasi due to the anstream dependency not compiling.

View File

@@ -23,7 +23,7 @@ pub enum AppIdent {
/// Backend app VERSION id like "dav_xxysw34234"
AppVersionId(String),
NamespacedName(String, String),
Alias(String),
Name(String),
}
impl AppIdent {
@@ -40,9 +40,18 @@ impl AppIdent {
.with_context(|| format!("Could not query for app version id '{}'", id))?;
Ok(app)
}
AppIdent::Alias(name) => wasmer_api::query::get_app_by_alias(client, name.clone())
.await?
.with_context(|| format!("Could not find app with name '{name}'")),
AppIdent::Name(name) => {
// The API only allows to query by owner + name,
// so default to the current user as the owner.
// To to so the username must first be retrieved.
let user = wasmer_api::query::current_user(client)
.await?
.context("not logged in")?;
wasmer_api::query::get_app(client, user.username, name.clone())
.await?
.with_context(|| format!("Could not find app with name '{name}'"))
}
AppIdent::NamespacedName(owner, name) => {
wasmer_api::query::get_app(client, owner.clone(), name.clone())
.await?
@@ -80,7 +89,7 @@ impl std::str::FromStr for AppIdent {
}
}
} else {
Ok(Self::Alias(s.to_string()))
Ok(Self::Name(s.to_string()))
}
}
}
@@ -160,8 +169,10 @@ impl AppIdentOpts {
let ident = if let Some(id) = &config.app_id {
AppIdent::AppId(id.clone())
} else if let Some(owner) = &config.owner {
AppIdent::NamespacedName(owner.clone(), config.name.clone())
} else {
AppIdent::Alias(config.name.clone())
AppIdent::Name(config.name.clone())
};
Ok(ResolvedAppIdent::Config {
@@ -197,7 +208,7 @@ mod tests {
);
assert_eq!(
AppIdent::from_str("lala").unwrap(),
AppIdent::Alias("lala".to_string()),
AppIdent::Name("lala".to_string()),
);
assert_eq!(

View File

@@ -12,6 +12,10 @@ pub struct HttpRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
/// HTTP headers added to the request.
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<Vec<HttpHeader>>,
/// Request body as a string.
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
@@ -26,6 +30,15 @@ pub struct HttpRequest {
pub expect: Option<HttpRequestExpect>,
}
/// Definition for an HTTP header.
#[derive(
schemars::JsonSchema, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone, Debug,
)]
pub struct HttpHeader {
pub name: String,
pub value: String,
}
/// Validation checks for an [`HttpRequest`].
#[derive(
schemars::JsonSchema, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone, Debug,

View File

@@ -195,6 +195,13 @@ pub struct AppConfigCapabilityMapV1 {
/// Enables app bootstrapping with startup snapshots.
#[serde(skip_serializing_if = "Option::is_none")]
pub instaboot: Option<AppConfigCapabilityInstaBootV1>,
/// Additional unknown capabilities.
///
/// This provides a small bit of forwards compatibility for newly added
/// capabilities.
#[serde(flatten)]
pub other: HashMap<String, serde_json::Value>,
}
/// Memory capability settings.
@@ -234,7 +241,17 @@ pub struct AppConfigCapabilityInstaBootV1 {
///
/// NOTE: if no requests are configured, then a single HTTP
/// request to '/' will be performed instead.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requests: Vec<HttpRequest>,
/// Maximum age of snapshots.
///
/// Format: 5m, 1h, 2d, ...
///
/// After the specified time new snapshots will be created, and the old
/// ones discarded.
#[serde(skip_serializing_if = "Option::is_none")]
pub max_age: Option<String>,
}
#[cfg(test)]

View File

@@ -29,16 +29,22 @@ pub async fn spawn_exec(
env: WasiEnv,
runtime: &Arc<dyn Runtime + Send + Sync + 'static>,
) -> Result<TaskJoinHandle, SpawnError> {
// Load the WASM
let wasm = spawn_load_wasm(&env, &binary, name).await?;
// Load the module
let module = spawn_load_module(&env, name, wasm, runtime).await?;
// Spawn union the file system
spawn_union_fs(&env, &binary).await?;
// Now run the module
let wasm = spawn_load_wasm(&env, &binary, name).await?;
spawn_exec_wasm(wasm, name, env, runtime).await
}
#[tracing::instrument(level = "trace", skip_all, fields(%name))]
pub async fn spawn_exec_wasm(
wasm: &[u8],
name: &str,
env: WasiEnv,
runtime: &Arc<dyn Runtime + Send + Sync + 'static>,
) -> Result<TaskJoinHandle, SpawnError> {
let module = spawn_load_module(&env, name, wasm, runtime).await?;
spawn_exec_module(module, env, runtime)
}

View File

@@ -8,6 +8,7 @@ use std::{
};
use anyhow::Context;
use exec::spawn_exec_wasm;
use virtual_fs::{AsyncReadExt, FileSystem};
use wasmer::FunctionEnvMut;
use wasmer_wasix_types::wasi::Errno;
@@ -52,54 +53,18 @@ impl BinFactory {
cache.insert(name.to_string(), Some(binary));
}
// TODO: remove allow once BinFactory is refactored
// currently fine because a BinFactory is only used by a single process tree
#[allow(clippy::await_holding_lock)]
pub async fn get_binary(
&self,
name: &str,
fs: Option<&dyn FileSystem>,
) -> Option<BinaryPackage> {
let name = name.to_string();
// Fast path
{
let cache = self.local.read().unwrap();
if let Some(data) = cache.get(&name) {
return data.clone();
}
}
// Slow path
let mut cache = self.local.write().unwrap();
// Check the cache
if let Some(data) = cache.get(&name) {
return data.clone();
}
// Check the filesystem for the file
if name.starts_with('/') {
if let Some(fs) = fs {
match load_package_from_filesystem(fs, name.as_ref(), self.runtime()).await {
Ok(pkg) => {
cache.insert(name, Some(pkg.clone()));
return Some(pkg);
}
Err(e) => {
tracing::warn!(
path = name,
error = &*e,
"Unable to load the package from disk"
);
}
}
}
}
// NAK
cache.insert(name, None);
None
self.get_executable(name, fs)
.await
.and_then(|executable| match executable {
Executable::Wasm(_) => None,
Executable::BinaryPackage(pkg) => Some(pkg),
})
}
pub fn spawn<'a>(
@@ -111,7 +76,7 @@ impl BinFactory {
Box::pin(async move {
// Find the binary (or die trying) and make the spawn type
let res = self
.get_binary(name.as_str(), Some(env.fs_root()))
.get_executable(name.as_str(), Some(env.fs_root()))
.await
.ok_or_else(|| SpawnError::BinaryNotFound {
binary: name.clone(),
@@ -119,10 +84,17 @@ impl BinFactory {
if res.is_err() {
env.on_exit(Some(Errno::Noent.into())).await;
}
let binary = res?;
let executable = res?;
// Execute
spawn_exec(binary, name.as_str(), store, env, &self.runtime).await
match executable {
Executable::Wasm(bytes) => {
spawn_exec_wasm(&bytes, name.as_str(), env, &self.runtime).await
}
Executable::BinaryPackage(pkg) => {
spawn_exec(pkg, name.as_str(), store, env, &self.runtime).await
}
}
})
}
@@ -145,13 +117,71 @@ impl BinFactory {
}
Err(SpawnError::BinaryNotFound { binary: name })
}
// TODO: remove allow once BinFactory is refactored
// currently fine because a BinFactory is only used by a single process tree
#[allow(clippy::await_holding_lock)]
pub async fn get_executable(
&self,
name: &str,
fs: Option<&dyn FileSystem>,
) -> Option<Executable> {
let name = name.to_string();
// Fast path
{
let cache = self.local.read().unwrap();
if let Some(data) = cache.get(&name) {
data.clone().map(Executable::BinaryPackage);
}
}
// Slow path
let mut cache = self.local.write().unwrap();
// Check the cache
if let Some(data) = cache.get(&name) {
return data.clone().map(Executable::BinaryPackage);
}
// Check the filesystem for the file
if name.starts_with('/') {
if let Some(fs) = fs {
match load_executable_from_filesystem(fs, name.as_ref(), self.runtime()).await {
Ok(executable) => {
if let Executable::BinaryPackage(pkg) = &executable {
cache.insert(name, Some(pkg.clone()));
}
return Some(executable);
}
Err(e) => {
tracing::warn!(
path = name,
error = &*e,
"Unable to load the package from disk"
);
}
}
}
}
// NAK
cache.insert(name, None);
None
}
}
async fn load_package_from_filesystem(
pub enum Executable {
Wasm(bytes::Bytes),
BinaryPackage(BinaryPackage),
}
async fn load_executable_from_filesystem(
fs: &dyn FileSystem,
path: &Path,
rt: &(dyn Runtime + Send + Sync),
) -> Result<BinaryPackage, anyhow::Error> {
) -> Result<Executable, anyhow::Error> {
let mut f = fs
.new_open_options()
.read(true)
@@ -161,10 +191,15 @@ async fn load_package_from_filesystem(
let mut data = Vec::with_capacity(f.size() as usize);
f.read_to_end(&mut data).await.context("Read failed")?;
let container = Container::from_bytes(data).context("Unable to parse the WEBC file")?;
let pkg = BinaryPackage::from_webc(&container, rt)
.await
.context("Unable to load the package")?;
let bytes: bytes::Bytes = data.into();
Ok(pkg)
if let Ok(container) = Container::from_bytes(bytes.clone()) {
let pkg = BinaryPackage::from_webc(&container, rt)
.await
.context("Unable to load the package")?;
Ok(Executable::BinaryPackage(pkg))
} else {
Ok(Executable::Wasm(bytes))
}
}

View File

@@ -531,6 +531,7 @@ fn wasix_exports_32(mut store: &mut impl AsStoreMut, env: &FunctionEnv<WasiEnv>)
"proc_join" => Function::new_typed_with_env(&mut store, env, proc_join::<Memory32>),
"proc_signal" => Function::new_typed_with_env(&mut store, env, proc_signal::<Memory32>),
"proc_exec" => Function::new_typed_with_env(&mut store, env, proc_exec::<Memory32>),
"proc_exec2" => Function::new_typed_with_env(&mut store, env, proc_exec2::<Memory32>),
"proc_raise" => Function::new_typed_with_env(&mut store, env, proc_raise),
"proc_raise_interval" => Function::new_typed_with_env(&mut store, env, proc_raise_interval),
"proc_spawn" => Function::new_typed_with_env(&mut store, env, proc_spawn::<Memory32>),
@@ -652,6 +653,7 @@ fn wasix_exports_64(mut store: &mut impl AsStoreMut, env: &FunctionEnv<WasiEnv>)
"proc_join" => Function::new_typed_with_env(&mut store, env, proc_join::<Memory64>),
"proc_signal" => Function::new_typed_with_env(&mut store, env, proc_signal::<Memory64>),
"proc_exec" => Function::new_typed_with_env(&mut store, env, proc_exec::<Memory64>),
"proc_exec2" => Function::new_typed_with_env(&mut store, env, proc_exec2::<Memory64>),
"proc_raise" => Function::new_typed_with_env(&mut store, env, proc_raise),
"proc_raise_interval" => Function::new_typed_with_env(&mut store, env, proc_raise_interval),
"proc_spawn" => Function::new_typed_with_env(&mut store, env, proc_spawn::<Memory64>),

View File

@@ -94,7 +94,10 @@ pub(crate) use self::types::{
},
*,
};
use self::{state::WasiInstanceGuardMemory, utils::WasiDummyWaker};
use self::{
state::{conv_env_vars, WasiInstanceGuardMemory},
utils::WasiDummyWaker,
};
pub(crate) use crate::os::task::{
process::{WasiProcessId, WasiProcessWait},
thread::{WasiThread, WasiThreadId},
@@ -1481,7 +1484,11 @@ where
}
// Function to prepare the WASI environment
pub(crate) fn _prepare_wasi(wasi_env: &mut WasiEnv, args: Option<Vec<String>>) {
pub(crate) fn _prepare_wasi(
wasi_env: &mut WasiEnv,
args: Option<Vec<String>>,
envs: Option<Vec<(String, String)>>,
) {
// Swap out the arguments with the new ones
if let Some(args) = args {
let mut wasi_state = wasi_env.state.fork();
@@ -1489,6 +1496,38 @@ pub(crate) fn _prepare_wasi(wasi_env: &mut WasiEnv, args: Option<Vec<String>>) {
wasi_env.state = Arc::new(wasi_state);
}
// Update the env vars
if let Some(envs) = envs {
let mut guard = wasi_env.state.envs.lock().unwrap();
let mut existing_envs = guard
.iter()
.map(|b| {
let string = String::from_utf8_lossy(b);
let (key, val) = string.split_once('=').expect("env var is malformed");
(key.to_string(), val.to_string().as_bytes().to_vec())
})
.collect::<Vec<_>>();
for (key, val) in envs {
let val = val.as_bytes().to_vec();
match existing_envs
.iter_mut()
.find(|(existing_key, _)| existing_key == &key)
{
Some((_, existing_val)) => *existing_val = val,
None => existing_envs.push((key, val)),
}
}
let envs = conv_env_vars(existing_envs);
*guard = envs;
drop(guard)
}
// Close any files after the STDERR that are not preopened
let close_fds = {
let preopen_fds = {

View File

@@ -205,6 +205,8 @@ pub(crate) fn path_open_internal(
open_options.options(minimum_rights.clone());
let orig_path = path;
let inode = if let Ok(inode) = maybe_inode {
// Happy path, we found the file we're trying to open
let processing_inode = inode.clone();
@@ -228,7 +230,7 @@ pub(crate) fn path_open_internal(
assert!(handle.is_some());
return Ok(Ok(*special_fd));
}
if o_flags.contains(Oflags::DIRECTORY) {
if o_flags.contains(Oflags::DIRECTORY) || orig_path.ends_with('/') {
return Ok(Err(Errno::Notdir));
}
@@ -330,11 +332,13 @@ pub(crate) fn path_open_internal(
// once we got the data we need from the parent, we lookup the host file
// todo: extra check that opening with write access is okay
let handle = {
// We set create_new because the path already didn't resolve to an existing file,
// so it must be created.
let open_options = open_options
.read(minimum_rights.read)
.append(minimum_rights.append)
.write(minimum_rights.write)
.create_new(minimum_rights.create_new);
.create_new(true);
if minimum_rights.read {
open_flags |= Fd::READ;
@@ -349,9 +353,19 @@ pub(crate) fn path_open_internal(
open_flags |= Fd::TRUNCATE;
}
Some(wasi_try_ok_ok!(open_options
.open(&new_file_host_path)
.map_err(|e| { fs_error_into_wasi_err(e) })))
match open_options.open(&new_file_host_path) {
Ok(handle) => Some(handle),
Err(err) => {
// Even though the file does not exist, it still failed to create with
// `AlreadyExists` error. This can happen if the path resolves to a
// symlink that points outside the FS sandbox.
if err == FsError::AlreadyExists {
return Ok(Err(Errno::Perm));
}
return Ok(Err(fs_error_into_wasi_err(err)));
}
}
};
let new_inode = {

View File

@@ -22,6 +22,7 @@ mod port_route_list;
mod port_route_remove;
mod port_unbridge;
mod proc_exec;
mod proc_exec2;
mod proc_fork;
mod proc_id;
mod proc_join;
@@ -90,6 +91,7 @@ pub use port_route_list::*;
pub use port_route_remove::*;
pub use port_unbridge::*;
pub use proc_exec::*;
pub use proc_exec2::*;
pub use proc_fork::*;
pub use proc_id::*;
pub use proc_join::*;

View File

@@ -25,230 +25,13 @@ pub fn proc_exec<M: MemorySize>(
args: WasmPtr<u8, M>,
args_len: M::Offset,
) -> Result<(), WasiError> {
WasiEnv::process_signals_and_exit(&mut ctx)?;
// If we were just restored the stack then we were woken after a deep sleep
if let Some(exit_code) = unsafe { handle_rewind::<M, i32>(&mut ctx) } {
// We should never get here as the process will be termined
// in the `WasiEnv::process_signals_and_exit()` call
let exit_code = ExitCode::from_native(exit_code);
ctx.data().process.terminate(exit_code);
return Err(WasiError::Exit(exit_code));
}
let memory = unsafe { ctx.data().memory_view(&ctx) };
let mut name = name.read_utf8_string(&memory, name_len).map_err(|err| {
warn!("failed to execve as the name could not be read - {}", err);
WasiError::Exit(Errno::Inval.into())
})?;
Span::current().record("name", name.as_str());
let args = args.read_utf8_string(&memory, args_len).map_err(|err| {
warn!("failed to execve as the args could not be read - {}", err);
WasiError::Exit(Errno::Inval.into())
})?;
let args: Vec<_> = args
.split(&['\n', '\r'])
.map(|a| a.to_string())
.filter(|a| !a.is_empty())
.collect();
// Convert relative paths into absolute paths
if name.starts_with("./") {
name = ctx.data().state.fs.relative_path_to_absolute(name);
}
trace!(name);
// Convert the preopen directories
let preopen = ctx.data().state.preopen.clone();
// Get the current working directory
let (_, cur_dir) = {
let (memory, state, inodes) =
unsafe { ctx.data().get_memory_and_wasi_state_and_inodes(&ctx, 0) };
match state.fs.get_current_dir(inodes, crate::VIRTUAL_ROOT_FD) {
Ok(a) => a,
Err(err) => {
warn!("failed to create subprocess for fork - {}", err);
return Err(WasiError::Exit(err.into()));
}
}
};
let new_store = ctx.data().runtime.new_store();
// If we are in a vfork we need to first spawn a subprocess of this type
// with the forked WasiEnv, then do a longjmp back to the vfork point.
if let Some(mut vfork) = ctx.data_mut().vfork.take() {
// We will need the child pid later
let child_process = ctx.data().process.clone();
let child_pid = child_process.pid();
let child_finished = child_process.finished;
// Restore the WasiEnv to the point when we vforked
vfork.env.swap_inner(ctx.data_mut());
std::mem::swap(vfork.env.as_mut(), ctx.data_mut());
let mut wasi_env = *vfork.env;
wasi_env.owned_handles.push(vfork.handle);
_prepare_wasi(&mut wasi_env, Some(args));
// Recrod the stack offsets before we give up ownership of the wasi_env
let stack_lower = wasi_env.layout.stack_lower;
let stack_upper = wasi_env.layout.stack_upper;
// Spawn a new process with this current execution environment
let mut err_exit_code: ExitCode = Errno::Success.into();
{
let bin_factory = Box::new(ctx.data().bin_factory.clone());
let tasks = wasi_env.tasks().clone();
let mut new_store = Some(new_store);
let mut config = Some(wasi_env);
match bin_factory.try_built_in(name.clone(), Some(&ctx), &mut new_store, &mut config) {
Ok(a) => {}
Err(err) => {
if !err.is_not_found() {
error!("builtin failed - {}", err);
}
let new_store = new_store.take().unwrap();
let env = config.take().unwrap();
let name_inner = name.clone();
__asyncify_light(ctx.data(), None, async {
let ret = bin_factory.spawn(name_inner, new_store, env).await;
match ret {
Ok(ret) => {
trace!(%child_pid, "spawned sub-process");
}
Err(err) => {
err_exit_code = conv_spawn_err_to_exit_code(&err);
debug!(%child_pid, "process failed with (err={})", err_exit_code);
child_finished.set_finished(Ok(err_exit_code));
warn!(
"failed to execve as the process could not be spawned (vfork) - {}",
err
);
let _ = unsafe {
stderr_write(
&ctx,
format!(
"wasm execute failed [{}] - {}\n",
name.as_str(),
err
)
.as_bytes(),
)
}
.await;
}
}
Ok(())
});
}
}
};
// Jump back to the vfork point and current on execution
// note: fork does not return any values hence passing `()`
let memory_stack = vfork.memory_stack.freeze();
let rewind_stack = vfork.rewind_stack.freeze();
let store_data = vfork.store_data;
unwind::<M, _>(ctx, move |mut ctx, _, _| {
// Rewind the stack
match rewind::<M, _>(
ctx,
memory_stack,
rewind_stack,
store_data,
ForkResult {
pid: child_pid.raw() as Pid,
ret: Errno::Success,
},
) {
Errno::Success => OnCalledAction::InvokeAgain,
err => {
warn!("fork failed - could not rewind the stack - errno={}", err);
OnCalledAction::Trap(Box::new(WasiError::Exit(err.into())))
}
}
})?;
Ok(())
}
// Otherwise we need to unwind the stack to get out of the current executing
// callstack, steal the memory/WasiEnv and switch it over to a new thread
// on the new module
else {
// Prepare the environment
let mut wasi_env = ctx.data().clone();
_prepare_wasi(&mut wasi_env, Some(args));
// Get a reference to the runtime
let bin_factory = ctx.data().bin_factory.clone();
let tasks = wasi_env.tasks().clone();
// Create the process and drop the context
let bin_factory = Box::new(ctx.data().bin_factory.clone());
let mut new_store = Some(new_store);
let mut builder = Some(wasi_env);
let process = match bin_factory.try_built_in(
name.clone(),
Some(&ctx),
&mut new_store,
&mut builder,
) {
Ok(a) => Ok(a),
Err(err) => {
if !err.is_not_found() {
error!("builtin failed - {}", err);
}
let new_store = new_store.take().unwrap();
let env = builder.take().unwrap();
// Spawn a new process with this current execution environment
InlineWaker::block_on(bin_factory.spawn(name, new_store, env))
}
};
match process {
Ok(mut process) => {
// If we support deep sleeping then we switch to deep sleep mode
let env = ctx.data();
let thread = env.thread.clone();
// The poller will wait for the process to actually finish
let res = __asyncify_with_deep_sleep::<M, _, _>(ctx, async move {
process
.wait_finished()
.await
.unwrap_or_else(|_| Errno::Child.into())
.to_native()
})?;
match res {
AsyncifyAction::Finish(mut ctx, result) => {
// When we arrive here the process should already be terminated
let exit_code = ExitCode::from_native(result);
ctx.data().process.terminate(exit_code);
WasiEnv::process_signals_and_exit(&mut ctx)?;
Err(WasiError::Exit(Errno::Unknown.into()))
}
AsyncifyAction::Unwind => Ok(()),
}
}
Err(err) => {
warn!(
"failed to execve as the process could not be spawned (fork)[0] - {}",
err
);
Err(WasiError::Exit(Errno::Noexec.into()))
}
}
}
proc_exec2(
ctx,
name,
name_len,
args,
args_len,
WasmPtr::null(),
M::ZERO,
)
}

View File

@@ -0,0 +1,280 @@
use wasmer::FromToNativeWasmType;
use super::*;
use crate::{
os::task::{OwnedTaskStatus, TaskStatus},
syscalls::*,
};
/// Replaces the current process with a new process
///
/// ## Parameters
///
/// * `name` - Name of the process to be spawned
/// * `args` - List of the arguments to pass the process
/// (entries are separated by line feeds)
/// * `envs` - List of the environment variables to pass process
///
/// ## Return
///
/// Returns a bus process id that can be used to invoke calls
#[instrument(level = "debug", skip_all, fields(name = field::Empty, %args_len), ret)]
pub fn proc_exec2<M: MemorySize>(
mut ctx: FunctionEnvMut<'_, WasiEnv>,
name: WasmPtr<u8, M>,
name_len: M::Offset,
args: WasmPtr<u8, M>,
args_len: M::Offset,
envs: WasmPtr<u8, M>,
envs_len: M::Offset,
) -> Result<(), WasiError> {
WasiEnv::process_signals_and_exit(&mut ctx)?;
// If we were just restored the stack then we were woken after a deep sleep
if let Some(exit_code) = unsafe { handle_rewind::<M, i32>(&mut ctx) } {
// We should never get here as the process will be termined
// in the `WasiEnv::process_signals_and_exit()` call
let exit_code = ExitCode::from_native(exit_code);
ctx.data().process.terminate(exit_code);
return Err(WasiError::Exit(exit_code));
}
let memory = unsafe { ctx.data().memory_view(&ctx) };
let mut name = name.read_utf8_string(&memory, name_len).map_err(|err| {
warn!("failed to execve as the name could not be read - {}", err);
WasiError::Exit(Errno::Inval.into())
})?;
Span::current().record("name", name.as_str());
let args = args.read_utf8_string(&memory, args_len).map_err(|err| {
warn!("failed to execve as the args could not be read - {}", err);
WasiError::Exit(Errno::Inval.into())
})?;
let args: Vec<_> = args
.split(&['\n', '\r'])
.map(|a| a.to_string())
.filter(|a| !a.is_empty())
.collect();
let envs = if !envs.is_null() {
let envs = envs.read_utf8_string(&memory, envs_len).map_err(|err| {
warn!("failed to execve as the envs could not be read - {}", err);
WasiError::Exit(Errno::Inval.into())
})?;
let envs = envs
.split(&['\n', '\r'])
.map(|a| a.to_string())
.filter(|a| !a.is_empty());
let mut vec = vec![];
for env in envs {
let (key, value) = env.split_once('=').unwrap();
vec.push((key.to_string(), value.to_string()));
}
Some(vec)
} else {
None
};
// Convert relative paths into absolute paths
if name.starts_with("./") {
name = ctx.data().state.fs.relative_path_to_absolute(name);
}
trace!(name);
// Convert the preopen directories
let preopen = ctx.data().state.preopen.clone();
// Get the current working directory
let (_, cur_dir) = {
let (memory, state, inodes) =
unsafe { ctx.data().get_memory_and_wasi_state_and_inodes(&ctx, 0) };
match state.fs.get_current_dir(inodes, crate::VIRTUAL_ROOT_FD) {
Ok(a) => a,
Err(err) => {
warn!("failed to create subprocess for fork - {}", err);
return Err(WasiError::Exit(err.into()));
}
}
};
let new_store = ctx.data().runtime.new_store();
// If we are in a vfork we need to first spawn a subprocess of this type
// with the forked WasiEnv, then do a longjmp back to the vfork point.
if let Some(mut vfork) = ctx.data_mut().vfork.take() {
// We will need the child pid later
let child_process = ctx.data().process.clone();
let child_pid = child_process.pid();
let child_finished = child_process.finished;
// Restore the WasiEnv to the point when we vforked
vfork.env.swap_inner(ctx.data_mut());
std::mem::swap(vfork.env.as_mut(), ctx.data_mut());
let mut wasi_env = *vfork.env;
wasi_env.owned_handles.push(vfork.handle);
_prepare_wasi(&mut wasi_env, Some(args), envs);
// Recrod the stack offsets before we give up ownership of the wasi_env
let stack_lower = wasi_env.layout.stack_lower;
let stack_upper = wasi_env.layout.stack_upper;
// Spawn a new process with this current execution environment
let mut err_exit_code: ExitCode = Errno::Success.into();
{
let bin_factory = Box::new(ctx.data().bin_factory.clone());
let tasks = wasi_env.tasks().clone();
let mut new_store = Some(new_store);
let mut config = Some(wasi_env);
match bin_factory.try_built_in(name.clone(), Some(&ctx), &mut new_store, &mut config) {
Ok(a) => {}
Err(err) => {
if !err.is_not_found() {
error!("builtin failed - {}", err);
}
let new_store = new_store.take().unwrap();
let env = config.take().unwrap();
let name_inner = name.clone();
__asyncify_light(ctx.data(), None, async {
let ret = bin_factory.spawn(name_inner, new_store, env).await;
match ret {
Ok(ret) => {
trace!(%child_pid, "spawned sub-process");
}
Err(err) => {
err_exit_code = conv_spawn_err_to_exit_code(&err);
debug!(%child_pid, "process failed with (err={})", err_exit_code);
child_finished.set_finished(Ok(err_exit_code));
warn!(
"failed to execve as the process could not be spawned (vfork) - {}",
err
);
let _ = unsafe {
stderr_write(
&ctx,
format!(
"wasm execute failed [{}] - {}\n",
name.as_str(),
err
)
.as_bytes(),
)
}
.await;
}
}
Ok(())
});
}
}
};
// Jump back to the vfork point and current on execution
// note: fork does not return any values hence passing `()`
let memory_stack = vfork.memory_stack.freeze();
let rewind_stack = vfork.rewind_stack.freeze();
let store_data = vfork.store_data;
unwind::<M, _>(ctx, move |mut ctx, _, _| {
// Rewind the stack
match rewind::<M, _>(
ctx,
memory_stack,
rewind_stack,
store_data,
ForkResult {
pid: child_pid.raw() as Pid,
ret: Errno::Success,
},
) {
Errno::Success => OnCalledAction::InvokeAgain,
err => {
warn!("fork failed - could not rewind the stack - errno={}", err);
OnCalledAction::Trap(Box::new(WasiError::Exit(err.into())))
}
}
})?;
Ok(())
}
// Otherwise we need to unwind the stack to get out of the current executing
// callstack, steal the memory/WasiEnv and switch it over to a new thread
// on the new module
else {
// Prepare the environment
let mut wasi_env = ctx.data().clone();
_prepare_wasi(&mut wasi_env, Some(args), envs);
// Get a reference to the runtime
let bin_factory = ctx.data().bin_factory.clone();
let tasks = wasi_env.tasks().clone();
// Create the process and drop the context
let bin_factory = Box::new(ctx.data().bin_factory.clone());
let mut new_store = Some(new_store);
let mut builder = Some(wasi_env);
let process = match bin_factory.try_built_in(
name.clone(),
Some(&ctx),
&mut new_store,
&mut builder,
) {
Ok(a) => Ok(a),
Err(err) => {
if !err.is_not_found() {
error!("builtin failed - {}", err);
}
let new_store = new_store.take().unwrap();
let env = builder.take().unwrap();
// Spawn a new process with this current execution environment
InlineWaker::block_on(bin_factory.spawn(name, new_store, env))
}
};
match process {
Ok(mut process) => {
// If we support deep sleeping then we switch to deep sleep mode
let env = ctx.data();
let thread = env.thread.clone();
// The poller will wait for the process to actually finish
let res = __asyncify_with_deep_sleep::<M, _, _>(ctx, async move {
process
.wait_finished()
.await
.unwrap_or_else(|_| Errno::Child.into())
.to_native()
})?;
match res {
AsyncifyAction::Finish(mut ctx, result) => {
// When we arrive here the process should already be terminated
let exit_code = ExitCode::from_native(result);
ctx.data().process.terminate(exit_code);
WasiEnv::process_signals_and_exit(&mut ctx)?;
Err(WasiError::Exit(Errno::Unknown.into()))
}
AsyncifyAction::Unwind => Ok(()),
}
}
Err(err) => {
warn!(
"failed to execve as the process could not be spawned (fork)[0] - {}",
err
);
Err(WasiError::Exit(Errno::Noexec.into()))
}
}
}
}

View File

@@ -0,0 +1,57 @@
#[link(wasm_import_module = "wasi_snapshot_preview1")]
extern "C" {
pub fn path_open(
fd: i32,
dirflags: i32,
path: i32,
path_len: i32,
oflags: i32,
fs_rights_base: i64,
fs_rights_inheriting: i64,
fdflags: i32,
result_fd: i32,
) -> i32;
}
const ERRNO_SUCCESS: i32 = 0;
const ERRNO_NOTDIR: i32 = 54;
const RIGHTS_FD_READ: i64 = 2;
fn main() {
unsafe {
let fd = 5;
let path_ok = "fyi/fs_open_trailing_slash.dir/file";
let path_bad = "fyi/fs_open_trailing_slash.dir/file/";
let errno = path_open(
fd,
0,
path_ok.as_ptr() as i32,
path_ok.len() as i32,
0,
RIGHTS_FD_READ,
0,
0,
1024,
);
assert_eq!(
errno, ERRNO_SUCCESS,
"opening a file without a trailing slash works"
);
let errno = path_open(
fd,
0,
path_bad.as_ptr() as i32,
path_bad.len() as i32,
0,
RIGHTS_FD_READ,
0,
0,
1024,
);
assert_eq!(
errno, ERRNO_NOTDIR,
"opening a regular file with a trailing slash should fail"
);
}
}

View File

@@ -0,0 +1,53 @@
#[link(wasm_import_module = "wasi_snapshot_preview1")]
extern "C" {
pub fn path_open(
fd: i32,
dirflags: i32,
path: i32,
path_len: i32,
oflags: i32,
fs_rights_base: i64,
fs_rights_inheriting: i64,
fdflags: i32,
result_fd: i32,
) -> i32;
}
const ERRNO_PERM: i32 = 63;
const LOOKUPFLAGS_SYMLINK_FOLLOW: i32 = 1;
const OFLAGS_CREAT: i32 = 1;
const RIGHTS_FD_WRITE: i64 = 64;
fn main() {
let link_path = "fyi/fs_sandbox_symlink.dir/link";
let link_path_non_existant = "fyi/fs_sandbox_symlink.dir/link-non-existant";
let mut fd: i32 = 0;
unsafe {
let errno = path_open(
5,
LOOKUPFLAGS_SYMLINK_FOLLOW,
link_path.as_ptr() as i32,
link_path.len() as i32,
OFLAGS_CREAT,
RIGHTS_FD_WRITE,
0,
0,
&mut fd as *mut i32 as i32,
);
assert_eq!(errno, ERRNO_PERM, "symlink cannot escape fs sandbox");
let errno = path_open(
5,
LOOKUPFLAGS_SYMLINK_FOLLOW,
link_path_non_existant.as_ptr() as i32,
link_path_non_existant.len() as i32,
OFLAGS_CREAT,
RIGHTS_FD_WRITE,
0,
0,
&mut fd as *mut i32 as i32,
);
assert_eq!(errno, ERRNO_PERM, "symlink cannot escape fs sandbox");
}
}

View File

@@ -0,0 +1 @@
../../README.md

View File

@@ -0,0 +1 @@
../../non-existant

View File

@@ -1 +0,0 @@
0

View File

@@ -2,4 +2,4 @@
$WASMER -q run main.wasm --dir=. > output
diff -u output expected 1>/dev/null
printf "0" | diff -u output - 1>/dev/null

View File

@@ -0,0 +1,34 @@
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
if (argc > 1 && argv[1] != NULL)
{
return 0;
}
pid_t pid = fork();
if (pid == -1)
{
exit(EXIT_FAILURE);
}
else if (pid == 0)
{
char *newargv[] = {argv[0], "child", NULL};
execv("/code/main.wasm", newargv);
exit(EXIT_FAILURE);
}
else
{
int status;
waitpid(pid, &status, 0);
printf("%d", status);
}
return 0;
}

5
tests/wasix/proc_exec/run.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
$WASMER -q run main.wasm --mapdir=/code:. > output
printf "0" | diff -u output - 1>/dev/null

View File

@@ -0,0 +1,37 @@
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
if (argc > 1 && argv[1] != NULL)
{
char *bar = getenv("foo");
return (bar == NULL);
}
pid_t pid = fork();
if (pid == -1)
{
exit(EXIT_FAILURE);
}
else if (pid == 0)
{
char *newargv[] = {argv[0], "child", NULL};
char *newenviron[] = {"foo=bar", NULL};
execve("/code/main.wasm", newargv, newenviron);
exit(EXIT_FAILURE);
}
else
{
int status;
waitpid(pid, &status, 0);
printf("%d", status);
}
return 0;
}

5
tests/wasix/proc_exec2/run.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
$WASMER -q run main.wasm --mapdir=/code:. > output
printf "0" | diff -u output - 1>/dev/null

View File

@@ -77,6 +77,7 @@ while read dir; do
cmd="cd $dir; \
$CC $CFLAGS $LDFLAGS -o main.wasm main.c; \
wasm-opt -O4 --asyncify -g main.wasm -o main.wasm; \
./run.sh"
if bash -c "$cmd"; then
@@ -85,6 +86,6 @@ while read dir; do
printf "\rTesting $dir ❌\n"
status=1
fi
done < <(find . -mindepth 1 -maxdepth 1 -type d)
done < <(find . -mindepth 1 -maxdepth 1 -type d | sort)
exit $status