Home · ASPLOS 2024 · Security, Privacy & Trusted Execution

Pythia: Compiler-Guided Defense Against Non-Control Data Attacks

Sharjeel Khan, Bodhisatwa Chatterjee and Santosh Pande

Conference
ASPLOS 2024
Published
2024
Pages
850-866
Citations
2

An attacker who corrupts a program's memory does not always need to hijack a jump or a return address — they can instead overwrite an ordinary data variable so that a security check like "is this user an admin" flips the wrong way, sending execution down a perfectly legal but privileged path, which control-flow integrity cannot catch because no illegal jump ever happens; this paper (Georgia Tech) builds Pythia, which uses the compiler to trace which variables can be corrupted through input functions and which ones decide branches, then protects just those with ARM's pointer-authentication hardware, cutting the naive approach's roughly 48 percent slowdown to about 13 percent while securing more branches than the leading data-integrity defense.

The argument in one glance

  1. Problem

    An attacker can bend a program without ever hijacking a jump

    The classic memory-corruption attack overwrites a code pointer — a return address or a function pointer — to redirect execution to code of the attacker's choosing. Modern defenses are good at stopping that. But there is a quieter, equally dangerous class: non-control data attacks, where the attacker corrupts ordinary program data and never touches a code pointer at all. In control-flow bending, the attacker overwrites the data that a branch depends on so the branch flips: imagine a program that checks whether a user string equals "admin" and, if so, runs privileged code. By overflowing a buffer to overwrite that user string, the attacker makes the comparison succeed and execution follows the privileged path — a path that is entirely legal in the program's control-flow graph. No illegal jump ever occurs; the program simply takes a valid branch it should not have. Because the diverted execution follows a legitimate edge, the defenses built to catch illegal control transfers see nothing wrong.

  2. What people did

    Defend control flow with control-flow integrity

    The dominant defense against control hijacking is control-flow integrity (CFI): instrument the program so that every indirect jump or call is checked against the set of legal targets in the control-flow graph, blocking any transfer that leaves the graph.

  3. Why it fell short

    CFI is blind to a branch bent through data, and data defenses are costly

    Control-flow bending defeats CFI by construction: the bent branch takes a target that is already in the control-flow graph, so CFI, which only enforces that transfers stay within the graph, cannot tell the difference between the correct path and the attacker's. Existing data-oriented defenses do address this — data-flow integrity checks that data reaches only along legal definition-use paths — but at a steep cost: reported overheads of forty to a hundred percent from constant runtime checking, poor compatibility with C++ codebases, and an inability to handle attacks that corrupt program pointers to alias one variable onto another. The problem is also large: in real code the majority of branch terminators are conditionals and a huge fraction of pointers can influence a branch, so protecting everything is both slow and hard to analyze.

  4. This paper's idea

    Trace what can be corrupted and what decides branches, then protect just those

    Pythia is a compiler-guided defense. Using program slicing, it works out two things: the forward slice of variables that an attacker can reach through the program's input channels (the functions like scanf and strcpy that copy external data in), and the backward slice of variables that influence a branch predicate, including pointers. Protecting all of these naively with ARM's pointer-authentication hardware costs roughly 48 percent runtime overhead. Pythia makes it affordable by protecting only where overflows actually originate: it adds pointer-authentication-backed canaries around statically-allocated variables and relocates dynamically-allocated ones into isolated secure heap sections, cutting the average overhead to about 13 percent while securing more branches than the leading data-flow-integrity defense.

Why this problem is worth solving

The attack works precisely because it stays inside the rules the main defense enforces.

A non-control data attack corrupts data that does not itself hold a control-transfer target — not a return address, not a function pointer — yet still changes what the program does. Control-flow bending is a prominent form: the attacker overwrites the data a conditional branch reads, flipping the branch outcome so execution is diverted onto a different path. Crucially, that path is a legal edge in the program’s control-flow graph; the branch had two legitimate successors and the attacker just forced the wrong one. Control-flow integrity, the standard defense against control hijacking, works by checking that every indirect control transfer lands on a target permitted by the control-flow graph. But a bent branch never leaves the graph — both the intended and the diverted successor are valid — so CFI has nothing to flag. This is why control-flow bending is considered a serious gap: it turns a memory-corruption bug into privilege escalation or worse while remaining invisible to the defense most systems rely on, because the malicious execution is, edge for edge, a control-flow-graph-legal run of the program.

