//! The WASM tier (oxygenflow#75): modules written as text, run by `flow //! run` through wasmtime. Every machine offers the tier, so nothing is //! skipped. use std::path::Path; use std::process::Command; const IMPORTS: &str = r#" (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32))) (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32))) (import "wasi_snapshot_preview1" "path_open" (func $path_open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32))) (import "wasi_snapshot_preview1" "args_sizes_get" (func $args_sizes_get (param i32 i32) (result i32))) (memory (export "memory") 1) "#; /// A module whose `_start` is `body`, with the WASI calls above. fn module(dir: &Path, path: &str, data: &str, body: &str) { let text = format!("(module {IMPORTS} {data} (func (export \"_start\") (local $e i32) {body}))"); let bytes = wat::parse_str(&text).unwrap_or_else(|e| panic!("{path}: {e}")); let file = dir.join(path); std::fs::create_dir_all(file.parent().unwrap()).unwrap(); std::fs::write(file, bytes).unwrap(); } /// Print the `len` bytes at 16 to fd `fd`. fn print(fd: i32, len: i32) -> String { format!("(i32.store (i32.const 0) (i32.const 16)) (i32.store (i32.const 4) (i32.const {len})) (drop (call $fd_write (i32.const {fd}) (i32.const 0) (i32.const 1) (i32.const 8)))") } #[test] fn wasi_modules_run_confined_in_the_wasm_tier() { confined(false); } /// The same in a thread of the runner, as on Android, where the runner is /// a library in an app that cannot start itself as a command. #[test] fn wasi_modules_run_confined_in_process() { confined(true); } fn confined(in_process: bool) { let dir = tempfile::tempdir().unwrap(); let home = dir.path().join("home"); let work = dir.path().join("work"); let tiers = Command::new(env!("CARGO_BIN_EXE_flow")).arg("--home").arg(&home).arg("tiers").output().unwrap(); assert!(String::from_utf8_lossy(&tiers.stdout).contains("wasm"), "{}", String::from_utf8_lossy(&tiers.stdout)); // Prints, and says how many arguments it had (2: its name and one). module( &work, "mods/hello.wasm", r#"(data (i32.const 16) "hello from wasm\n")"#, &format!( "{} (drop (call $args_sizes_get (i32.const 100) (i32.const 104))) (if (i32.ne (i32.load (i32.const 100)) (i32.const 2)) (then (call $proc_exit (i32.const 7))))", print(1, 16) ), ); // Two modules under one glob: the first passes, the second exits 3. module(&work, "mods/each/a.wasm", r#"(data (i32.const 16) "module a\n")"#, &print(1, 9)); module(&work, "mods/each/b.wasm", r#"(data (i32.const 16) "module b\n")"#, &format!("{} (call $proc_exit (i32.const 3))", print(2, 9))); // Creates a file in the workspace (preopen 3 is /src). module( &work, "mods/writer.wasm", r#"(data (i32.const 64) "made.txt") (data (i32.const 16) "made in wasm\n")"#, "(local.set $e (call $path_open (i32.const 3) (i32.const 0) (i32.const 64) (i32.const 8) (i32.const 9) (i64.const 0x1fffffff) (i64.const 0x1fffffff) (i32.const 0) (i32.const 200))) (if (local.get $e) (then (call $proc_exit (i32.const 10)))) (i32.store (i32.const 0) (i32.const 16)) (i32.store (i32.const 4) (i32.const 13)) (drop (call $fd_write (i32.load (i32.const 200)) (i32.const 0) (i32.const 1) (i32.const 8)))", ); // Tries to create a file outside the workspace: refused, so it exits 0. module( &work, "mods/escape.wasm", r#"(data (i32.const 64) "../escaped")"#, "(local.set $e (call $path_open (i32.const 3) (i32.const 0) (i32.const 64) (i32.const 10) (i32.const 9) (i64.const 0x1fffffff) (i64.const 0x1fffffff) (i32.const 0) (i32.const 200))) (if (i32.eqz (local.get $e)) (then (call $proc_exit (i32.const 9))))", ); // Asks for 512 MiB in a 256 MiB sandbox: refused, so it exits 0. module( &work, "mods/greedy.wasm", "", "(if (i32.ne (memory.grow (i32.const 8192)) (i32.const -1)) (then (call $proc_exit (i32.const 9))))", ); module(&work, "mods/trap.wasm", "", "unreachable"); std::fs::create_dir_all(work.join(".flow")).unwrap(); std::fs::write( work.join(".flow/pipeline.kdl"), r#"schema-version 1 sandbox "w" { runtime "wasm" memory-limit "256MiB" } job "hello" { sandbox "w" exec "/src/mods/hello.wasm" "one" } job "each" { sandbox "w" exec "mods/each/*.wasm" } job "writer" { sandbox "w" exec "mods/writer.wasm" } job "escape" { sandbox "w" exec "mods/escape.wasm" } job "greedy" { sandbox "w" exec "mods/greedy.wasm" } job "trap" { sandbox "w" exec "mods/trap.wasm" } "#, ) .unwrap(); let mut flow = Command::new(env!("CARGO_BIN_EXE_flow")); if in_process { flow.env("FLOW_WASM_IN_PROCESS", "1"); } let run = flow.arg("--home").arg(&home).arg("run").current_dir(&work).output().unwrap(); let out = String::from_utf8_lossy(&run.stdout).to_string() + &String::from_utf8_lossy(&run.stderr); for want in [ "hello from wasm", "── hello passed", "running mods/each/a.wasm", "module a", "mods/each/b.wasm: exit 3", "── each failed (exit 3)", "── writer passed", "── escape passed", "── greedy passed", "mods/trap.wasm trapped", "── trap failed (exit 134)", ] { assert!(out.contains(want), "{want:?} not in:\n{out}"); } assert!(!dir.path().join("escaped").exists() && !work.join("escaped").exists(), "a module wrote outside the workspace"); assert_eq!(run.status.code(), Some(1), "two jobs failed:\n{out}"); }