diff --git a/CHANGELOG.md b/CHANGELOG.md index 927ccc82199..fe87ff68171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Looking for changes that affect our C API? See the [C API Changelog](lib/c-api/C - [#2535](https://github.com/wasmerio/wasmer/pull/2435) Added iOS support for Wasmer. This relies on the `dylib-engine`. - [#2427](https://github.com/wasmerio/wasmer/pull/2427) Wasmer can now compile to Javascript via `wasm-bindgen`. Use the `js-default` (and no default features) feature to try it!. - [#2436](https://github.com/wasmerio/wasmer/pull/2436) Added the x86-32 bit variant support to LLVM compiler. +- [#2499](https://github.com/wasmerio/wasmer/pull/2499) Added a subcommand to linux wasmer-cli to register wasmer with binfmt_misc + ### Changed - [#2460](https://github.com/wasmerio/wasmer/pull/2460) **breaking change** `wasmer` API usage with `no-default-features` requires now the `sys` feature to preserve old behavior. diff --git a/PACKAGING.md b/PACKAGING.md index 4a5f47445f1..f6a98b218e9 100644 --- a/PACKAGING.md +++ b/PACKAGING.md @@ -46,3 +46,9 @@ * `libwasmer-static`, containing `libwasmer.a`. The Wasmer distro packaging story is still in its infancy, so feedback is very welcome. + +## Miscellaneous: binfmt_misc + +Wasmer can be registered as a binfmt interpreter for wasm binaries. +An example systemd [.service](./scripts/wasmer-binfmt.service.example) is included here. +Please consider statically linking the wasmer binary so that this capability is also available in mount namespaces. diff --git a/lib/cli/src/cli.rs b/lib/cli/src/cli.rs index 9b807faf08b..f7bbb5cdf88 100644 --- a/lib/cli/src/cli.rs +++ b/lib/cli/src/cli.rs @@ -1,5 +1,7 @@ //! The logic for the Wasmer CLI tool. +#[cfg(target_os = "linux")] +use crate::commands::Binfmt; #[cfg(feature = "compiler")] use crate::commands::Compile; #[cfg(all(feature = "staticlib", feature = "compiler"))] @@ -66,6 +68,11 @@ enum WasmerCLIOptions { #[cfg(feature = "wast")] #[structopt(name = "wast")] Wast(Wast), + + /// Unregister and/or register wasmer as binfmt interpreter + #[cfg(target_os = "linux")] + #[structopt(name = "binfmt")] + Binfmt(Binfmt), } impl WasmerCLIOptions { @@ -83,6 +90,8 @@ impl WasmerCLIOptions { Self::Inspect(inspect) => inspect.execute(), #[cfg(feature = "wast")] Self::Wast(wast) => wast.execute(), + #[cfg(target_os = "linux")] + Self::Binfmt(binfmt) => binfmt.execute(), } } } @@ -97,21 +106,29 @@ pub fn wasmer_main() { // Eg. `wasmer ` // In case that fails, we fallback trying the Run subcommand directly. // Eg. `wasmer myfile.wasm --dir=.` + // + // In case we've been run as wasmer-binfmt-interpreter myfile.wasm args, + // we assume that we're registered via binfmt_misc let args = std::env::args().collect::>(); + let binpath = args.get(0).map(|s| s.as_ref()).unwrap_or(""); let command = args.get(1); - let options = match command.unwrap_or(&"".to_string()).as_ref() { - "cache" | "compile" | "config" | "create-exe" | "help" | "inspect" | "run" - | "self-update" | "validate" | "wast" => WasmerCLIOptions::from_args(), - _ => { - WasmerCLIOptions::from_iter_safe(args.iter()).unwrap_or_else(|e| { - match e.kind { - // This fixes a issue that: - // 1. Shows the version twice when doing `wasmer -V` - // 2. Shows the run help (instead of normal help) when doing `wasmer --help` - ErrorKind::VersionDisplayed | ErrorKind::HelpDisplayed => e.exit(), - _ => WasmerCLIOptions::Run(Run::from_args()), - } - }) + let options = if cfg!(target_os = "linux") && binpath.ends_with("wasmer-binfmt-interpreter") { + WasmerCLIOptions::Run(Run::from_binfmt_args()) + } else { + match command.unwrap_or(&"".to_string()).as_ref() { + "cache" | "compile" | "config" | "create-exe" | "help" | "inspect" | "run" + | "self-update" | "validate" | "wast" | "binfmt" => WasmerCLIOptions::from_args(), + _ => { + WasmerCLIOptions::from_iter_safe(args.iter()).unwrap_or_else(|e| { + match e.kind { + // This fixes a issue that: + // 1. Shows the version twice when doing `wasmer -V` + // 2. Shows the run help (instead of normal help) when doing `wasmer --help` + ErrorKind::VersionDisplayed | ErrorKind::HelpDisplayed => e.exit(), + _ => WasmerCLIOptions::Run(Run::from_args()), + } + }) + } } }; diff --git a/lib/cli/src/commands.rs b/lib/cli/src/commands.rs index 2e7b2926a8c..b0b53c05215 100644 --- a/lib/cli/src/commands.rs +++ b/lib/cli/src/commands.rs @@ -1,4 +1,6 @@ //! The commands available in the Wasmer binary. +#[cfg(target_os = "linux")] +mod binfmt; mod cache; #[cfg(feature = "compiler")] mod compile; @@ -12,6 +14,8 @@ mod validate; #[cfg(feature = "wast")] mod wast; +#[cfg(target_os = "linux")] +pub use binfmt::*; #[cfg(feature = "compiler")] pub use compile::*; #[cfg(all(feature = "staticlib", feature = "compiler"))] diff --git a/lib/cli/src/commands/binfmt.rs b/lib/cli/src/commands/binfmt.rs new file mode 100644 index 00000000000..e668740a6d6 --- /dev/null +++ b/lib/cli/src/commands/binfmt.rs @@ -0,0 +1,153 @@ +use anyhow::{Context, Result}; +use std::env; +use std::fs; +use std::io::Write; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::MetadataExt; +use std::path::{Path, PathBuf}; +use structopt::StructOpt; +use Action::*; + +#[derive(StructOpt, Clone, Copy)] +enum Action { + /// Register wasmer as binfmt interpreter + Register, + /// Unregister a binfmt interpreter for wasm32 + Unregister, + /// Soft unregister, and register + Reregister, +} + +/// Unregister and/or register wasmer as binfmt interpreter +/// +/// Check the wasmer repository for a systemd service definition example +/// to automate the process at start-up. +#[derive(StructOpt)] +pub struct Binfmt { + // Might be better to traverse the mount list + /// Mount point of binfmt_misc fs + #[structopt(long, default_value = "/proc/sys/fs/binfmt_misc/")] + binfmt_misc: PathBuf, + + #[structopt(subcommand)] + action: Action, +} + +// Quick safety check: +// This folder isn't world writeable (or else its sticky bit is set), and neither are its parents. +// +// If somebody mounted /tmp wrong, this might result in a TOCTOU problem. +fn seccheck(path: &Path) -> Result<()> { + if let Some(parent) = path.parent() { + seccheck(parent)?; + } + let m = std::fs::metadata(path) + .with_context(|| format!("Can't check permissions of {}", path.to_string_lossy()))?; + anyhow::ensure!( + m.mode() & 0o2 == 0 || m.mode() & 0o1000 != 0, + "{} is world writeable and not sticky", + path.to_string_lossy() + ); + Ok(()) +} + +impl Binfmt { + /// execute [Binfmt] + pub fn execute(&self) -> Result<()> { + if !self.binfmt_misc.exists() { + panic!("{} does not exist", self.binfmt_misc.to_string_lossy()); + } + let temp_dir; + let specs = match self.action { + Register | Reregister => { + temp_dir = tempfile::tempdir().context("Make temporary directory")?; + seccheck(temp_dir.path())?; + let bin_path_orig: PathBuf = env::args_os() + .nth(0) + .map(Into::into) + .filter(|p: &PathBuf| p.exists()) + .context("Cannot get path to wasmer executable")?; + let bin_path = temp_dir.path().join("wasmer-binfmt-interpreter"); + fs::copy(&bin_path_orig, &bin_path).context("Copy wasmer binary to temp folder")?; + let bin_path = fs::canonicalize(&bin_path).with_context(|| { + format!( + "Couldn't get absolute path for {}", + bin_path.to_string_lossy() + ) + })?; + Some([ + [ + b":wasm32:M::\\x00asm\\x01\\x00\\x00::".as_ref(), + bin_path.as_os_str().as_bytes(), + b":PFC", + ] + .concat(), + [ + b":wasm32-wat:E::wat::".as_ref(), + bin_path.as_os_str().as_bytes(), + b":PFC", + ] + .concat(), + ]) + } + _ => None, + }; + let wasm_registration = self.binfmt_misc.join("wasm32"); + let wat_registration = self.binfmt_misc.join("wasm32-wat"); + match self.action { + Reregister | Unregister => { + let unregister = [wasm_registration, wat_registration] + .iter() + .map(|registration| { + if registration.exists() { + let mut registration = fs::OpenOptions::new() + .write(true) + .open(registration) + .context("Open existing binfmt entry to remove")?; + registration + .write_all(b"-1") + .context("Couldn't write binfmt unregister request")?; + Ok(true) + } else { + eprintln!( + "Warning: {} does not exist, not unregistered.", + registration.to_string_lossy() + ); + Ok(false) + } + }) + .collect::>() + .into_iter() + .collect::>>()?; + match (self.action, unregister.into_iter().any(|b| b)) { + (Unregister, false) => bail!("Nothing unregistered"), + _ => (), + } + } + _ => (), + }; + if let Some(specs) = specs { + if cfg!(target_env = "gnu") { + // Approximate. ELF parsing for a proper check feels like overkill here. + eprintln!("Warning: wasmer has been compiled for glibc, and is thus likely dynamically linked. Invoking wasm binaries in chroots or mount namespaces (lxc, docker, ...) may not work."); + } + specs + .iter() + .map(|spec| { + let register = self.binfmt_misc.join("register"); + let mut register = fs::OpenOptions::new() + .write(true) + .open(register) + .context("Open binfmt misc for registration")?; + register + .write_all(&spec) + .context("Couldn't register binfmt")?; + Ok(()) + }) + .collect::>() + .into_iter() + .collect::>>()?; + } + Ok(()) + } +} diff --git a/lib/cli/src/commands/run.rs b/lib/cli/src/commands/run.rs index 879ba3a2881..96e15fbc958 100644 --- a/lib/cli/src/commands/run.rs +++ b/lib/cli/src/commands/run.rs @@ -19,7 +19,7 @@ mod wasi; #[cfg(feature = "wasi")] use wasi::Wasi; -#[derive(Debug, StructOpt, Clone)] +#[derive(Debug, StructOpt, Clone, Default)] /// The options for the `wasmer run` subcommand pub struct Run { /// Disable the cache @@ -421,4 +421,59 @@ impl Run { .collect::>>()?; Ok(func.call(&invoke_args)?) } + + /// Create Run instance for arguments/env, + /// assuming we're being run from a CFP binfmt interpreter. + pub fn from_binfmt_args() -> Run { + Self::from_binfmt_args_fallible().unwrap_or_else(|e| { + crate::error::PrettyError::report::<()>( + Err(e).context("Failed to set up wasmer binfmt invocation"), + ) + }) + } + + #[cfg(target_os = "linux")] + fn from_binfmt_args_fallible() -> Result { + let argv = std::env::args_os().collect::>(); + let (_interpreter, executable, original_executable, args) = match &argv[..] { + [a, b, c, d @ ..] => (a, b, c, d), + _ => { + bail!("Wasmer binfmt interpreter needs at least three arguments (including $0) - must be registered as binfmt interpreter with the CFP flags. (Got arguments: {:?})", argv); + } + }; + // TODO: Optimally, args and env would be passed as an UTF-8 Vec. + // (Can be pulled out of std::os::unix::ffi::OsStrExt) + // But I don't want to duplicate or rewrite run.rs today. + let args = args + .iter() + .enumerate() + .map(|(i, s)| { + s.clone().into_string().map_err(|s| { + anyhow!( + "Cannot convert argument {} ({:?}) to UTF-8 string", + i + 1, + s + ) + }) + }) + .collect::>>()?; + let original_executable = original_executable + .clone() + .into_string() + .map_err(|s| anyhow!("Cannot convert executable name {:?} to UTF-8 string", s))?; + let store = StoreOptions::default(); + // TODO: store.compiler.features.all = true; ? + Ok(Self { + args, + path: executable.into(), + command_name: Some(original_executable), + store: store, + wasi: Wasi::for_binfmt_interpreter()?, + ..Self::default() + }) + } + #[cfg(not(target_os = "linux"))] + fn from_binfmt_args_fallible() -> Result { + bail!("binfmt_misc is only available on linux.") + } } diff --git a/lib/cli/src/commands/run/wasi.rs b/lib/cli/src/commands/run/wasi.rs index a6e1fd418c9..318f599863a 100644 --- a/lib/cli/src/commands/run/wasi.rs +++ b/lib/cli/src/commands/run/wasi.rs @@ -7,7 +7,7 @@ use wasmer_wasi::{get_wasi_versions, WasiError, WasiState, WasiVersion}; use structopt::StructOpt; -#[derive(Debug, StructOpt, Clone)] +#[derive(Debug, StructOpt, Clone, Default)] /// WASI Options pub struct Wasi { /// WASI pre-opened directory @@ -111,4 +111,17 @@ impl Wasi { } .with_context(|| "failed to run WASI `_start` function") } + + pub fn for_binfmt_interpreter() -> Result { + use std::env; + let dir = env::var_os("WASMER_BINFMT_MISC_PREOPEN") + .map(Into::into) + .unwrap_or(PathBuf::from(".")); + Ok(Self { + deny_multiple_wasi_versions: true, + env_vars: env::vars().collect(), + pre_opened_directories: vec![dir], + ..Self::default() + }) + } } diff --git a/lib/cli/src/common.rs b/lib/cli/src/common.rs index 57e45e6aa86..863e154103f 100644 --- a/lib/cli/src/common.rs +++ b/lib/cli/src/common.rs @@ -5,7 +5,7 @@ use std::env; use std::path::PathBuf; use structopt::StructOpt; -#[derive(Debug, StructOpt, Clone)] +#[derive(Debug, StructOpt, Clone, Default)] /// The WebAssembly features that can be passed through the /// Command Line args. pub struct WasmFeatures { diff --git a/lib/cli/src/store.rs b/lib/cli/src/store.rs index 0614272309a..9b53db97766 100644 --- a/lib/cli/src/store.rs +++ b/lib/cli/src/store.rs @@ -12,7 +12,7 @@ use wasmer::*; #[cfg(feature = "compiler")] use wasmer_compiler::CompilerConfig; -#[derive(Debug, Clone, StructOpt)] +#[derive(Debug, Clone, StructOpt, Default)] /// The compiler and engine options pub struct StoreOptions { #[structopt(flatten)] @@ -43,7 +43,7 @@ pub struct StoreOptions { object_file: bool, } -#[derive(Debug, Clone, StructOpt)] +#[derive(Debug, Clone, StructOpt, Default)] /// The compiler options pub struct CompilerOptions { /// Use Singlepass compiler. diff --git a/scripts/wasmer-binfmt.service.example b/scripts/wasmer-binfmt.service.example new file mode 100644 index 00000000000..414aef70f04 --- /dev/null +++ b/scripts/wasmer-binfmt.service.example @@ -0,0 +1,15 @@ +[Unit] +Description=Set up wasmer to handle execution of wasm binaries +DefaultDependencies=no +Conflicts=shutdown.target +After=proc-sys-fs-binfmt_misc.automount +After=proc-sys-fs-binfmt_misc.mount +Before=sysinit.target shutdown.target +ConditionPathIsReadWrite=/proc/sys/ + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/bin/wasmer binfmt reregister +ExecStop=/usr/bin/wasmer binfmt unregister +TimeoutSec=10s diff --git a/tests/examples/memory.wat b/tests/examples/memory.wat old mode 100644 new mode 100755