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 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 23a9deea..2b4878e8 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 = ["wat"] [dependencies] sgx = { git = "https://github.com/enarx/sgx", rev = "57df3753a0ea1777963dbf3023452993df2edb8c", features = ["openssl"], optional = true } @@ -54,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 88b5c3eb..590a3832 100644 --- a/build.rs +++ b/build.rs @@ -107,6 +107,111 @@ 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`. +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 +223,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"); @@ -142,128 +247,39 @@ 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); + #[cfg(feature = "wasmldr")] + build_wasm_tests(&Path::new(CRATE).join("tests/wasm"), &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(); + std::fs::copy(cargo_tml, cargo_toml)?; } - } - - 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 dir_name = path.file_name().unwrap().to_str().unwrap_or_default(); - let path: String = path_buf.into_os_string().into_string().unwrap(); + match dir_name { + #[cfg(feature = "wasmldr")] + "wasmldr" => cargo_build_bin(&path, &out_dir, target, "wasmldr")?, - 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); + #[cfg(feature = "backend-kvm")] + "shim-sev" => cargo_build_bin(&path, &out_dir, target, "shim-sev")?, - rerun_src(&path); + #[cfg(feature = "backend-sgx")] + "shim-sgx" => cargo_build_bin(&path, &out_dir, target, "shim-sgx")?, - if !shim_name.starts_with("shim-") { - continue; - } - - #[cfg(not(any(feature = "backend-kvm")))] - if shim_name.starts_with("shim-sev") { - continue; - } - - #[cfg(not(feature = "backend-sgx"))] - if shim_name.starts_with("shim-sgx") { - continue; - } - - let target_dir = shim_out_dir.clone().into_os_string().into_string().unwrap(); - - 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") - .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(()) } 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") diff --git a/internal/wasmldr/Cargo.toml b/internal/wasmldr/Cargo.toml new file mode 100644 index 00000000..be185608 --- /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" + +[dev-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/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/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..7f70aef6 --- /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, 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 + #[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..04d8067c --- /dev/null +++ b/internal/wasmldr/src/main.rs @@ -0,0 +1,94 @@ +// 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; +use std::os::unix::io::{FromRawFd, RawFd}; + +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); + + 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 + .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); + info!("got result: {:#?}", result); + + // 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, + }); +} diff --git a/internal/wasmldr/src/workload.rs b/internal/wasmldr/src/workload.rs new file mode 100644 index 00000000..dd4e144e --- /dev/null +++ b/internal/wasmldr/src/workload.rs @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: Apache-2.0 + +use log::{debug, warn}; +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))?; + } + + // 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())?; + + 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; + + 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 = wat::parse_str(RETURN_1_WAT).expect("error parsing wat"); + + 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 = wat::parse_str(NO_EXPORT_WAT).expect("error parsing wat"); + + match workload::run(&bytes, empty::(), empty::<(String, String)>()) { + Err(workload::Error::ExportNotFound) => {} + _ => panic!("unexpected error"), + }; + } + + #[test] + 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, + 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]); + } + + #[test] + 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(); + + assert_eq!(results.len(), 0); + + // TODO/FIXME: we need a way to configure WASI stdout so we can capture + // and check it here... + } +} 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" 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")) + ); + } +} 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(); diff --git a/tests/wasm/hello_wasi_snapshot1.wat b/tests/wasm/hello_wasi_snapshot1.wat new file mode 100644 index 00000000..8fbea6f6 --- /dev/null +++ b/tests/wasm/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/tests/wasm/no_export.wat b/tests/wasm/no_export.wat new file mode 100644 index 00000000..b6a48adf --- /dev/null +++ b/tests/wasm/no_export.wat @@ -0,0 +1,5 @@ +;;; SPDX-License-Identifier: Apache-2.0 + +(module + (memory (export "") 1) +) diff --git a/tests/wasm/return_1.wat b/tests/wasm/return_1.wat new file mode 100644 index 00000000..070f8de0 --- /dev/null +++ b/tests/wasm/return_1.wat @@ -0,0 +1,5 @@ +;;; SPDX-License-Identifier: Apache-2.0 + +(module + (func (export "") (result i32) + i32.const 1)) diff --git a/tests/wasm/wasi_snapshot1.wat b/tests/wasm/wasi_snapshot1.wat new file mode 100644 index 00000000..1f0e1d52 --- /dev/null +++ b/tests/wasm/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/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); +}