A plugin needs to read a file from one folder. It asks for that file and gets its contents, then tries a second folder.

The second read fails.

That's the server-side WebAssembly example I keep coming back to. One folder is a small, understandable amount of access to give somebody else's code. “Whatever the server process can reach” is rather more generous.

I spend plenty of time running containers for clients, so I'm interested in what happens when the unit I need is much smaller than an application environment.

My video on containerisation ends with WebAssembly for that reason. The module can run under a host on a server whose services I choose for the plugin's little job.

One folder, two reads

Here's a small Go program that tries two paths and prints the contents or the error. Both files will exist and the second contains ordinary sample text, so we can try this without anyone's real secrets. Save the program as read-files.go in an empty working directory. I'm using TinyGo 0.27 to compile Go for WASI, with Go 1.20.3 and Wasmtime 8.0.0 installed. The commands below build it and grant the input directory.

package main

import (
    "fmt"
    "os"
    "strings"
)

func main() {
    for _, path := range []string{"input/message.txt", "private/secret.txt"} {
        contents, err := os.ReadFile(path)
        if err != nil {
            fmt.Printf("%s: error: %v\n", path, err)
            continue
        }
        fmt.Printf("%s: %s\n", path, strings.TrimSpace(string(contents)))
    }
}
mkdir input private
printf 'hello from input\n' > input/message.txt
printf 'sample private text\n' > private/secret.txt
tinygo build -target=wasi -o read-files.wasm read-files.go
wasmtime --dir=input read-files.wasm

Change the grant

Reading the granted file succeeds. The other read cannot find an authorised directory through which to open the file. Granting private as well allows it:

wasmtime --dir=input --dir=private read-files.wasm

The program hasn't changed. Its access has.

The second Wasmtime command is my favourite bit. One more --dir, and now it can read the other file. The host could read both files all along, but the module's file access comes from the directories I give it.

What the host hands over

A WebAssembly module's linear memory is a numbered region of bytes with bounds-checked loads and stores, so a bad module address isn't permission to read an arbitrary address in the host process. Its own data can still suffer from its bugs because it can corrupt data within that memory. How does it open a file or send a network request? Core WebAssembly has no instruction for either, so it calls functions imported from the host and I have to read what effects those functions allow. The runtime can expose clocks, output streams and other services alongside the directory grant. --dir=input describes filesystem access, not every permission the module has.

WASI is the WebAssembly System Interface. It gives programs a shared interface for services such as file access. Its directory capability model is what makes the example useful. File operations use a directory the runtime has opened for the module. A path string alone isn't authority to open anything on the machine.

Three ways to house the same plugin

Suppose the job stays small, reading an input file, transforming it and returning a result. How much surrounding system do we want to provide? A Linux container gives the process a separate view of parts of the system through namespaces, while still sharing the host kernel. Mounts, user permissions and restrictions on system calls control what it can reach. That leaves me configuring an operating-system interface for this little file transformation. A virtual machine puts a guest kernel between the application and the virtual hardware. The boundary moves to the virtual-machine implementation and its exposed devices. The application can have an ordinary operating system inside the guest, at the cost of providing and maintaining that guest environment.

A mount to arrange and a user to choose and another permission to check....

With WebAssembly I can start by supplying the runtime and the functions this transformation needs. No guest kernel or container filesystem image to provide. I can read the interface before deciding what else the plugin gets.

That's the smaller unit I'm after.

The runtime is still software I have to trust and maintain. A flaw in it or an overpowered host function can break the intended boundary. I also need execution and memory limits so a plugin can't consume the server by refusing to finish.

(File permissions won't persuade an infinite loop to go home.)

Here's your input and here are the functions you may call, and here's where the result goes. That's a server-plugin contract I can work with. WebAssembly gives me a portable module format around that contract, provided the destination host supports its imports.

What am I granting? Which functions are present, what do they accept, and what can they reach? A .wasm suffix doesn't answer any of those questions (however pleasingly small the file is).

WASI's interfaces are still developing, so I need to check the destination runtime supports the ones I use. I can picture the requests growing. Processes and particular networking APIs and a complete Unix environment. My nice little contract would become a different project.

This plugin has one folder and a few functions to start with. Let's see what else it actually asks for.

This entry came over from blog.haddon.me. I’ve tidied up the grammar and flow, bringing it closer to how I write now. I used AI to help with the grammar, structure and flow. I have dyslexia, and my writing often comes out as a rambling scrawl, so it can be hard to see what I was actually working on at the time. I think the tidied versions are easier to digest and more interesting to read. The artefacts got the same tidy-up while I was at it.