Transleight-of-Hand: WebAssembly, Null Pointers, and Other Fun Ways to Break the Internet


TL;DR
You take some good ol’ trusty C code, toss it through your shiny new WASM cross compiler, and voilà! You’ve got a WebAssembly (WASM) binary ready to run in your browser, server, toaster, or whatever else is trending this week.
After all, WASM’s promise is straightforward: take your existing native code, sandbox it in a fast, portable runtime, and get close-to-native performance in a secure environment.
What could go wrong?
Plenty, as it turns out, actually. Not fire-and-brimstone wrong. More like “Wait, why is this null pointer still alive and making HTTP requests?” wrong.
WASM doesn’t always play by the rules you’re used to. The cross compilation translation introduces subtle behavioral changes and sometimes those changes create vulnerabilities. And it’s not always obvious anything has gone wrong until your safe code starts doing very unsafe things.
In this post, I walk through my recent foray into how the translation from native code to WASM can quietly introduce unintended behaviors and shed light on some quirks that turn boring old bugs into shiny new attack surfaces.
This study and proof-of-concept was the result of a team effort between me, Eddie Federmeyer (email, linkedin), and Chang Won Choi (email, linkedin), my colleagues and friends at UIC as part of the project research for CS588 by Prof. Chris Kanich.
Let’s explore how WASM, in all its sandboxed glory, still finds ways to surprise you.
Let’s Set the Stage: What Even Is WASM?
WASM is a portable, sandboxed virtual instruction set for running code (mostly C/C++, Rust, etc.) in your browser at near-native speeds. It's like the bytecode that runs your favorite browser-based applications, but cooler and with more ambition. It was originally built to power performance-heavy apps on the web, like Photoshop or Google Earth. But it has since spread its wings to things like IoT devices, edge computing, and even cloud runtimes.
The cool part? WASM gives you a platform-independent binary format. It lets you write in languages like C, C++, or Rust, and then cross-compile that code into a compact .wasm binary. The runtime executes it in a sandbox, keeping things fast and, in theory, secure and consistent.
The less cool part? Things don’t always behave the way you expect them to. And by “don’t always,” I mean “might literally leak secrets, rewrite memory, or jump to shady websites if not carefully implemented.”
This is not to say that WASM cannot be secure, rather that one cannot simply assume it to be secure just because the native code was. What is being argued here is that a WASM program should have WASM-specific checks and tests before being put out into the world.
The Idea: What If Translation Isn’t Perfect?
The goal was simple: take very simple and typical C/C++ programs, compile them to WASM using Emscripten, the most generally used WASM cross compiler, and see if anything breaks in interesting, or unexpected, ways.
Spoiler alert: it does.
WASM implementations make design trade-offs that differ from native execution. Those differences, especially around memory safety and control flow, quietly create exciting new flavors of unintended behavior.
The result? Your native code might not be as perfect as you thought when it passes through the magical gates of the WASM cross compiler.
Let’s walk through some of the most interesting things we observed.
Null Pointers Are Just… Memory?
In C, dereferencing a null pointer is the moral equivalent of sticking a fork in a power outlet. Your program crashes. Lesson learned.
In WASM, though? The null pointer is just address 0. It’s a totally valid, writable memory location in the module’s linear memory.
Let us take this simple code block as an example:
int* ptr = NULL;
*ptr = 42;
printf("%d\n”, *ptr);
On native systems, this code crashes(as expected).
In WASM, it runs perfectly and prints 42. No trap, no warning. You’ve just written to null, and everyone’s fine with it.
Granted, the code will show a NULL pointer exception and stop further execution once this code block has been completely executed, but there are no crashes or issues until that point.
The result? Yes, this program eventually crashes, but not before printing the value which was written to the null pointer, i.e. 42.
This alone is a bit unsettling. But it gets worse when one realises that the null address can now potentially start storing secrets, control data, or anything you didn’t mean to expose.
The Return of the Format String Bug
You might think format string vulnerabilities are a 2003 problem. And you'd be mostly right! Mostly, because WASM exists.
Consider this sample code:
char secret[] = "SensitiveInfo123";
printf(user_input, &secret);
If user_input
includes a %n
, printf
will write to the address of secret
, and suddenly printf
becomes a memory write primitive again.
Why? Because in WASM, format string protections are... relaxed. Hardened C libraries don’t always carry over, and ASLR is nowhere to be seen.
Now, does this require the user to specifically know the memory space they are working with in a WASM runtime? Yes, but we know that even though it is highly improbable, it is not impossible.
Is it subtle? Yes. Can it be dangerous? Also yes.
Bounds Checking: Somewhat Optional
Yes, you didn’t read that wrong. WASM enforces memory access within the bounds of the total allocated linear memory. But it doesn’t necessarily care much about your arrays.
char buffer[10];
buffer[15] = 'x';
As long as index 15 is within the total module memory, you’re good. No bounds checking on individual buffers, no segmentation faults.
Now, Emscripten does include a stack overflow check. But a single compiler flag disables it, and there’s no way to know if it was disabled or not by simply looking at the wasm binary:
-s STACK_OVERFLOW_CHECK=0 //compiler flag to disable stack overflow checks
What does this mean?
Unless you’re being deliberate, out-of-bounds access might go completely unnoticed. More importantly, it means that issues arising from bounds check CAN be avoided, but that requires you to not trust third-party compiled binaries and cross compile the native code yourself, thus ensuring no sneaky compilation flags cause any issues.
The Great Null Pointer Exception Delay
And now, for the sleight-of-hand trick that inspired the title.
In WASM, dereferencing a null pointer doesn’t crash your program immediately. Instead, the runtime checks for problems after the function ends.
Remember how you could write to the null pointer(mem address 0), print out the value of a supposed null pointer, and the program didn’t crash until after it had printed out the written value? Well here is the reason for it.
You can do this:
User* data = NULL;
data->id = 1337;
printf("%d\n", data->id); // Seems fine—for now
As long as nothing complains before the function exits, the null pointer dereference goes unnoticed.
It gets weirder. If you jump out of that function into another one (say, by calling another function), the check is reset. No exception is ever thrown. You just… get away with it.
Why? It’s how Emscripten uses “stack cookies” to detect tampering. But if you hop functions mid-execution, the runtime never realizes the original function did anything wrong.
While this is another quirk that can potentially be circumvented by a combination of deliberate coding decisions alongwith enabling specific compiler flags, one runs into the same problems as we have been seeing throughout this post:
You need to compile the binaries yourself, ensuring all the correct flags are enabled/disabled since there is no way to distinguish this from a compiled WASM binary.
You cannot just have a functioning native code be cross compiled into WASM without needing to change anything, something that is supposed to be the original promise of WASM.
Proof of Concept: A Subtle Exploit
Let’s say you have a function like this:
void simulate(...) {
UserMeta* meta = NULL; //UserMeta is represents any user defined struct
if (debug) {
test_path(meta, debug_code, uid);
}
}
Looks innocent. But inside test_path()
, we do something sneaky:
meta->on_trigger = hidden_callback; // Assignment to a null ptr
meta->on_trigger();
Just as aforementioned, this crashes at the assignment line in native C.
In WASM? No error. The function pointer is assigned, and the callback executes. In our proof of concept, that callback simply redirected the browser to a meme. But it could just as easily hit an external endpoint or start a full-blown exploit chain.
And because this code looks harmless from a native perspective, it’s easy to miss during review if not careful.
What This Means for Developers
WASM isn’t doing anything “wrong” here. These behaviors are all part of its spec/implementation. But the implications can catch you off guard if you're expecting native-like behavior.
Key lessons:
WASM is its own environment: Don’t assume native behavior maps 1:1. Even simple things like null pointer handling can be an (un)pleasant surprise.
Legacy code needs a second look: Just because your C code is secure on Linux doesn’t mean it’s safe in a browser after cross-compilation using WASM.
Audit your compiler flags: The defaults aren’t always conservative. Know what you’re enabling, or disabling, to prevent unwanted behaviors.
Memory layout matters: WASM’s linear memory is predictable. That predictability can be a gift for attackers.
Boundaries are looser than they appear: WASM isolates code at a high level, but within a module, things can get very cozy, unless actively dealt with.
Want to See It for Yourself?
We’ve published our proof-of-concept code on GitHub. You’ll find examples of all the behaviors described here: sneaky little null pointer tricks, format string write primitives, and even a few I didn’t specifically mention here:
👉 https://github.com/EddieFed/wasm-exploits
Clone the repo, run the WASM binaries, and try the native versions side by side. It’s a fun way to see just how subtle (and sneaky) these mismatches can be.
Thanks for reading!
This whole endeavour was a pretty fun experience for me and my teammates, with each new quirky revelation leaving us more bewildered and excited. If you found this post an interesting read, or you are inspired to discover just how whacky WASM can potentially be yourself, feel free to connect with us, we’d love to hear from you!
The team:
Subscribe to my newsletter
Read articles from Sujot Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
