# WASI tutorial We'll split the tutorial into two parts: in the first part we'll walk through compiling C and Rust programs to WASI and executing the compiled WebAssembly module using `wasmtime` runtime. In the second part we will discuss the compilation of a simpler WebAssembly program written using the WebAssembly text format, and executing this using the `wasmtime` runtime. - [WASI tutorial](#wasi-tutorial) - [Running common languages with WASI](#running-common-languages-with-wasi) - [Compiling to WASI](#compiling-to-wasi) - [From C](#from-c) - [From Rust](#from-rust) - [Executing in `wasmtime` runtime](#executing-in-wasmtime-runtime) - [Web assembly text example](#web-assembly-text-example) ## Running common languages with WASI ## Compiling to WASI #### From C Let's start with a simple C program which performs a file copy, which will show to compile and run programs, as well as perform simple sandbox configuration. The C code here uses standard POSIX APIs, and doesn't have any knowledge of WASI, WebAssembly, or sandboxing. ```c #include #include #include #include #include #include int main(int argc, char **argv) { int n, m; char buf[BUFSIZ]; if (argc != 3) { fprintf(stderr, "usage: %s \n", argv[0]); exit(1); } int in = open(argv[1], O_RDONLY); if (in < 0) { fprintf(stderr, "error opening input %s: %s\n", argv[1], strerror(errno)); exit(1); } int out = open(argv[2], O_WRONLY | O_CREAT, 0660); if (out < 0) { fprintf(stderr, "error opening output %s: %s\n", argv[2], strerror(errno)); exit(1); } while ((n = read(in, buf, BUFSIZ)) > 0) { while (n > 0) { m = write(out, buf, n); if (m < 0) { fprintf(stderr, "write error: %s\n", strerror(errno)); exit(1); } n -= m; } } if (n < 0) { fprintf(stderr, "read error: %s\n", strerror(errno)); exit(1); } return EXIT_SUCCESS; } ``` We'll put this source in a file called `demo.c`. The [wasi-sdk](https://github.com/CraneStation/wasi-sdk/releases) provides a clang which is configured to target WASI and use the WASI sysroot by default if you put the extracted tree into `/`, so we can compile our program like so: ``` $ clang demo.c -o demo.wasm ``` If you would want to extract it elsewhere, you can specify the sysroot directory like so ``` $ clang demo.c --sysroot -o demo.wasm ``` If you're using the wasi-sdk, the sysroot directory is located in `opt/wasi-sdk/share/sysroot/` on Linux and mac. This is just regular clang, configured to use a WebAssembly target and sysroot. The output name specified with the "-o" flag can be anything you want, and *does not* need to contain the `.wasm` extension. In fact, the output of clang here is a standard WebAssembly module: ``` $ file demo.wasm demo.wasm: WebAssembly (wasm) binary module version 0x1 (MVP) ``` #### From Rust The same effect can be achieved with Rust. Firstly, go ahead and create a new binary crate: ``` $ cargo new --bin demo ``` You can also clone the Rust code with the crate preset for you from [here](https://github.com/kubkon/rust-wasi-tutorial). Now, let's port the C program defined in [From C](#from-c) section to Rust: ```rust use std::env; use std::fs; use std::io::{Read, Write}; fn process(input_fname: &str, output_fname: &str) -> Result<(), String> { let mut input_file = fs::File::open(input_fname).map_err(|err| format!("error opening input: {}", err))?; let mut contents = Vec::new(); input_file .read_to_end(&mut contents) .map_err(|err| format!("read error: {}", err))?; let mut output_file = fs::File::create(output_fname) .map_err(|err| format!("error opening output '{}': {}", output_fname, err))?; output_file .write_all(&contents) .map_err(|err| format!("write error: {}", err)) } fn main() { let args: Vec = env::args().collect(); let program = args[0].clone(); if args.len() < 3 { eprintln!("{} ", program); return; } if let Err(err) = process(&args[1], &args[2]) { eprintln!("{}", err) } } ``` Let's put this source in the main file of our crate `src/main.rs`. In order to build it, we first need to install a WASI-enabled Rust toolchain: ``` $ rustup target add wasm32-wasi $ cargo build --target wasm32-wasi ``` We should now have the WebAssembly module created in `target/wasm32-wasi/debug`: ``` $ file target/wasm32-wasi/debug/demo.wasm demo.wasm: WebAssembly (wasm) binary module version 0x1 (MVP) ``` ## Executing in `wasmtime` runtime The resultant WebAssembly module `demo.wasm` compiled either from C or Rust is simply a single file containing a self-contained wasm module, that doesn't require any supporting JS code. We can execute it with `wasmtime` directly, like so: ``` $ wasmtime demo.wasm usage: demo.wasm ``` Ok, this program needs some command-line arguments. So let's give it some: ``` $ echo hello world > test.txt $ wasmtime demo.wasm test.txt /tmp/somewhere.txt error opening input test.txt: Capabilities insufficient ``` Aha, now we're seeing the sandboxing in action. This program is attempting to access a file by the name of `test.txt`, however it hasn't been given the capability to do so. So let's give it capabilities to access files in the requisite directories: ``` $ wasmtime --dir=. --dir=/tmp demo.wasm test.txt /tmp/somewhere.txt $ cat /tmp/somewhere.txt hello world ``` Now our program runs as expected! What's going on under the covers? The `--dir=` option instructs `wasmtime` to *preopen* a directory, and make it available to the program as a capability which can be used to open files inside that directory. Now when the program calls the C/Rust `open` function, passing it either an absolute or relative path, the WASI libc transparently translates that path into a path that's relative to one of the given preopened directories, if possible (using a technique based on [libpreopen](https://github.com/musec/libpreopen)). This way, we can have a simple capability-oriented model at the system call level, while portable application code doesn't have to do anything special. As a brief aside, note that we used the path `.` above to grant the program access to the current directory. This is needed because the mapping from paths to associated capabilities is performed by libc, so it's part of the WebAssembly program, and we don't expose the actual current working directory to the WebAssembly program. So providing a full path doesn't work: ``` $ wasmtime --dir=$PWD --dir=/tmp demo.wasm test.txt /tmp/somewhere.txt $ cat /tmp/somewhere.txt error opening input test.txt: Capabilities insufficient ``` So, we always have to use `.` to refer to the current directory. Speaking of `.`, what about `..`? Does that give programs a way to break out of the sandbox? Let's see: ``` $ wasmtime --dir=. --dir=/tmp demo.wasm test.txt /tmp/../etc/passwd error opening output /tmp/../etc/passwd: Capabilities insufficient ``` The sandbox says no. And note that this is the capabilities system saying no here ("Capabilities insufficient"), rather than Unix access controls ("Permission denied"). Even if the user running `wasmtime` had write access to `/etc/passwd`, WASI programs don't have the capability to access files outside of the directories they've been granted. This is true when resolving symbolic links as well. `wasmtime` also has the ability to remap directories, with the `--mapdir` command-line option: ``` $ wasmtime --dir=. --mapdir=/tmp::/var/tmp demo.wasm test.txt /tmp/somewhere.txt $ cat /var/tmp/somewhere.txt hello world ``` This maps the name `/tmp` within the WebAssembly program to `/var/tmp` in the host filesystem. So the WebAssembly program itself never sees the `/var/tmp` path, but that's where the output file goes. See [here](WASI-capabilities.md) for more information on the capability-based security model. The capability model is very powerful, and what's shown here is just the beginning. In the future, we'll be exposing much more functionality, including finer-grained capabilities, capabilities for network ports, and the ability for applications to explicitly request capabilities. ## Web assembly text example In this example we will look at compiling the WebAssembly text format into wasm, and running the compiled WebAssembly module using the `wasmtime` runtime. This example makes use of WASI's `fd_write` implementation to write `hello world` to stdout. First, create a new `demo.wat` file: ```wat (module ;; Import the required fd_write WASI function which will write the given io vectors to stdout ;; The function signature for fd_write is: ;; (File Descriptor, *iovs, iovs_len, nwritten) -> Returns number of bytes written (import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32))) (memory 1) (export "memory" (memory 0)) ;; Write 'hello world\n' to memory at an offset of 8 bytes ;; Note the trailing newline which is required for the text to appear (data (i32.const 8) "hello world\n") (func $main (export "_start") ;; Creating a new io vector within linear memory (i32.store (i32.const 0) (i32.const 8)) ;; iov.iov_base - This is a pointer to the start of the 'hello world\n' string (i32.store (i32.const 4) (i32.const 12)) ;; iov.iov_len - The length of the 'hello world\n' string (call $fd_write (i32.const 1) ;; file_descriptor - 1 for stdout (i32.const 0) ;; *iovs - The pointer to the iov array, which is stored at memory location 0 (i32.const 1) ;; iovs_len - We're printing 1 string stored in an iov - so one. (i32.const 20) ;; nwritten - A place in memory to store the number of bytes written ) drop ;; Discard the number of bytes written from the top of the stack ) ) ``` `wasmtime` can directly execute `.wat` files: ``` $ wasmtime demo.wat hello world ``` Or, you can compile the `.wat` WebAssembly text format into the wasm binary format yourself using the [wabt] command line tools: ``` $ wat2wasm demo.wat ``` The created `.wasm` file can now be executed with `wasmtime` directly like so: ``` $ wasmtime demo.wasm hello world ``` To run this example within the browser, simply upload the compiled `.wasm` file to the [WASI browser polyfill]. [wabt]: https://github.com/WebAssembly/wabt [WASI browser polyfill]: https://wasi.dev/polyfill/