WASM as a Secure Sandbox: From the Browser to Distributed Runners
02 Oct 2025 - tsp
Last update 02 Oct 2025
8 mins
When I first looked into WebAssembly (WASM), I thought of it only as âthat thing that lets you run high-performance code in the browserâ and didnât give it too much of relevance from my point of view - I donât like the idea of moving everything into the browser. A closer look revealed something much more powerful: WASM is a general-purpose, sandboxed runtime. Much like the Java VM once promised with its security manager or .NETs Silverlight attempted in the browser, WASM provides a controlled execution environment for potentially untrusted code. The crucial difference is that it does so as an open standard, with a well-specified security model, and with multiple independent implementations.
This makes WASM not just interesting for web developers, but also a great tool if you need to execute third-party or user-supplied code in your own infrastructure. In my case, I wanted to safely run untrusted functions on self coordinating distributed service runners. WASM turned out to be a perfect fit.
Think of this article as a personal reference: a way to capture the workflow and reasoning so that when I revisit the topic years from now, Iâll have a clear guide to follow.

Compiling C/C++ Code with Emscripten
WASM is a low level assembly language and a binary bytecode format - so in practical day to day use its less of a programming language that you use directly (usually - Iâve written also whole applications in assembly back in the days on x86) but as a compilation target. Thanks to the versatile LLVM projects with itâs modular frontends and backends, any LLVM-fronted language (C, C++, Rust, Zig, etc.) can emit WASM. On FreeBSD, installing Emscripten is as simple as:
Emscripten can do more than just compile C/C++ into .wasm
modules. It can produce .wasm
together with JavaScript glue code for browsers, or it can also emit pure JavaScript without WASM at all. In fact, historically Emscripten was first created before WASM existed, targeting asm.js and pure JavaScript as a way to run C/C++ in the browser. Its design goal was to make it possible to port complex native applications - such as SDL-based desktop games and multimedia apps - into the browser. Today, with WASM, it has become a powerful bridge between native code and both browser and non-browser runtimes.
Letâs take, for example, a tiny add.c
:
int add(int a, int b) {
return a + b;
}
One can compile this for the browser with glue code:
env BINARYEN=/usr/local/bin LLVM=/usr/local/bin emcc add.c -s WASM=1 -s EXPORTED_FUNCTIONS='["_add"]' -o add.js
This generates both add.wasm
(binary) and add.js
(loader). A minimal HTML harness could look like the following:
<script src="add.js"></script>
<script>
Module.onRuntimeInitialized = () => {
console.log("7 + 3 =", Module._add(7, 3));
};
</script>
Emscripten can also produce so called side modules without boilerplate JavaScript, using SIDE_MODULE=2
. A side module is essentially a compiled WASM unit that omits the default runtime glue code and standard library exports, leaving only the functions you explicitly choose to export. This makes it smaller and more flexible, but it also means you must provide any required imports (like callback functions) from your own JavaScript environment. Then you load the raw .wasm
directly in JavaScript:
const imports = { env: { log_number: x => console.log("WASM says:", x) } };
WebAssembly.instantiateStreaming(fetch("add.wasm"), imports).then(obj => {
console.log("7 + 4 =", obj.instance.exports.add(7, 4));
});
This demonstrates the modularity of WASM: native code compiled down to a well-defined bytecode, sandboxed by the browser or runtime.
Running WASM Outside the Browser
The real power of WASM shows when you leave the browser. There are multiple runtimes available:
- JIT-based runtimes (for example
wasmtime
/ wasmer
): These compile WASM to native code at runtime and can achieve very high performance. However, they can be painful to build crossâplatform, and in my experience even after spending days trying to get them to run on FreeBSD, they were nearly impossible to make work because they are not as portable as often claimed.
- Interpreters (for example wasm3): These execute WASM without JIT compilation. wasm3 is one of the most powerful interpreters available, and it was the only one that actually built and worked out of the box on FreeBSD without any manual intervention for me at the time of writing. While interpreters are slower, they are much easier to integrate and very predictable.
For this article, I chose wasm3 as the example interpreter since it provided a straightforward and working solution where JITâbased options failed.
The Python package of wasm3 - it is actually a C based runtime that you can embed in a variety of languages - can be installed via pip:
Letâs take again a minimal standalone WASM module (sum.c
):
int sum(int a, int b) {
return a + b;
}
Compile it with the following Makefile
:
sum.wasm: sum.c
env BINARYEN=/usr/local LLVM=/usr/local/bin emcc sum.c -O3 -s STANDALONE_WASM=1 -Wl,--no-entry -s EXPORTED_FUNCTIONS='["_sum"]' -o sum.wasm
Now itâs extremly simple to load and execute this from Python:
import wasm3
from pathlib import Path
wasm_path = Path("sum.wasm").read_bytes()
env = wasm3.Environment()
rt = env.new_runtime(8 * 1024) # 8 KiB stack
mod = env.parse_module(wasm_path)
rt.load(mod)
sum_fn = rt.find_function("sum")
print("sum(21,21) =", sum_fn(21, 21))
The function executes inside wasm3âs interpreter - sandboxed, portable, and isolated from the host system.
Why WASM Is Safer
Unlike older plugin-like runtimes (Java applets, Silverlight, Flash), WASM is designed with safety as a first principle:
- Memory model: linear memory, bounds-checked, no direct pointers into host memory.
- Capability model: WASM modules cannot access files, sockets, or syscalls unless explicitly granted.
- WASI: The WebAssembly System Interface - in case it is used - defines a minimal, capability-based API (e.g., âopen this directoryâ) rather than exposing arbitrary system calls.
- Open standard: multiple runtimes, multiple vendors, formal specification.
This makes WASM a safer building block for embedding untrusted code execution into your own systems. There is even work about provably safe sandboxing in WASM.
Why itâs not a magical solution
Please note that even though WebAssembly has been designed as a safe sandbox as with any dynamic execution environment where you run other peoples untrusted code - there is the possibility of evading the sandbox. WASM is no exception from this so there already had been known cases of escaping the sandbox via bugs in the runtime (if you really want to be safe you cannot execute arbitrary code - itâs simple as that):
Conclusion
WebAssembly has grown far beyond its browser origins. It is a portable, secure, and open sandbox for running untrusted code, making it ideal not only for web apps but also for distributed computing, serverless functions, and edge devices. With tools like Emscripten for compilation and runtimes like wasm3
or wasmtime
, itâs straightforward to take C (or Rust, Go, âŚ) code, compile it, and execute it safely across platforms.
In a world where running code you did not write yourself is increasingly common, having a reliable sandbox that is easy to embed is a big deal. This same bytecode is also increasingly adopted by cloud providers, who let users deploy short functions or transformations directly at their edge nodes. In those environments, WASM serves as a safe, portable building block for distributed data processing close to the user.
This article is tagged: