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.