Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow CLI to exec WASI w/ multiple namespaces #2251

Merged
merged 3 commits into from
Apr 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Looking for changes that affect our C API? See the [C API Changelog](lib/c-api/C
- [#2135](https://github.com/wasmerio/wasmer/pull/2135) [Documentation](./PACKAGING.md) for linux distribution maintainers

### Changed
- [#2251](https://github.com/wasmerio/wasmer/pull/2251) Wasmer CLI will now execute WASI modules with multiple WASI namespaces in them by default. Use `--allow-multiple-wasi-versions` to suppress the warning and use `--deny-multiple-wasi-versions` to make it an error.
- [#2201](https://github.com/wasmerio/wasmer/pull/2201) Implement `loupe::MemoryUsage` for `wasmer::Instance`.
- [#2200](https://github.com/wasmerio/wasmer/pull/2200) Implement `loupe::MemoryUsage` for `wasmer::Module`.
- [#2199](https://github.com/wasmerio/wasmer/pull/2199) Implement `loupe::MemoryUsage` for `wasmer::Store`.
Expand Down
24 changes: 22 additions & 2 deletions lib/cli/src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,28 @@ impl Run {
// If WASI is enabled, try to execute it with it
#[cfg(feature = "wasi")]
{
let wasi_version = Wasi::get_version(&module);
if wasi_version.is_some() {
use std::collections::BTreeSet;
use wasmer_wasi::WasiVersion;

let wasi_versions = Wasi::get_versions(&module);
if let Some(wasi_versions) = wasi_versions {
if wasi_versions.len() >= 2 {
let get_version_list = |versions: &BTreeSet<WasiVersion>| -> String {
versions
.iter()
.map(|v| format!("`{}`", v.get_namespace_str()))
.collect::<Vec<String>>()
.join(", ")
};
if self.wasi.deny_multiple_wasi_versions {
let version_list = get_version_list(&wasi_versions);
bail!("Found more than 1 WASI version in this module ({}) and `--deny-multiple-wasi-versions` is enabled.", version_list);
} else if !self.wasi.allow_multiple_wasi_versions {
let version_list = get_version_list(&wasi_versions);
warning!("Found more than 1 WASI version in this module ({}). If this is intentional, pass `--allow-multiple-wasi-versions` to suppress this warning.", version_list);
}
}

let program_name = self
.command_name
.clone()
Expand Down
23 changes: 16 additions & 7 deletions lib/cli/src/commands/run/wasi.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::utils::{parse_envvar, parse_mapdir};
use anyhow::{Context, Result};
use std::collections::BTreeSet;
use std::path::PathBuf;
use wasmer::{Instance, Module};
use wasmer_wasi::{get_wasi_version, WasiError, WasiState, WasiVersion};
use wasmer_wasi::{get_wasi_versions, WasiError, WasiState, WasiVersion};

use clap::Clap;

Expand All @@ -13,7 +14,7 @@ pub struct Wasi {
#[clap(long = "dir", name = "DIR", multiple = true, group = "wasi")]
pre_opened_directories: Vec<PathBuf>,

/// Map a host directory to a different location for the wasm module
/// Map a host directory to a different location for the Wasm module
#[clap(long = "mapdir", name = "GUEST_DIR:HOST_DIR", multiple = true, parse(try_from_str = parse_mapdir))]
mapped_dirs: Vec<(String, PathBuf)>,

Expand All @@ -25,22 +26,30 @@ pub struct Wasi {
#[cfg(feature = "experimental-io-devices")]
#[clap(long = "enable-experimental-io-devices")]
enable_experimental_io_devices: bool,

/// Allow WASI modules to import multiple versions of WASI without a warning.
#[clap(long = "allow-multiple-wasi-versions")]
pub allow_multiple_wasi_versions: bool,
MarkMcCaskey marked this conversation as resolved.
Show resolved Hide resolved

/// Require WASI modules to only import 1 version of WASI.
#[clap(long = "deny-multiple-wasi-versions")]
pub deny_multiple_wasi_versions: bool,
}

#[allow(dead_code)]
impl Wasi {
/// Gets the WASI version (if any) for the provided module
pub fn get_version(module: &Module) -> Option<WasiVersion> {
pub fn get_versions(module: &Module) -> Option<BTreeSet<WasiVersion>> {
// Get the wasi version in strict mode, so no other imports are
// allowed.
get_wasi_version(&module, true)
get_wasi_versions(&module, true)
}

/// Checks if a given module has any WASI imports at all.
pub fn has_wasi_imports(module: &Module) -> bool {
// Get the wasi version in non-strict mode, so no other imports
// are allowed
get_wasi_version(&module, false).is_some()
get_wasi_versions(&module, false).is_some()
}

/// Helper function for executing Wasi from the `Run` command.
Expand All @@ -63,8 +72,8 @@ impl Wasi {
}

let mut wasi_env = wasi_state_builder.finalize()?;
let import_object = wasi_env.import_object(&module)?;
let instance = Instance::new(&module, &import_object)?;
let resolver = wasi_env.import_object_for_all_wasi_versions(&module)?;
let instance = Instance::new(&module, &resolver)?;

let start = instance.exports.get_function("_start")?;
let result = start.call(&[]);
Expand Down
33 changes: 31 additions & 2 deletions lib/wasi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ pub use crate::state::{
WasiStateCreationError, ALL_RIGHTS, VIRTUAL_ROOT_FD,
};
pub use crate::syscalls::types;
pub use crate::utils::{get_wasi_version, is_wasi_module, WasiVersion};
pub use crate::utils::{get_wasi_version, get_wasi_versions, is_wasi_module, WasiVersion};

use thiserror::Error;
use wasmer::{imports, Function, ImportObject, LazyInit, Memory, Module, Store, WasmerEnv};
use wasmer::{
imports, ChainableNamedResolver, Function, ImportObject, LazyInit, Memory, Module,
NamedResolver, Store, WasmerEnv,
};
#[cfg(all(target_os = "macos", target_arch = "aarch64",))]
use wasmer::{FunctionType, ValType};

Expand Down Expand Up @@ -67,6 +70,7 @@ impl WasiEnv {
}
}

/// Get an `ImportObject` for a specific version of WASI detected in the module.
pub fn import_object(&mut self, module: &Module) -> Result<ImportObject, WasiError> {
let wasi_version = get_wasi_version(module, false).ok_or(WasiError::UnknownWasiVersion)?;
Ok(generate_import_object_from_env(
Expand All @@ -76,6 +80,31 @@ impl WasiEnv {
))
}

/// Like `import_object` but containing all the WASI versions detected in
/// the module.
MarkMcCaskey marked this conversation as resolved.
Show resolved Hide resolved
pub fn import_object_for_all_wasi_versions(
&mut self,
module: &Module,
) -> Result<Box<dyn NamedResolver>, WasiError> {
let wasi_versions =
get_wasi_versions(module, false).ok_or(WasiError::UnknownWasiVersion)?;
let mut version_iter = wasi_versions.iter();
let mut resolver: Box<dyn NamedResolver> = {
let version = version_iter.next().ok_or(WasiError::UnknownWasiVersion)?;
Box::new(generate_import_object_from_env(
module.store(),
self.clone(),
*version,
))
};
for version in version_iter {
let new_import_object =
generate_import_object_from_env(module.store(), self.clone(), *version);
resolver = Box::new(new_import_object.chain_front(resolver));
}
Ok(resolver)
}

/// Get the WASI state
///
/// Be careful when using this in host functions that call into Wasm:
Expand Down
123 changes: 116 additions & 7 deletions lib/wasi/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use wasmer::{ExternType, Module};
use std::collections::BTreeSet;
use wasmer::Module;

#[allow(dead_code)]
/// Check if a provided module is compiled for some version of WASI.
Expand All @@ -9,7 +10,7 @@ pub fn is_wasi_module(module: &Module) -> bool {

/// The version of WASI. This is determined by the imports namespace
/// string.
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, Eq)]
pub enum WasiVersion {
/// `wasi_unstable`.
Snapshot0,
Expand All @@ -30,6 +31,52 @@ pub enum WasiVersion {
Latest,
}

impl WasiVersion {
/// Get the version as its namespace str as it appears in Wasm modules.
pub const fn get_namespace_str(&self) -> &'static str {
match *self {
WasiVersion::Snapshot0 => SNAPSHOT0_NAMESPACE,
WasiVersion::Snapshot1 => SNAPSHOT1_NAMESPACE,
WasiVersion::Latest => SNAPSHOT1_NAMESPACE,
}
}
}

impl PartialEq<WasiVersion> for WasiVersion {
fn eq(&self, other: &Self) -> bool {
match (*self, *other) {
(Self::Snapshot1, Self::Latest)
| (Self::Latest, Self::Snapshot1)
| (Self::Latest, Self::Latest)
| (Self::Snapshot0, Self::Snapshot0)
| (Self::Snapshot1, Self::Snapshot1) => true,
_ => false,
}
}
}

impl PartialOrd for WasiVersion {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}

impl Ord for WasiVersion {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
if self == other {
return std::cmp::Ordering::Equal;
}
match (*self, *other) {
// if snapshot0 is not equal, it must be less
(Self::Snapshot0, _) => std::cmp::Ordering::Less,
(Self::Snapshot1, Self::Snapshot0) | (Self::Latest, Self::Snapshot0) => {
std::cmp::Ordering::Greater
}
_ => unreachable!("Missing info about ordering of WasiVerison"),
}
}
}

/// Namespace for the `Snapshot0` version.
const SNAPSHOT0_NAMESPACE: &str = "wasi_unstable";

Expand All @@ -41,13 +88,10 @@ const SNAPSHOT1_NAMESPACE: &str = "wasi_snapshot_preview1";
///
/// A strict detection expects that all imports live in a single WASI
/// namespace. A non-strict detection expects that at least one WASI
/// namespace exits to detect the version. Note that the strict
/// namespace exists to detect the version. Note that the strict
/// detection is faster than the non-strict one.
pub fn get_wasi_version(module: &Module, strict: bool) -> Option<WasiVersion> {
let mut imports = module.imports().filter_map(|extern_| match extern_.ty() {
ExternType::Function(_f) => Some(extern_.module().to_owned()),
_ => None,
});
let mut imports = module.imports().functions().map(|f| f.module().to_owned());
MarkMcCaskey marked this conversation as resolved.
Show resolved Hide resolved

if strict {
let first_module = imports.next()?;
Expand All @@ -70,3 +114,68 @@ pub fn get_wasi_version(module: &Module, strict: bool) -> Option<WasiVersion> {
})
}
}

/// Like [`get_wasi_version`] but detects multiple WASI versions in a single module.
/// Thus `strict` behaves differently in this function as multiple versions are
/// always supported. `strict` indicates whether non-WASI imports should trigger a
/// failure or be ignored.
MarkMcCaskey marked this conversation as resolved.
Show resolved Hide resolved
pub fn get_wasi_versions(module: &Module, strict: bool) -> Option<BTreeSet<WasiVersion>> {
let mut out = BTreeSet::new();
let imports = module.imports().functions().map(|f| f.module().to_owned());

let mut non_wasi_seen = false;
for ns in imports {
match ns.as_str() {
SNAPSHOT0_NAMESPACE => {
out.insert(WasiVersion::Snapshot0);
}
SNAPSHOT1_NAMESPACE => {
out.insert(WasiVersion::Snapshot1);
}
_ => {
non_wasi_seen = true;
}
}
}
if strict && non_wasi_seen {
None
} else {
Some(out)
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn wasi_version_equality() {
assert_eq!(WasiVersion::Snapshot0, WasiVersion::Snapshot0);
assert_eq!(WasiVersion::Snapshot1, WasiVersion::Snapshot1);
assert_eq!(WasiVersion::Snapshot1, WasiVersion::Latest);
assert_eq!(WasiVersion::Latest, WasiVersion::Snapshot1);
assert_eq!(WasiVersion::Latest, WasiVersion::Latest);
assert!(WasiVersion::Snapshot0 != WasiVersion::Snapshot1);
assert!(WasiVersion::Snapshot1 != WasiVersion::Snapshot0);
assert!(WasiVersion::Snapshot0 != WasiVersion::Latest);
assert!(WasiVersion::Latest != WasiVersion::Snapshot0);
}

#[test]
fn wasi_version_ordering() {
assert!(WasiVersion::Snapshot0 <= WasiVersion::Snapshot0);
assert!(WasiVersion::Snapshot1 <= WasiVersion::Snapshot1);
assert!(WasiVersion::Latest <= WasiVersion::Latest);
assert!(WasiVersion::Snapshot0 >= WasiVersion::Snapshot0);
assert!(WasiVersion::Snapshot1 >= WasiVersion::Snapshot1);
assert!(WasiVersion::Latest >= WasiVersion::Latest);

assert!(WasiVersion::Snapshot0 < WasiVersion::Snapshot1);
assert!(WasiVersion::Snapshot1 > WasiVersion::Snapshot0);
assert!(WasiVersion::Snapshot0 < WasiVersion::Latest);
assert!(WasiVersion::Latest > WasiVersion::Snapshot0);

assert!(!(WasiVersion::Snapshot1 < WasiVersion::Latest));
assert!(!(WasiVersion::Latest > WasiVersion::Snapshot1));
}
}