How WebAssembly Ensures Control-Flow Integrity
WebAssembly (Wasm) provides robust security guarantees by design, particularly through its enforcement of Control-Flow Integrity (CFI). This article explains how Wasm prevents unauthorized code execution and invalid memory jumps by utilizing a structured control flow, typed function calls, a segregated call stack, and strict validation. These mechanisms collectively ensure that an attacker cannot redirect execution to arbitrary or malicious memory addresses.
Structured Control Flow
In traditional assembly languages (like x86 or ARM), control flow is
managed using arbitrary jumps (jmp) to direct memory
addresses. This flexibility makes software vulnerable to exploits like
Return-Oriented Programming (ROP), where attackers hijack the control
flow by jumping to malicious “gadgets” in memory.
WebAssembly eliminates arbitrary jumps by enforcing structured
control flow. Instead of raw jump instructions, Wasm uses structured
constructs such as block, loop, and
if. * Branch targets are static: Branch
instructions (br, br_if,
br_table) can only target the boundaries of these
pre-defined control blocks. * No arbitrary
destinations: It is physically impossible to jump into the
middle of a function or instruction sequence. The execution path must
conform strictly to the nested structure of the code.
The Protected Call Stack
In languages like C or C++, the execution call stack (containing return addresses) and local variables share the same memory space. A buffer overflow vulnerability can allow an attacker to overwrite a function’s return address, redirecting execution to malicious code when the function returns.
Wasm prevents this vulnerability by separating the execution stack from the linear memory: * Inaccessible Call Stack: The Wasm call stack, which manages function calls and holds return addresses, is managed entirely by the runtime environment (the browser or Wasm engine). It is completely inaccessible to the running Wasm program. * Safe Linear Memory: The Wasm program can only read and write to its designated “linear memory.” Because return addresses do not reside in linear memory, a buffer overflow in user data cannot overwrite a return address or alter the program’s execution flow.
Type-Safe Indirect Calls via Tables
When programs require dynamic execution paths—such as implementing virtual functions, function pointers, or callbacks—they use indirect function calls. In native environments, this involves jumping to a memory address stored in a pointer, which an attacker could overwrite.
Wasm secures indirect calls using tables and
runtime type checks: 1. Indirect Call Tables
(call_indirect): Instead of using raw memory
addresses, Wasm references functions via an index in a restricted
“Table.” The runtime maps these indices to actual function pointers. 2.
Bounds Checking: Before performing an indirect call,
the Wasm runtime checks the index. If the index is out of the table’s
bounds, execution halts immediately. 3. Signature
Verification: Every indirect call instruction specifies an
expected function signature (return type and parameter types). At
runtime, the engine verifies that the signature of the target function
in the table matches the expected signature. If there is a mismatch, the
program traps (halts execution), preventing type-confusion attacks.
Static Validation
Before a Wasm module is compiled or executed, the runtime performs a mandatory static validation phase. This validation ensures that: * All control flow structures are correctly nested. * All branch targets refer to valid, existing blocks. * Stack types are consistent across all paths of execution.
Any binary that attempts to perform invalid jumps or bypass these safety rules is rejected immediately before a single line of code is run.