Two example Rust workloads that read and write files, run under per-task WASI security policies
This example runs the same Wasm binary three times under three different WASI security policies and watches its filesystem access change: writes allowed, writes denied, and the filesystem removed entirely.
Everything here uses WASI Preview 2 command components built from Rust with the wasm32-wasip2 target.
A minimal Rust program that writes a file, reads it back, and prints what happened. It never panics on an I/O error — it reports the error kind instead, so a denied operation still produces a completed task whose output shows the denial.
cargo new --bin file-writercd file-writermkdir .cargo
.cargo/config.toml — build for WASI Preview 2 by default:
[build]target = "wasm32-wasip2"
src/main.rs:
use std::fs;use std::io::Write;fn main() { println!("file-writer: WASI filesystem check"); // The guest paths below are what the policy maps host directories onto. report("write /data/report.txt", write_file("/data/report.txt")); report("read /data/report.txt", read_file("/data/report.txt")); report("list /data", list_dir("/data"));}fn write_file(path: &str) -> std::io::Result<String> { let body = "written by the guest\n"; let mut file = fs::File::create(path)?; file.write_all(body.as_bytes())?; Ok(format!("{} bytes", body.len()))}fn read_file(path: &str) -> std::io::Result<String> { let body = fs::read_to_string(path)?; Ok(format!("{:?}", body.trim_end()))}fn list_dir(path: &str) -> std::io::Result<String> { let mut names: Vec<String> = fs::read_dir(path)? .filter_map(|entry| entry.ok()) .map(|entry| entry.file_name().to_string_lossy().into_owned()) .collect(); names.sort(); Ok(names.join(", "))}fn report(label: &str, outcome: std::io::Result<String>) { match outcome { Ok(detail) => println!(" ALLOWED {label} -> {detail}"), Err(err) => println!(" DENIED {label} -> {:?}: {err}", err.kind()), }}
The policy maps host directories into the guest, so they have to exist on the machine running the Proplet — and the Proplet's user has to be able to open them. This example keeps everything under /tmp/propeller-demo:
mkdir -p /tmp/propeller-demo/models /tmp/propeller-demo/dataecho "v1 model weights" > /tmp/propeller-demo/models/weights.txt
If the Proplet runs in Docker, these are paths inside the container, and the container's /tmp is not your host's. Bind-mount them in docker/compose.propeller.yaml first, otherwise the policy names a directory that does not exist and the task fails at start:
version = "0.0.1"[storage]# host::guest — the guest sees /data, the host writes land in /tmp/propeller-demo/data.mount = ["/tmp/propeller-demo/data::/data"]
Create the task with the policy attached, upload the binary, and start it:
tasks results prints the stored results as JSON, so the guest's output comes back as one escaped string. To read it as plain text, pipe the results endpoint through jq -r:
file-writer: WASI filesystem check DENIED write /data/report.txt -> PermissionDenied: Operation not permitted (os error 63) ALLOWED read /data/report.txt -> "written by the guest" ALLOWED list /data -> report.txt
The read still works — report.txt is the file the previous run wrote — but the write is refused by the runtime. The exact error text comes from the Rust standard library and may vary between toolchains.
tasks update replaces the whole metadata map. Any free-form labels on the task must be passed again with --metadata; other metadata.elastic keys (such as --wasi-pep) are preserved for you by the CLI.
A policy with no [storage] section grants no filesystem access — even if the Proplet has PROPLET_DIRS set, because a policy replaces the Proplet-wide preopens for that task.
file-writer: WASI filesystem check DENIED write /data/report.txt -> NotFound: No such file or directory (os error 44) DENIED read /data/report.txt -> NotFound: No such file or directory (os error 44) DENIED list /data -> NotFound: No such file or directory (os error 44)
A path outside every preopen does not exist as far as the guest is concerned.
To see read-only, read-write and unreachable paths in one run, create a new crate named fs-probe:
use std::fs;use std::io::Write;fn main() { println!("fs-probe: what can this guest reach?"); // Read-only mount from the policy. report("read /models/weights.txt", read_file("/models/weights.txt")); report("write /models/tampered.txt", write_file("/models/tampered.txt")); // Read-write mount from the policy. report("write /data/report.txt", write_file("/data/report.txt")); report("read /data/report.txt", read_file("/data/report.txt")); // Never granted by any policy below: the host filesystem is not reachable. report("read /etc/passwd", read_file("/etc/passwd")); report("list /", list_dir("/"));}fn write_file(path: &str) -> std::io::Result<String> { let body = "written by the guest\n"; let mut file = fs::File::create(path)?; file.write_all(body.as_bytes())?; Ok(format!("{} bytes", body.len()))}fn read_file(path: &str) -> std::io::Result<String> { let body = fs::read_to_string(path)?; Ok(format!("{:?}", body.trim_end()))}fn list_dir(path: &str) -> std::io::Result<String> { let mut names: Vec<String> = fs::read_dir(path)? .filter_map(|entry| entry.ok()) .map(|entry| entry.file_name().to_string_lossy().into_owned()) .collect(); names.sort(); Ok(names.join(", "))}fn report(label: &str, outcome: std::io::Result<String>) { match outcome { Ok(detail) => println!(" ALLOWED {label} -> {detail}"), Err(err) => println!(" DENIED {label} -> {:?}: {err}", err.kind()), }}
policy-probe.toml:
version = "0.0.1"[storage]readonly = ["/tmp/propeller-demo/models::/models"]mount = ["/tmp/propeller-demo/data::/data"]
fs-probe: what can this guest reach? ALLOWED read /models/weights.txt -> "v1 model weights" DENIED write /models/tampered.txt -> PermissionDenied: Operation not permitted (os error 63) ALLOWED write /data/report.txt -> 21 bytes ALLOWED read /data/report.txt -> "written by the guest" DENIED read /etc/passwd -> NotFound: No such file or directory (os error 44) DENIED list / -> NotFound: No such file or directory (os error 44)
fs-probe: what can this guest reach? ALLOWED read /models/weights.txt -> "v1 model weights" DENIED write /models/tampered.txt -> PermissionDenied: Operation not permitted (os error 63) ALLOWED write /data/report.txt -> 21 bytes ALLOWED read /data/report.txt -> "written by the guest" DENIED read /etc/passwd -> NotFound: No such file or directory (os error 44) DENIED list / -> NotFound: No such file or directory (os error 44)