The hardware Pythia leans on was built to protect pointers, and repurposing it for data integrity is the key move.

ARM pointer authentication is a hardware feature that protects the integrity of a pointer by attaching a cryptographic tag to it. On a 64-bit machine the usable address space is much smaller than 64 bits, leaving spare high bits in every pointer; pointer authentication fills those bits with a pointer authentication code, a keyed cryptographic signature computed over the pointer value and a context. A sign instruction stamps the code into a pointer, and an authenticate instruction verifies it before the pointer is used; if an attacker has tampered with the pointer, the verification fails, because forging a valid code without the secret key is astronomically unlikely. The tag here is on the order of twenty-four bits, giving roughly a one-in-sixteen-million chance of a blind forgery, and each sign or authenticate operation costs only a couple of nanoseconds. Pointer authentication was designed mainly to protect return addresses and code pointers against control hijacking, but Pythia repurposes it as a general integrity primitive for ordinary data and for the canaries that guard it, using the same cheap hardware signatures to detect when a protected value has been corrupted.

What people did before, and where it broke

  • Control-flow integrity. Check every indirect control transfer against the legal targets in the control-flow graph. Where it broke. Control-flow bending diverts execution along an edge that is already legal in the graph, so CFI, which only enforces graph membership, cannot detect a branch that was bent through corrupted data.
  • Data-flow integrity. Enforce that data only flows along legitimate definition-use paths, catching illegal data reaching a branch. Where it broke. It does address bending but with heavy runtime overhead (reported at forty to a hundred percent) from pervasive checking, it is incompatible with many C++ codebases, and it cannot handle pointer-positioning attacks that corrupt a pointer to alias one variable onto another.
  • Address-space randomization. Re-lay out memory so an attacker cannot predict where to write, hindering bending. Where it broke. Randomizing at runtime costs a substantial twenty to fifty-five percent, has issues with C++ code, and only rearranges memory rather than verifying the integrity of the specific data that decides a branch.
  • Memory-safety enforcement. Add spatial and temporal memory safety through language extensions or pervasive pointer checks. Where it broke. It carries significant overhead from checking every pointer access and is typically specific to the stack or heap, a broad and expensive hammer rather than a targeted defense for the data that actually influences control.
  • The gap. No defense efficiently and generally protected exactly the data that lets an attacker bend a branch — the variables reachable from input channels and the variables that decide branch predicates — while working on pointer-heavy and C++ code. Doing that cheaply, by protecting only the vulnerable data with hardware-backed integrity, is what Pythia sets out to provide.

How Pythia works

From compiler slices to targeted, hardware-backed protection

  1. new

  2. new

  3. new

  4. new

  5. new

The idea

Protect the integrity of the data that decides control, not the control transfers themselves. The blind spot of control-flow integrity is structural: it guards where execution goes, but a bent branch goes somewhere legal, so the corruption has to be caught at the data instead. Pythia’s foundation is to shift the enforcement from control to data — to verify that the variables a branch depends on have not been tampered with, using ARM pointer authentication as a cheap hardware integrity check that signs a value and later confirms the signature. If the data behind a branch is intact, the branch cannot be bent; if it was corrupted, the authentication fails and the attack is caught, all without ever inspecting the control transfer. Enforcing integrity on data flows rather than only on control flows, so that an attack which keeps control on a legal path is still caught at the corrupted value, is the same combined-integrity stance the runtime type-and-scope enforcement work covered elsewhere on this site takes, protecting both control-flow and data-flow rather than assuming that guarding control is enough.

Let compiler slicing pinpoint the small set worth protecting. Protecting every variable that could influence a branch is both slow and, given how many pointers can reach a branch in real code, close to intractable. Pythia’s performance comes from using the compiler to narrow the target precisely: a backward slice from each branch predicate finds the variables that decide it, and a forward slice from the input channels finds the variables an attacker can actually corrupt, and the intersection is the small, genuinely vulnerable set that needs hardware protection. This is a security defense guided by exactly the dependency analysis a compiler already knows how to do, spending expensive protection only where a real attack path exists. Using precise program analysis to identify the specific variables and paths that matter, rather than instrumenting everything uniformly, is the same targeted-analysis discipline the path-sensitive, alias-aware analysis work covered elsewhere on this site relies on, concentrating effort on the program elements where a security-relevant fact actually turns.

