Adding isolation with the Wasm sandbox backend

The noop backend makes it easy to add security checks. However, it does not enforce isolation. To finish sandboxing your library, we will need to.

  1. Update the application main.cpp to use the wasm2c sandbox backend instead of noop.

  2. Compile our library e.g. mylib.c to wasm i.e. mylib.wasm - adding isolation to your library.

  3. Compile that resulting mylib.wasm file to C (mylib.wasm.c and mylib.wasm.h) with the wasm2c compiler - allow it to be compiled and linked with our application.

  4. Compile and link the sandboxed library and our application.

We will look at each these steps next.

Switching to the wasm2c backend

Show below is a diff of main.cpp in our first example (using the noop sandbox backend) and in our current example (using the wasm2c backend).

1   $diff ../noop-hello-example/main.cpp main.cpp
2   3a4,7
3   > 
4   > // Provide the sandbox a name
5   > #define RLBOX_WASM2C_MODULE_NAME mylib
6   > 
7   5c9
8   < #define RLBOX_USE_STATIC_CALLS() rlbox_noop_sandbox_lookup_symbol
9   ---
10  > #define RLBOX_USE_STATIC_CALLS() rlbox_wasm2c_sandbox_lookup_symbol
11  9,11d12
12  < #include <rlbox/rlbox.hpp>
13  < #include <rlbox/rlbox_noop_sandbox.hpp>
14  < 
15  12a14,17
16  > #include "mylib.wasm.h"
17  > #include "rlbox.hpp"
18  > #include "rlbox_wasm2c_sandbox.hpp"
19  > 
20  18c23
21  < RLBOX_DEFINE_BASE_TYPES_FOR(mylib, noop);
22  ---
23  > RLBOX_DEFINE_BASE_TYPES_FOR(mylib, wasm2c);

As you can see, most of what has changed is renaming a few key instances of noop to wasm2c, most notably to change the backend type that we include.

The only other changes are #include "mylib.wasm.h" on line 13, which brings in the new header for our sandboxed library generated by wasm2c, and #define RLBOX_WASM2C_MODULE_NAME mylib which will provide a unique name for this sandbox.

These are essentially all the changes you will need to make to your application to switch wasm backends.

The changes to how rlbox is included on lines 17 and 18 are just an artifact of differences how our examples are built.

Our example Makefile

Doing all of these steps a command at a time would be terribly tedious. Instead, we automate all these steps with a simple make file. Lets take a look at our full Makefile, then walk through each part.


#RLBOX headers

#Our Wasi-SDK

#location of our wasi/wasm runtime
WASI_RUNTIME_FILES=$(addprefix $(WASM2C_RUNTIME_PATH), /wasm-rt-impl.c /wasm-rt-mem-impl.c)

#Some more wasi/wasm runtime files
WASM2C_RUNTIME_FILES=$(addprefix $(RLBOX_ROOT)/src, /wasm2c_rt_minwasi.c /wasm2c_rt_mem.c)


#CFLAGS for compiling files to place nice with wasm2c
WASM_CFLAGS=-Wl,--export-all -Wl,--stack-first -Wl,-z,stack-size=262144 -Wl,--no-entry -Wl,--growable-table -Wl,--import-memory -Wl,--import-table

all: mylib.wasm mylib.wasm.c myapp

	rm -rf mylib.wasm mylib.wasm.c mylib.wasm.h myapp *.o

#Step 1: build our library into wasm, using clang from the wasi-sdk
mylib.wasm: mylib.c
	$(WASI_CLANG) --sysroot $(WASI_SYSROOT) $(WASM_CFLAGS) dummy_main.c mylib.c -o mylib.wasm

#Step 2: use wasm2c to convert our wasm to a C implementation of wasm we can link with our app.
mylib.wasm.c: mylib.wasm
	$(WASM2C) mylib.wasm -o mylib.wasm.c

#Step 3: compiling and linking our application with our library
myapp: mylib.wasm.c
	$(CXX) -std=c++17 main.cpp -o myapp -I$(RLBOX_INCLUDE) -I$(RLBOX_ROOT)/include -I$(WASM2C_RUNTIME_PATH) *.o -lpthread


To start we can see our Makefile begins with RLBOX_ROOT:


Which just specifies where our rlbox_wasm2c_sandbox repo's root directory lives. This repo contains all the tools we will need build our sandboxed library e.g. wasm2c, wasi-sdk (which CMake downloads), RLBox, etc.

Step 1: Compiling our library to Wasm

mylib.wasm: mylib.c
	$(WASI_CLANG) --sysroot $(WASI_SYSROOT) $(WASM_CFLAGS) dummy_main.c mylib.c -o mylib.wasm

Here we are building our library to wasm. Typically you will just want to update your build system to use the wasi-sdk clang (or wasi-clang) as your compiler. wasi-clang will link wasi-libc (a custom version of musl) with your library instead of the system libc. The wasi-libc library and headers live in $WASI_SYSROOT

Also note worthy are the $(WASM_CFLAGS) which are important to ensure that our output plays nicely with the rest of the toolchain. By default, the flags we use forces the Wasm module to use an "imported" memory allocation and "imported" indirection function table that is created by RLBox for optimal performance. (See Using a Wasm module with imported memory and imported tables for other options)

Notice the dummy_main.c file to keep wasi-clang happy. You can find a copy of this file at rlbox_wasm2c_sandbox/c_src/wasm2c_sandbox_wrapper.c.

wasi-libc at the moment has a variety of limitations such as lack of pthread support (though this should be fixed soon!). Anything platform specific such as OS specific system calls (or just system calls that Wasi doesn't support), or platform specific code e.g. inline assembly will also fail at this stage.

Step 2: Using wasm2c to generate our sandboxed library

mylib.wasm.c: mylib.wasm
	$(WASM2C) mylib.wasm -o mylib.wasm.c

Here we use wasm2c to generates a mylib.wasm.c C file which implements and can be linked with an application.

The wasi runtime that ships with the rlbox_wasm2c_sandbox plugin at present implements only a subset of the Wasi API and denies all access to the file system and network.

Step 3: Compiling and linking our application with our library

myapp: mylib.wasm.c
	$(CXX) -std=c++17 main.cpp -o myapp -I$(RLBOX_INCLUDE) -I$(RLBOX_ROOT)/include -I$(WASM2C_RUNTIME_PATH) *.o -lpthread