OXYGENHUB
# cc462b56fbbb oxygenflow / crates / flow / tests / wasm.rs
5.8 KiB · Rust · last changed A local flow run of a WASM or syscall job copies the workspace whole 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
    confined(false);
34
}
35
36
/// The same in a thread of the runner, as on Android, where the runner is
37
/// a library in an app that cannot start itself as a command.
38
#[test]
39
fn wasi_modules_run_confined_in_process() {
40
    confined(true);
41
}
42
43
fn confined(in_process: bool) {
44
    let dir = tempfile::tempdir().unwrap();
45
    let home = dir.path().join("home");
46
    let work = dir.path().join("work");
47
    let tiers = Command::new(env!("CARGO_BIN_EXE_flow")).arg("--home").arg(&home).arg("tiers").output().unwrap();
48
    assert!(String::from_utf8_lossy(&tiers.stdout).contains("wasm"), "{}", String::from_utf8_lossy(&tiers.stdout));
49
50
    // Prints, and says how many arguments it had (2: its name and one).
51
    module(
52
        &work,
53
        "mods/hello.wasm",
54
        r#"(data (i32.const 16) "hello from wasm\n")"#,
55
        &format!(
56
            "{} (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))))",
57
            print(1, 16)
58
        ),
59
    );
60
    // Two modules under one glob: the first passes, the second exits 3.
61
    module(&work, "mods/each/a.wasm", r#"(data (i32.const 16) "module a\n")"#, &print(1, 9));
62
    module(&work, "mods/each/b.wasm", r#"(data (i32.const 16) "module b\n")"#, &format!("{} (call $proc_exit (i32.const 3))", print(2, 9)));
63
    // Creates a file in the workspace (preopen 3 is /src).
64
    module(
65
        &work,
66
        "mods/writer.wasm",
67
        r#"(data (i32.const 64) "made.txt") (data (i32.const 16) "made in wasm\n")"#,
68
        "(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)))
69
         (if (local.get $e) (then (call $proc_exit (i32.const 10))))
70
         (i32.store (i32.const 0) (i32.const 16)) (i32.store (i32.const 4) (i32.const 13))
71
         (drop (call $fd_write (i32.load (i32.const 200)) (i32.const 0) (i32.const 1) (i32.const 8)))",
72
    );
73
    // Tries to create a file outside the workspace: refused, so it exits 0.
74
    module(
75
        &work,
76
        "mods/escape.wasm",
77
        r#"(data (i32.const 64) "../escaped")"#,
78
        "(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)))
79
         (if (i32.eqz (local.get $e)) (then (call $proc_exit (i32.const 9))))",
80
    );
81
    // Asks for 512 MiB in a 256 MiB sandbox: refused, so it exits 0.
82
    module(
83
        &work,
84
        "mods/greedy.wasm",
85
        "",
86
        "(if (i32.ne (memory.grow (i32.const 8192)) (i32.const -1)) (then (call $proc_exit (i32.const 9))))",
87
    );
88
    module(&work, "mods/trap.wasm", "", "unreachable");
89
90
    // A file at the top of the workspace, beside the directories.
91
    std::fs::write(work.join("README"), "top\n").unwrap();
92
    std::fs::create_dir_all(work.join(".flow")).unwrap();
93
    std::fs::write(
94
        work.join(".flow/pipeline.kdl"),
95
        r#"schema-version 1
96
sandbox "w" {
97
    runtime "wasm"
98
    memory-limit "256MiB"
99
}
100
job "hello" {
101
    sandbox "w"
102
    exec "/src/mods/hello.wasm" "one"
103
}
104
job "each" {
105
    sandbox "w"
106
    exec "mods/each/*.wasm"
107
}
108
job "writer" {
109
    sandbox "w"
110
    exec "mods/writer.wasm"
111
}
112
job "escape" {
113
    sandbox "w"
114
    exec "mods/escape.wasm"
115
}
116
job "greedy" {
117
    sandbox "w"
118
    exec "mods/greedy.wasm"
119
}
120
job "trap" {
121
    sandbox "w"
122
    exec "mods/trap.wasm"
123
}
124
"#,
125
    )
126
    .unwrap();
127
    let mut flow = Command::new(env!("CARGO_BIN_EXE_flow"));
128
    if in_process {
129
        flow.env("FLOW_WASM_IN_PROCESS", "1");
130
    }
131
    let run = flow.arg("--home").arg(&home).arg("run").current_dir(&work).output().unwrap();
132
    let out = String::from_utf8_lossy(&run.stdout).to_string() + &String::from_utf8_lossy(&run.stderr);
133
    for want in [
134
        "hello from wasm",
135
        "── hello passed",
136
        "running mods/each/a.wasm",
137
        "module a",
138
        "mods/each/b.wasm: exit 3",
139
        "── each failed (exit 3)",
140
        "── writer passed",
141
        "── escape passed",
142
        "── greedy passed",
143
        "mods/trap.wasm trapped",
144
        "── trap failed (exit 134)",
145
    ] {
146
        assert!(out.contains(want), "{want:?} not in:\n{out}");
147
    }
148
    assert!(!dir.path().join("escaped").exists() && !work.join("escaped").exists(), "a module wrote outside the workspace");
149
    assert_eq!(run.status.code(), Some(1), "two jobs failed:\n{out}");
150
}