Ruling Out Code Execution Without Full Memory Safety

A compiler-based approach to provable RCE immunity in C/C++

The goal is a guarantee that an attacker cannot run code of their choosing. Reaching it takes less than full memory safety. It is enough to protect the integrity of two small, non-overlapping sets of values: the pointers that decide where execution goes, and the data that decides what a dangerous operation does once execution legitimately reaches it. Both sets are small enough that protecting them with strong, always-on guarantees is affordable in production, and under the conditions below, doing so rules out attacker-directed code execution entirely.

Two Mechanisms

M1, control-edge integrity. Every value that can end up in the instruction pointer keeps its integrity: return addresses, function pointers, C++ vtables, GOT/PLT entries, and anything that transitively reaches them. This is fine-grained control-flow integrity (CFI), not the coarse variant that published bypasses exploit by chaining gadgets inside an over-broad permitted set, and it is that precision which closes the gap.

M2, sink data integrity. A short list of dangerous calls (system, exec, posix_spawn, dlopen, mmap/mprotect with executable permissions, and similar) can only run with the arguments the untouched program would have produced. This protects the full chain of data feeding those arguments, not just the last write to them.

M1 descends from CFI and code-pointer integrity, and M2 from data-flow integrity restricted to a handful of sinks. What is different is applying both narrowly, to the specific weaknesses that decide whether code execution is reachable. Full CFI plus full data-flow integrity is expensive and protects far more than the code-execution question needs. Held to their own narrow closures, the cost mostly disappears.

Why Two Mechanisms Are Enough

Every route to attacker-chosen code reduces to one of three cases: bytes the attacker supplied directly (shellcode, stopped by W⊕X plus M1, since reaching it requires a hijacked pointer); existing code executed out of order (ROP/JOP, stopped by M1); or code the operating system runs on the program's behalf, reached either out of turn (M1) or on a legitimate path with corrupted arguments (M2). There is no fourth case, so the pair together closes the space.

Every route to attacker-chosen code, and what closes it Three routes run left to right toward code execution. Each is cut by the mechanism that closes it: shellcode by W-xor-X plus M1, ROP and JOP by M1, operating-system-run code by M1 or M2. A fourth lane is left empty because no fourth route exists. ROUTE TO ATTACKER-CHOSEN CODE CLOSED BY EXECUTION shellcode bytes the attacker supplied W⊕X + M1 ROP / JOP existing code, executed out of order M1 OS-run code reached out of turn, or with corrupted arguments M1 / M2 no fourth case; the pair closes the space
Every route to attacker-chosen code reduces to one of three cases, and each is closed by M1, M2, or both.

The Harder Case: Attacks Built From Legitimate Steps

The real objection is not a single corrupted argument but composition. An attacker who leaves control flow untouched can still repurpose the program's own operations as an instruction set by corrupting ordinary data such as loop counters and indices (data-oriented programming). Checking each write in isolation is not enough, because a value can be laundered through a chain of individually legitimate stores.

What is sufficient is protecting the entire dependence closure behind each sink argument, meaning both the data it derives from and the control-flow decisions that shaped it, rather than just its immediate assignment. Code-loading sinks take structured operands (a path, a command string) that real programs build from constants and configuration, not from long attacker-influenced arithmetic. That asymmetry is why the closures behind M2's sinks stay short in practice, even though the general data-oriented attack surface is large. Where a program genuinely does pipe rich external input into a code-loading call, as a plugin loader or a scripting host does, that chain is traceable and gets protected explicitly; it remains a small, enumerable slice of the program's total dataflow.

The classic pointer-walking gadget (a legitimate loop advancing a corrupted counter into an arbitrary read/write) is handled by isolating the protected regions themselves, so no amount of pointer arithmetic can address into them. The landing site is enforced, not the arithmetic that produced the address.

A Guaranteed Fallback

Completeness is what the whole guarantee rests on: the closures have to be sound, not merely convenient. Any value the compiler cannot cheaply prove protected is routed to a memory-safe interpreter as a fallback, which is safe by construction regardless of analysis precision. This keeps the failure mode conservative, so that uncertainty costs performance and never correctness. Most real values, though, resolve cheaply: constants get folded away, read-only data is protected by the MMU at zero cost, and only genuinely mutable, attacker-reachable values need runtime instrumentation or sandboxing.

Proven on FFmpeg

We validated this on FFmpeg, a large, heavily fuzzed C codebase with a long history of memory-corruption CVEs and one of the harder real-world targets available. FFmpeg's decode paths almost never reach a code-loading sink; they transform data rather than executing it, so M2's closures are nearly empty on the paths that matter. What FFmpeg does use heavily is indirect calls through codec and DSP function tables, which is exactly M1's job. With both mechanisms in place, an attacker holding a memory-corruption bug in a decoder has no path to code execution, for any input file. Measured overhead on FFmpeg stays within a few percent of an unprotected build. Against an IBT baseline on CET hardware it is lower still: four of the five FFmpeg workloads measured at or below baseline, and the fifth at +2.06%.

The property is deliberately narrow. It rules out execution of attacker-introduced or attacker-selected code, not attacker-influenced behaviour in general. A crafted input can still make a decoder compute the wrong answer or crash. But the highest-severity outcome, remote code execution, is the one this construction closes.

Beyond RCE: A Configurable Guarantee

The construction is not specific to code execution. A capability is just a set of sink operations plus the data reaching them. RCE is the code-loading sinks; data exfiltration is the outbound network and file-write sinks; privilege escalation is the authorization state gating privileged operations. Denying any of these follows the same recipe: enumerate the sinks, protect their dependence closures, and let the compiler pick the cheapest sound strategy per value. Customers choose which capabilities matter for their threat model and pay only for those.

CapabilitySinks whose closures are protected
Remote code executionsystem, exec, posix_spawn, dlopen, mmap/mprotect with executable permissions
Data exfiltrationOutbound network calls and file writes
Privilege escalationThe authorization state gating privileged operations

Summary

Protecting two small, well-defined closures, control-edge pointers and dangerous-call arguments, is enough to rule out attacker-directed code execution by construction, with a formally guaranteed fallback for anything the analysis cannot cheaply resolve. That is a narrower target than full memory safety and a far cheaper one. On a real, heavily-attacked target like FFmpeg it holds within a few percent of baseline, and at or below baseline on CET hardware for most workloads.