OXYGENHUB
# 33682a3988fc oxygenflow / crates / flow / tests / wasm.rs
5.3 KiB · Rust · last changed The WASM tier: WASI modules run by wasmtime on any runner (closes #75) 10 days ago source blame history raw
1
//! The WASM tier (oxygenflow#75): modules written as text, run by `flow
2
//! run` through wasmtime. Every machine offers the tier, so nothing is
3
//! skipped.
4
5
use std::path::Path;
6
use std::process::Command;
7
8
const IMPORTS: &str = r#"
9
  (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
10
  (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32)))
11
  (import "wasi_snapshot_preview1" "path_open"
12
    (func $path_open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32)))
13
  (import "wasi_snapshot_preview1" "args_sizes_get" (func $args_sizes_get (param i32 i32) (result i32)))
14
  (memory (export "memory") 1)
15
"#;
16
17
/// A module whose `_start` is `body`, with the WASI calls above.
18
fn module(dir: &Path, path: &str, data: &str, body: &str) {
19
    let text = format!("(module {IMPORTS} {data} (func (export \"_start\") (local $e i32) {body}))");
20
    let bytes = wat::parse_str(&text).unwrap_or_else(|e| panic!("{path}: {e}"));
21
    let file = dir.join(path);
22
    std::fs::create_dir_all(file.parent().unwrap()).unwrap();
23
    std::fs::write(file, bytes).unwrap();
24
}
25
26
/// Print the `len` bytes at 16 to fd `fd`.
27
fn print(fd: i32, len: i32) -> String {
28
    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)))")
29
}
30
31
#[test]
32
fn wasi_modules_run_confined_in_the_wasm_tier() {
33
    let dir = tempfile::tempdir().unwrap();
34
    let home = dir.path().join("home");
35
    let work = dir.path().join("work");
36
    let tiers = Command::new(env!("CARGO_BIN_EXE_flow")).arg("--home").arg(&home).arg("tiers").output().unwrap();
37
    assert!(String::from_utf8_lossy(&tiers.stdout).contains("wasm"), "{}", String::from_utf8_lossy(&tiers.stdout));
38
39
    // Prints, and says how many arguments it had (2: its name and one).
40
    module(
41
        &work,
42
        "mods/hello.wasm",
43
        r#"(data (i32.const 16) "hello from wasm\n")"#,
44
        &format!(
45
            "{} (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))))",
46
            print(1, 16)
47
        ),
48
    );
49
    // Two modules under one glob: the first passes, the second exits 3.
50
    module(&work, "mods/each/a.wasm", r#"(data (i32.const 16) "module a\n")"#, &print(1, 9));
51
    module(&work, "mods/each/b.wasm", r#"(data (i32.const 16) "module b\n")"#, &format!("{} (call $proc_exit (i32.const 3))", print(2, 9)));
52
    // Creates a file in the workspace (preopen 3 is /src).
53
    module(
54
        &work,
55
        "mods/writer.wasm",
56
        r#"(data (i32.const 64) "made.txt") (data (i32.const 16) "made in wasm\n")"#,
57
        "(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)))
58
         (if (local.get $e) (then (call $proc_exit (i32.const 10))))
59
         (i32.store (i32.const 0) (i32.const 16)) (i32.store (i32.const 4) (i32.const 13))
60
         (drop (call $fd_write (i32.load (i32.const 200)) (i32.const 0) (i32.const 1) (i32.const 8)))",
61
    );
62
    // Tries to create a file outside the workspace: refused, so it exits 0.
63
    module(
64
        &work,
65
        "mods/escape.wasm",
66
        r#"(data (i32.const 64) "../escaped")"#,
67
        "(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)))
68
         (if (i32.eqz (local.get $e)) (then (call $proc_exit (i32.const 9))))",
69
    );
70
    // Asks for 512 MiB in a 256 MiB sandbox: refused, so it exits 0.
71
    module(
72
        &work,
73
        "mods/greedy.wasm",
74
        "",
75
        "(if (i32.ne (memory.grow (i32.const 8192)) (i32.const -1)) (then (call $proc_exit (i32.const 9))))",
76
    );
77
    module(&work, "mods/trap.wasm", "", "unreachable");
78
79
    std::fs::create_dir_all(work.join(".flow")).unwrap();
80
    std::fs::write(
81
        work.join(".flow/pipeline.kdl"),
82
        r#"schema-version 1
83
sandbox "w" {
84
    runtime "wasm"
85
    memory-limit "256MiB"
86
}
87
job "hello" {
88
    sandbox "w"
89
    exec "/src/mods/hello.wasm" "one"
90
}
91
job "each" {
92
    sandbox "w"
93
    exec "mods/each/*.wasm"
94
}
95
job "writer" {
96
    sandbox "w"
97
    exec "mods/writer.wasm"
98
}
99
job "escape" {
100
    sandbox "w"
101
    exec "mods/escape.wasm"
102
}
103
job "greedy" {
104
    sandbox "w"
105
    exec "mods/greedy.wasm"
106
}
107
job "trap" {
108
    sandbox "w"
109
    exec "mods/trap.wasm"
110
}
111
"#,
112
    )
113
    .unwrap();
114
    let run = Command::new(env!("CARGO_BIN_EXE_flow")).arg("--home").arg(&home).arg("run").current_dir(&work).output().unwrap();
115
    let out = String::from_utf8_lossy(&run.stdout).to_string() + &String::from_utf8_lossy(&run.stderr);
116
    for want in [
117
        "hello from wasm",
118
        "── hello passed",
119
        "running mods/each/a.wasm",
120
        "module a",
121
        "mods/each/b.wasm: exit 3",
122
        "── each failed (exit 3)",
123
        "── writer passed",
124
        "── escape passed",
125
        "── greedy passed",
126
        "mods/trap.wasm trapped",
127
        "── trap failed (exit 134)",
128
    ] {
129
        assert!(out.contains(want), "{want:?} not in:\n{out}");
130
    }
131
    assert!(!dir.path().join("escaped").exists() && !work.join("escaped").exists(), "a module wrote outside the workspace");
132
    assert_eq!(run.status.code(), Some(1), "two jobs failed:\n{out}");
133
}