From a5489981af03ae9f1e8376ad34715159172315d4 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Wed, 29 Sep 2021 23:23:26 -0400 Subject: [PATCH 01/11] build.rs refactoring * Make `main()` return Result so we can use `?` * Factor shim-building code out into `fn cargo_build_bin(..)` This shouldn't change the actual behavior of build.rs, but it will make adding new internal/embedded binaries a little easier. Signed-off-by: Will Woods --- build.rs | 214 +++++++++++++++++++++++++++---------------------------- 1 file changed, 107 insertions(+), 107 deletions(-) diff --git a/build.rs b/build.rs index 88b5c3eb..413e5a4f 100644 --- a/build.rs +++ b/build.rs @@ -107,6 +107,99 @@ fn build_cc_tests(in_path: &Path, out_path: &Path) { } } +// Build a binary named `bin_name` from the crate located at `in_dir`, +// targeting `target_name`, then strip the resulting binary and place it +// at `out_dir`/bin/`bin_name`. +fn cargo_build_bin( + in_dir: &Path, + out_dir: &Path, + target_name: &str, + bin_name: &str, +) -> std::io::Result<()> { + let profile: &[&str] = match std::env::var("PROFILE").unwrap().as_str() { + "release" => &["--release"], + _ => &[], + }; + + let filtered_env: HashMap = std::env::vars() + .filter(|&(ref k, _)| { + k == "TERM" || k == "TZ" || k == "LANG" || k == "PATH" || k == "RUSTUP_HOME" + }) + .collect(); + + let path = in_dir.as_os_str().to_str().unwrap(); + + println!("cargo:rerun-if-changed={}/Cargo.tml", path); + println!("cargo:rerun-if-changed={}/Cargo.toml", path); + println!("cargo:rerun-if-changed={}/Cargo.lock", path); + println!("cargo:rerun-if-changed={}/layout.ld", path); + println!("cargo:rerun-if-changed={}/.cargo/config", path); + + rerun_src(&path); + + let target_dir = out_dir.join(path); + + let stdout: Stdio = OpenOptions::new() + .write(true) + .open("/dev/tty") + .map(Stdio::from) + .unwrap_or_else(|_| Stdio::inherit()); + + let stderr: Stdio = OpenOptions::new() + .write(true) + .open("/dev/tty") + .map(Stdio::from) + .unwrap_or_else(|_| Stdio::inherit()); + + let status = Command::new("cargo") + .current_dir(&path) + .env_clear() + .envs(&filtered_env) + .stdout(stdout) + .stderr(stderr) + .arg("+nightly-2021-09-30") // See rust-lang/rust#89432 + .arg("build") + .args(profile) + .arg("--target-dir") + .arg(&target_dir) + .arg("--target") + .arg(target_name) + .arg("--bin") + .arg(bin_name) + .status()?; + + if !status.success() { + eprintln!("Failed to build in {}", path); + std::process::exit(1); + } + + // This is the path to the newly-built binary. + // See https://doc.rust-lang.org/cargo/guide/build-cache.html for details. + let target_bin = target_dir + .join(target_name) + .join(std::env::var("PROFILE").unwrap()) + .join(bin_name); + + // And here's where we'd like to place the final (stripped) binary + let out_bin = out_dir.join("bin").join(bin_name); + + // Strip the binary + let status = Command::new("strip") + .arg("--strip-unneeded") + .arg("-o") + .arg(&out_bin) + .arg(&target_bin) + .status()?; + + // Failing that, just copy it into place + if !status.success() { + println!("cargo:warning=Failed to run `strip` on {:?}", target_bin); + std::fs::rename(&target_bin, &out_bin)?; + } + + Ok(()) +} + fn create(path: &Path) { match std::fs::create_dir(&path) { Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {} @@ -118,7 +211,7 @@ fn create(path: &Path) { } } -fn main() { +fn main() -> Result<(), Box> { println!("cargo:rerun-if-env-changed=OUT_DIR"); println!("cargo:rerun-if-env-changed=PROFILE"); @@ -143,127 +236,34 @@ fn main() { build_cc_tests(&Path::new(CRATE).join(TEST_BINS_IN), &out_dir_bin); build_rs_tests(&Path::new(CRATE).join(TEST_BINS_IN), &out_dir_bin); - let profile: &[&str] = match std::env::var("PROFILE").unwrap().as_str() { - "release" => &["--release"], - _ => &[], - }; - - let target_name = "x86_64-unknown-linux-musl"; - - let filtered_env: HashMap = std::env::vars() - .filter(|&(ref k, _)| { - k == "TERM" || k == "TZ" || k == "LANG" || k == "PATH" || k == "RUSTUP_HOME" - }) - .collect(); + let target = "x86_64-unknown-linux-musl"; // internal crates are not included, if there is a `Cargo.toml` file // trick cargo by renaming the `Cargo.toml` to `Cargo.tml` before // publishing and rename it back here. - for entry in std::fs::read_dir("internal").unwrap() { - let path = entry.unwrap().path(); + for entry in std::fs::read_dir("internal")? { + let path = entry?.path(); let cargo_toml = path.join("Cargo.toml"); let cargo_tml = path.join("Cargo.tml"); if cargo_tml.exists() { - std::fs::copy(cargo_tml, cargo_toml).unwrap(); - } - } - - for entry in std::fs::read_dir("internal").unwrap() { - let path_buf = entry.unwrap().path(); - - let shim_name = path_buf.clone(); - let shim_name = shim_name - .file_name() - .unwrap() - .to_os_string() - .into_string() - .unwrap(); - - let shim_out_dir = out_dir.join(&path_buf); - - let path: String = path_buf.into_os_string().into_string().unwrap(); - - println!("cargo:rerun-if-changed={}/Cargo.tml", path); - println!("cargo:rerun-if-changed={}/Cargo.toml", path); - println!("cargo:rerun-if-changed={}/Cargo.lock", path); - println!("cargo:rerun-if-changed={}/layout.ld", path); - println!("cargo:rerun-if-changed={}/.cargo/config", path); - - rerun_src(&path); - - if !shim_name.starts_with("shim-") { - continue; + std::fs::copy(cargo_tml, cargo_toml)?; } - #[cfg(not(any(feature = "backend-kvm")))] - if shim_name.starts_with("shim-sev") { - continue; - } + let dir_name = path.file_name().unwrap().to_str().unwrap_or_default(); - #[cfg(not(feature = "backend-sgx"))] - if shim_name.starts_with("shim-sgx") { - continue; - } + match dir_name { - let target_dir = shim_out_dir.clone().into_os_string().into_string().unwrap(); + #[cfg(feature = "backend-kvm")] + "shim-sev" => cargo_build_bin(&path, &out_dir, target, "shim-sev")?, - let stdout: Stdio = OpenOptions::new() - .write(true) - .open("/dev/tty") - .map(Stdio::from) - .unwrap_or_else(|_| Stdio::inherit()); + #[cfg(feature = "backend-sgx")] + "shim-sgx" => cargo_build_bin(&path, &out_dir, target, "shim-sgx")?, - let stderr: Stdio = OpenOptions::new() - .write(true) - .open("/dev/tty") - .map(Stdio::from) - .unwrap_or_else(|_| Stdio::inherit()); - - let status = Command::new("cargo") - .current_dir(&path) - .env_clear() - .envs(&filtered_env) - .stdout(stdout) - .stderr(stderr) - .arg("+nightly") - .arg("build") - .args(profile) - .arg("--target-dir") - .arg(&target_dir) - .arg("--target") - .arg(target_name) - .arg("--bin") - .arg(&shim_name) - .status() - .expect("failed to build shim"); - - if !status.success() { - eprintln!("Failed to build shim {}", path); - std::process::exit(1); - } - - let out_bin = out_dir_bin.join(&shim_name); - - let shim_out_bin = shim_out_dir - .join(&target_name) - .join(&std::env::var("PROFILE").unwrap()) - .join(&shim_name); - - let status = Command::new("strip") - .arg("--strip-unneeded") - .arg("-o") - .arg(&out_bin) - .arg(&shim_out_bin) - .status(); - - match status { - Ok(status) if status.success() => {} - _ => { - println!("cargo:warning=Failed to run `strip` on {:?}", &shim_out_bin); - std::fs::rename(&shim_out_bin, &out_bin).expect("move failed") - } + _ => continue, } } + + Ok(()) } From 4399c89cfa0fa9f9bcdfcab742f67e1b81213053 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Wed, 29 Sep 2021 23:53:02 -0400 Subject: [PATCH 02/11] Add internal/wasmldr Pulled a copy of enarx-wasmldr[^1] and tweaked its Config.toml just a tiny bit so it builds like the shims. Also added the "wasmldr" feature to control whether it gets built or not. [^1]: https://github.com/enarx/enarx-wasmldr/tree/819f15e Signed-off-by: Will Woods --- Cargo.toml | 3 +- build.rs | 2 + internal/wasmldr/Cargo.toml | 35 +++ internal/wasmldr/LICENSE | 201 ++++++++++++++++++ internal/wasmldr/README.md | 34 +++ internal/wasmldr/build.rs | 26 +++ internal/wasmldr/deny.toml | 52 +++++ internal/wasmldr/fixtures/bundle/config.yaml | 8 + internal/wasmldr/fixtures/bundle/stdin.txt | 1 + .../wasmldr/fixtures/hello_wasi_snapshot1.wat | 29 +++ internal/wasmldr/fixtures/no_export.wat | 5 + internal/wasmldr/fixtures/return_1.wat | 5 + internal/wasmldr/fixtures/wasi_snapshot1.wat | 16 ++ internal/wasmldr/src/bundle.rs | 72 +++++++ internal/wasmldr/src/cli.rs | 42 ++++ internal/wasmldr/src/main.rs | 68 ++++++ internal/wasmldr/src/workload.rs | 169 +++++++++++++++ 17 files changed, 767 insertions(+), 1 deletion(-) create mode 100644 internal/wasmldr/Cargo.toml create mode 100644 internal/wasmldr/LICENSE create mode 100644 internal/wasmldr/README.md create mode 100644 internal/wasmldr/build.rs create mode 100644 internal/wasmldr/deny.toml create mode 100644 internal/wasmldr/fixtures/bundle/config.yaml create mode 100644 internal/wasmldr/fixtures/bundle/stdin.txt create mode 100644 internal/wasmldr/fixtures/hello_wasi_snapshot1.wat create mode 100644 internal/wasmldr/fixtures/no_export.wat create mode 100644 internal/wasmldr/fixtures/return_1.wat create mode 100644 internal/wasmldr/fixtures/wasi_snapshot1.wat create mode 100644 internal/wasmldr/src/bundle.rs create mode 100644 internal/wasmldr/src/cli.rs create mode 100644 internal/wasmldr/src/main.rs create mode 100644 internal/wasmldr/src/workload.rs diff --git a/Cargo.toml b/Cargo.toml index 23a9deea..ed484297 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,10 +23,11 @@ is-it-maintained-issue-resolution = { repository = "enarx/enarx-keepldr" } is-it-maintained-open-issues = { repository = "enarx/enarx-keepldr" } [features] -default = ["backend-kvm", "backend-sgx"] +default = ["backend-kvm", "backend-sgx", "wasmldr"] backend-kvm = ["x86_64", "kvm-bindings", "kvm-ioctls"] backend-sgx = ["x86_64", "sgx"] +wasmldr = [] [dependencies] sgx = { git = "https://github.com/enarx/sgx", rev = "57df3753a0ea1777963dbf3023452993df2edb8c", features = ["openssl"], optional = true } diff --git a/build.rs b/build.rs index 413e5a4f..a6adff48 100644 --- a/build.rs +++ b/build.rs @@ -254,6 +254,8 @@ fn main() -> Result<(), Box> { let dir_name = path.file_name().unwrap().to_str().unwrap_or_default(); match dir_name { + #[cfg(feature = "wasmldr")] + "wasmldr" => cargo_build_bin(&path, &out_dir, target, "wasmldr")?, #[cfg(feature = "backend-kvm")] "shim-sev" => cargo_build_bin(&path, &out_dir, target, "shim-sev")?, diff --git a/internal/wasmldr/Cargo.toml b/internal/wasmldr/Cargo.toml new file mode 100644 index 00000000..1b7d144e --- /dev/null +++ b/internal/wasmldr/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "wasmldr" +version = "0.2.0" +authors = ["Will Woods "] +edition = "2018" +license = "Apache-2.0" +description = "Enarx WebAssembly Loader" +readme = "README.md" + +# TODO: merge these into the toplevel actions/gitignore +exclude = [ ".gitignore", ".github/*" ] + +[[bin]] +name = "wasmldr" + +[dependencies] +wasmtime = { version = "0.30", default-features = false, features = ["cranelift"] } +wasmtime-wasi = { version = "0.30", default-features = false, features = ["sync"] } +wasi-common = { version = "0.30", default-features = false } +wasmparser = "0.80" +structopt = { version = "0.3", default-features = false } +anyhow = "1.0" +env_logger = { version = "0.9", default-features = false } +log = "0.4" + +[build-dependencies] +wat = "1.0" + +[profile.release] +incremental = false +codegen-units = 1 +panic = "abort" +lto = true +debug = 1 +opt-level = "s" \ No newline at end of file diff --git a/internal/wasmldr/LICENSE b/internal/wasmldr/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/internal/wasmldr/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/internal/wasmldr/README.md b/internal/wasmldr/README.md new file mode 100644 index 00000000..6baaa754 --- /dev/null +++ b/internal/wasmldr/README.md @@ -0,0 +1,34 @@ +[![Workflow Status](https://github.com/enarx/enarx-wasmldr/workflows/test/badge.svg)](https://github.com/enarx/enarx-wasmldr/actions?query=workflow%3A%22test%22) +[![Average time to resolve an issue](https://isitmaintained.com/badge/resolution/enarx/enarx-wasmldr.svg)](https://isitmaintained.com/project/enarx/enarx-wasmldr "Average time to resolve an issue") +[![Percentage of issues still open](https://isitmaintained.com/badge/open/enarx/enarx-wasmldr.svg)](https://isitmaintained.com/project/enarx/enarx-wasmldr "Percentage of issues still open") +![Maintenance](https://img.shields.io/badge/maintenance-activly--developed-brightgreen.svg) + +# enarx-wasmldr + +The Enarx Keep runtime binary. + +It can be used to run a Wasm file with given command-line +arguments and environment variables. + +### Example invocation + +```console +$ wat2wasm fixtures/return_1.wat +$ RUST_LOG=enarx_wasmldr=info RUST_BACKTRACE=1 cargo run return_1.wasm + Finished dev [unoptimized + debuginfo] target(s) in 0.07s + Running `target/x86_64-unknown-linux-musl/debug/enarx-wasmldr target/x86_64-unknown-linux-musl/debug/build/enarx-wasmldr-c374d181f6abdda0/out/fixtures/return_1.wasm` +[2020-09-10T17:56:18Z INFO enarx_wasmldr] got result: [ + I32( + 1, + ), + ] +``` + +On Unix platforms, the command can also read the workload from the +file descriptor (3): +```console +$ RUST_LOG=enarx_wasmldr=info RUST_BACKTRACE=1 cargo run 3< return_1.wasm +``` + + +License: Apache-2.0 diff --git a/internal/wasmldr/build.rs b/internal/wasmldr/build.rs new file mode 100644 index 00000000..a70310fc --- /dev/null +++ b/internal/wasmldr/build.rs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::path::Path; + +fn main() { + let in_dir = Path::new("fixtures"); + let out_dir = + std::env::var_os("OUT_DIR").expect("The OUT_DIR environment variable must be set"); + let out_dir = Path::new(&out_dir).join("fixtures"); + std::fs::create_dir_all(&out_dir).expect("Can't create output directory"); + + for entry in in_dir.read_dir().unwrap().flatten() { + let wat = entry.path(); + match wat.extension() { + Some(ext) if ext == "wat" => { + let wasm = out_dir + .join(wat.file_name().unwrap()) + .with_extension("wasm"); + let binary = wat::parse_file(&wat).expect("Can't parse wat file"); + std::fs::write(&wasm, &binary).expect("Can't write wasm file"); + println!("cargo:rerun-if-changed={}", &wat.display()); + } + _ => {} + } + } +} diff --git a/internal/wasmldr/deny.toml b/internal/wasmldr/deny.toml new file mode 100644 index 00000000..aceefdab --- /dev/null +++ b/internal/wasmldr/deny.toml @@ -0,0 +1,52 @@ +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# The lint level for crates which do not have a detectable license +unlicensed = "deny" +# List of explictly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. +allow = [ + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "CC0-1.0", + "ISC", + "MIT", + "OpenSSL", + "Zlib", +] +# Lint level for licenses considered copyleft +copyleft = "deny" +# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses +# * both - The license will be approved if it is both OSI-approved *AND* FSF +# * either - The license will be approved if it is either OSI-approved *OR* FSF +# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF +# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved +# * neither - This predicate is ignored and the default lint level is used +allow-osi-fsf-free = "either" +# Lint level used when no other predicates are matched +# 1. License isn't in the allow or deny lists +# 2. License isn't copyleft +# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" +default = "deny" +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.8 + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries +ignore = false + +[[licenses.clarify]] +name = "ring" +version = "*" +expression = "MIT AND ISC AND OpenSSL" +license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 } +] diff --git a/internal/wasmldr/fixtures/bundle/config.yaml b/internal/wasmldr/fixtures/bundle/config.yaml new file mode 100644 index 00000000..6b4695fb --- /dev/null +++ b/internal/wasmldr/fixtures/bundle/config.yaml @@ -0,0 +1,8 @@ +--- +stdio: + stdin: + bundle: stdin.txt + stdout: + file: stdout.txt + stderr: + file: stderr.txt diff --git a/internal/wasmldr/fixtures/bundle/stdin.txt b/internal/wasmldr/fixtures/bundle/stdin.txt new file mode 100644 index 00000000..cd087558 --- /dev/null +++ b/internal/wasmldr/fixtures/bundle/stdin.txt @@ -0,0 +1 @@ +Hello world! diff --git a/internal/wasmldr/fixtures/hello_wasi_snapshot1.wat b/internal/wasmldr/fixtures/hello_wasi_snapshot1.wat new file mode 100644 index 00000000..8fbea6f6 --- /dev/null +++ b/internal/wasmldr/fixtures/hello_wasi_snapshot1.wat @@ -0,0 +1,29 @@ +;;; SPDX-License-Identifier: Apache-2.0 +;;; Copied from wasmtime's test suite under Apache-2.0 license. + +;;; Write "Hello, world!\n" to stdout. +(module + (import "wasi_snapshot_preview1" "proc_exit" + (func $__wasi_proc_exit (param i32))) + (import "wasi_snapshot_preview1" "fd_write" + (func $__wasi_fd_write (param i32 i32 i32 i32) (result i32))) + (func $_start + (i32.store (i32.const 24) (i32.const 14)) + (i32.store (i32.const 20) (i32.const 0)) + (block + (br_if 0 + (call $__wasi_fd_write + (i32.const 1) + (i32.const 20) + (i32.const 1) + (i32.const 16))) + (br_if 0 (i32.ne (i32.load (i32.const 16)) (i32.const 14))) + (br 1) + ) + (call $__wasi_proc_exit (i32.const 1)) + ) + (memory 1) + (export "memory" (memory 0)) + (export "_start" (func $_start)) + (data (i32.const 0) "Hello, world!\0a") +) diff --git a/internal/wasmldr/fixtures/no_export.wat b/internal/wasmldr/fixtures/no_export.wat new file mode 100644 index 00000000..b6a48adf --- /dev/null +++ b/internal/wasmldr/fixtures/no_export.wat @@ -0,0 +1,5 @@ +;;; SPDX-License-Identifier: Apache-2.0 + +(module + (memory (export "") 1) +) diff --git a/internal/wasmldr/fixtures/return_1.wat b/internal/wasmldr/fixtures/return_1.wat new file mode 100644 index 00000000..070f8de0 --- /dev/null +++ b/internal/wasmldr/fixtures/return_1.wat @@ -0,0 +1,5 @@ +;;; SPDX-License-Identifier: Apache-2.0 + +(module + (func (export "") (result i32) + i32.const 1)) diff --git a/internal/wasmldr/fixtures/wasi_snapshot1.wat b/internal/wasmldr/fixtures/wasi_snapshot1.wat new file mode 100644 index 00000000..1f0e1d52 --- /dev/null +++ b/internal/wasmldr/fixtures/wasi_snapshot1.wat @@ -0,0 +1,16 @@ +;;; SPDX-License-Identifier: Apache-2.0 + +;;; Return the number of command-line arguments +(module + (import "wasi_snapshot_preview1" "args_sizes_get" + (func $__wasi_args_sizes_get (param i32 i32) (result i32))) + (func (export "_start") (result i32) + (i32.store (i32.const 0) (i32.const 0)) + (i32.store (i32.const 4) (i32.const 0)) + (call $__wasi_args_sizes_get (i32.const 0) (i32.const 4)) + drop + (i32.load (i32.const 0)) + ) + (memory 1) + (export "memory" (memory 0)) +) diff --git a/internal/wasmldr/src/bundle.rs b/internal/wasmldr/src/bundle.rs new file mode 100644 index 00000000..8d287c27 --- /dev/null +++ b/internal/wasmldr/src/bundle.rs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::io::{ErrorKind, Read, Result}; +use wasmparser::{Chunk, Parser, Payload::*}; + +const RESOURCES_SECTION: &str = ".enarx.resources"; + +pub fn parse( + mut input: impl Read, + mut handle_custom: impl FnMut(&[u8]) -> Result<()>, + mut handle_default: impl FnMut(&[u8]) -> Result<()>, +) -> Result<()> { + let mut buf = Vec::new(); + let mut parser = Parser::new(0); + let mut eof = false; + let mut stack = Vec::new(); + + loop { + let (payload, consumed) = match parser.parse(&buf, eof).or(Err(ErrorKind::InvalidInput))? { + Chunk::NeedMoreData(hint) => { + assert!(!eof); // otherwise an error would be returned + + // Use the hint to preallocate more space, then read + // some more data into our buffer. + // + // Note that the buffer management here is not ideal, + // but it's compact enough to fit in an example! + let len = buf.len(); + buf.extend((0..hint).map(|_| 0u8)); + let n = input.read(&mut buf[len..])?; + buf.truncate(len + n); + eof = n == 0; + continue; + } + + Chunk::Parsed { consumed, payload } => (payload, consumed), + }; + + match payload { + CustomSection { name, data, .. } => { + if name == RESOURCES_SECTION { + handle_custom(data)?; + } else { + handle_default(&buf[..consumed])?; + } + } + // When parsing nested modules we need to switch which + // `Parser` we're using. + ModuleSectionEntry { + parser: subparser, .. + } => { + stack.push(parser); + parser = subparser; + } + End => { + if let Some(parent_parser) = stack.pop() { + parser = parent_parser; + } else { + break; + } + } + _ => { + handle_default(&buf[..consumed])?; + } + } + + // once we're done processing the payload we can forget the + // original. + buf.drain(..consumed); + } + Ok(()) +} diff --git a/internal/wasmldr/src/cli.rs b/internal/wasmldr/src/cli.rs new file mode 100644 index 00000000..f41c825c --- /dev/null +++ b/internal/wasmldr/src/cli.rs @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 + +#![allow(missing_docs, unused_variables)] // This is a work-in-progress, so... + +use structopt::{clap::AppSettings, StructOpt}; + +use anyhow::{bail, Result}; +use std::path::PathBuf; + +// The main StructOpt for running `wasmldr` directly +#[derive(StructOpt, Debug)] +#[structopt(setting=AppSettings::TrailingVarArg)] +pub struct RunOptions { + /// Pass an environment variable to the program + #[structopt( + short = "e", + long = "env", + number_of_values = 1, + value_name = "NAME=VAL", + parse(try_from_str=parse_env_var), + )] + pub envs: Vec<(String, String)>, + + // TODO: --inherit-env + // TODO: --stdin, --stdout, --stderr + /// Path of the WebAssembly module to run + #[structopt(index = 1, required = true, value_name = "MODULE", parse(from_os_str))] + pub module: PathBuf, + + // NOTE: this has to come last for TrailingVarArg + /// Arguments to pass to the WebAssembly module + #[structopt(value_name = "ARGS")] + pub args: Vec, +} + +fn parse_env_var(s: &str) -> Result<(String, String)> { + let parts: Vec<&str> = s.splitn(2, '=').collect(); + if parts.len() != 2 { + bail!("must be of the form `NAME=VAL`"); + } + Ok((parts[0].to_owned(), parts[1].to_owned())) +} diff --git a/internal/wasmldr/src/main.rs b/internal/wasmldr/src/main.rs new file mode 100644 index 00000000..0c542085 --- /dev/null +++ b/internal/wasmldr/src/main.rs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The Enarx Keep runtime binary. +//! +//! It can be used to run a Wasm file with given command-line +//! arguments and environment variables. +//! +//! ## Example invocation +//! +//! ```console +//! $ wat2wasm fixtures/return_1.wat +//! $ RUST_LOG=enarx_wasmldr=info RUST_BACKTRACE=1 cargo run return_1.wasm +//! Finished dev [unoptimized + debuginfo] target(s) in 0.07s +//! Running `target/x86_64-unknown-linux-musl/debug/enarx-wasmldr target/x86_64-unknown-linux-musl/debug/build/enarx-wasmldr-c374d181f6abdda0/out/fixtures/return_1.wasm` +//! [2020-09-10T17:56:18Z INFO enarx_wasmldr] got result: [ +//! I32( +//! 1, +//! ), +//! ] +//! ``` +//! +//! On Unix platforms, the command can also read the workload from the +//! file descriptor (3): +//! ```console +//! $ RUST_LOG=enarx_wasmldr=info RUST_BACKTRACE=1 cargo run 3< return_1.wasm +//! ``` +//! +#![deny(missing_docs)] +#![deny(clippy::all)] + +mod cli; +mod workload; + +use log::{debug, info}; +use structopt::StructOpt; + +use std::fs::File; +use std::io::Read; + +fn main() { + // Initialize the logger, taking settings from the default env vars + env_logger::Builder::from_default_env().init(); + + info!("version {} starting up", env!("CARGO_PKG_VERSION")); + + debug!("parsing argv"); + let opts = cli::RunOptions::from_args(); + info!("opts: {:#?}", opts); + + info!("reading {:?}", opts.module); + // TODO: don't just panic here... + let mut reader = File::open(&opts.module).expect("Unable to open file"); + + let mut bytes = Vec::new(); + reader + .read_to_end(&mut bytes) + .expect("Failed to load workload"); + + // FUTURE: measure opts.envs, opts.args, opts.wasm_features + // FUTURE: fork() the workload off into a separate memory space + + info!("running workload"); + // TODO: pass opts.wasm_features + let result = workload::run(bytes, opts.args, opts.envs).expect("Failed to run workload"); + info!("got result: {:#?}", result); + // TODO: exit with the resulting code, if the result is a return code + // FUTURE: produce attestation report here +} diff --git a/internal/wasmldr/src/workload.rs b/internal/wasmldr/src/workload.rs new file mode 100644 index 00000000..a8bff733 --- /dev/null +++ b/internal/wasmldr/src/workload.rs @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: Apache-2.0 + +use log::debug; +use wasmtime_wasi::sync::WasiCtxBuilder; + +/// The error codes of workload execution. +// clippy doesn't like how "ConfigurationError" ends with "Error", so.. +#[allow(clippy::enum_variant_names)] +// TODO: use clippy-approved names when we rework these and refactor run(); +// until then +#[derive(Debug)] +pub enum Error { + /// configuration error + ConfigurationError, + /// export not found + ExportNotFound, + /// module instantiation failed + InstantiationFailed, + /// call failed + CallFailed, + /// I/O error + IoError(std::io::Error), + /// WASI error + WASIError(wasmtime_wasi::Error), + /// Arguments or environment too large + StringTableError, +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Self::IoError(err) + } +} + +impl From for Error { + fn from(err: wasmtime_wasi::Error) -> Self { + Self::WASIError(err) + } +} + +/// Result type used throughout the library. +pub type Result = std::result::Result; + +/// Runs a WebAssembly workload. +// TODO: refactor this into multiple steps +// Since we're not bundling the launch/deployment config into `bytes`, the +// naive solution would just be to add new arguments for those things, like +// WasmFeatures, stdio handling, etc - but that gets messy quick. +// Instead we should probably refactor this into distinct steps, each with +// its own config options (and error variants - see above). +pub fn run, U: AsRef>( + bytes: impl AsRef<[u8]>, + args: impl IntoIterator, + envs: impl IntoIterator, +) -> Result> { + debug!("configuring wasmtime engine"); + let mut config = wasmtime::Config::new(); + // Support module-linking (https://github.com/webassembly/module-linking) + config.wasm_module_linking(true); + // module-linking requires multi-memory + config.wasm_multi_memory(true); + // Prefer dynamic memory allocation style over static memory + config.static_memory_maximum_size(0); + let engine = wasmtime::Engine::new(&config).or(Err(Error::ConfigurationError))?; + + debug!("instantiating wasmtime linker"); + let mut linker = wasmtime::Linker::new(&engine); + + // TODO: read config, set up filehandles & sockets, etc etc + + debug!("adding WASI to linker"); + wasmtime_wasi::add_to_linker(&mut linker, |s| s)?; + + debug!("creating WASI context"); + let mut wasi = WasiCtxBuilder::new(); + for arg in args { + wasi = wasi.arg(arg.as_ref()).or(Err(Error::StringTableError))?; + } + for kv in envs { + wasi = wasi + .env(kv.0.as_ref(), kv.1.as_ref()) + .or(Err(Error::StringTableError))?; + } + + debug!("creating wasmtime Store"); + let mut store = wasmtime::Store::new(&engine, wasi.build()); + + debug!("instantiating module from bytes"); + let module = wasmtime::Module::from_binary(&engine, bytes.as_ref())?; + //.or(Err(Error::InstantiationFailed))?; + + debug!("adding module to store"); + linker + .module(&mut store, "", &module) + .or(Err(Error::InstantiationFailed))?; + + // TODO: use the --invoke FUNCTION name, if any + debug!("getting module's default function"); + let func = linker + .get_default(&mut store, "") + .or(Err(Error::ExportNotFound))?; + + debug!("calling function"); + func.call(store, Default::default()) + .or(Err(Error::CallFailed)) +} + +#[cfg(test)] +pub(crate) mod test { + use crate::workload; + use std::iter::empty; + + #[test] + fn workload_run_return_1() { + let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/fixtures/return_1.wasm")).to_vec(); + + let results: Vec = + workload::run(&bytes, empty::(), empty::<(String, String)>()) + .unwrap() + .iter() + .map(|v| v.unwrap_i32()) + .collect(); + + assert_eq!(results, vec![1]); + } + + #[test] + fn workload_run_no_export() { + let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/fixtures/no_export.wasm")).to_vec(); + + match workload::run(&bytes, empty::(), empty::<(String, String)>()) { + Err(workload::Error::ExportNotFound) => {} + _ => panic!("unexpected error"), + }; + } + + #[test] + fn workload_run_wasi_snapshot1() { + let bytes = + include_bytes!(concat!(env!("OUT_DIR"), "/fixtures/wasi_snapshot1.wasm")).to_vec(); + + let results: Vec = workload::run( + &bytes, + vec!["a".to_string(), "b".to_string(), "c".to_string()], + vec![("k", "v")], + ) + .unwrap() + .iter() + .map(|v| v.unwrap_i32()) + .collect(); + + assert_eq!(results, vec![3]); + } + + #[cfg(bundle_tests)] + #[test] + fn workload_run_bundled() { + let bytes = include_bytes!(concat!( + env!("OUT_DIR"), + "/fixtures/hello_wasi_snapshot1.bundled.wasm" + )) + .to_vec(); + + workload::run(&bytes, empty::<&str>(), empty::<(&str, &str)>()).unwrap(); + + let output = std::fs::read("stdout.txt").unwrap(); + assert_eq!(output, "Hello, world!\n".to_string().into_bytes()); + } +} From 2b01337a522ad09a2e203ee1fcc33ec10698fc3c Mon Sep 17 00:00:00 2001 From: Will Woods Date: Thu, 30 Sep 2021 19:17:46 -0400 Subject: [PATCH 03/11] Add Workldr trait, use it to load internal wasmldr In parallel with the Backend trait, this commit adds a Workldr trait for handling `wasmldr` (and any future workldr implementations.) We've also got a naive function for picking the "best" workldr (there's only one, so that's easy) and using that for `exec` if nothing was passed on the CLI. Signed-off-by: Will Woods --- src/main.rs | 33 ++++++++++++++++++++++++++------ src/workldr/mod.rs | 39 ++++++++++++++++++++++++++++++++++++++ src/workldr/wasmldr/mod.rs | 31 ++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 src/workldr/mod.rs create mode 100644 src/workldr/wasmldr/mod.rs diff --git a/src/main.rs b/src/main.rs index 1c0a702a..a43479e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,8 +58,10 @@ mod backend; mod protobuf; +mod workldr; use backend::{Backend, Command}; +use workldr::Workldr; use std::convert::TryInto; use std::path::PathBuf; @@ -78,7 +80,7 @@ struct Info {} #[derive(StructOpt)] struct Exec { /// The payload to run inside the keep - code: PathBuf, + code: Option, } #[derive(StructOpt)] @@ -97,9 +99,24 @@ fn main() -> Result<()> { Box::new(backend::kvm::Backend), ]; + let workldrs: &[Box] = &[ + #[cfg(feature = "wasmldr")] + Box::new(workldr::wasmldr::Wasmldr), + ]; + match Options::from_args() { Options::Info(_) => info(backends), - Options::Exec(e) => exec(backends, e), + Options::Exec(e) => { + // FUTURE: accept tenant-provided shim, or fall back to builtin.. + let backend = backend(backends); + let shim_bytes = backend.shim(); + if let Some(path) = e.code { + let map = mmarinus::Kind::Private.load::(&path)?; + exec(backend, shim_bytes, map) + } else { + exec(backend, shim_bytes, workldr(workldrs).exec()) + } + } } } @@ -151,12 +168,16 @@ fn backend(backends: &[Box]) -> &dyn Backend { } } -fn exec(backends: &[Box], opts: Exec) -> Result<()> { - let backend = backend(backends); +#[inline] +fn workldr(workldrs: &[Box]) -> &dyn Workldr { + // NOTE: this is stupid, but we only have one workldr, so... ¯\_(ツ)_/¯ + &*workldrs[0] +} - let map = mmarinus::Kind::Private.load::(&opts.code)?; +fn exec(backend: &dyn Backend, shim: impl AsRef<[u8]>, exec: impl AsRef<[u8]>) -> Result<()> { + //let map = mmarinus::Kind::Private.load::(&opts.code)?; - let keep = backend.keep(backend.shim(), &map)?; + let keep = backend.keep(shim.as_ref(), exec.as_ref())?; let mut thread = keep.clone().spawn()?.unwrap(); loop { match thread.enter()? { diff --git a/src/workldr/mod.rs b/src/workldr/mod.rs new file mode 100644 index 00000000..a4d24aab --- /dev/null +++ b/src/workldr/mod.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 + +// FUTURE: right now we only have one Workldr, `wasmldr`. +// In the future there may be other workload types - in theory we can run +// any static PIE ELF binary. We could have a Lua interpreter, or a +// JavaScript interpreter, or whatever. +// So there's two parts to this trait - call them KeepSetup and Engine. +// +// KeepSetup is the part that actually sets up the Keep for the Workload, +// which might involve setting up network sockets, storage devices, etc. +// This part must be implemented by any Workldr, since we want the +// Enarx environment to be platform-agnostic. +// +// Engine is the (workload-specific) portion that actually interprets or +// executes the workload. It's responsible for taking the sockets / devices +// etc. that were set up by KeepSetup and making them usable in a way that +// the workload will understand. +// +// So: someday we might want to split this into two traits, and we might +// have multiple Workldrs for different languages/environments, and we +// might need to examine the workload and determine which Workldr is +// the right one to use. But first... we gotta make wasmldr work. + +#[cfg(feature = "wasmldr")] +pub mod wasmldr; + +/// A trait for the "Workloader" - shortened to Workldr, also known as "exec" +/// (as in Backend::keep(shim, exec) [q.v.]) and formerly known as the "code" +/// layer. This is the part that runs inside the keep, prepares the workload +/// environment, and then actually executes the tenant's workload. +/// +/// Basically, this is a generic view of wasmldr. +pub trait Workldr { + /// The name of the Workldr + fn name(&self) -> &'static str; + + /// The builtin Workldr binary (e.g. wasmldr) + fn exec(&self) -> &'static [u8]; +} diff --git a/src/workldr/wasmldr/mod.rs b/src/workldr/wasmldr/mod.rs new file mode 100644 index 00000000..e7d868ab --- /dev/null +++ b/src/workldr/wasmldr/mod.rs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 + +pub struct Wasmldr; + +impl crate::workldr::Workldr for Wasmldr { + #[inline] + fn name(&self) -> &'static str { + "wasmldr" + } + + #[inline] + fn exec(&self) -> &'static [u8] { + include_bytes!(concat!(env!("OUT_DIR"), "/bin/wasmldr")) + } +} + +#[cfg(test)] +pub(crate) mod test { + use super::Wasmldr; + use crate::workldr::Workldr; + + // Check that wasmldr.exec() gives us the binary contents + #[test] + fn is_builtin() { + let wasmldr = Box::new(Wasmldr); + assert_eq!( + wasmldr.exec(), + include_bytes!(concat!(env!("OUT_DIR"), "/bin/wasmldr")) + ); + } +} From 9a9529ec368be45fc4b473313cf15ae901f6ae6f Mon Sep 17 00:00:00 2001 From: Will Woods Date: Thu, 30 Sep 2021 20:12:39 -0400 Subject: [PATCH 04/11] wasmldr: inherit stdio from calling process (for now) We want stdout/stderr to default to a secure channel. /dev/null is extremely secure, but not exactly developer-friendly. (How are you supposed to print "Hello World!" without stdout?) So, for now, we'll default to inheriting stdio from the calling process. But we're also going to print a warning - with stupid unicode hotdogs - to higlight the fact that this is developer-only behavior, and that it should definitely be removed before release. Signed-off-by: Will Woods --- internal/wasmldr/src/workload.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/wasmldr/src/workload.rs b/internal/wasmldr/src/workload.rs index a8bff733..b17b8faf 100644 --- a/internal/wasmldr/src/workload.rs +++ b/internal/wasmldr/src/workload.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -use log::debug; +use log::{debug, warn}; use wasmtime_wasi::sync::WasiCtxBuilder; /// The error codes of workload execution. @@ -82,12 +82,16 @@ pub fn run, U: AsRef>( .or(Err(Error::StringTableError))?; } + // TODO: plaintext stdio to/from the (untrusted!) host system isn't a + // secure default behavior. But.. we don't have any *trusted* I/O yet, so.. + warn!("🌭DEV-ONLY BUILD🌭: inheriting stdio from calling process"); + wasi = wasi.inherit_stdio(); + debug!("creating wasmtime Store"); let mut store = wasmtime::Store::new(&engine, wasi.build()); debug!("instantiating module from bytes"); let module = wasmtime::Module::from_binary(&engine, bytes.as_ref())?; - //.or(Err(Error::InstantiationFailed))?; debug!("adding module to store"); linker From 9683ddbb073fa54defb9a3f17c41b7e4436c0668 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Thu, 30 Sep 2021 20:54:09 -0400 Subject: [PATCH 05/11] wasmldr: read module from fd 3 if no path given This commit makes wasmldr try to read the wasm module from fd3 if no file path was given. (Since keepldr doesn't currently pass arguments to wasmldr, this will be the default behavior.) This means you should be able to run a .wasm module in a keep by doing: cargo run -- exec 3< hello.wasm This behavior will likely change soon, but hey.. Hello World! Signed-off-by: Will Woods --- internal/wasmldr/src/cli.rs | 4 ++-- internal/wasmldr/src/main.rs | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/internal/wasmldr/src/cli.rs b/internal/wasmldr/src/cli.rs index f41c825c..7f70aef6 100644 --- a/internal/wasmldr/src/cli.rs +++ b/internal/wasmldr/src/cli.rs @@ -24,8 +24,8 @@ pub struct RunOptions { // TODO: --inherit-env // TODO: --stdin, --stdout, --stderr /// Path of the WebAssembly module to run - #[structopt(index = 1, required = true, value_name = "MODULE", parse(from_os_str))] - pub module: PathBuf, + #[structopt(index = 1, value_name = "MODULE", parse(from_os_str))] + pub module: Option, // NOTE: this has to come last for TrailingVarArg /// Arguments to pass to the WebAssembly module diff --git a/internal/wasmldr/src/main.rs b/internal/wasmldr/src/main.rs index 0c542085..8160d117 100644 --- a/internal/wasmldr/src/main.rs +++ b/internal/wasmldr/src/main.rs @@ -36,6 +36,7 @@ use structopt::StructOpt; use std::fs::File; use std::io::Read; +use std::os::unix::io::{FromRawFd, RawFd}; fn main() { // Initialize the logger, taking settings from the default env vars @@ -47,9 +48,13 @@ fn main() { let opts = cli::RunOptions::from_args(); info!("opts: {:#?}", opts); - info!("reading {:?}", opts.module); - // TODO: don't just panic here... - let mut reader = File::open(&opts.module).expect("Unable to open file"); + let mut reader = if let Some(module) = opts.module { + info!("reading module from {:?}", &module); + File::open(&module).expect("Unable to open file") + } else { + info!("reading module from fd 3"); + unsafe { File::from_raw_fd(RawFd::from(3)) } + }; let mut bytes = Vec::new(); reader From efcc3dd97e3bdcc06bf5bd4001af8575c7bf4850 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Mon, 4 Oct 2021 11:07:33 -0400 Subject: [PATCH 06/11] sev: bump size to fit wasmldr `_ENARX_EXEC_LEN = 4M` isn't big enough for wasmldr, which currently weighs in at ~4.6MB. This value is an upper limit, not a static allocation, so we can safely bump it to 128MB. Signed-off-by: Will Woods --- internal/shim-sev/layout.ld | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/shim-sev/layout.ld b/internal/shim-sev/layout.ld index d09c9621..8c9e5d9b 100644 --- a/internal/shim-sev/layout.ld +++ b/internal/shim-sev/layout.ld @@ -37,7 +37,7 @@ reset_vector = 0xFFFFF000; _ENARX_SHIM_START = reset_vector; _ENARX_SALLYPORT_START = _ENARX_SHIM_START - _ENARX_SALLYPORT_SIZE - 2 * CONSTANT(COMMONPAGESIZE); _ENARX_SALLYPORT_END = _ENARX_SALLYPORT_START + _ENARX_SALLYPORT_SIZE; -_ENARX_EXEC_LEN = 4M; +_ENARX_EXEC_LEN = 128M; ASSERT((_ENARX_SHIM_START >= (3 * 0x40000000)), "SHIM_START is too low for current initial identity page table") ASSERT((_ENARX_EXEC_START < (6 * 0x40000000)), "SHIM is too large for current initial identity page table") From b5a728fff01b5cdcfc27e0a6ea481125379628fd Mon Sep 17 00:00:00 2001 From: Will Woods Date: Mon, 4 Oct 2021 11:17:18 -0400 Subject: [PATCH 07/11] rust-toolchain: pin to nightly-2021-09-30 Rust nightly started giving us compilation problems, so pin to a known-good version until it's fixed/reverted. See https://github.com/rust-lang/rust/issues/89432 for details. Signed-off-by: Will Woods --- rust-toolchain.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6a9f06c2..c79c33cb 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,5 @@ [toolchain] -channel = "nightly" +# Pinned until https://github.com/rust-lang/rust/issues/89432 is fixed +channel = "nightly-2021-09-30" targets = ["x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"] profile = "minimal" From d3135ccb34f9dc32b45b6fddc9d4caf404ce5721 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Mon, 4 Oct 2021 11:57:49 -0400 Subject: [PATCH 08/11] tests: move common code to tests/common/mod.rs Move commonly-used integration_tests code (assert_eq_slices(), run_test(), and a bunch of consts) out into a common test module so that other integration tests can use them. Also, split the run_test() function into keepldr_exec() and check_output(), which makes the names a little clearer and also means I don't have to rewrite the whole thing if I want to tweak the Command args to handle wasmldr tests. Signed-off-by: Will Woods --- tests/common/mod.rs | 138 +++++++++++++++++++++++++++++++++++++ tests/integration_tests.rs | 118 +------------------------------ 2 files changed, 140 insertions(+), 116 deletions(-) create mode 100644 tests/common/mod.rs diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 00000000..c27a70f7 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 + +use process_control::{ChildExt, Output, Timeout}; +use std::io::Write; +use std::path::Path; +use std::process::{Command, Stdio}; +use std::time::Duration; + +pub const CRATE: &str = env!("CARGO_MANIFEST_DIR"); +pub const KEEP_BIN: &str = env!("CARGO_BIN_EXE_enarx-keepldr"); +pub const OUT_DIR: &str = env!("OUT_DIR"); +pub const TEST_BINS_OUT: &str = "bin"; +pub const TIMEOUT_SECS: u64 = 10; +pub const MAX_ASSERT_ELEMENTS: usize = 100; + +pub fn assert_eq_slices(expected_output: &[u8], output: &[u8], what: &str) { + let max_len = usize::min(output.len(), expected_output.len()); + let max_len = max_len.min(MAX_ASSERT_ELEMENTS); + assert_eq!( + output[..max_len], + expected_output[..max_len], + "Expected contents of {} differs", + what + ); + assert_eq!( + output.len(), + expected_output.len(), + "Expected length of {} differs", + what + ); + assert_eq!( + output, expected_output, + "Expected contents of {} differs", + what + ); +} + +/// Returns a handle to a child process through which output (stdout, stderr) can +/// be accessed. +pub fn keepldr_exec<'a>(bin: &str, input: impl Into>) -> Output { + let bin_path = Path::new(CRATE).join(OUT_DIR).join(TEST_BINS_OUT).join(bin); + + let mut child = Command::new(&String::from(KEEP_BIN)) + .current_dir(CRATE) + .arg("exec") + .arg(bin_path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap_or_else(|e| panic!("failed to run `{}`: {:#?}", bin, e)); + + if let Some(input) = input.into() { + child + .stdin + .as_mut() + .unwrap() + .write_all(input) + .expect("failed to write stdin to child"); + + drop(child.stdin.take()); + } + + let output = child + .with_output_timeout(Duration::from_secs(TIMEOUT_SECS)) + .terminating() + .wait() + .unwrap_or_else(|e| panic!("failed to run `{}`: {:#?}", bin, e)) + .unwrap_or_else(|| panic!("process `{}` timed out", bin)); + + assert!( + output.status.code().is_some(), + "process `{}` terminated by signal {:?}", + bin, + output.status.signal() + ); + + output +} + +pub fn check_output<'a>( + output: &Output, + expected_status: i32, + expected_stdout: impl Into>, + expected_stderr: impl Into>, +) { + let expected_stdout = expected_stdout.into(); + let expected_stderr = expected_stderr.into(); + + // Output potential error messages + if expected_stderr.is_none() && !output.stderr.is_empty() { + let _ = std::io::stderr().write_all(&output.stderr); + } + + if let Some(expected_stdout) = expected_stdout { + if output.stdout.len() < MAX_ASSERT_ELEMENTS && expected_stdout.len() < MAX_ASSERT_ELEMENTS + { + assert_eq!( + output.stdout, expected_stdout, + "Expected contents of stdout output differs" + ); + } else { + assert_eq_slices(expected_stdout, &output.stdout, "stdout output"); + } + } + + if let Some(expected_stderr) = expected_stderr { + if output.stderr.len() < MAX_ASSERT_ELEMENTS && expected_stderr.len() < MAX_ASSERT_ELEMENTS + { + assert_eq!( + output.stderr, expected_stderr, + "Expected contents of stderr output differs." + ); + } else { + assert_eq_slices(expected_stderr, &output.stderr, "stderr output"); + } + } + + assert_eq!( + output.status.code().unwrap(), + expected_status as i64, + "Expected exit status differs." + ); +} + +/// Returns a handle to a child process through which output (stdout, stderr) can +/// be accessed. +pub fn run_test<'a>( + bin: &str, + status: i32, + input: impl Into>, + expected_stdout: impl Into>, + expected_stderr: impl Into>, +) -> Output { + let output = keepldr_exec(bin, input); + check_output(&output, status.into(), expected_stdout, expected_stderr); + output +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 41465408..5e9e0bdf 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -6,130 +6,16 @@ use std::io::{Read, Write}; use std::mem::{size_of, MaybeUninit}; use std::os::unix::ffi::OsStrExt; use std::os::unix::net::{UnixListener, UnixStream}; -use std::path::Path; -use std::process::{Command, Stdio}; use std::slice::from_raw_parts_mut; use std::thread; use std::time::Duration; -use process_control::{ChildExt, Output, Timeout}; use serial_test::serial; use std::sync::Arc; use tempdir::TempDir; -const CRATE: &str = env!("CARGO_MANIFEST_DIR"); -const KEEP_BIN: &str = env!("CARGO_BIN_EXE_enarx-keepldr"); -const OUT_DIR: &str = env!("OUT_DIR"); -const TEST_BINS_OUT: &str = "bin"; -const TIMEOUT_SECS: u64 = 10; -const MAX_ASSERT_ELEMENTS: usize = 100; - -fn assert_eq_slices(expected_output: &[u8], output: &[u8], what: &str) { - let max_len = usize::min(output.len(), expected_output.len()); - let max_len = max_len.min(MAX_ASSERT_ELEMENTS); - assert_eq!( - output[..max_len], - expected_output[..max_len], - "Expected contents of {} differs", - what - ); - assert_eq!( - output.len(), - expected_output.len(), - "Expected length of {} differs", - what - ); - assert_eq!( - output, expected_output, - "Expected contents of {} differs", - what - ); -} - -/// Returns a handle to a child process through which output (stdout, stderr) can -/// be accessed. -fn run_test<'a>( - bin: &str, - status: i32, - input: impl Into>, - expected_stdout: impl Into>, - expected_stderr: impl Into>, -) -> Output { - let expected_stdout = expected_stdout.into(); - let expected_stderr = expected_stderr.into(); - let bin_path = Path::new(CRATE).join(OUT_DIR).join(TEST_BINS_OUT).join(bin); - - let mut child = Command::new(&String::from(KEEP_BIN)) - .current_dir(CRATE) - .arg("exec") - .arg(bin_path) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap_or_else(|e| panic!("failed to run `{}`: {:#?}", bin, e)); - - if let Some(input) = input.into() { - child - .stdin - .as_mut() - .unwrap() - .write_all(input) - .expect("failed to write stdin to child"); - - drop(child.stdin.take()); - } - - let output = child - .with_output_timeout(Duration::from_secs(TIMEOUT_SECS)) - .terminating() - .wait() - .unwrap_or_else(|e| panic!("failed to run `{}`: {:#?}", bin, e)) - .unwrap_or_else(|| panic!("process `{}` timed out", bin)); - - let exit_status = output.status.code().unwrap_or_else(|| { - panic!( - "process `{}` terminated by signal {:?}", - bin, - output.status.signal() - ) - }); - - // Output potential error messages - if expected_stderr.is_none() && !output.stderr.is_empty() { - let _ = std::io::stderr().write_all(&output.stderr); - } - - if let Some(expected_stdout) = expected_stdout { - if output.stdout.len() < MAX_ASSERT_ELEMENTS && expected_stdout.len() < MAX_ASSERT_ELEMENTS - { - assert_eq!( - output.stdout, expected_stdout, - "Expected contents of stdout output differs" - ); - } else { - assert_eq_slices(expected_stdout, &output.stdout, "stdout output"); - } - } - - if let Some(expected_stderr) = expected_stderr { - if output.stderr.len() < MAX_ASSERT_ELEMENTS && expected_stderr.len() < MAX_ASSERT_ELEMENTS - { - assert_eq!( - output.stderr, expected_stderr, - "Expected contents of stderr output differs." - ); - } else { - assert_eq_slices(expected_stderr, &output.stderr, "stderr output"); - } - } - - if exit_status != status as i64 { - assert_eq!(exit_status, status as i64, "Expected exit status differs."); - } - - output -} +mod common; +use common::{assert_eq_slices, run_test}; fn read_item(mut rdr: impl Read) -> std::io::Result { let mut item = MaybeUninit::uninit(); From bcfcfa72000b4cc80b1d68d8c14bf3246cc7bcd8 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Mon, 4 Oct 2021 18:03:10 -0400 Subject: [PATCH 09/11] wasmldr: exit with a useful exit code Currently, when wasmldr exits due to ExportNotFound, it returns an exit code of... 101. Why 101? Great question! I have absolutely no idea. In order to make things a bit more sensible, I've defined different exit codes for different errors. They roughly match FreeBSD's "preferable" exit codes, as defined in `sysexits.h` - see [sysexits(3)] for more info. [sysexits(3)]: https://www.freebsd.org/cgi/man.cgi?sektion=3&query=sysexits Signed-off-by: Will Woods --- internal/wasmldr/src/main.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/internal/wasmldr/src/main.rs b/internal/wasmldr/src/main.rs index 8160d117..04d8067c 100644 --- a/internal/wasmldr/src/main.rs +++ b/internal/wasmldr/src/main.rs @@ -66,8 +66,29 @@ fn main() { info!("running workload"); // TODO: pass opts.wasm_features - let result = workload::run(bytes, opts.args, opts.envs).expect("Failed to run workload"); + let result = workload::run(bytes, opts.args, opts.envs); info!("got result: {:#?}", result); - // TODO: exit with the resulting code, if the result is a return code + // FUTURE: produce attestation report here + // TODO: print the returned value(s) in some format (json?) + + // Choose an appropriate exit code + // TODO: exit with the resulting code, if the result is a return code + std::process::exit(match result { + // Success -> EX_OK + Ok(_) => 0, + + // wasmtime/WASI/module setup errors -> EX_DATAERR + Err(workload::Error::ConfigurationError) => 65, + Err(workload::Error::StringTableError) => 65, + Err(workload::Error::InstantiationFailed) => 65, + Err(workload::Error::ExportNotFound) => 65, + Err(workload::Error::CallFailed) => 65, + + // Internal WASI errors -> EX_SOFTWARE + Err(workload::Error::WASIError(_)) => 70, + + // General IO errors -> EX_IOERR + Err(workload::Error::IoError(_)) => 74, + }); } From 6d6c487e7129bfb3c69fd8b62ea5c53b3acb5af2 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Mon, 4 Oct 2021 10:24:51 -0400 Subject: [PATCH 10/11] Add wasmldr integration test(s) We can run wasm inside keeps now - let's prove it! Here's what this patch does: * Move wasm sources (*.wat) from internal/wasmldr/fixtures to tests/wasm * Copy .wat contents into wasmldr unittests and build inside the test * Remove wasmldr build.rs and update Cargo.toml * Build tests/wasm/*.wat to OUT_DIR/bin/*.wasm in build.rs * Add tests/wasmldr_tests.rs, which runs the .wasm test binaries The intent is that we should be adding more wasm tests over time; this gives us a place to put them and a harness to run them. The tricky bit was getting Rust to pass the module to the child process, since right now the only way we have to do that is to pass it on FD3 - but Rust doesn't have `dup2()` and it *really* wants to set FD_CLOEXEC on everything it opens, so we have to do some unsafe things to actually pass FDs to a child process. This isn't pretty, but it's a start. Signed-off-by: Will Woods --- Cargo.lock | 25 +++ Cargo.toml | 3 +- build.rs | 14 ++ internal/wasmldr/Cargo.toml | 2 +- internal/wasmldr/build.rs | 26 --- internal/wasmldr/fixtures/bundle/config.yaml | 8 - internal/wasmldr/fixtures/bundle/stdin.txt | 1 - internal/wasmldr/src/workload.rs | 76 +++++++-- .../wasm}/hello_wasi_snapshot1.wat | 0 .../fixtures => tests/wasm}/no_export.wat | 0 .../fixtures => tests/wasm}/return_1.wat | 0 .../wasm}/wasi_snapshot1.wat | 0 tests/wasmldr_tests.rs | 154 ++++++++++++++++++ 13 files changed, 257 insertions(+), 52 deletions(-) delete mode 100644 internal/wasmldr/build.rs delete mode 100644 internal/wasmldr/fixtures/bundle/config.yaml delete mode 100644 internal/wasmldr/fixtures/bundle/stdin.txt rename {internal/wasmldr/fixtures => tests/wasm}/hello_wasi_snapshot1.wat (100%) rename {internal/wasmldr/fixtures => tests/wasm}/no_export.wat (100%) rename {internal/wasmldr/fixtures => tests/wasm}/return_1.wat (100%) rename {internal/wasmldr/fixtures => tests/wasm}/wasi_snapshot1.wat (100%) create mode 100644 tests/wasmldr_tests.rs diff --git a/Cargo.lock b/Cargo.lock index aa517b5f..bc4e8ec7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,7 @@ dependencies = [ "tempdir", "vdso", "walkdir", + "wat", "x86_64", ] @@ -325,6 +326,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "leb128" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3576a87f2ba00f6f106fdfcd16db1d698d648a26ad8e0573cad8537c3c362d2a" + [[package]] name = "libc" version = "0.2.103" @@ -870,6 +877,24 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wast" +version = "38.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0d7b256bef26c898fa7344a2d627e8499f5a749432ce0a05eae1a64ff0c271" +dependencies = [ + "leb128", +] + +[[package]] +name = "wat" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcfaeb27e2578d2c6271a45609f4a055e6d7ba3a12eff35b1fd5ba147bdf046" +dependencies = [ + "wast", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index ed484297..2b4878e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ default = ["backend-kvm", "backend-sgx", "wasmldr"] backend-kvm = ["x86_64", "kvm-bindings", "kvm-ioctls"] backend-sgx = ["x86_64", "sgx"] -wasmldr = [] +wasmldr = ["wat"] [dependencies] sgx = { git = "https://github.com/enarx/sgx", rev = "57df3753a0ea1777963dbf3023452993df2edb8c", features = ["openssl"], optional = true } @@ -55,6 +55,7 @@ vdso = "0.1" [build-dependencies] cc = "1.0" +wat = { version = "1.0", optional = true } walkdir = "2" protobuf-codegen-pure = "2.25" sallyport = { git = "https://github.com/enarx/sallyport", rev = "a567a22665c7e5ba88a8c4acd64ab43ee32b4681", features = [ "asm" ] } diff --git a/build.rs b/build.rs index a6adff48..590a3832 100644 --- a/build.rs +++ b/build.rs @@ -107,6 +107,18 @@ fn build_cc_tests(in_path: &Path, out_path: &Path) { } } +#[cfg(feature = "wasmldr")] +fn build_wasm_tests(in_path: &Path, out_path: &Path) { + for wat in find_files_with_extensions(&["wat"], &in_path) { + let wasm = out_path + .join(wat.file_stem().unwrap()) + .with_extension("wasm"); + let bin = wat::parse_file(&wat).unwrap_or_else(|_| panic!("failed to compile {:?}", &wat)); + std::fs::write(&wasm, &bin).unwrap_or_else(|_| panic!("failed to write {:?}", &wasm)); + println!("cargo:rerun-if-changed={}", &wat.display()); + } +} + // Build a binary named `bin_name` from the crate located at `in_dir`, // targeting `target_name`, then strip the resulting binary and place it // at `out_dir`/bin/`bin_name`. @@ -235,6 +247,8 @@ fn main() -> Result<(), Box> { build_cc_tests(&Path::new(CRATE).join(TEST_BINS_IN), &out_dir_bin); build_rs_tests(&Path::new(CRATE).join(TEST_BINS_IN), &out_dir_bin); + #[cfg(feature = "wasmldr")] + build_wasm_tests(&Path::new(CRATE).join("tests/wasm"), &out_dir_bin); let target = "x86_64-unknown-linux-musl"; diff --git a/internal/wasmldr/Cargo.toml b/internal/wasmldr/Cargo.toml index 1b7d144e..be185608 100644 --- a/internal/wasmldr/Cargo.toml +++ b/internal/wasmldr/Cargo.toml @@ -23,7 +23,7 @@ anyhow = "1.0" env_logger = { version = "0.9", default-features = false } log = "0.4" -[build-dependencies] +[dev-dependencies] wat = "1.0" [profile.release] diff --git a/internal/wasmldr/build.rs b/internal/wasmldr/build.rs deleted file mode 100644 index a70310fc..00000000 --- a/internal/wasmldr/build.rs +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -use std::path::Path; - -fn main() { - let in_dir = Path::new("fixtures"); - let out_dir = - std::env::var_os("OUT_DIR").expect("The OUT_DIR environment variable must be set"); - let out_dir = Path::new(&out_dir).join("fixtures"); - std::fs::create_dir_all(&out_dir).expect("Can't create output directory"); - - for entry in in_dir.read_dir().unwrap().flatten() { - let wat = entry.path(); - match wat.extension() { - Some(ext) if ext == "wat" => { - let wasm = out_dir - .join(wat.file_name().unwrap()) - .with_extension("wasm"); - let binary = wat::parse_file(&wat).expect("Can't parse wat file"); - std::fs::write(&wasm, &binary).expect("Can't write wasm file"); - println!("cargo:rerun-if-changed={}", &wat.display()); - } - _ => {} - } - } -} diff --git a/internal/wasmldr/fixtures/bundle/config.yaml b/internal/wasmldr/fixtures/bundle/config.yaml deleted file mode 100644 index 6b4695fb..00000000 --- a/internal/wasmldr/fixtures/bundle/config.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -stdio: - stdin: - bundle: stdin.txt - stdout: - file: stdout.txt - stderr: - file: stderr.txt diff --git a/internal/wasmldr/fixtures/bundle/stdin.txt b/internal/wasmldr/fixtures/bundle/stdin.txt deleted file mode 100644 index cd087558..00000000 --- a/internal/wasmldr/fixtures/bundle/stdin.txt +++ /dev/null @@ -1 +0,0 @@ -Hello world! diff --git a/internal/wasmldr/src/workload.rs b/internal/wasmldr/src/workload.rs index b17b8faf..dd4e144e 100644 --- a/internal/wasmldr/src/workload.rs +++ b/internal/wasmldr/src/workload.rs @@ -114,9 +114,57 @@ pub(crate) mod test { use crate::workload; use std::iter::empty; + const NO_EXPORT_WAT: &'static str = r#"(module + (memory (export "") 1) + )"#; + + const RETURN_1_WAT: &'static str = r#"(module + (func (export "") (result i32) i32.const 1) + )"#; + + const WASI_COUNT_ARGS_WAT: &'static str = r#"(module + (import "wasi_snapshot_preview1" "args_sizes_get" + (func $__wasi_args_sizes_get (param i32 i32) (result i32))) + (func (export "_start") (result i32) + (i32.store (i32.const 0) (i32.const 0)) + (i32.store (i32.const 4) (i32.const 0)) + (call $__wasi_args_sizes_get (i32.const 0) (i32.const 4)) + drop + (i32.load (i32.const 0)) + ) + (memory 1) + (export "memory" (memory 0)) + )"#; + + const HELLO_WASI_WAT: &'static str = r#"(module + (import "wasi_snapshot_preview1" "proc_exit" + (func $__wasi_proc_exit (param i32))) + (import "wasi_snapshot_preview1" "fd_write" + (func $__wasi_fd_write (param i32 i32 i32 i32) (result i32))) + (func $_start + (i32.store (i32.const 24) (i32.const 14)) + (i32.store (i32.const 20) (i32.const 0)) + (block + (br_if 0 + (call $__wasi_fd_write + (i32.const 1) + (i32.const 20) + (i32.const 1) + (i32.const 16))) + (br_if 0 (i32.ne (i32.load (i32.const 16)) (i32.const 14))) + (br 1) + ) + (call $__wasi_proc_exit (i32.const 1)) + ) + (memory 1) + (export "memory" (memory 0)) + (export "_start" (func $_start)) + (data (i32.const 0) "Hello, world!\0a") + )"#; + #[test] fn workload_run_return_1() { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/fixtures/return_1.wasm")).to_vec(); + let bytes = wat::parse_str(RETURN_1_WAT).expect("error parsing wat"); let results: Vec = workload::run(&bytes, empty::(), empty::<(String, String)>()) @@ -130,7 +178,7 @@ pub(crate) mod test { #[test] fn workload_run_no_export() { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/fixtures/no_export.wasm")).to_vec(); + let bytes = wat::parse_str(NO_EXPORT_WAT).expect("error parsing wat"); match workload::run(&bytes, empty::(), empty::<(String, String)>()) { Err(workload::Error::ExportNotFound) => {} @@ -139,9 +187,8 @@ pub(crate) mod test { } #[test] - fn workload_run_wasi_snapshot1() { - let bytes = - include_bytes!(concat!(env!("OUT_DIR"), "/fixtures/wasi_snapshot1.wasm")).to_vec(); + fn workload_run_wasi_count_args() { + let bytes = wat::parse_str(WASI_COUNT_ARGS_WAT).expect("error parsing wat"); let results: Vec = workload::run( &bytes, @@ -156,18 +203,17 @@ pub(crate) mod test { assert_eq!(results, vec![3]); } - #[cfg(bundle_tests)] #[test] - fn workload_run_bundled() { - let bytes = include_bytes!(concat!( - env!("OUT_DIR"), - "/fixtures/hello_wasi_snapshot1.bundled.wasm" - )) - .to_vec(); + fn workload_run_hello_wasi() { + let bytes = wat::parse_str(HELLO_WASI_WAT).expect("error parsing wat"); + let args: Vec = vec![]; + let envs: Vec<(String, String)> = vec![]; + + let results = workload::run(&bytes, args, envs).unwrap(); - workload::run(&bytes, empty::<&str>(), empty::<(&str, &str)>()).unwrap(); + assert_eq!(results.len(), 0); - let output = std::fs::read("stdout.txt").unwrap(); - assert_eq!(output, "Hello, world!\n".to_string().into_bytes()); + // TODO/FIXME: we need a way to configure WASI stdout so we can capture + // and check it here... } } diff --git a/internal/wasmldr/fixtures/hello_wasi_snapshot1.wat b/tests/wasm/hello_wasi_snapshot1.wat similarity index 100% rename from internal/wasmldr/fixtures/hello_wasi_snapshot1.wat rename to tests/wasm/hello_wasi_snapshot1.wat diff --git a/internal/wasmldr/fixtures/no_export.wat b/tests/wasm/no_export.wat similarity index 100% rename from internal/wasmldr/fixtures/no_export.wat rename to tests/wasm/no_export.wat diff --git a/internal/wasmldr/fixtures/return_1.wat b/tests/wasm/return_1.wat similarity index 100% rename from internal/wasmldr/fixtures/return_1.wat rename to tests/wasm/return_1.wat diff --git a/internal/wasmldr/fixtures/wasi_snapshot1.wat b/tests/wasm/wasi_snapshot1.wat similarity index 100% rename from internal/wasmldr/fixtures/wasi_snapshot1.wat rename to tests/wasm/wasi_snapshot1.wat diff --git a/tests/wasmldr_tests.rs b/tests/wasmldr_tests.rs new file mode 100644 index 00000000..a558994c --- /dev/null +++ b/tests/wasmldr_tests.rs @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 +#![cfg(feature = "wasmldr")] + +use process_control::{ChildExt, Output, Timeout}; +use std::fs::File; +use std::os::unix::io::{IntoRawFd, RawFd}; +use std::os::unix::process::CommandExt; +use std::path::Path; +use std::process::{Command, Stdio}; + +extern crate libc; +use libc::c_int; + +use std::io; +use std::io::Write; +use std::time::Duration; + +pub mod common; +use common::{check_output, CRATE, KEEP_BIN, OUT_DIR, TEST_BINS_OUT, TIMEOUT_SECS}; + +use serial_test::serial; + +const MODULE_FD: RawFd = 3; + +// wrap a libc call to return io::Result +fn cvt(rv: c_int) -> io::Result { + if rv == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(rv) + } +} + +// wrap a libc call to return io::Result<()> +fn cv(rv: c_int) -> io::Result<()> { + cvt(rv).and(Ok(())) +} + +trait CommandFdExt { + fn inherit_with_fd(&mut self, file: impl IntoRawFd, child_fd: RawFd) -> &mut Self; +} + +impl CommandFdExt for Command { + fn inherit_with_fd(&mut self, file: impl IntoRawFd, child_fd: RawFd) -> &mut Self { + let fd = file.into_raw_fd(); + if fd == child_fd { + unsafe { + self.pre_exec(move || cv(libc::fcntl(fd, libc::F_SETFD, 0))); + } + } else { + unsafe { + self.pre_exec(move || cv(libc::dup2(fd, child_fd))); + } + } + self + } +} + +pub fn wasmldr_exec<'a>(wasm: &str, input: impl Into>) -> Output { + let wasm_path = Path::new(CRATE) + .join(OUT_DIR) + .join(TEST_BINS_OUT) + .join(wasm); + let wasm_file = + File::open(wasm_path).unwrap_or_else(|e| panic!("failed to open `{}`: {:#?}", wasm, e)); + + let mut child = Command::new(&String::from(KEEP_BIN)) + .current_dir(CRATE) + .arg("exec") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .inherit_with_fd(wasm_file, MODULE_FD) + .spawn() + .unwrap_or_else(|e| panic!("failed to run `{}`: {:#?}", wasm, e)); + + if let Some(input) = input.into() { + child + .stdin + .as_mut() + .unwrap() + .write_all(input) + .expect("failed to write stdin to child"); + + drop(child.stdin.take()); + } + + let output = child + .with_output_timeout(Duration::from_secs(TIMEOUT_SECS)) + .terminating() + .wait() + .unwrap_or_else(|e| panic!("failed to run `{}`: {:#?}", wasm, e)) + .unwrap_or_else(|| panic!("process `{}` timed out", wasm)); + + assert!( + output.status.code().is_some(), + "process `{}` terminated by signal {:?}", + wasm, + output.status.signal() + ); + + output +} + +fn run_wasm_test<'a>( + wasm: &str, + status: i32, + input: impl Into>, + expected_stdout: impl Into>, + expected_stderr: impl Into>, +) -> Output { + let output = wasmldr_exec(wasm, input); + check_output(&output, status, expected_stdout, expected_stderr); + output +} + +#[test] +#[serial] +fn return_1() { + // This module does, in fact, return 1. But function return values + // are separate from setting the process exit status code, so + // we still expect a return code of '0' here. + run_wasm_test("return_1.wasm", 0, None, None, None); +} + +#[test] +#[serial] +fn wasi_snapshot1() { + // This module uses WASI to return the number of commandline args. + // Since we don't currently do anything with the function return value, + // we don't get any output here, and we expect '0', as above. + run_wasm_test("wasi_snapshot1.wasm", 0, None, None, None); +} + +#[test] +#[serial] +fn hello_wasi_snapshot1() { + // This module just prints "Hello, world!" to stdout. Hooray! + run_wasm_test( + "hello_wasi_snapshot1.wasm", + 0, + None, + &b"Hello, world!\n"[..], + None, + ); +} + +#[test] +#[serial] +fn no_export() { + // This module has no exported functions, so we get Error::ExportNotFound, + // which wasmldr maps to EX_DATAERR (65) at process exit. + run_wasm_test("no_export.wasm", 65, None, None, None); +} From 2e2388f59de7e587d8d90160052e88180cd715f1 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Mon, 4 Oct 2021 19:32:03 -0400 Subject: [PATCH 11/11] Add wasmldr to github workflows * Enable `--feature=wasmldr` when running the main test action * Add `wasmldr` to the list of internal crates to test Signed-off-by: Will Woods --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 66e8e162..cbddff98 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: test - args: ${{ matrix.profile.flag }} --no-default-features --features=backend-${{ matrix.backend.name }} + args: ${{ matrix.profile.flag }} --no-default-features --features=backend-${{ matrix.backend.name }} --features=wasmldr strategy: fail-fast: false matrix: @@ -50,6 +50,7 @@ jobs: crate: - shim-sgx - shim-sev + - wasmldr profile: - name: debug - name: release