Enforcing isolation with the Wasm2c sandbox
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:
-
Update the application
main.cpp
to use thewasm2c
sandbox backend instead ofnoop
. -
Compile our library e.g.
mylib.c
to wasm i.e.mylib.wasm
- adding isolation to your library. Compile that resultingmylib.wasm
file to C (mylib.wasm.c
andmylib.wasm.h
) with thewasm2c
compiler - allow it to be compiled and linked with our application.
We will look at each these steps next and end with instructions on how you can try this out.
Modifying the application to use the wasm2c RLBox plugin/backend
Making this change is very simple with RLBox. In fact, it can be done
exclusively in the boilerplate. Here is the boilerplate to use the wasm2c
backend.
// We're going to use RLBox in a single-threaded environment.
#define RLBOX_SINGLE_THREADED_INVOCATIONS
// The fixed configuration line we need to use for the wasm2c sandbox.
// It specifies that all calls into the sandbox are resolved statically.
#define RLBOX_USE_STATIC_CALLS() rlbox_wasm2c_sandbox_lookup_symbol
// The rlbox wasm2c plugin requires that you provide the wasm2c module's name
#define RLBOX_WASM2C_MODULE_NAME mylib
// Include the produced header from wasm2c
#include "mylib.wasm.h"
#include "rlbox.hpp"
#include "rlbox_wasm2c_sandbox.hpp"
using namespace rlbox;
// Define base type for mylib using the noop sandbox
RLBOX_DEFINE_BASE_TYPES_FOR(mylib, wasm2c);
You'll probably notice that there are only a handful of changes.
- We now use
"rlbox_wasm2c_sandbox.hpp
instead ofrlbox_noop_sandbox.hpp
- The sandbox type which is the second parameter to the macro
RLBOX_DEFINE_BASE_TYPES_FOR
has now changed towasm2c
fromnoop
- The boilerplate for
RLBOX_USE_STATIC_CALLS
has changed to use the wasm2c backend's boilerplate - The wasm2c backend/plugin requires an extra piece of boilerplate which is the name of the wasm module as specified in the macro
RLBOX_WASM2C_MODULE_NAME
- The wasm2c backend/plugin requires the produces
mylib.wasm.h
(we'll discuss how to produce this in the next section), to be included in the file
These are mostly mechanical changes and are straightforward. Modifying the build is perhaps slightly more challenging as building wasm libraries involves multiple steps.
Modifying the build to produce to wasm sandboxed library
To show how we will update the build, we will use two CMakeLists.txt files as a reference
- The CMakeLists used by the noop sandbox
- The CMakeLists used by the wasm2c sandbox
As you can see the wasm CMakeLists is quite a bit longer. Below, we will give a high-level overview of the steps, so you can follow what is happening in the Wasm build.
To build and use the Wasm sandboxed library, we need several additional repos/tools
- We will need the rlbox wasm2c plugin/backend
- We will need a version of clang that can produce Wasm files, specifically, Wasm files that target WebAssembly System Interface (WASI). WASI is a group of standards-track API specifications designed to provide a secure standard interface for Wasm applications. Specifically, you need WASI if you want to use printf, timers, anything that makes a syscall. We will thus rely on the wasi-sdk, which provides wasi-clang, a version of clang that can target WASI, and wasi-libc (a custom version of musl libc modified for this use case).
- Finally, we will use the wasm2c Wasm
compiler. Wasm files need to be compiled into native libraries that can be
linked in your application. Unlike regular native libraries however, these
libraries are produces by sandboxed compiler is guaranteed to be sandboxed.
The
wasm2c
compiler in particular compiles Wasm files by first transpiling it to C (this produced C is basically machine code with a lot of sandboxing checks, and is not going to be readable), and then compiling the resulting C with a regular C compiler to produce native objects.
After we download these repos, we can then take the following steps
-
Build the wasm2c sandbox compiler and runtime. This is a project that can be built using CMake. You can read more about how to build wasm2c in their readme
-
We need to compile our
mylib.c
tomylib.wasm
using wasi-clang. The command in theCMakeLists.txt
that does this is${wasiclang_SOURCE_DIR}/bin/clang --sysroot ${wasiclang_SOURCE_DIR}/share/wasi-sysroot/ -O3 -Wl,--export-all -Wl,--no-entry -Wl,--growable-table -Wl,--stack-first -Wl,-z,stack-size=1048576 -Wl,--export-table -o ${MYLIB_WASM} ${C_DUMMY_MAIN} ${CMAKE_SOURCE_DIR}/mylib.c
There are a number of flags that start with
-Wl
that must be specified so we produce a Wasm file with the properties we'd expect. You can read more about these flags in the Wasm lld docs page. The output file${MYLIB_WASM}
corresponds tomylib.wasm
and the input C files aremylib.c
and${C_DUMMY_MAIN}
.${C_DUMMY_MAIN}
as the name indicates is an empty main function seen here. You could avoid this dummy main by using Wasm's reactor flag. -
Next we need to run
wasm2c
to transpile our wasm file back to a C file with checks. This is fairly straightforward, and you can read more about it in the wasm2c documentation -
We now have to compile the transpiled wasm file. The process for doing this is described in detail in the wasm2c repo. Broadly, we need to compile the transpiled files with the wasm2c runtime and appropriate includes. This will now generate our native sandboxed library
mylib
-
We can now build our application using
mylib
Building and running the wasm2c backend
To build this example on your machine, run the following commands
cd rlbox-book/src/examples/wasm-hello-example
cmake -S . -B ./build -DCMAKE_BUILD_TYPE=Debug
cmake --build ./build --config Debug --parallel
Then run it to make sure everything is working.
./build/main
You should see the following output:
Hello from mylib
Got array value 7
echo: hi hi!
hello_cb: hi again!