When an attacker can corrupt a program's memory but cannot inject new code, they instead stitch together short snippets of the program's own code — gadgets — into a chain that does what they want, and bloated software offers a huge supply of them; this paper (Georgia Tech) presents Decker, which does not delete code (that risks crashing the program) but keeps most of it un-mapped and only makes each function executable on demand when the current phase of execution actually needs it, so at any moment the attacker sees only a small fraction of the gadgets — cutting available gadgets by roughly three-quarters with a few percent slowdown, no user configuration, and every feature intact.
The argument in one glance
-
Problem
Bloated programs hand attackers a huge supply of reusable code snippets
Modern systems stop attackers from injecting their own machine code — memory that can be written cannot also be executed. So attackers turn to code that is already there. Given a memory-corruption bug, they overwrite the places that decide where execution goes next, and instead of running injected code they chain together short sequences of the program own instructions, each ending in a jump or return, called gadgets. Enough gadgets, stitched into the right order, can do anything: open a shell, disable a protection, exfiltrate data. The supply of gadgets grows with the size of the program, and modern software is enormously bloated, carrying vast amounts of code that a given deployment never actually uses. That unused code is pure ammunition: it contributes gadgets while contributing nothing the running program needs.
-
What people did
Debloat the program by subsetting it to what a user says they need
Attack-surface-reduction work removes the parts of a program a particular user will not use, guided by user-specified inputs, configurations, or features, or breaks up gadget chains. Cutting out that code cuts out its gadgets, and reported gadget reductions can be high.
-
Why it fell short
Removing code is either unsound or too timid
Deciding what to delete is the hard part. If the analysis is aggressive, it may remove code that an ordinary, non-attack execution genuinely needs, so the program crashes on a legitimate input — unsound. If it is careful enough to be safe, it must keep anything that might be reached, so it leaves a large amount of attack surface in place. And relying on the user to specify inputs, configurations, or features pushes a security-critical, error-prone decision onto people, and narrows the program to less than its real functionality.
-
This paper's idea
Do not remove code — map it in only on demand, phase by phase
Decker keeps the whole program intact but does not keep it all executable at once. It divides execution into phases (a phase is called a deck) and, using static analysis, computes which sets of functions each phase actually needs. At runtime only those functions are mapped in and executable; the rest are absent, so their gadgets are simply not available to an attacker during that phase. Nothing is deleted and no user input is required, so all features stay intact and ordinary runs never crash for lack of code — yet at any moment the attacker sees only a small slice of the program gadgets.
Why this problem is worth solving
The defense everyone relies on is exactly what pushes attackers toward reusing existing code. A code-reuse attack is how an adversary achieves malicious execution on a system that forbids injecting new code. Because modern platforms enforce that writable memory is not executable, an attacker who has gained the ability to corrupt memory (through a buffer overflow or similar bug) cannot simply write their shellcode and run it. Instead they reuse the code already loaded in the process: they overwrite return addresses or function pointers so that, when the program follows them, control lands on short fragments of legitimate code and hops from one fragment to the next. Return-oriented and jump-oriented programming are the well-known forms. The attacker is effectively writing a program whose instructions are pieces of the victim, and the richer the victim in reusable pieces, the easier that is — which is why shrinking the set of reusable pieces available at any moment is a direct and general defense.
The reusable pieces themselves are the unit of attack, and counting them is how defenses are scored. A gadget is a short sequence of machine instructions, already present in a program, that ends in an instruction transferring control elsewhere — a return or an indirect jump or call. That terminating transfer is what lets an attacker chain gadgets: by controlling the stack or a register, they make each gadget hand off to the next, composing a larger malicious computation out of fragments that were never meant to run in that order. A useful gadget might load a value into a register, or make a system call, or adjust the stack. Because a whole attack is built by chaining gadgets, the number and variety of gadgets reachable in a process is a standard measure of its attack surface, and breaking the specific chains an attacker needs — for example the sequence that spawns a shell — can defeat the attack even if some gadgets remain. Reducing how many gadgets are accessible at once, and breaking their chains, is precisely what attack-surface-reduction techniques aim to do.
What people did before, and where it broke
- Debloat by removing unused code, guided by user inputs. Ask the user which inputs, configurations, or features they need, and strip the rest of the program, taking its gadgets with it. Where it broke. The reduction is only as good and as safe as the user specification; guess wrong and the program loses functionality or crashes on a legitimate input, and getting it right is a security-critical burden placed on people.
- Aggressive automatic subsetting for high gadget reduction. Use static analysis to cut as much code as possible to maximize the reported gadget reduction. Where it broke. Aggressive removal is unsound — it can delete code that an ordinary, no-attack execution actually reaches, so the program fails during normal operation, which is unacceptable for a defense meant to run in production.
- Conservative subsetting to preserve correctness. Keep anything the analysis cannot prove is unreachable, to be sure normal runs never break. Where it broke. Soundness bought this way is timid: it must retain large amounts of code that is rarely or never used but cannot be proven dead, so a great deal of attack surface survives untouched.
- Focus on third-party libraries. Concentrate reduction on libraries, since they carry many vulnerabilities and much unused code. Where it broke. It leaves the rest of the application unaddressed; whole-program attack surface, including the application own code, remains available for gadget chaining.
- Break gadget chains rather than remove gadgets. Rearrange or instrument code so that chains of gadgets can no longer be linked together. Where it broke. Useful, but a complementary tactic; on its own it does not shrink the sheer quantity of gadgets present, and its guarantees depend on the specific chaining it disrupts.
- The gap. No approach reduced whole-program attack surface substantially while staying sound (never crashing normal runs) and requiring no user input and removing no features. What was missing was a way to make most of a program gadgets unavailable to an attacker without permanently removing any code the program might legitimately need.
How Decker works
Keeping code intact but executable only on demand
-
-
new
-
new
-
new
-
new
-
new
The idea
Reduce attack surface in time, not by amputation. Every prior debloating technique faces the same dilemma because it makes a permanent, spatial decision: this code stays, that code goes. Cut too much and a legitimate run crashes; cut too little and gadgets remain. Decker sidesteps the dilemma by making the decision temporal instead. It keeps all the code — so nothing a legitimate execution might need is ever missing — but changes what is executable over time, so that at any given moment only the functions the current phase of execution actually uses are mapped in. The attack surface an adversary faces is no longer the whole program but the small window of code live right now, and that window shrinks and shifts as execution moves through its phases. Defending by continuously changing what an attacker can reach in the address space, rather than by fixing a smaller program once, is the same moving-target instinct the continuous address-space re-randomization work covered elsewhere on this site is built on, keeping the layout an attacker must exploit in constant flux so that stale knowledge of it is useless.
Let whole-program static analysis, not the user, decide what each phase needs. The soundness problem in debloating comes from asking a fallible source — often the user, via specified inputs or features — to declare what the program will need, and then deleting the rest. Decker instead computes the needed function sets itself, by analyzing the whole program to find, for each execution phase, the code that phase can legitimately call, and the program points where the enabled set should change. Because the enabling is derived from the program own structure rather than a user guess, no feature is dropped and a legitimate execution always finds its code mapped when it reaches it; the reduction requires no user input at all. Deriving a runtime security policy automatically from a whole-program static analysis, so protection does not depend on a human correctly enumerating what is allowed, is the same discipline the runtime type-and-scope enforcement work covered elsewhere on this site relies on, turning static program facts into checks the running system applies without asking the developer to specify them by hand.
Shrinking the live code set both starves gadgets and breaks their chains. A code-reuse chain needs its particular gadgets to all be present and reachable at the moment the attack fires. By keeping only the current phase functions executable, Decker removes most gadgets from availability at once — the abstract reports roughly three-quarters or more of gadgets made inaccessible — and, crucially, it can break the specific chains an attack depends on: if even one gadget in a shell-spawning sequence lives in code that is not mapped during the phase where the attacker gains control, the chain cannot be assembled. Confining what code a running program can actually execute at a given time, so that the reachable-and-abusable set is a small fraction of the binary, is the same containment principle the software fault-isolation work covered elsewhere on this site is built on, hemming in the code that can run so that the damage any single component can do is bounded by construction.
Following an attack attempt against Decker
A shell-spawning chain meets on-demand code mapping
An illustrative walk, based on the paper's abstract and title, of how Decker frustrates a code-reuse attack. The internal details of how phases are computed and code is mapped come from the paper body, which was not available for this draft.
Step 1 of 5
The attacker corrupts memory and aims to hijack control
Through a memory-corruption bug the attacker overwrites a return address or function pointer. Since injected code cannot execute, their plan is a code-reuse chain: land on a first gadget, then hop through a sequence of gadgets that together set up and make the system call to spawn a shell.
This is the standard threat model for attack-surface-reduction work: a strong memory-corruption primitive, blocked only from executing freshly written code.
Why it works
- Temporal reduction dodges the delete-or-keep dilemma. Removing code must choose between crashing legitimate runs and leaving attack surface → but keeping all code and only changing what is executable over time removes neither functionality nor safety → so Decker can be both sound on normal inputs and aggressive in what it denies an attacker at any instant.
- A program uses only a fraction of itself at a time. All code is normally executable throughout the run even though each phase touches little of it → but enabling only the current phase functions matches executability to actual need → so the gadgets reachable at any moment shrink to those in a small live set, reported as roughly a quarter or less of the total.
- Automatic analysis removes the user from the loop. Debloating guided by user-specified inputs or features is error-prone and drops functionality → but computing the needed function sets by whole-program static analysis derives the policy from the program itself → so the reduction needs no user input and keeps every feature intact.
- Breaking one link breaks the chain. A code-reuse attack needs all of its gadgets present together at the moment it fires → but on-demand mapping means at least one required gadget can fall in currently-unmapped code → so specific chains, like the shell-spawning sequence, cannot be assembled even if some gadgets remain.
- Confining live code bounds the abuse, cheaply. Making the whole binary executable maximizes what an attacker can reuse → but restricting execution to the phase current functions limits the reusable set by construction → so the defense is general and whole-program while costing, per the abstract, only a few percent in runtime.
What it costs (trade-offs and scope)
- This is a draft written without the paper’s main text. The full paper was behind a publisher access wall for this draft (no preprint, author copy, or web-archive snapshot was available), so the account here is built from the paper’s complete abstract and its title. The reported numbers are the abstract’s own figures and are not invented, but the internal mechanisms — how phases are computed, how code is mapped and unmapped, and the chain-breaking case-study details — are not reproduced and should be read from the paper.
- Protection is per-phase, not absolute. The reduction is in the gadgets accessible during an execution phase; gadgets in the currently-mapped functions remain usable, and how much protection a given moment provides depends on how small and how well-separated the phases are. An attacker who can act within a phase that maps the code they need is not stopped by the code being intact elsewhere.
- It rests on the quality of the static analysis. The soundness and the reduction both depend on the whole-program analysis correctly identifying, for every phase, the functions it may legitimately need. Imprecision that over-approximates needs erodes the reduction; any under-approximation would risk correctness, so the analysis must be conservative in the safe direction, which bounds how aggressive the reduction can be.
- There is a real, if modest, runtime cost. Enabling and disabling function sets at phase transitions is not free; the abstract reports about 5.2 percent average slowdown on SPEC CPU 2017 and 2 percent on nginx, with negligible cost on coreutils, so the overhead is small but non-zero and workload-dependent.
- The evidence is a set of benchmarks, and the metric is gadget availability. Results are reported on SPEC CPU 2017, ten coreutils programs, and nginx, with a jump-oriented-programming chain-breaking case study on Linux and Windows. This is a strong demonstration against a standard attack-surface metric, but reduction in reachable gadgets is a proxy for security, not a proof that no code-reuse attack is possible.
The evaluation
Three stances on shrinking code-reuse attack surface
| Property | User-guided debloating | Conservative auto-subsetting | Decker (on-demand mapping) |
|---|---|---|---|
| What happens to code | |||
| Soundness on normal runs | |||
| Attack surface reduced | |||
| User input required | |||
| Features and runtime cost | All kept, little gain |
Check yourself
Check your understanding
1. What is a code-reuse attack, and why do gadgets matter?
2. How does Decker differ from debloating techniques that remove code?
3. Why can Decker break a specific attack chain even if some gadgets remain reachable?
4. What should a reader keep in mind about this particular explainer?
Sources
The paper is “Decker: Attack Surface Reduction via On-Demand Code Mapping” by Chris Porter, Sharjeel Khan, and Santosh Pande (Georgia Institute of Technology), published at ASPLOS 2023 (Volume 2), DOI 10.1145/3575693.3575734.
This explainer is a draft. The paper’s main text was behind a publisher access wall (the ACM Digital Library gold PDF was not retrievable, there is no arXiv version, no author preprint was found, and no web-archive snapshot exists). It is written from the paper’s complete abstract and its title. The abstract supplies the problem framing (code-reuse attacks assembling gadgets from bloated software; existing debloating being unsound or conservative and reliant on user inputs), the core technique (a whole-program, general approach that reduces gadgets accessible during an execution phase called a deck; static analysis determines key function sets to enable or disable at runtime; the runtime enables these sets at specified program points; no user inputs are required and all features are kept), and the reported results (73.2 percent total gadget reduction with 5.2 percent average slowdown on SPEC CPU 2017; 87.2 percent reduction with negligible slowdown on ten GNU coreutils applications; 80.3 percent reduction with 2 percent slowdown on the nginx server; and a chain-breaking case study with jump-oriented-programming gadget metrics on Linux and Windows, breaking the shell-spawning chain in all cases). The characterization of on-demand code mapping as a temporal, phase-by-phase enabling of code follows the abstract and the paper’s title; the internal design of the static analysis and the runtime mapping mechanism, and the detailed case-study metrics, are not reproduced here and should be read from the paper. No performance numbers beyond the abstract’s own have been introduced.