Wasm is secure through sandboxing and capability based design. What does this mean for networking? And why has networking taken so long to be supported by WASI?

Why this post?

While listening to Nathaniel McCallum, CTO of Profian talk about WASI networking he mentioned concepts like openat, poll_oneoff and explained why they had to make changes to Rust’s standard lib for WASI networking. Although intrigued, it took me a few times listening and searching for definitions before I really understood what he was talking about. This post is meant as an explanation to the above referred post of Nathaniel McCallum.

What is WASI?

The Web Assembly System Interface is the standardized interface between webassembly code and the native code underneath it. This interface provides portability because Wasm can be run in any host environment complying to this interface. It also provides modularity because it’s possible to implement just a part of the interface which is important for your specific host environment. Ultimately WASI lead to using Wasm outside of the browser and opening a lot of opportunities and possibilities, like serverless- and edge computing.

Wasm and WASI are secure by design, running sandboxed and using a capability based security model. That means a Wasm module cannot access the filesystem or networking unless given the right to do so. To go even further, every library used in a Wasm module or an other host environment must declare what rights it needs. That means that malicious third party libraries cannot access or do anything unless you’ve given them the right to do so. This is called nano processes and is quite a big thing!

WebAssembly System Interface

Network namespace: the current way of secure networking

Namespaces are a feature of the linux kernel providing isolation of global resources. There are different kind of namespaces, like cgroup, mount, process and network namespaces. The network namespace is interesting to compare with WASI’s network isolation model because it’s the standard for containerization technologies.

Baeldung has a good definition of the network namespaces:

''The network namespace essentially virtualizes and isolates the network stack of a process. In other words, different processes can have their own unique firewall configuration, private IP address, and routing rules. It is through this network namespace we can give each Docker container an isolated network stack from the host network.''

Linux network namespaces

Capability based secure networking

Network namespaces are great for isolating resources for different processes. But WASI’s nano process model takes isolation a step further: with WASI you also define the capabilities of guest Wasm modules loaded in as third party libraries. This way you can restrict a module to make a network call to only a certain host, while an other module can only call another host.

socket()

''Unlike BSD sockets (also called berkeley sockets), WASI sockets require capability handles to create sockets and perform domain name lookups. On top of capability handles, WASI Socket implementations should implement deny-by-default firewalling''

Unfortunately, it’s not possible yet to create a socket inside of your Wasm module. You can use pre created sockets, those are provided by the Wasm runtime. For example Wasmtime can pre create a socket if run with the --tcplisten argument:

wasmtime run --tcplisten 127.0.0.1:6000 demo-server.wasm

The demo-server.wasm can then access this socket to accept incoming connections.

sock_accept()

Rust provides a method to create a TcpListener from a raw file descriptor (in UNIX a socket is just a file). So if the runtime pre creates a socket like mentioned above, your Wasm module can retrieve this as a raw file descriptor, create a TcpListener from it, and then accept incoming connections. All the ingredients needed to create a server!

#[cfg(target_os = "wasi")]
async fn get_tcplistener() -> TcpListener {
    use std::os::wasi::io::FromRawFd;
    let stdlistener = unsafe { std::net::TcpListener::from_raw_fd(4) };
    stdlistener.set_nonblocking(true).unwrap();
    TcpListener::from_std(stdlistener).unwrap()
}
openat()

Not all posix syscalls are implemented in WASI because of the principle of least privilege. The open syscall lets the OS check permissions for the path to open, and is therefore not implemented in WASI. Instead the openat syscall is implemented, because it takes a file descriptor with attached permissions as an argument. This provides a very precise way of handling access on a module basis like described in the nano process model.

poll_oneoff()

Berkeley sockets can operate in one of two modes: blocking or non-blocking. Whereas the new networking proposal for WASI contains three modes: blocking, non-blocking and async.

blocking io

In blocking mode, the socket returns control after finishing all or some of the data for that operation.

non blocking io

In non blocking mode the socket returns whatever is in it’s buffer. A polling mechanism can be used for notifying of I/O activity. In posix there is the poll() syscall, in WASI they called it poll_oneoff(). So poll_oneoff() returns when there is I/O ready to be performed.

async io

Async is like non blocking with the difference that it returns when the I/O operation has completed. So calling the I/O operation immediately returns, then in your flow you can call another blocking function which returns on completion of the I/O operation.

Conclusion

Wasm is new high potential technology which opens a lot of new opportunities for us developers. I really like following and sharing my thoughts on the developments of Wasm and more specifically WASI to learn more about Wasm itself but also about underlying technologies, and aboutthe design process. People like Nathaniel McCallum put a lot of effort in designing WASI with principles as the least privilege and capability based security, and we can learn a lot from them, so thank you!

shadow-left