Stop the overflow at its source with cheap, placement-aware guards. Even the vulnerable set can be expensive to protect if every access is signed and authenticated, so Pythia does something cleverer than checking each variable directly: it defends the memory regions where overflows happen. For statically-allocated data it rearranges the stack frame and inserts pointer-authentication-backed canaries around the vulnerable variables, so an overflow trips a tamper-evident canary before it reaches the data — and because the canary is signed with the hardware key, an attacker cannot simply overwrite it with a valid value. For dynamically-allocated data it relocates vulnerable variables into an isolated heap section via a custom allocator, so a stray write from elsewhere cannot reach them. Both moves detect corruption at the boundary where input channels write, which is far cheaper than pervasive checking. Hardening a program against memory-corruption exploitation by controlling where sensitive data lives and interposing tamper-evident guards, rather than validating every operation, is the same placement-and-isolation instinct the attack-surface-reduction work covered elsewhere on this site is built on, shrinking what an attacker can reach so a bug cannot be turned into a compromise.

Following a privilege-escalation attack against Pythia

Bending an admin check, and why it fails under Pythia

A trace, from the paper's abstract and the authors' own materials, of the classic control-flow-bending example: overflowing a buffer to flip an admin check, and how Pythia's canaries stop it.

Step 1 of 5

The vulnerable code compares a user string to admin

A function reads a user name and later checks whether it equals "admin" to decide between privileged and normal code. Nearby stack buffers, one of them filled from an input channel via a copy like strcpy, sit close enough in memory that an overflow could reach the user variable.

The branch on the admin comparison is exactly the kind of security-relevant conditional an attacker wants to bend.

Why it works

  • Guarding data catches attacks that keep control legal. Control-flow integrity only checks that transfers stay in the graph, and a bent branch stays in the graph → but verifying the integrity of the data behind a branch catches the corruption directly → so an attack that never makes an illegal jump is still detected at the tampered value.
  • Slicing shrinks the protection target to what matters. Most branches are conditional and vast numbers of pointers can influence them, so protecting all is infeasible → but a backward slice from branches intersected with a forward slice from input channels isolates the genuinely vulnerable variables → so hardware protection is spent only where a real attack path exists.
  • Hardware signatures make integrity checks cheap. Software data-flow integrity pays forty to a hundred percent from pervasive checks → but ARM pointer authentication signs and verifies in a couple of nanoseconds with a hard-to-forge tag → so integrity enforcement becomes affordable enough to deploy.
  • Guarding the overflow boundary beats checking every access. Authenticating every protected variable on every load and store is still costly, giving the naive scheme about 48 percent overhead → but interposing authenticated canaries and isolating heap sections detects corruption at the input-channel boundary → so the average overhead drops to about 13 percent.
  • Signed canaries and isolation resist the attacker’s workarounds. A plain canary or a co-located heap object could be overwritten or reached → but signing the canary with the hardware key and relocating vulnerable heap data into an isolated section blocks forgery and cross-allocation writes → so the guards hold against the corruption they are meant to stop.

What it costs (trade-offs and scope)

  • This is a draft assembled without the paper’s main text. The full paper was behind a publisher access wall for this draft (the ACM PDF was not retrievable and there is no preprint), so this account is built from the paper’s abstract together with the authors’ own conference slides and poster, which supply the mechanism and the numbers. The quantitative results are the authors’ reported figures; the paper’s detailed algorithms, threat model, and full evaluation should be read from the paper itself.
  • It requires ARM pointer-authentication hardware. Pythia’s efficiency comes from a specific hardware feature, so it is tied to platforms that provide pointer authentication; the approach does not directly transfer to hardware without such support, and its security rests on the secrecy of the pointer-authentication key and the roughly one-in-sixteen-million forgery bound of the tag.
  • Protection is scoped to sliced, input-reachable branch data. Pythia protects the variables its slices identify as both branch-influencing and reachable from input channels; corruption that reaches a branch through a path the analysis misses, or attacks that do not flow through the modeled input channels, fall outside the guarantee, and the completeness depends on the precision of the compiler analysis over pointer-heavy code.
  • There is real, if reduced, overhead and binary growth. The performance-aware scheme still adds about 13 percent runtime overhead and around 10 percent binary size, down from roughly 48 percent and 22 percent for the complete scheme; this is a substantial improvement but not free, and the exact cost varies with how many input channels and vulnerable variables a program has.
  • It is a targeted defense, not full memory safety. Pythia defends against control-flow bending of branches through corrupted data; it is not a general spatial-and-temporal memory-safety solution, and it composes with, rather than replaces, other defenses. Its advantage over data-flow integrity is shown on pointer-intensive and C++ applications and on real exploits, but as a security result its coverage is defined by its threat model.

