diff --git a/Cargo.lock b/Cargo.lock index 164483f2..c69a1278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -791,15 +791,13 @@ dependencies = [ "bytemuck", "cc", "chrono", - "cmake", "common", "crossbeam-queue", "derive_more", + "fix-wasm", "fixhandle", - "fixshell", "futures", "hex", - "include_directory", "kernel", "log", "macros", @@ -810,6 +808,16 @@ dependencies = [ "user", ] +[[package]] +name = "fix-wasm" +version = "0.1.0" +dependencies = [ + "anyhow", + "cmake", + "fixshell", + "include_directory", +] + [[package]] name = "fixhandle" version = "0.1.0" @@ -1993,6 +2001,8 @@ dependencies = [ "elevate", "elf", "env_logger", + "fix", + "fix-wasm", "futures", "kvm-bindings", "kvm-ioctls", diff --git a/Cargo.toml b/Cargo.toml index 604fc715..1c9d918b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "common", "vmm", "kernel", "macros" , "user", "arca" , "arcane", "fix", "fix/handle", "fix/shell" ] +members = [ "common", "vmm", "kernel", "macros" , "user", "arca" , "arcane", "fix", "fix/handle", "fix/shell", "fix/wasm" ] default-members = [ "common", "vmm", "macros", "arca", "fix/handle" ] resolver = "2" diff --git a/fix/Cargo.toml b/fix/Cargo.toml index 6572031c..d4b40aa0 100644 --- a/fix/Cargo.toml +++ b/fix/Cargo.toml @@ -44,10 +44,8 @@ crossbeam-queue = { } [build-dependencies] -fixshell = { path = "shell", artifact="staticlib", target = "x86_64-unknown-none" } +fix-wasm = { path = "wasm" } anyhow = "1.0.98" bindgen = "0.72.1" cc = "1.2.30" autotools = "0.2.7" -cmake = "0.1.54" -include_directory = "0.1.1" diff --git a/fix/build.rs b/fix/build.rs index c6067989..c7f2ded1 100644 --- a/fix/build.rs +++ b/fix/build.rs @@ -1,184 +1,22 @@ -use std::fs::create_dir_all; -use std::io::ErrorKind; use std::os::unix::fs::symlink; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::sync::OnceLock; +use std::path::Path; use std::{env, fs}; -use anyhow::{Result, anyhow}; -use cmake::Config; - -use include_directory::{Dir, include_directory}; - -static FIX_SHELL_INC: Dir<'_> = include_directory!("$CARGO_MANIFEST_DIR/shell/inc"); -static FIX_SHELL_ETC: Dir<'_> = include_directory!("$CARGO_MANIFEST_DIR/shell/etc"); - -static INTERMEDIATEOUT: OnceLock = OnceLock::new(); -static WASM2C: OnceLock = OnceLock::new(); -static WAT2WASM: OnceLock = OnceLock::new(); - -fn wat2wasm(wat: &[u8]) -> Result> { - if &wat[..4] == b"\0asm" { - Ok(wat.into()) - } else { - let mut wat_file = INTERMEDIATEOUT.get().unwrap().clone(); - wat_file.push("module.wat"); - std::fs::write(&wat_file, wat)?; - let mut wasm_file = INTERMEDIATEOUT.get().unwrap().clone(); - wasm_file.push("module.wasm"); - let wat2wasm = Command::new(WAT2WASM.get().unwrap()) - .args([ - "-o", - wasm_file.to_str().unwrap(), - wat_file.to_str().unwrap(), - "--enable-multi-memory", - ]) - .status() - .map_err(|e| { - if let ErrorKind::NotFound = e.kind() { - anyhow!("Could not find wat2wasm. Did you install wabt?") - } else { - e.into() - } - })?; - assert!(wat2wasm.success()); - Ok(std::fs::read(wasm_file)?) - } -} - -fn wasm2c(wasm: &[u8]) -> Result<(Vec, Vec)> { - let mut wasm_file = INTERMEDIATEOUT.get().unwrap().clone(); - wasm_file.push("module.wasm"); - std::fs::write(&wasm_file, wasm)?; - let mut c_file = INTERMEDIATEOUT.get().unwrap().clone(); - c_file.push("module.c"); - let mut h_file = INTERMEDIATEOUT.get().unwrap().clone(); - h_file.push("module.h"); - - // Using wasm2c 1.0.34 from the Ubuntu repos - let wasm2c = Command::new(WASM2C.get().unwrap()) - .args([ - "-o", - c_file.to_str().unwrap(), - "-n", - "module", - wasm_file.to_str().unwrap(), - "--enable-multi-memory", - ]) - .status()?; - assert!(wasm2c.success()); - Ok((std::fs::read(c_file)?, std::fs::read(h_file)?)) -} - -fn c2elf(c: &[u8], h: &[u8]) -> Result> { - FIX_SHELL_INC.extract(INTERMEDIATEOUT.get().unwrap())?; - FIX_SHELL_ETC.extract(INTERMEDIATEOUT.get().unwrap())?; - - let mut wasm_rt = INTERMEDIATEOUT.get().unwrap().clone(); - wasm_rt.push("wasm-rt.c"); - - let mut c_file = INTERMEDIATEOUT.get().unwrap().clone(); - c_file.push("module.c"); - - let mut h_file = INTERMEDIATEOUT.get().unwrap().clone(); - h_file.push("module.h"); - - std::fs::write(c_file.clone(), c)?; - std::fs::write(h_file, h)?; - - let mut src = vec![c_file, wasm_rt]; - - let shell_top = env::var_os("CARGO_STATICLIB_FILE_FIXSHELL_fixshell").unwrap(); - src.push(PathBuf::from(shell_top)); - - println!("{src:?}"); - - let mut o_file = INTERMEDIATEOUT.get().unwrap().clone(); - o_file.push("module.o"); - - let mut memmap = INTERMEDIATEOUT.get().unwrap().clone(); - memmap.push("memmap.ld"); - - let cc = Command::new("gcc") - .args([ - "-o", - o_file.to_str().unwrap(), - "-T", - memmap.to_str().unwrap(), - "-O2", - "-fno-optimize-sibling-calls", - "-frounding-math", - // "-fsignaling-nans", - "-ffreestanding", - "-nostdlib", - "-nostartfiles", - "--verbose", - "-mcmodel=large", - "-static", - // "-fno-pic", - // "-fno-pie", - // "-Wl,-no-pie", - // "-static", - ]) - .args(src) - .status().map_err(|e| if let ErrorKind::NotFound = e.kind() {anyhow!("Compilation failed. Please make sure you have installed gcc-multilib if you are on Ubuntu.")} else {e.into()})?; - assert!(cc.success()); - - let o = std::fs::read(o_file)?; - - Ok(o) -} +use anyhow::Result; fn main() -> Result<()> { let out_dir = env::var_os("OUT_DIR").unwrap(); - let mut intermediateout: PathBuf = out_dir.clone().into(); - intermediateout.push("inter-out"); - if intermediateout.exists() { - std::fs::remove_dir_all(&intermediateout)?; - } - create_dir_all(&intermediateout)?; - INTERMEDIATEOUT.set(intermediateout).unwrap(); - - let mut dst: PathBuf = out_dir.clone().into(); - dst.push("wabt"); - if !dst.exists() { - create_dir_all(&dst)? - } - - let dst = Config::new("wabt") - .define("BUILD_TESTS", "OFF") - .define("BUILD_LIBWASM", "OFF") - .define("BUILD_TOOLS", "ON") - .out_dir(dst) - .build(); - - WASM2C.set(dst.join("bin/wasm2c")).unwrap(); - WAT2WASM.set(dst.join("bin/wat2wasm")).unwrap(); - - for f in std::fs::read_dir("wasm")? { - let f = f?; - let path = f.path(); - let base = path.file_stem().unwrap(); - let dst = Path::new(&out_dir).join(base); - println!( - "cargo::rerun-if-changed=wasm/{}", - f.file_name().to_str().unwrap() - ); - let wat = std::fs::read(f.path())?; - let wasm = wat2wasm(&wat)?; - let (c, h) = wasm2c(&wasm)?; - let elf = c2elf(&c, &h)?; + for &(name, elf) in fix_wasm::ARTIFACTS { + let dst = Path::new(&out_dir).join(name); std::fs::write(&dst, elf)?; - let link = Path::new(&out_dir).ancestors().nth(4).unwrap().join(base); + let link = Path::new(&out_dir).ancestors().nth(4).unwrap().join(name); let _ = fs::remove_file(&link); symlink(dst, link)?; } let cwd = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - println!("cargo::rerun-if-changed={cwd}/etc/memmap.ld"); println!("cargo::rustc-link-arg=-T{cwd}/etc/memmap.ld"); println!("cargo::rustc-link-arg=-no-pie"); diff --git a/fix/wasm/Cargo.toml b/fix/wasm/Cargo.toml new file mode 100644 index 00000000..3fba9027 --- /dev/null +++ b/fix/wasm/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "fix-wasm" +version = "0.1.0" +edition = "2024" + +[build-dependencies] +fixshell = { path = "../shell", artifact = "staticlib", target = "x86_64-unknown-none" } +anyhow = "1.0.98" +cmake = "0.1.54" +include_directory = "0.1.1" diff --git a/fix/wasm/build.rs b/fix/wasm/build.rs new file mode 100644 index 00000000..361bd199 --- /dev/null +++ b/fix/wasm/build.rs @@ -0,0 +1,187 @@ +use std::env; +use std::ffi::OsStr; +use std::fs::create_dir_all; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::OnceLock; + +use anyhow::{Result, anyhow}; +use cmake::Config; + +use include_directory::{Dir, include_directory}; + +static FIX_SHELL_INC: Dir<'_> = include_directory!("$CARGO_MANIFEST_DIR/../shell/inc"); +static FIX_SHELL_ETC: Dir<'_> = include_directory!("$CARGO_MANIFEST_DIR/../shell/etc"); + +static INTERMEDIATEOUT: OnceLock = OnceLock::new(); +static WASM2C: OnceLock = OnceLock::new(); +static WAT2WASM: OnceLock = OnceLock::new(); + +fn wat2wasm(wat: &[u8]) -> Result> { + if &wat[..4] == b"\0asm" { + Ok(wat.into()) + } else { + let mut wat_file = INTERMEDIATEOUT.get().unwrap().clone(); + wat_file.push("module.wat"); + std::fs::write(&wat_file, wat)?; + let mut wasm_file = INTERMEDIATEOUT.get().unwrap().clone(); + wasm_file.push("module.wasm"); + let wat2wasm = Command::new(WAT2WASM.get().unwrap()) + .args([ + "-o", + wasm_file.to_str().unwrap(), + wat_file.to_str().unwrap(), + "--enable-multi-memory", + ]) + .status() + .map_err(|e| { + if let ErrorKind::NotFound = e.kind() { + anyhow!("Could not find wat2wasm. Did you install wabt?") + } else { + e.into() + } + })?; + assert!(wat2wasm.success()); + Ok(std::fs::read(wasm_file)?) + } +} + +fn wasm2c(wasm: &[u8]) -> Result<(Vec, Vec)> { + let mut wasm_file = INTERMEDIATEOUT.get().unwrap().clone(); + wasm_file.push("module.wasm"); + std::fs::write(&wasm_file, wasm)?; + let mut c_file = INTERMEDIATEOUT.get().unwrap().clone(); + c_file.push("module.c"); + let mut h_file = INTERMEDIATEOUT.get().unwrap().clone(); + h_file.push("module.h"); + + // Using wasm2c 1.0.34 from the Ubuntu repos + let wasm2c = Command::new(WASM2C.get().unwrap()) + .args([ + "-o", + c_file.to_str().unwrap(), + "-n", + "module", + wasm_file.to_str().unwrap(), + "--enable-multi-memory", + ]) + .status()?; + assert!(wasm2c.success()); + Ok((std::fs::read(c_file)?, std::fs::read(h_file)?)) +} + +fn c2elf(c: &[u8], h: &[u8]) -> Result> { + FIX_SHELL_INC.extract(INTERMEDIATEOUT.get().unwrap())?; + FIX_SHELL_ETC.extract(INTERMEDIATEOUT.get().unwrap())?; + + let mut wasm_rt = INTERMEDIATEOUT.get().unwrap().clone(); + wasm_rt.push("wasm-rt.c"); + + let mut c_file = INTERMEDIATEOUT.get().unwrap().clone(); + c_file.push("module.c"); + + let mut h_file = INTERMEDIATEOUT.get().unwrap().clone(); + h_file.push("module.h"); + + std::fs::write(c_file.clone(), c)?; + std::fs::write(h_file, h)?; + + let mut src = vec![c_file, wasm_rt]; + + let shell_top = env::var_os("CARGO_STATICLIB_FILE_FIXSHELL_fixshell").unwrap(); + src.push(PathBuf::from(shell_top)); + + println!("{src:?}"); + + let mut o_file = INTERMEDIATEOUT.get().unwrap().clone(); + o_file.push("module.o"); + + let mut memmap = INTERMEDIATEOUT.get().unwrap().clone(); + memmap.push("memmap.ld"); + + let cc = Command::new("gcc") + .args([ + "-o", + o_file.to_str().unwrap(), + "-T", + memmap.to_str().unwrap(), + "-O2", + "-fno-optimize-sibling-calls", + "-frounding-math", + // "-fsignaling-nans", + "-ffreestanding", + "-nostdlib", + "-nostartfiles", + "--verbose", + "-mcmodel=large", + "-static", + // "-fno-pic", + // "-fno-pie", + // "-Wl,-no-pie", + // "-static", + ]) + .args(src) + .status().map_err(|e| if let ErrorKind::NotFound = e.kind() {anyhow!("Compilation failed. Please make sure you have installed gcc-multilib if you are on Ubuntu.")} else {e.into()})?; + assert!(cc.success()); + + let o = std::fs::read(o_file)?; + + Ok(o) +} + +fn main() -> Result<()> { + let out_dir = env::var_os("OUT_DIR").unwrap(); + + let mut intermediateout: PathBuf = out_dir.clone().into(); + intermediateout.push("inter-out"); + if intermediateout.exists() { + std::fs::remove_dir_all(&intermediateout)?; + } + create_dir_all(&intermediateout)?; + INTERMEDIATEOUT.set(intermediateout).unwrap(); + + let mut dst: PathBuf = out_dir.clone().into(); + dst.push("wabt"); + if !dst.exists() { + create_dir_all(&dst)? + } + + let dst = Config::new("../wabt") + .define("BUILD_TESTS", "OFF") + .define("BUILD_LIBWASM", "OFF") + .define("BUILD_TOOLS", "ON") + .out_dir(dst) + .build(); + + WASM2C.set(dst.join("bin/wasm2c")).unwrap(); + WAT2WASM.set(dst.join("bin/wat2wasm")).unwrap(); + + let mut generated = String::from("pub const ARTIFACTS: &[(&str, &[u8])] = &[\n"); + for f in std::fs::read_dir(".")? { + let f = f?; + let path = f.path(); + if path.extension() != Some(OsStr::new("wat")) { + continue; + } + let base = path.file_stem().unwrap(); + let name = base.to_str().unwrap(); + let dst = Path::new(&out_dir).join(base); + println!( + "cargo::rerun-if-changed={}", + f.file_name().to_str().unwrap() + ); + let wat = std::fs::read(f.path())?; + let wasm = wat2wasm(&wat)?; + let (c, h) = wasm2c(&wasm)?; + let elf = c2elf(&c, &h)?; + std::fs::write(&dst, elf)?; + + generated.push_str(&format!(" (\"{name}\", include_bytes!(\"{name}\")),\n")); + } + + generated.push_str("];\n"); + std::fs::write(Path::new(&out_dir).join("artifacts.rs"), generated)?; + + Ok(()) +} diff --git a/fix/wasm/src/lib.rs b/fix/wasm/src/lib.rs new file mode 100644 index 00000000..bf31e55d --- /dev/null +++ b/fix/wasm/src/lib.rs @@ -0,0 +1,7 @@ +include!(concat!(env!("OUT_DIR"), "/artifacts.rs")); + +pub fn artifact(name: &str) -> Option<&'static [u8]> { + ARTIFACTS + .iter() + .find_map(|(artifact_name, bytes)| (*artifact_name == name).then_some(*bytes)) +} diff --git a/vmm/Cargo.toml b/vmm/Cargo.toml index 0f8b5779..7fd635c2 100644 --- a/vmm/Cargo.toml +++ b/vmm/Cargo.toml @@ -36,5 +36,7 @@ serde = "1.0.228" anyhow = "1.0.97" bindgen = "0.72.0" -# [dev-dependencies] +[dev-dependencies] # kernel = { path = "../kernel", artifact = "bin", target = "x86_64-unknown-none" } +fix-guest = { package = "fix", path = "../fix", artifact = "bin:fix", target = "x86_64-unknown-none" } +fix-wasm = { path = "../fix/wasm" } diff --git a/vmm/tests/test_fix_eval.rs b/vmm/tests/test_fix_eval.rs new file mode 100644 index 00000000..3c3fbee6 --- /dev/null +++ b/vmm/tests/test_fix_eval.rs @@ -0,0 +1,124 @@ +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Output, Stdio}; +use std::thread; +use std::time::{Duration, Instant}; + +const EXPECTED_OUTPUT: &str = "as a u64: 6"; +const FIX_GUEST: &[u8] = include_bytes!(env!("CARGO_BIN_FILE_FIX_GUEST_fix")); +const FIX_PROGRAM: &str = r#"(let ((add @"{addblob}")) + !*(add !*(add 2 3) 1)) +"#; +const GUEST_PROCESS_ENV: &str = "ARCA_FIX_EVAL_GUEST_PROCESS"; +// Time limited so this test doesn't stall the test suite (usually runs in <10s) +const DEADLOCK_LIMIT: Duration = Duration::from_secs(30); +// Poll lightly instead of busy-waiting for the child +const POLL_INTERVAL: Duration = Duration::from_millis(10); + +struct ProgramFile { + path: PathBuf, + addblob_path: PathBuf, +} + +impl ProgramFile { + fn new() -> Self { + // The CLI requires paths, so materialize the embedded helper and inline program + let addblob_path = + std::env::temp_dir().join(format!("arca-fix-eval-addblob-{}", std::process::id())); + let addblob = fix_wasm::artifact("addblob").expect("find embedded addblob guest"); + std::fs::write(&addblob_path, addblob).expect("write temporary addblob guest"); + + let path = std::env::temp_dir().join(format!("arca-fix-eval-{}.fix", std::process::id())); + let program = FIX_PROGRAM.replace("{addblob}", addblob_path.to_string_lossy().as_ref()); + std::fs::write(&path, program).expect("write temporary Fix program"); + Self { path, addblob_path } + } + + fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for ProgramFile { + fn drop(&mut self) { + // Clean up the temp file + let _ = std::fs::remove_file(&self.path); + let _ = std::fs::remove_file(&self.addblob_path); + } +} + +// Fail instead of hanging if the file-close handshake deadlocks +fn wait_for_output(mut child: Child) -> Output { + let start = Instant::now(); + + loop { + match child.try_wait().expect("poll Fix-on-Arca process") { + Some(_) => { + // Kill the child and collect both captured streams + return child + .wait_with_output() + .expect("collect Fix-on-Arca output"); + } + None if start.elapsed() < DEADLOCK_LIMIT => thread::sleep(POLL_INTERVAL), + None => { + // Stop the hung VM before reporting diagnostics + let _ = child.kill(); + let output = child + .wait_with_output() + .expect("collect timed-out Fix-on-Arca output"); + panic!( + "fix eval did not complete its file-close handshake\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + } + } +} + +#[test] +fn fix_eval_completes_and_prints_result() { + if std::env::var_os(GUEST_PROCESS_ENV).is_some() { + let program = ProgramFile::new(); + let mut runtime = vmm::runtime::Runtime::new(1, 1 << 34, FIX_GUEST.into()); + runtime.run(vec![ + "fix".to_owned(), + "eval".to_owned(), + program.path().to_string_lossy().into_owned(), + ]); + return; + } + + // Run from the workspace so relative Fix paths match normal CLI use. + let workspace = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("vmm crate is inside the workspace"); + + // Re-run this test binary so the VM can be timed out and its output captured. + let child = Command::new(std::env::current_exe().expect("locate Fix-on-Arca test binary")) + .arg("--exact") + .arg("fix_eval_completes_and_prints_result") + .arg("--nocapture") + .env(GUEST_PROCESS_ENV, "1") + .current_dir(workspace) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("launch Fix-on-Arca under the VMM"); + + // Decode captured bytes for assertions and failure diagnostics + let output = wait_for_output(child); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + // A successful status proves the guest reached kernel shutdown + assert!( + output.status.success(), + "fix eval failed with {}\nstdout:\n{stdout}\nstderr:\n{stderr}", + output.status, + ); + // VMM forwards guest debug-console output to stderr + assert!( + stderr.contains(EXPECTED_OUTPUT), + "fix eval did not print the expected result\nstdout:\n{stdout}\nstderr:\n{stderr}", + ); +}