The evaluation

Defending against control-flow bending

PropertyControl-flow integrityData-flow integrityPythia
Catches branches bent through data
Runtime overhead
Pointer-intensive and C++ codeVaries
Uses hardware supportSometimesMostly software
Branches securedn/a for bending
From the paper's abstract and the authors' conference materials. Pythia cuts the complete-protection scheme's roughly 48 percent overhead to about 13 percent, reduces pointer-authentication instructions several-fold, and secures more branches than data-flow integrity (about 92 percent versus 87 percent on average, fully securing three applications), while handling pointer-intensive and C++ code and real exploits like ProFTPd.Underlined values explain themselves when clicked.

Check yourself

Check your understanding

  1. 1. What is control-flow bending, and why does control-flow integrity fail to stop it?

  2. 2. How does Pythia decide which variables to protect?

  3. 3. How does Pythia use ARM pointer authentication, and how does it keep overhead low?

  4. 4. What did Pythia achieve, and how should this explainer be read?

Sources

The paper is “Pythia: Compiler-Guided Defense Against Non-Control Data Attacks” by Sharjeel Khan, Bodhisatwa Chatterjee, and Santosh Pande (Georgia Institute of Technology), published at ASPLOS 2024 (Volume 3), DOI 10.1145/3620666.3651343.

This explainer is a draft. The full paper text was not accessible at the time of writing (the ACM Digital Library PDF returned a bot-verification challenge, there is no arXiv version, and the authors’ publication pages linked only supplementary materials), so it is written from the paper’s abstract together with the authors’ own ASPLOS 2024 conference slides and poster, which present the same work in detail. From these sources: the framing of non-control data attacks and control-flow bending (an attacker corrupts program data to flip a branch predicate and divert control along a path that remains legal in the control-flow graph, defeating control-flow integrity, illustrated by the admin-check privilege-escalation example and a pointer-positioning example where an overflow corrupts a data pointer to alias one variable onto another); the limitations of prior defenses (data-flow integrity with forty-to-a-hundred-percent overhead and C++ and pointer-aliasing issues, address randomization with twenty-to-fifty-five-percent overhead, and memory-safety schemes with pervasive-pointer-check overhead); the background on ARM pointer authentication (a pointer authentication code stored in spare high bits, roughly twenty-four bits giving a one-in-sixteen-million forgery chance, with sign and authenticate instructions costing about two nanoseconds); the scale of the problem (over half of branch terminators being conditionals in SPEC 2017 and Nginx, and over two hundred thousand of nine hundred thousand pointer instructions in SPEC 2017 affecting conditional branches); the complete-pointer-authentication baseline that backslices branch predicates and signs and authenticates all vulnerable variables at about forty-eight percent overhead and twenty-two percent binary growth; Pythia’s performance-aware design that forward-slices input channels (scanf, strcpy, memcpy, and similar) to refine the vulnerable set, rearranges the stack and inserts pointer-authentication-signed canaries for statically-allocated variables, and relocates dynamically-allocated variables into an isolated secure heap section via a custom allocator; and the reported results (average overhead of about thirteen percent and binary growth of about ten percent, several-fold fewer pointer-authentication instructions, branch protection of about ninety-two percent versus about eighty-seven percent for data-flow integrity with three applications fully secured, and effectiveness on real-world exploits such as ProFTPd). The paper’s detailed algorithms, formal threat model, and complete evaluation are not reproduced here and should be read from the paper; no numbers beyond the authors’ reported figures have been introduced.

Related papers on Security, Privacy & Trusted Execution

See all 153 →