# Process Mitigation Policies: CFG, ACG, CIG, and the Layer Between App Identity and the Kernel

> A thirty-year history of Windows process mitigation policies -- DEP, ASLR, CFG, XFG, CET, ACG, CIG -- and the structural reason each one exists.

*Published: 2026-05-11*
*Canonical: https://paragmali.com/blog/process-mitigation-policies-cfg-acg-cig-and-the-layer-betwee*
*License: CC BY 4.0 - https://creativecommons.org/licenses/by/4.0/*

---
<TLDR>
Windows ships every modern memory-corruption mitigation as a per-process flag rather than a system-wide setting -- because Outlook can't enable CIG, Defender can't enable ACG, and Notepad doesn't need Disable-Win32k. `SetProcessMitigationPolicy` exposes twenty of these knobs (plus a `MaxProcessMitigationPolicy` sentinel that terminates the enum); the canonical six (DEP, ASLR, CFG, CET shadow stack, ACG, CIG) constrain the control-flow primitives, and the other fourteen cover adjacent attack surfaces. Each knob is a tombstone for an exploit primitive that worked in the previous generation. This article walks the thirty-year arc that built that surface, then names the residual attacks that survive even a fully-stacked process.
</TLDR>

## 1. The bug is still there. Why didn't the exploit work?

A vulnerability researcher has just landed a type-confusion bug in a JavaScript engine inside an Edge content process. The primitive is exactly what they expected: a writable heap address holding a corrupted vtable pointer. From that pointer the renderer will, on its very next virtual-method call, jump into an address the attacker chose.

That is supposed to be game over. It is, in the language of every exploit-development textbook from 1996 onward, a working write-what-where. The CPU loads the corrupted pointer into a register. It dereferences it. It calls.

And the process dies.

There is no shell. There is no remote code execution. There is a Windows Error Reporting dialog and a `STATUS_STACK_BUFFER_OVERRUN` (also written `FAST_FAIL_GUARD_ICALL_CHECK_FAILURE`) in the crash log, raised from a thunk named `ntdll!LdrpValidateUserCallTarget` the researcher has never seen in their disassembler before. The bug fired exactly as the recipe said. The exploit chain didn't.

What stopped it?

> **Note:** Every per-process mitigation in `SetProcessMitigationPolicy` is a tombstone for an exploit primitive that worked in the previous generation. The list of policies is, read top to bottom, an attacker's autobiography [@ms-setprocessmitigationpolicy].

<Definition term="Process Mitigation Policy">
A per-process, opt-in security policy installed via the Win32 `SetProcessMitigationPolicy` API (or, more safely, via `UpdateProcThreadAttribute` before a child process executes its first user-mode instruction). The `PROCESS_MITIGATION_POLICY` enum lists twenty-one values -- twenty actual policies plus the `MaxProcessMitigationPolicy` sentinel that terminates the enum -- as of Windows 11 24H2, each one a separate axis on which an exploit can fail [@ms-process-mitigation-enum, @ms-setprocessmitigationpolicy].
</Definition>

The fastest way to see this is to compare two PowerShell sessions. Pick a maximally-hardened process, the Edge content process, and run `Get-ProcessMitigation -Name MicrosoftEdgeCP.exe`. Six mitigations show as ON: CFG, CET shadow stack, ACG, CIG, Disable-Win32k, and Disable-Extension-Points. Now do the same for `Notepad.exe`. One or two show as ON. Notepad is a different *kind* of process -- it is not parsing attacker-controlled bytes from the public internet, so the mitigation surface it carries is correspondingly small.

<MarginNote>The mitigation set is not just an enable-everything list. Several of the policies are mutually expensive (CET costs cycles on every call/ret; ACG forbids any in-process JIT; CIG forbids any third-party plugin); turning them all on is only viable for a process whose owner accepts those costs. The PowerShell `Set-ProcessMitigation` and `Get-ProcessMitigation` cmdlets ship in the `ProcessMitigations` module that succeeded EMET in 2018.</MarginNote>

Edge carries six mitigations because it has six structurally separate ways the attacker can win. CFG addresses the indirect-call hijack. CET addresses the return-address hijack. ACG addresses the "redirect the JIT to emit my shellcode" hijack. CIG addresses the "plant a Microsoft-signed DLL where the loader picks it up" hijack. Disable-Win32k addresses the renderer-to-kernel escape. Disable-Extension-Points addresses the `AppInit_DLLs`-class injection.

Each one is the closing footnote on a different generation of offensive research. CFG closes return-oriented programming. CET closes the shadow-stack-less era. ACG closes JIT spray. CIG closes signed-DLL planting. `Get-ProcessMitigation` lays them out as a flat list of `ON` checkmarks, as if they had always been there -- as if they had not each cost a decade of research to design and ship.

So the chain failed. But *which* mitigation caught the indirect-call hijack we started with -- and why was that one on? Where do these mitigations come from, and how did Windows arrive at this exact set? To answer that, we have to go back three decades.

## 2. How attackers stopped being able to put bytes on the stack and run them

The story starts in November 1996. *Phrack* magazine, issue forty-nine, file fourteen of sixteen. Aleph One -- the handle of Elias Levy, a security columnist who would later moderate the BugTraq mailing list -- publishes *Smashing The Stack For Fun And Profit* [@phrack-49-14]. The article is a recipe. It walks the reader through process memory layout on Unix, the structure of the call stack on x86, the mechanics of overwriting the saved return address, the construction of `/bin/sh` shellcode, and the use of NOP sleds. By the end the reader has working exploit code against `syslog`, `splitvt`, `sendmail 8.7.5`, and Linux/FreeBSD `mount`.

Buffer overflows existed before Aleph One. The 1988 Morris Worm used one in `fingerd`; Mudge's 1995 *How to Write Buffer Overflows* L0pht paper had pieces of the technique. But it was an oral tradition -- something you learned at DEFCON or from someone who learned it at DEFCON. Aleph One's contribution was pedagogical: a step-by-step recipe anyone with a debugger and an afternoon could follow. Once that recipe was published, every memory-safety bug in C and C++ -- and there were many -- became a candidate for shell-as-the-vendor.

The defensive response came fast, and it came with a brutal honesty that has shaped every later mitigation. In August 1997, Alexander Peslyak, writing under the handle Solar Designer and running the Openwall Project, posted to BugTraq [@solar-designer-bugtraq-1997]. He had two things. The first was a Linux kernel patch -- still documented at the Openwall README to this day -- that made user-mode stack pages non-executable in software, since AMD's hardware NX bit was six years away [@openwall-readme]. The second was a working return-into-libc exploit against `lpr`, which redirected execution into `system()` in the C library rather than into stack-resident shellcode.

<Sidenote>Solar Designer was honest enough to publish the bypass on the same day as the patch. This is a defender-publishes-own-bypass precedent that has governed almost every Microsoft mitigation announcement since: ship the mitigation, name the residual attack class, set the expectation that the mitigation is a speed bump rather than a fix.</Sidenote>

<Definition term="W^X">
A memory protection invariant -- "write XOR execute" -- requiring that any page in the process address space be either writable or executable, but never both at the same time. PaX shipped the first complete implementation of W^X on Linux in 2000; AMD's NX bit in 2003 moved it from software emulation to hardware enforcement; the per-process ACG policy in Windows generalises W^X to apply for the lifetime of an entire process, with no per-thread escape hatch.
</Definition>

The next move was structural. In September 2000 the pseudonymous PaX Team released PAGEEXEC, the Linux non-executable-page implementation that made every writable page non-executable (not just the stack), using clever x86 segment-limit and split-TLB tricks [@wiki-pax]. PaX is also where the term "ASLR" comes from. The July 2001 PaX patch series randomized the executable base, the stack, the heap, the `mmap`'d library region, and (with `RANDEXEC`) even the position of the executable's code segment. The PaX design document for ASLR is unusually rigorous about probability -- it derives the expected number of brute-force attempts as a function of entropy bits, decades before anyone framed it that way in the academic literature [@pax-aslr, @wiki-aslr].

<Definition term="ASLR">
Address Space Layout Randomization. Per-boot or per-load randomization of the locations at which the kernel maps modules, the stack, the heap, and `mmap`'d regions into a process's virtual address space. On x86-32 Windows Vista, modules had one of 256 possible base addresses (about 8 bits of entropy). On x64 with `/HIGHENTROPYVA`, entropy is much higher because the virtual address space is larger. ASLR is the precondition that makes every later forward-edge CFI scheme worth deploying -- without it, the attacker just hardcodes the call target.
</Definition>

Hardware finally caught up on September 23, 2003. AMD shipped the no-execute bit -- "NX bit," bit 63 of the 64-bit long-mode page-table entry -- with the Athlon 64 launch [@wiki-nx-bit]. Intel followed with the marketing-renamed "XD bit" in later Pentium 4 Prescott silicon. From 2003 onward, marking a page non-executable was a single PTE flag away.

Microsoft consumed the hardware almost immediately. Windows XP Service Pack 2, RTM August 6, 2004, shipped Data Execution Prevention as a system-wide feature [@ms-dep, @wiki-data-execution-prevention, @wiki-windows-xp-sp2]. DEP defaulted to OptIn but supported four system-level modes (OptIn, OptOut, AlwaysOn, AlwaysOff) and exposed a per-binary opt-in via the `/NXCOMPAT` PE-header flag. On hardware without NX, DEP fell back to a software emulation limited to system-supplied binaries.

The Wikipedia ROP article frames this moment exactly: "Microsoft Windows provided no buffer-overrun protections until 2004" [@wiki-rop]. After XP SP2, Windows joined PaX, OpenBSD, and Solar Designer's Openwall on the W^X side of the line.

Three years later, in January 2007, Microsoft shipped Vista. Vista randomized DLL and EXE module bases at boot, with 256 possible load locations per module on x86. Michael Howard's MSDN design blog from May 2006 gives a worked example showing `wsock32.dll` at `0x73ad0000` on one boot and `0x73200000` on the next [@ms-howard-vista-aslr]. Vista paired ASLR with `/GS` stack canaries, `/SafeSEH` validated SEH chains, DEP, and pointer obfuscation -- the first Microsoft OS to ship a layered exploit-mitigation stack as policy.

<Mermaid caption="The 1996-2007 timeline: from stack smashing to system-wide DEP plus ASLR">
flowchart LR
    A[1996 Nov<br/>Aleph One<br/>Phrack 49 14] --> B[1997 Aug<br/>Solar Designer<br/>non-exec stack<br/>+ return-into-libc]
    B --> C[2000 Sep<br/>PaX Team<br/>PAGEEXEC]
    C --> D[2001 Jul<br/>PaX<br/>first ASLR]
    D --> E[2003 Sep<br/>AMD NX bit<br/>Athlon 64]
    E --> F[2004 Aug<br/>Microsoft DEP<br/>Windows XP SP2]
    F --> G[2006 May<br/>Microsoft<br/>Vista ASLR design]
    G --> H[2007 Jan<br/>Vista GA<br/>layered mitigation]
</Mermaid>

DEP and ASLR are not per-process mitigations in the modern sense. They are the system-wide foundation that the per-process surface sits on top of. The reason `ProcessDEPPolicy` still exists in the modern enum at all is to give 32-bit processes a way to enforce DEP locally even when the system policy is permissive. On x64, DEP is unconditionally on; the per-process knob is a vestigial 32-bit-only flag. `ProcessASLRPolicy` is more useful -- it allows a process to force-on high-entropy bottom-up randomization with `ForceRelocateImages` -- but it too is a refinement of a system-wide foundation, not a new defensive primitive [@ms-setprocessmitigationpolicy].

By 2007, the story should have been over. DEP had made shellcode unrunnable. ASLR had made gadget addresses unpredictable. Every attacker primitive Aleph One named in 1996 was, in principle, defended. It was not.

Because the attacker did not need to write new bytes. They could reuse the bytes that were already there.

## 3. ASLR plus DEP made shellcode hard, so attackers stopped writing shellcode

October 2007. Hovav Shacham, then on the UC San Diego computer-science faculty after a postdoctoral fellowship at the Weizmann Institute, presents *The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls (on the x86)* at ACM CCS [@shacham-rop-paper-page, @shacham-rop-pdf]. The paper's existence claim is simple and devastating: in any sufficiently large C library, the set of short instruction sequences ending in `ret` is Turing-complete. The attacker does not need to inject any new code. They only need to write data -- a sequence of return addresses on the stack -- and the CPU obediently executes already-mapped, already-executable libc bytes in the attacker's chosen order.

The mechanism is small enough to explain in a paragraph. Shacham named the technique *return-oriented programming*. The attacker arranges for the program to return into a *gadget* -- a short sequence of one to four instructions ending in `ret`. The gadget is selected from existing executable memory: libc, ntdll, the program's own code segment. The instructions perform a useful primitive (load a register, do arithmetic, dereference a pointer). The trailing `ret` pops the next stack slot, which the attacker has populated with the address of the next gadget. The stack is now the program counter; the CPU is now a Turing-complete machine for whatever language the gadget catalog implements.

<Definition term="Return-Oriented Programming (ROP)">
An exploitation technique in which the attacker chains short, existing instruction sequences ("gadgets") each ending in `ret`. Control transfers happen via the program's own return instructions, executing already-mapped, already-executable code. ROP defeats W^X (DEP, NX) because the attacker injects no new code; it weakens against ASLR but does not break under it because info-leak primitives recover the gadget base address. Coined by Hovav Shacham in 2007 [@shacham-rop-pdf].
</Definition>

The follow-up Black Hat USA 2008 talk generalised the result to RISC architectures [@shacham-bhusa-2008], killing "x86's variable-length instructions are why ROP works" as a defensive direction. ROP works on ARM. ROP works on MIPS. ROP works wherever an attacker can predict the address of executable bytes and control the stack.

<PullQuote>
"Return-oriented programming allows an attacker to execute code in the presence of security defenses such as executable space protection." -- Wikipedia, *Return-oriented programming*, lead paragraph [@wiki-rop]
</PullQuote>

After 2007, the structural agenda of every defensive engineering team on Windows changes. The question is no longer "can we stop the attacker from writing bytes into executable pages?" -- DEP solved that, and ROP routed around it. The question is now: "which control transfers is the attacker allowed to cause?"

<MarginNote>Shacham's UCSD lab (later UT Austin) kept exploring the boundary between code-reuse attacks and provable software defenses. The 2007 paper is the field-shaping one; the 2008 BHUSA generalisation to RISC was the closing argument.</MarginNote>

> **Key idea:** After Shacham 2007, every defensive engineering decision in Windows mitigation has been about which control-flow transfers the attacker is allowed to cause, not about what bytes the attacker can write. This is the article's load-bearing axis. CFG, XFG, CET, ACG, CIG, and every smaller mitigation in `PROCESS_MITIGATION_POLICY` follows from this one shift.

Microsoft's first response was behavioral, not structural. In 2009 the company released the *Enhanced Mitigation Experience Toolkit* (EMET), a free shim DLL that injected runtime checks into existing user-mode processes to detect ROP-shaped behavior. EMET checked for stack pivots, for unaligned `ret`-targets, for known-malicious gadget sequences, for unusual SEH chain layouts. It worked, intermittently, for a while. Then attackers adjusted, gadget-replacing around EMET's heuristics, and Microsoft slowly conceded the behavioral-detection direction was a dead end. EMET's final release was 5.52 in November 2016; end of life was July 31, 2018 [@wiki-emet]. Microsoft's stated successors are the `ProcessMitigations` PowerShell module and Windows Defender Exploit Guard -- i.e., the formal `SetProcessMitigationPolicy` surface this article catalogs [@wiki-emet].

<Aside label="A short detour through EMET, 2009-2018">
EMET was an honorable failure. It taught the security industry that you cannot detect a control-flow hijack by looking at its symptoms; you can only prevent it by enforcing an invariant on the control flow itself. That lesson is exactly what Control Flow Guard (CFG) and Control-Flow Enforcement Technology (CET) embody. Every behavioral-ROP-detection product since EMET (Carbon Black's BB exploit protection, Symantec's Heat Shield, vendor-specific EDR ROP checks) has had the same fate against motivated adversaries -- you can buy time but you cannot fix the problem in heuristics.
</Aside>

The structural answer arrived two years before the offensive proof that motivated it. In November 2005, at ACM CCS, Martín Abadi, Mihai Budiu, Úlfar Erlingsson, and Jay Ligatti published *Control-Flow Integrity* (also released as Microsoft Research Technical Report MSR-TR-2005-18) [@msr-cfi]. Their formal definition is short: *the execution of a program dynamically follows only paths defined by a static control-flow graph*. They proved CFI is enforceable using compile-time-inserted runtime checks and demonstrated a software rewriting implementation.

<Definition term="Control-Flow Integrity (CFI)">
A defensive property formalized by Abadi, Budiu, Erlingsson, and Ligatti in 2005 [@msr-cfi]: the execution of a program must dynamically follow only paths defined by the static control-flow graph (CFG) of the program. CFI partitions into a forward-edge property (the targets of indirect calls and jumps must be valid) and a backward-edge property (the targets of returns must be the call-sites that called them). CFG, XFG, kCFG, and Apple's PAC are forward-edge CFI implementations. CET's shadow stack is a backward-edge CFI implementation.
</Definition>

CFI was a research framework looking for a vendor. It would wait nine years. The reader's belief at this point might be "DEP plus ASLR is enough." The honest belief, after Shacham, is that DEP plus ASLR raises the cost but does not change the game. The attacker still wins if they can choose where the next `ret` lands. The structural answer -- constraining the control transfer rather than the write -- is what makes Control Flow Guard make sense.

What does *constraining the control transfer* look like in machine code?

## 4. Control Flow Guard (CFG): compile-time, load-time, runtime

Where DEP was enforced by hardware on every page, CFG is enforced by software on every indirect call. The compiler is now a security tool.

CFG's ship history is more complicated than the marketing remembers. The canonical primary on the early dates is Yunhai Zhang's Black Hat USA 2015 deck, *Bypass Control Flow Guard Comprehensively*, which states verbatim: "It was first introduced in Windows 8.1 Preview, but disabled in Windows 8.1 RTM for compatibility reason. Then, it was improved and enabled in Windows 10 Technical Preview and Windows 8.1 Update" [@zhang-bhusa15]. Visual Studio 2015 added the compiler and linker flags. By the time Windows 10 shipped to consumers in July 2015, CFG was a documented Win32 security feature [@ms-cfg-doc].

<Sidenote>Stage 1 had this ship date as "Windows 8.1 Update 3 November 2014 vs Windows 10 July 2015". Zhang's deck is the contemporaneous primary that resolves the dispute. CFG was in Windows 8.1 Preview, was *removed* from Windows 8.1 RTM for compatibility, returned in Windows 8.1 Update and Windows 10 Technical Preview, and shipped widely with Windows 10 in 2015.</Sidenote>

The mechanism has four phases. Each phase is a separate engineering subsystem, owned by a different team.

**Phase 1: Compile-time** (`/guard:cf`). The MSVC compiler emits, before every indirect call instruction, a call to one of two compiler-supplied thunks: `__guard_check_icall_fptr` for the standard pattern, or `__guard_dispatch_icall_fptr` for the tail-call optimization where the validator itself jumps to the target [@ms-guard-cf-compiler]. The thunk is a single indirection through ntdll. At compile time it is a stub; at load time it is patched to point at the active validator.

**Phase 2: Link-time** (`/GUARD:CF`, which requires `/DYNAMICBASE`). The linker writes the *Guard CF Function Table* (FID table) into the PE image's `IMAGE_LOAD_CONFIG_DIRECTORY` [@ms-guard-cf-linker]. This table is the static catalog of every CFG-valid call target in this binary: every function whose address is taken, plus every function exported. `dumpbin /headers /loadconfig <binary>` prints the table contents -- you can read the actual `Guard CF` flag word and the `FID table present` line.

> **Note:** The MSVC linker only emits the FID table when `/DYNAMICBASE` is also set [@ms-guard-cf-compiler, @ms-guard-cf-linker]. A binary compiled with `/guard:cf` but linked without `/DYNAMICBASE` will pass code review, ship, and provide zero protection at runtime. This is the single most common CFG misconfiguration in third-party software. Always confirm with `dumpbin /headers /loadconfig` that the `Guard Flags` word is non-zero and that `FID Table present` is in the output.

**Phase 3: Load-time.** At process startup and on every subsequent `LoadLibrary`, `ntdll!LdrpProtectAndRelocateImage` unions the FID table of the loaded image into a per-process *bitmap*. The bitmap is a sparse data structure with one bit per 8 bytes of virtual address space. On 32-bit Windows, that is about 32 megabytes of address space worth of valid-target bits. On x64, the address space is so large the bitmap is hundreds of megabytes sparse-allocated -- but the memory only commits on access, so the resident set stays small.

<Definition term="CFG bitmap">
A sparse, per-process bit vector indexed by virtual address (one bit per 8 bytes). A set bit at index `addr / 8` means that `addr` is a CFG-valid indirect-call target in some loaded image. The kernel commits the bitmap pages on first access and shares them copy-on-write across processes with identical module-load layouts. The bitmap is the runtime data structure that `LdrpValidateUserCallTarget` consults on every indirect call.
</Definition>

**Phase 4: Runtime.** Every indirect call goes through `ntdll!LdrpValidateUserCallTarget`. The validator takes the call target in `rcx` (x64 calling convention), divides by 8, indexes into the bitmap, and tests the bit. If set, return; the call proceeds. If clear, fall through to `__fastfail(FAST_FAIL_GUARD_ICALL_CHECK_FAILURE)`, which raises `STATUS_STACK_BUFFER_OVERRUN`. The process dies.

<Mermaid caption="CFG's four phases: compile, link, load, runtime">
sequenceDiagram
    participant Src as C++ source
    participant CC as "MSVC /guard:cf"
    participant Ln as "Linker /GUARD:CF /DYNAMICBASE"
    participant Ldr as ntdll loader
    participant Rt as Runtime
    Src->>CC: address-taken funcs plus indirect call sites
    CC->>Ln: object file plus FID hints
    Ln->>Ldr: PE with FID table in load-config dir
    Ldr->>Ldr: union FID table into bitmap
    Note over Ldr: one bit per 8 bytes
    Rt->>Ldr: indirect call via LdrpValidateUserCallTarget
    alt bit set
        Ldr->>Rt: proceed
    else bit clear
        Ldr->>Rt: fastfail STATUS_STACK_BUFFER_OVERRUN
    end
</Mermaid>

There is an exception: code that is generated at runtime, like a JavaScript JIT, cannot have its targets pre-baked into a static FID table. For this case, CFG exposes `SetProcessValidCallTargets`, which lets a process programmatically mark an in-process address range as a permitted call target [@ms-cfg-doc]. The companion `PAGE_TARGETS_INVALID` and `PAGE_TARGETS_NO_UPDATE` page-protection flags let the process control which newly-allocated pages start with a clear bitmap. The reason this API exists at all is the structural collision between W^X-via-CFG and runtime code generation -- a collision that section 8 (ACG) will eventually resolve by moving the JIT out of process.

You can read the load-config flag word directly. The hex value is a bit field of `IMAGE_GUARD_*` constants. The most common bits are `IMAGE_GUARD_CF_INSTRUMENTED` (the binary has CFG indirect-call checks), `IMAGE_GUARD_CFW_INSTRUMENTED` (the binary has CFG indirect-call checks plus write-protection checks), `IMAGE_GUARD_CF_FUNCTION_TABLE_PRESENT` (the FID table is in the PE), `IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT`, and `IMAGE_GUARD_RETPOLINE_PRESENT`. The decoder is short enough to inline:

<RunnableCode lang="js" title="Decode a Guard CF flag word from dumpbin /loadconfig output">{`
const FLAGS = [
  [0x00000100, 'IMAGE_GUARD_CF_INSTRUMENTED'],
  [0x00000200, 'IMAGE_GUARD_CFW_INSTRUMENTED'],
  [0x00000400, 'IMAGE_GUARD_CF_FUNCTION_TABLE_PRESENT'],
  [0x00000800, 'IMAGE_GUARD_SECURITY_COOKIE_UNUSED'],
  [0x00001000, 'IMAGE_GUARD_PROTECT_DELAYLOAD_IAT'],
  [0x00002000, 'IMAGE_GUARD_DELAYLOAD_IAT_IN_ITS_OWN_SECTION'],
  [0x00004000, 'IMAGE_GUARD_CF_EXPORT_SUPPRESSION_INFO_PRESENT'],
  [0x00008000, 'IMAGE_GUARD_CF_ENABLE_EXPORT_SUPPRESSION'],
  [0x00010000, 'IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT'],
  [0x00020000, 'IMAGE_GUARD_RF_INSTRUMENTED'],
  [0x00040000, 'IMAGE_GUARD_RF_ENABLE'],
  [0x00080000, 'IMAGE_GUARD_RF_STRICT'],
  [0x00100000, 'IMAGE_GUARD_RETPOLINE_PRESENT'],
];

// Real-world example value from a fully-instrumented MSVC 2022 binary
const guardFlags = 0x0001050C;
console.log('Guard Flags = 0x' + guardFlags.toString(16).padStart(8, '0'));
for (const [bit, name] of FLAGS) {
  if (guardFlags & bit) console.log('  set: ' + name);
}
`}</RunnableCode>

CFG is forward-edge only. The `ret` instruction is invisible to it. A ROP chain that uses only return-target gadgets -- the original Shacham construction -- is not affected by CFG at all, because CFG never asks "where did this `ret` go?" It only asks "where did this indirect call go?" Closing the backward edge is a separate problem (section 6).

CFG is also *coarse-grained*. The bitmap records "is this address a valid function entry?" but not "is this address a valid function entry *for this particular call site's prototype?*" Any function entry in the entire process is a valid CFG target for every indirect call site. If the attacker finds a legitimate function that takes a controllable argument and does something useful, they can chain it into a working exploit without ever flipping a clear bit to set.

Those two limitations -- forward-edge only, coarse-grained -- are precisely the open questions section 5 (XFG, fine-graining) and section 6 (CET shadow stack, backward edge) answer. CFG was the first floor. The next two sections build out the rest.

## 5. eXtended Flow Guard (XFG): type-hash, fine-grained CFI for indirect calls

CFG knows *is this a function entry?* XFG asks the better question: *is this the right kind of function entry?*

The structural reason XFG exists has a name and a paper. May 2015, IEEE Symposium on Security and Privacy. Felix Schuster, Thomas Tendyck, Christopher Liebchen, Lucas Davi, Ahmad-Reza Sadeghi, and Thorsten Holz publish *Counterfeit Object-oriented Programming: On the Difficulty of Preventing Code Reuse Attacks in C++ Applications* [@coop-ieeesecurity-pdf, @coop-ieeexplore]. The paper's abstract is constructive and brutal: COOP is "the first code-reuse attack to enable the synthesis of malicious behavior on x86 and ARM platforms" that "fully complies with previously presented coarse-grained CFI defenses."

<PullQuote>
"We propose a new attack technique, called Counterfeit Object-Oriented Programming (COOP), which is the first code-reuse attack to enable the synthesis of malicious behavior on x86 and ARM platforms and which fully complies with previously presented coarse-grained CFI defenses." -- Schuster et al., IEEE S&P 2015 [@coop-ieeesecurity-pdf]
</PullQuote>

<Definition term="COOP (Counterfeit Object-Oriented Programming)">
A code-reuse attack technique that chains legitimate C++ virtual function calls in attacker-chosen order, achieved by corrupting vtable pointers or vtable contents. Each individual callee is a real, address-taken function entry that passes any coarse-grained CFI bitmap. The attacker assembles Turing-complete computation by chaining these legitimate calls. Published by Schuster, Tendyck, Liebchen, Davi, Sadeghi, and Holz at IEEE S&P 2015 [@coop-ieeesecurity-pdf, @coop-ieeexplore].
</Definition>

The mechanism is simple to describe but hard to detect. The attacker corrupts a heap-resident C++ object's vtable pointer to point at a fake vtable they have crafted from gadget-like *virtual functions* of real classes in the binary. Each entry in the fake vtable points at the entry of a real virtual method. The program's own virtual dispatch sequence performs the calls. The control transfers all land at legitimate function entries. CFG, which only asks "is this a function entry?", sees nothing wrong.

Microsoft's first public disclosure of the answer came at BlueHat Shanghai in 2019. David Weston -- listed on the title slide of the deck as "Microsoft OS Security Group Manager" -- presented the design of *eXtended Flow Guard* (XFG) [@weston-bhshanghai-2019]. Microsoft never published a written XFG specification; the canonical public deconstruction is Connor McGarr's August 2020 reverse-engineering, which remains the best public account of how the mechanism actually works [@mcgarr-xfg].

The mechanism is elegant. At compile time, MSVC computes a 64-bit type hash for every function: a truncated SHA-1 of the concatenated function name, parameter types, return type, and calling convention. The compiler stores this hash 8 bytes *before* each CFG-valid function entry [@mcgarr-xfg]. At each indirect call site, the compiler knows the *expected* prototype (from the call's static type), emits the same hash inline, and the dispatch thunk reads the 8 bytes preceding the target and compares.

<Mermaid caption="XFG call site vs CFG call site: the prototype hash">
flowchart TD
    A[Indirect call site] --> B&#123;"CFG bitmap<br/>bit set?"&#125;
    B -->|No| F1[__fastfail]
    B -->|Yes| C&#123;"XFG enabled?"&#125;
    C -->|No| D[Proceed<br/>CFG only]
    C -->|Yes| E[Read hash<br/>at target - 8]
    E --> G&#123;"Hash matches<br/>expected prototype?"&#125;
    G -->|No| F2[__fastfail<br/>same status]
    G -->|Yes| H[Proceed<br/>full XFG]
</Mermaid>

A COOP attacker who replaces a vtable pointer with the address of a different real virtual function passes CFG: the new target is a valid function entry. They fail XFG: the 8 bytes preceding the new target encode a *different* prototype hash than the call site expects. The fix moves the granularity from "every function entry" to "every function entry compatible with this exact prototype" -- orders of magnitude closer to perfect forward-edge CFI.

XFG shipped in Windows 10 21H1 internals. The `/guard:xfg` MSVC flag was added. The XFG dispatch thunks (`__guard_dispatch_icall_fptr_xfg`) appeared in `ntdll.dll`. Then it didn't enable by default.

<Sidenote>Connor McGarr's Black Hat USA 2025 deck, *Out of Control: How KCFG and KCET Redefine Control Flow Integrity in the Windows Kernel*, states verbatim: "XFG was never fully instrumented (UM/KM) and is now deprecated." McGarr is listed on the title slide as Software Engineer, Prelude Security [@mcgarr-bhusa25].</Sidenote>

<Aside label="Why a strictly-better CFI scheme can still lose">
Two reasons XFG didn't ship enforcement-by-default. First, compatibility cost: XFG breaks any C-style cast through a different prototype. Windows is full of these, including in third-party drivers and inbox-COM components, and every breakage costs a customer ticket. Second, hardware overtook software. CET shadow stack arrived on Tiger Lake in September 2020 (section 6) and gave the entire backward edge for free, leaving the forward-edge problem partially un-fine-grained but the *complete* CFI surface achievable by composing CFG (forward, coarse) with CET (backward, perfect). The math worked out: ship CET strictly, and a coarse-grained forward edge is good enough -- because the backward edge, the bigger half of the call graph, is now perfect.

XFG remains the most interesting almost-shipped Windows mitigation. The instrumentation is in MSVC. The dispatch thunks are in `ntdll`. Enforcement-by-default never arrived, and the McGarr 2025 deck names it as deprecated. The strategic pivot to hardware is what Microsoft made instead.
</Aside>

What does that hardware look like, and what edge does it protect? Tiger Lake shipped in September 2020. For the first time since Shacham 2007, the kind of ROP that chains `ret`-terminated gadgets could be killed by the CPU itself.

## 6. Hardware-enforced Stack Protection (Intel CET shadow stack)

The Microsoft Tech Community post that introduced CET shadow stack on Windows -- preserved on the Wayback Machine because the live URL is a JavaScript-rendered shell -- gives the framing in one sentence:

<PullQuote>
"We shipped Control Flow Guard (CFG) in Windows 10 to enforce integrity on indirect calls (forward-edge CFI). Hardware-enforced Stack Protection will enforce integrity on return addresses on the stack (backward-edge CFI), via Shadow Stacks." -- Microsoft Tech Community, *Understanding Hardware-enforced Stack Protection* [@cet-techcommunity-wayback]
</PullQuote>

<Definition term="Shadow stack">
A second, per-thread stack maintained by the CPU in parallel with the regular call stack. Every `call` instruction pushes the return address to both stacks. Every `ret` pops both and compares. A mismatch raises a `#CP` (Control Protection) fault, which Windows surfaces as `STATUS_STACK_BUFFER_OVERRUN`. The shadow stack page is hardware-protected: only the new instructions `INCSSP`, `RDSSP`, `WRSS`, and the call/ret/IRET microcode can write to it. User-mode stores into a shadow-stack page fault.
</Definition>

The mechanism, drawn from Intel's CET specification and Microsoft's Windows enabling documents [@cet-techcommunity-wayback, @wiki-intel-cet, @ms-cetcompat]:

- Every `call` instruction now writes the return address twice -- once to the regular stack, and once to the per-thread shadow stack at `[SSP]`.
- The shadow-stack page is marked with a new MMU bit that makes it readable but not writable by general store instructions. Only the new instructions `INCSSP`, `RDSSP`, `WRSS`, `WRUSS`, and the call/ret/IRET microcode can store to it.
- Every `ret` pops the regular stack and pops the shadow stack and compares. Equal: proceed. Different: raise `#CP`. On Windows, `#CP` is routed through the `KiRaiseException` path as `STATUS_STACK_BUFFER_OVERRUN`.
- New instructions exist for legitimate unwinding. `INCSSP imm` advances the SSP across unwound frames -- the C++ `longjmp` and the Windows SEH unwinder both use this. `RDSSP` reads the current SSP into a register.
- The `/CETCOMPAT` MSVC linker flag, available from Visual Studio 2019 onward, marks an x64 image as shadow-stack-compatible by setting the `IMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT` bit in the extended DLL characteristics word [@ms-cetcompat].

Tiger Lake shipped CET first, in September 2020. AMD followed with the same architectural spec in Zen 3 in November 2020 [@wiki-intel-cet]. The two vendors implement the same instructions, the same MMU bit, the same fault. The shadow-stack image format is identical. Windows uses the same code paths on both.

<Sidenote>AMD Zen 3 was launched on November 5, 2020, two months after Tiger Lake [@wiki-intel-cet]. Both vendors implement the Intel CET specification verbatim, so Microsoft's Windows enabling code is single-source.</Sidenote>

<Mermaid caption="Shadow-stack mechanics: every call writes both stacks; every ret compares">
sequenceDiagram
    participant CPU
    participant RStack as Regular stack
    participant SStack as Shadow stack
    Note over CPU,SStack: function prologue
    CPU->>RStack: push retaddr_A
    CPU->>SStack: push retaddr_A (shadow)
    Note over CPU,SStack: attacker corrupts retaddr_A on regular stack to retaddr_X
    Note over CPU,SStack: function epilogue
    CPU->>RStack: pop -> retaddr_X
    CPU->>SStack: pop -> retaddr_A
    CPU->>CPU: compare retaddr_X vs retaddr_A
    CPU->>CPU: mismatch CP fault then STATUS_STACK_BUFFER_OVERRUN
</Mermaid>

The Windows policy surface for CET is `ProcessUserShadowStackPolicy`, structured exactly like every other policy in the enum -- a `DWORD` of bitfields and a "reserved" tail [@ms-user-shadow-stack-policy]. Ten flags are documented:

- `EnableUserShadowStack` -- turn it on (compatibility mode: only shadow-stack violations in CETCOMPAT-marked modules are fatal)
- `AuditUserShadowStack` -- log without enforcing
- `SetContextIpValidation` -- block `SetThreadContext` (and the equivalent `NtSetContextThread` from a peer process) from setting an instruction pointer to an unguarded address
- `AuditSetContextIpValidation` -- log version
- `EnableUserShadowStackStrictMode` -- upgrade from compatibility mode (only CETCOMPAT-module shadow-stack violations are fatal) to strict mode (all shadow-stack violations are fatal, even in non-CETCOMPAT modules)
- `BlockNonCetBinaries` -- the loader refuses to map non-`/CETCOMPAT` DLLs into the process; strict policy for the most-hardened sandboxes
- `BlockNonCetBinariesNonEhcont` -- like `BlockNonCetBinaries`, but also requires images to carry `/guard:ehcont` exception-handling continuation metadata
- `AuditBlockNonCetBinaries` -- log version of `BlockNonCetBinaries`
- `SetContextIpValidationRelaxedMode` -- permits some legacy patterns
- `CetDynamicApisOutOfProcOnly` -- requires `SetProcessValidCallTargets`-style operations to come from a peer process

The `SetContextIpValidation` flag is worth a separate paragraph. The original CET shadow-stack design protected against attackers who corrupted return addresses on the regular stack. A more subtle attack used `SetThreadContext` from a peer process (or, equivalently, the in-process `NtSetContextThread`) to write a register-state structure containing an attacker-chosen `RIP`. The thread, when resumed, would jump to that `RIP` -- with no `ret` instruction involved, so the shadow stack saw nothing. `SetContextIpValidation` closes that hole by validating the requested `RIP` against the bitmap before the kernel resumes the thread. Without it, CET shadow stack has a documented bypass [@ms-user-shadow-stack-policy].

<Definition term="`#CP` (Control Protection fault)">
A new CPU exception introduced with Intel CET. Raised when a shadow-stack compare fails on `ret`, when an `endbranch` instruction is missing at an indirect-branch target (for IBT-style CET, separate from shadow stack), or when an attempt is made to write to a shadow-stack page from a non-shadow-stack instruction. Windows routes `#CP` through `STATUS_STACK_BUFFER_OVERRUN`, the same status used for stack-canary violations and CFG failures.
</Definition>

Compose CFG with CET shadow stack and you have the result the entire arc since Aleph One has been pointing at:

> **Key idea:** CFG (forward edge) plus CET shadow stack (backward edge) equals full Control-Flow Integrity on x86-64, from compiler plus hardware. This is the cleanest moment in the article: two mitigations, from two different layers, compose into a property that took twenty years to assemble.

Full CFI is not the same as full security. CET still does not cover three structural attack classes. *Call-oriented programming* and *jump-oriented programming* chain gadgets ending in `call` or `jmp` rather than `ret`; the call/return invariant is preserved, so CET sees nothing. *COOP* chains entire legitimate virtual functions with matching call/return pairs; CET sees nothing. *Data-oriented* attacks (section 13) never violate any control-flow invariant at all, because they never hijack control flow in the first place.

We have constrained the control flow. We have not constrained which *code* is in the process. An attacker can still load a malicious-but-signed-looking DLL through the loader, or persuade a JIT to emit attacker-chosen bytes into the JIT heap and then redirect a legitimate call to that JIT-allocated address. That is the *code* layer, not the *control flow* layer. The parallel mitigation path -- CIG and ACG -- is what closes it.

## 7. Code Integrity Guard (CIG): only signed images can load

Even if the attacker can't generate code and can't redirect control flow, they can still ask the loader to do it for them. Plant a Microsoft-signed DLL somewhere the loader will pick it up; `LoadLibrary` runs the planted DLL's `DllMain`; you have remote code execution through a trusted entry point. The structural answer is to restrict the universe of DLLs the loader will ever map into a hardened process.

That is the function of *Code Integrity Guard*. CIG first appeared in Microsoft Edge in Windows 10 1511 (November 2015) [@miller-acg-blog]. The canonical primary on its design is Matt Miller's February 2017 Edge blog *Mitigating arbitrary native code execution in Microsoft Edge* [@miller-acg-blog]. The corresponding policy in `SetProcessMitigationPolicy` is `ProcessSignaturePolicy`, with the bitfield `PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY` [@ms-binary-signature-policy].

<Definition term="CIG (Code Integrity Guard)">
A per-process policy that restricts the set of binaries the loader will map into the process to images signed by an allowed code-signing root. Implemented in Windows via the `ProcessSignaturePolicy` mitigation policy. The most common configuration is `MicrosoftSignedOnly`, which restricts loads to Microsoft-rooted catalogue chains. Bypass attempts that load a malicious DLL into the process return `STATUS_INVALID_IMAGE_HASH` from `LoadLibrary` / `LoadLibraryEx` / `NtMapViewOfSection` [@miller-acg-blog, @ms-binary-signature-policy].
</Definition>

The policy structure carries three levels:

- `MicrosoftSignedOnly` -- only images chaining to a Microsoft root will load
- `StoreSignedOnly` -- only Microsoft Store-signed images
- `MitigationOptIn` -- the loader accepts any image signed by Microsoft, the Windows Store, *or* the Windows Hardware Quality Labs (WHQL); the broadest of the three signing-level settings

Plus an `AuditMicrosoftSignedOnly` audit-only flag that logs without blocking, for compatibility testing in the run-up to enforcement.

<Definition term="UMCI (User-Mode Code Integrity)">
The kernel subsystem that enforces image-signing policy on user-mode binary loads. UMCI is the user-mode counterpart of KMCI (Kernel-Mode Code Integrity, used by Windows Driver Signature Enforcement and HVCI). CIG calls into UMCI on every `NtMapViewOfSection` to verify that the section's backing image is signed by an allowed root before the loader maps it.
</Definition>

The mechanism is small. Every `LoadLibrary`, every `LoadLibraryEx`, and every `NtMapViewOfSection` consults UMCI (User-Mode Code Integrity). If the image is not signed by a Microsoft-rooted catalogue chain when `MicrosoftSignedOnly` is in effect, the load returns `STATUS_INVALID_IMAGE_HASH` [@miller-acg-blog, @ms-binary-signature-policy]. The process keeps running; the DLL just doesn't load. (Most attack chains aren't structured to handle that gracefully, so in practice the process crashes shortly afterward when it tries to dereference a function pointer the failed DLL was supposed to provide.)

CIG is a publisher check, not a content check. A Microsoft-signed DLL with a controllable side effect -- a DLL-search-order hijack against a signed Windows component, or the CVE-2013-3900 Authenticode-padding family that allows a signed binary to carry attacker-controlled trailing data without invalidating the signature -- still loads normally. CIG can't tell. *App Control* (formerly Windows Defender Application Control) and the Microsoft Driver Block List are the partial answer: a curated list of banned-but-signed binaries UMCI consults and rejects even when their signatures verify.

<MarginNote>CVE-2013-3900 was disclosed in December 2013. Microsoft shipped an opt-in registry fix (`EnableCertPaddingCheck`) and left the strict default off for over a decade for compatibility reasons; in July 2024 the company republished the CVE in the Security Update Guide to formally reaffirm that the strict-Authenticode behaviour remains available as an opt-in across all currently supported releases of Windows 10 and Windows 11 ("Microsoft does not plan to enforce the stricter verification behavior as a default functionality on supported releases of Microsoft Windows") [@nvd-cve-2013-3900]. The structural-vulnerable-but-signed class has been operationally hard to retire for the same reason every backwards-compatibility constraint is hard to retire.</MarginNote>

> **Note:** `ProcessSignaturePolicy` is applied to subsequent loader operations after the policy is installed. DLLs that were already mapped into the process before the call to `SetProcessMitigationPolicy` are *not* unloaded retroactively. This is the structural reason serious sandboxed processes (Edge content, Chrome renderer) use `UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY)` at `CreateProcess` time -- the kernel installs the policy *before* the child's first user-mode instruction runs, so even the loader's initial sweep of static imports is policed.

<Aside label="The signed-but-vulnerable residual risk">
The Microsoft-signed DLL universe is large. Many of those binaries have controllable side effects: search-order hijacks, Authenticode-padding writes, signed-driver privilege primitives, signed-tooling code-injection helpers. CIG does not look at side effects; it only looks at the signature. The residual class that survives `MicrosoftSignedOnly` -- "signed but vulnerable" -- is precisely the class App Control's reactive blocklist tries to keep up with. As of the 2025 Driver Block List there are hundreds of blocked-but-signed binaries; the list grows every quarter. This is one of the unsolved problems the article closes with in section 14.
</Aside>

CIG and ACG are siblings but not synonyms. CIG prohibits *loading unsigned images*. ACG prohibits *generating new executable code at runtime*. They attack different attack surfaces. The signed-DLL-injection bypass that defeats CIG does not defeat ACG, because the planted DLL is not generating new code -- it is using its (signed but vulnerable) existing code. The JIT-spray-as-CFG-bypass that defeats ACG does not defeat CIG, because the JIT was not loading a new DLL. An attacker who solves one still has to solve the other.

What does the *generation* half look like?

## 8. Arbitrary Code Guard (ACG): W^X for the entire process

March 2017. Windows 10 Creators Update ships. Microsoft Edge enables a single flag in the new `ProcessDynamicCodePolicy` structure. Every JavaScript JIT engine in the world has to be rearchitected.

<Definition term="ACG (Arbitrary Code Guard)">
A per-process policy that prevents *any* code that did not originate as a signed image at startup from becoming executable. With ACG enabled, calls to `VirtualAlloc` with `PAGE_EXECUTE_*` return `STATUS_DYNAMIC_CODE_BLOCKED`. Calls to `VirtualProtect` that attempt to *add* execute permission to an existing page return the same status. `MapViewOfSection` with `SECTION_MAP_EXECUTE` requires the section's backing image to be signed. The net effect: every executable byte in the process originated as a Microsoft-signed PE mapped by the loader at startup, and nothing else can ever become runnable in this process's address space [@miller-acg-blog, @ms-dynamic-code-policy].
</Definition>

The `PROCESS_MITIGATION_DYNAMIC_CODE_POLICY` structure carries four flags [@ms-dynamic-code-policy]:

- `ProhibitDynamicCode` -- the core enforcement flag
- `AllowThreadOptOut` -- a thread can call `SetThreadInformation(ThreadDynamicCodePolicy, 0)` to escape, which Microsoft's documentation warns against using with `ProhibitDynamicCode` because the two flags together leak the policy's intent
- `AllowRemoteDowngrade` -- a higher-privileged peer can disable the policy via `SetProcessMitigationPolicy`
- `AuditProhibitDynamicCode` -- log without enforcing

The structural rule, restated mechanically [@miller-acg-blog, @ms-dynamic-code-policy]:

1. `VirtualAlloc` with `PAGE_EXECUTE`, `PAGE_EXECUTE_READ`, `PAGE_EXECUTE_READWRITE`, or `PAGE_EXECUTE_WRITECOPY`: blocked.
2. `VirtualProtect` that adds any executable permission to an existing page: blocked.
3. `MapViewOfSection` with `SECTION_MAP_EXECUTE` for a section *not* backed by a signed image: blocked.
4. The only way new executable pages enter the process: the loader maps signed PEs at module load time, and (with CIG also on) only Microsoft-signed PEs.

The browser-JIT architectural consequence is the most-cited single change in the entire Windows mitigation literature. Pre-2017, every JavaScript JIT generated native code at runtime into a `RWX`-permission heap inside its own browser process. The pattern was simple: allocate a page, write machine code into it, mark it executable, jump. ACG turned that pattern into a fatal error.

Chakra (then Edge's engine), V8 (Chrome's engine, when Edge later switched to Chromium), SpiderMonkey (Firefox), and JavaScriptCore (Safari) all responded by moving the JIT compilation step out of the renderer process [@miller-acg-blog]. The architecture became: the renderer ships JavaScript source over an authenticated IPC channel to a *JIT process*; the JIT process compiles to machine code; the JIT process owns a signed section backing the compiled output; the renderer maps that signed section read-execute via `MapViewOfFile` and dispatches into it. The renderer is locked into ACG. The JIT process is not (it has to write code), but it never parses untrusted content -- only pre-validated bytecode from the renderer over a typed IPC schema.

<Mermaid caption="JIT architecture: pre-ACG single process vs post-ACG broker plus JIT process plus renderer">
flowchart LR
    subgraph Pre["Pre-ACG (before March 2017)"]
        direction TB
        R1[Renderer process]
        R1 --> J1[In-process JIT]
        J1 --> H1["RWX JIT heap<br/>(W^X violation)"]
        H1 --> E1[Execute jitted<br/>JS]
    end
    subgraph Post["Post-ACG (Edge 1703 and later)"]
        direction TB
        R2[Renderer<br/>ACG on]
        R2 -->|IPC bytecode| J2[JIT process<br/>ACG off]
        J2 -->|signed<br/>section| S2[Shared mapping]
        R2 -->|MapViewOfFile<br/>R-X| S2
        S2 --> E2[Execute jitted<br/>JS in renderer]
    end
</Mermaid>

That rearchitecture is the structural cost ACG imposed. It is not small. Out-of-process JIT adds roughly a millisecond per JIT compilation for the IPC round-trip, which matters for short-lived JavaScript (lots of small functions, one-shot pages). It also creates a new trust boundary -- between renderer and JIT process -- which is itself an attack surface, and which the next paragraph names.

The bypass tradition starts almost immediately. December 2017, Project Zero issue 42450607. James Forshaw and Ivan Fratric document the *race-the-mitigation-window* class [@p0-issue-42450607, @exploit-db-44467]. The PoC is small enough to read in one paragraph.

<Aside label="The Forshaw-Fratric race: two bytes that disable ACG">
Each Edge content process (`MicrosoftEdgeCP.exe`) called `SetProcessMitigationPolicy(ProcessDynamicCodePolicy, ...)` on itself shortly after startup. The advisory documents the verbatim callstack: `MicrosoftEdgeCP!SetProcessDynamicCodePolicy+0xc0`. Forshaw and Fratric discovered that there is a window between `CreateProcess` returning the new content process's handle and that child's first call into `SetProcessDynamicCodePolicy`. During that window, a peer content process in the same AppContainer can `OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION)` the new child and `WriteProcessMemory` two specific bytes -- at Edge offsets `0x23090` and `0x23092` on the version Forshaw and Fratric tested, build "up-to-date on Windows 10 version 1709" [@p0-issue-42450607]. The two bytes are global flags that, if set, cause `SetProcessDynamicCodePolicy` to short-circuit and return success without installing the policy. The result: a child renderer that *thinks* ACG is on, that the parent thinks has ACG on, but in which `VirtualAlloc(PAGE_EXECUTE_READWRITE)` succeeds normally. Microsoft's fix was structural: migrate to `UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY)`, so the policy is installed *by the kernel* before the child's first user-mode instruction runs and the race window closes.
</Aside>

The second-generation bypass came faster than anyone expected. May 2018, Ivan Fratric publishes *Bypassing Mitigations by Attacking the JIT Server* on the Project Zero blog [@p0-fratric-jit-2018]. Once ACG forced JIT out of process, the *new* attack surface was the IPC channel and the JIT-server allocation address. Fratric writes: "we believe that any other attempt to implement out-of-process JIT would encounter similar problems." That sentence is the deeper lesson of the entire mitigation tradition: a new trust boundary -- between renderer and JIT process, between user and kernel, between content process and broker -- is a new attack class. You did not eliminate the attack surface; you moved it.

ACG plus CIG, then, closes "what code can run in this process": no unsigned image loads (CIG), no dynamic code generation (ACG), no executable allocations of any kind that did not originate as a signed PE on disk. That is a closed surface for the *code* dimension. But the attacker has more options than memory and signatures. There is the kernel surface beneath the renderer's syscalls. There is the legacy extension-point loader. There are fonts, image loads, side channels. Those are the smaller, operationally-critical mitigations -- the rest of the twenty.

## 9. The smaller, operationally critical mitigations

DEP, ASLR, CFG, CET, CIG, ACG -- that is the canonical six. But the `PROCESS_MITIGATION_POLICY` enum lists twenty-one values [@ms-process-mitigation-enum]. The other fourteen actual policies are not afterthoughts. Each one is a tombstone for a specific attack class that did not fit into "don't let the attacker write code" or "don't let the attacker pick the call target."

### `ProcessSystemCallDisablePolicy` -- Disable Win32k System Calls

Edge content process, 2017 onward. The Win32k.sys driver implements the GUI subsystem and was, for many years, the single largest contributor to Windows kernel CVEs. A renderer process that does not draw windows can refuse Win32k syscalls entirely, eliminating an enormous swath of kernel attack surface for a compromised renderer. The Edge content process is the canonical user. The Edge sandbox blog documents the AC architecture and capability model the renderer runs inside [@edge-sandbox-blog]; the policy enum entry itself is in `ms-setprocessmitigationpolicy` [@ms-setprocessmitigationpolicy]. Connor McGarr's 2025 deck addresses the Win32k surface explicitly: "Call targets in Win32k can be corrupted with a valid NT call target" -- which is the structural reason the policy exists [@mcgarr-bhusa25].

### `ProcessExtensionPointDisablePolicy`

Disables legacy extension-point classes that have historically been DLL-injection vectors: `AppInit_DLLs` (registry-driven inject-into-everything), IME modules, Layered Service Providers (LSP, the Winsock provider chain), `WinEventHook`/`SetWindowsHookEx` global hooks. Enabling the policy makes the loader refuse to map any DLL through these legacy paths into the process [@ms-setprocessmitigationpolicy, @ms-process-mitigation-enum]. This is one of the lowest-cost mitigations to enable for any process that does not knowingly need legacy IME or LSP integration.

### `ProcessFontDisablePolicy`

Refuses non-system fonts. The historical motivation was a 2015 wave of ATMFD.DLL kernel-font-parser CVEs (the Adobe Type Manager font driver). Microsoft moved the font parser out of the kernel into user mode after that wave, and this per-process policy then refuses non-system fonts entirely for browser-class sandboxed processes that do not need them [@ms-setprocessmitigationpolicy].

### `ProcessImageLoadPolicy`

Three loader-time flags, all about *where* a DLL can come from:

- `NoRemoteImages` -- block DLLs whose path is a UNC `\\server\share\dll`. Eliminates a remote-DLL family that crossed administrative boundaries.
- `NoLowMandatoryLabelImages` -- block DLLs whose file was written by a low-integrity-label process. A compromised sandboxed process could write a DLL to disk; this flag stops a peer broker from picking that DLL up.
- `PreferSystem32Images` -- search `\Windows\System32\` before the application directory in the DLL search order. Closes the DLL-search-order-hijack class, a very old attack surface.

All three are in [@ms-image-load-policy]. Together they collapse the DLL-loading attack surface to a small, well-controlled set of code paths.

### `ProcessStrictHandleCheckPolicy`

Causes the process to fault immediately on any use of an invalid handle (use-after-close, double-close, opaque-mismatch) [@ms-setprocessmitigationpolicy]. Handle bugs are an obscure but exploitable class -- a freed kernel object's handle can be reissued, and a process that does not detect this can be tricked into operating on an attacker-controlled replacement. Strict handle checking turns a subtle handle-confusion bug into an immediate crash, before the attacker can pivot.

### `ProcessRedirectionTrustPolicy` -- RedirectionGuard

Mitigates symbolic-link, junction, and mount-point confused-deputy attacks. James Forshaw documented the attack family at Project Zero starting in August 2015 with the Windows 10 symbolic-link mitigations post [@p0-forshaw-symlink-2015]. Microsoft shipped the per-process mitigation a decade later, in June 2025 [@msrc-redirectionguard]. RedirectionGuard refuses to traverse a junction if the junction's target was created by a less-trusted user than the process performing the open -- closing the "a low-IL caller plants a junction; a high-IL service follows it" pattern that has been a steady source of local privilege escalation since at least Windows Vista.

<Sidenote>RedirectionGuard's June 2025 ship date makes it the freshest entry in the `PROCESS_MITIGATION_POLICY` enum. The MSRC blog states the structural framing in one sentence: "Junctions remain the biggest existing gap. Outside of a sandbox, they can be created by standard users and target any folder on the system" [@msrc-redirectionguard].</Sidenote>

### `ProcessSideChannelIsolationPolicy`

Two distinct sub-mitigations [@ms-setprocessmitigationpolicy]:

- `IsolateSecurityDomain` -- on context switch, issue `IBPB` (Indirect Branch Predictor Barrier) and `STIBP` (Single Thread Indirect Branch Prediction) flushes. This is the per-process Spectre v2 / MDS side-channel mitigation. Performance cost is real, in the 2-5% range on indirect-branch-heavy workloads, and is the reason this policy is opt-in rather than default.
- `DisablePageCombining` -- prevents the kernel from merging identical physical pages across processes. Page-combining is a memory-saving feature that creates a cross-process side-channel: timing the cost of a write to a shared, copy-on-write page leaks whether the page was previously merged with another process's identical page.

### `ProcessUserShadowStackPolicy`

The CET-on switch from section 6 [@ms-user-shadow-stack-policy]. Listed here for enum completeness.

### `ProcessChildProcessPolicy`

Refuses any `CreateProcess` call originating from the process [@ms-setprocessmitigationpolicy]. Edge content processes and Chromium renderers enable this. The structural attack class it closes is "renderer is compromised; renderer spawns `cmd.exe` or `powershell.exe` and the attacker pivots to a non-sandboxed cousin." With `ProcessChildProcessPolicy` on, the renderer cannot spawn anything; the attacker has to either bypass within the sandbox or attack the broker process.

### `ProcessPayloadRestrictionPolicy` -- EAF / IAF / ROP checks

The mitigations that EMET originally bundled, carried forward into Windows Defender Exploit Guard [@ms-defender-exploit-protection]: Export Address Filter (EAF), Import Address Filter (IAF), ROP-Stack-Pivot, ROP-Caller-Check, ROP-Sim-Exec. Five sub-mitigations that detect heuristic exploit patterns. The honest assessment: these are defense-in-depth against legacy 32-bit binaries that cannot be recompiled with CFG, XFG, or CET. On modern x64 binaries built with `/guard:cf /CETCOMPAT`, the payload-restriction checks are largely redundant. They remain useful as a backstop for unrecompilable third-party code that runs in a hardened parent process.

### `ProcessASLRPolicy` and `ProcessDEPPolicy`

The per-process knobs on top of the system-wide foundations [@ms-setprocessmitigationpolicy]. `ProcessASLRPolicy` exposes `BottomUpRandomization`, `HighEntropy`, `ForceRelocateImages`, and other refinements -- useful for forcing a paranoid configuration on processes that load third-party DLLs without `/DYNAMICBASE`. `ProcessDEPPolicy` is a 32-bit-only vestigial knob; on x64 it does nothing because DEP is unconditionally on.

### The other policies

`ProcessActivationContextTrustPolicy` (restricts manifest-driven activation contexts), `ProcessMitigationOptionsMask` (a meta-policy returning the mask of supported bits), `ProcessSystemCallFilterPolicy` (per-process syscall allowlist; rare in production), `ProcessUserPointerAuthPolicy` (the ARM64-Windows switch for ARM Pointer Authentication, comparatively discussed in section 11), and `ProcessSEHOPPolicy` (the per-process Structured Exception Handling Overwrite Protection knob -- a Vista-era mitigation predating the modern enum) fill out the enum to twenty-one values. None are individually load-bearing for the article's narrative; they exist for completeness of the kernel ABI.

Twenty policies plus a sentinel. The canonical six handle the control-flow primitives. The other fourteen handle adjacent surfaces. What does it look like when all of these are turned on at once, and which binaries actually do that?

## 10. What does a maximally hardened modern Windows process look like?

It is one thing to enumerate policies. It is another to ask: who actually turns them on? Where does Microsoft itself enable each one, and what is the structural reason it cannot be enabled on the others?

The fastest way to answer that question is a single matrix. Each column is a binary; each row is a `PROCESS_MITIGATION_POLICY` value. Each cell is either *enabled*, or the structural reason it cannot be. The matrix below summarizes the typical `Get-ProcessMitigation` output for representative binaries, with structural-can't reasons drawn from public Microsoft documentation, Matt Miller's Edge mitigation blog [@miller-acg-blog], and the policy-enum reference [@ms-process-mitigation-enum, @ms-setprocessmitigationpolicy].

| Policy | Edge content (`MicrosoftEdgeCP.exe`) | Chrome renderer | Outlook (Office) | Defender (`MsMpEng.exe`) | Recall (Windows AI service) | `Notepad.exe` |
|--------|---|---|---|---|---|---|
| DEP / ASLR (system foundation) | yes | yes | yes | yes | yes | yes |
| CFG | yes | yes | yes | yes | yes | yes |
| CET shadow stack | yes (strict) | yes | partial | yes | yes (strict) | yes (default) |
| ACG (`ProcessDynamicCodePolicy`) | yes | yes (with OOP JIT) | no -- COM/MAPI add-ins | no -- engine generates scanner code at runtime | yes | n/a (no JIT) |
| CIG (`ProcessSignaturePolicy`) | yes (`MicrosoftSignedOnly`) | partial -- plugins | no -- third-party add-ins | yes | yes (`MicrosoftSignedOnly`) | n/a |
| Disable-Win32k (`SystemCallDisable`) | yes | yes (renderer process) | n/a (GUI) | yes (no GUI) | yes (no GUI) | n/a (GUI) |
| Disable-Extension-Points | yes | yes | partial | yes | yes | default |
| Image-Load (all three flags) | yes | yes | partial | yes | yes | default |
| StrictHandleCheck | yes | yes | yes | yes | yes | yes |
| ChildProcess | yes | yes | no -- launches `winword`, etc. | yes (no children) | yes (no children) | no |
| FontDisable | yes | yes | n/a (renders fonts) | n/a | n/a | n/a |
| RedirectionGuard | yes (since 2025) | yes (since 2025) | partial | yes | yes | partial |
| SideChannelIsolation | optional | optional | optional | optional | yes (high-trust) | optional |
| PayloadRestriction (EAF/IAF/ROP) | yes | yes | yes | yes | yes | n/a |

The pattern that emerges from this matrix is the article's most important practical observation. The matrix is *a threat-model artefact*.

For any sandboxed-parser design -- a renderer, a font rasterizer, a PDF previewer, an image decoder -- the structurally-correct policy set is the union of what Edge and Recall enable. Both binaries parse untrusted content from the internet or from local files; both run in isolation; neither needs to load third-party signed DLLs, draw windows, or launch child processes. They can enable the full canonical recipe.

For any extensibility-by-design surface, the policy set is smaller and the threat model has to absorb the gap. Outlook cannot enable CIG because the MAPI plugin model and third-party COM add-ins are an existential product feature. Outlook cannot enable `ChildProcess` because it launches Word to open attachments. Defender cannot enable ACG because the scanner engine generates emulator bytecode, signature-compilation routines, and regex JITs at runtime -- it is, by design, a JIT for AV signatures, and that JIT runs in `MsMpEng.exe`. Chromium cannot enable CIG by default because of the third-party plugin model (Widevine, native messaging hosts, accessibility integrations).

> **Key idea:** The canonical 2026 hardened-process recipe is CFG plus CET shadow stack plus ACG plus CIG plus Disable-Win32k plus Disable-Extension-Points plus Image-Load (all three flags) plus StrictHandleCheck plus ChildProcess plus, for parsers, FontDisable, plus RedirectionGuard for filesystem-interacting binaries. Every binary that misses one of these does so for a documentable structural reason -- which is exactly the threat-model artefact the matrix above produces.

<Sidenote>This is the recipe the *VBS and Trustlets* sibling article in this series calls "user-mode hardened." The VBS-isolated Trustlets in the Secure Kernel layer have a separate, complementary surface; see that article for the kernel-side parallel.</Sidenote>

Stacking the recipe is the best a 2026 user-mode process can be. But the attacker is still in the room. What survives even a fully-stacked process? What are the bypasses that work after every mitigation is on? Section 12 answers that. First, a quick comparison: what other operating systems do, and what they do differently.

## 11. What other operating systems do that Windows doesn't

Microsoft is not the only vendor with a per-process mitigation surface. Apple, Linux distributions, Chromium, and ARM-the-vendor are all in the same business, and they have made different structural choices. The honest comparison surfaces where Windows is ahead, where it is behind, and where the gap is not really a gap because the platforms solve slightly different problems.

**Apple: Hardened Runtime, ARM PAC, and JIT entitlement.** Apple shipped Pointer Authentication Codes (PAC) on the A12 (iPhone XS, September 2018) and on every Mac M1 onward [@wiki-apple-a12, @apple-platform-security]. PAC signs a code pointer with a per-process cryptographic key held in privileged hardware registers, storing the signature in the unused upper bits of a 64-bit pointer. The ARM `PACIA`, `AUTIA`, `PACIB`, and `AUTIB` instructions sign and verify [@wiki-armv83a]; an unsigned or wrongly-signed pointer dereferenced through a `BR`/`BLR` instruction with the AUT variant faults. PAC is *structurally stronger* than CFG/XFG/CET because the key is held in privileged state and is unforgeable from user mode -- there is no bitmap to lift the validation through.

Apple's JIT entitlement (`com.apple.security.cs.allow-jit`) is a stronger architectural answer than ACG [@apple-hardened-runtime]. Code that wants to JIT must declare it at build time and is granted a specific in-process W^X carve-out *only if* the entitlement is signed into the binary's code signature. The result: JIT capability is an attribute of the *signed binary* rather than a runtime API call, which closes the race-the-mitigation-window class structurally rather than by API migration (`UpdateProcThreadAttribute`).

**Linux: SELinux, landlock, LLVM `-fsanitize=kcfi`, LLVM `-fsanitize=cfi-icall`.** Forward-edge CFI in the Linux kernel first arrived in version 5.13 (June 2021) as an LTO-based jump-table implementation; the second-generation `-fsanitize=kcfi` scheme, which places a 32-bit type hash immediately before each function entry and does not require link-time optimization, replaced it in 6.1 (December 2022) [@lwn-corbet-kcfi]. The kCFI design is conceptually very close to XFG, but cheap enough to deploy on a kernel build because it sheds the LTO requirement. LLVM's user-mode `-fsanitize=cfi-icall` provides per-prototype CFI via jump-table dispatch but still requires LTO [@clang-cfi-doc]. SELinux operates at a different layer of the stack (mandatory access control on filesystem and IPC resources) and is not directly comparable to a control-flow defense -- it constrains *what the process can do* rather than *what control flows the process can follow*.

**Chromium / V8 sandbox.** Chrome enables CFG on Windows, leans on ARM PAC on macOS, and is layering the V8 sandbox on top of all of them [@v8-sandbox-blog]. The V8 sandbox is a Chrome-side software defense: it confines a compromised renderer to a specific bounded memory range, so a renderer-process compromise cannot synthesize pointers to arbitrary out-of-sandbox memory. The V8 sandbox sits inside the renderer (different from the OOP-JIT trust boundary above it) and aims to make even a fully-compromised JIT-output bug non-fatal at the system level.

**Android: Scudo allocator and ARM Memory Tagging Extension (MTE).** MTE attaches a 4-bit tag to every 16-byte allocation [@arm-mte-newsroom]. The CPU enforces the tag on every pointer dereference: tag mismatch raises a synchronous exception. Pixel 8 (October 2023) was the first consumer device with MTE-default-on for the kernel and key system services [@wiki-pixel-8, @arm-mte-newsroom]. MTE catches the *cause* (use-after-free, linear overflow into the next allocation) rather than the *symptom* (control-flow hijack). It is conceptually orthogonal to CFI. The hard part is perf cost on memory-tagged loads, meaningful enough that even Apple has not enabled MTE on iOS as of 2026.

| Platform | Forward-edge | Backward-edge | Dynamic code | Memory safety |
|----------|---|---|---|---|
| Windows (x64) | CFG (coarse), XFG (deprecated) | CET shadow stack | ACG | none structural |
| Apple (ARM64) | PAC (cryptographic, per-process key) | PAC (signs return addresses too) | JIT entitlement (declarative) | none structural |
| Linux kernel | `-fsanitize=kcfi` (LLVM 6.1+) | shadow stack on x86 CET; PAC-RA on ARM | not a kernel issue | Rust-in-kernel pilot |
| Android | PAC + BTI on supported SoCs | BTI / shadow call stack | sandboxed by selinux + seccomp | MTE on Pixel 8 |
| Chromium | per-platform forward-edge | per-platform backward-edge | OOP JIT + V8 sandbox | layered |

The honest accounting:

- ARM PAC plus MTE is structurally stronger than CFG plus CET, because the cryptographic key (PAC) and the tag (MTE) are CPU-enforced state that no user-mode primitive can forge.
- Apple's JIT entitlement is a stronger architectural answer than ACG because it is declarative at signing time rather than imperative at process startup.
- SELinux/landlock is at a different layer (data access control) and is not directly comparable -- it solves a different problem.
- Windows's mitigation surface is the *most extensively deployed and most frequently extended* per-process surface in industry use, by a wide margin. Twenty actual policies is more than any other vendor exposes to applications, and the API is stable, documented, and ABI-compatible across Windows versions back to Windows 8.

<Sidenote>MTE catches what CFI cannot. A use-after-free that produces a controllable write -- but never violates the control-flow graph -- is invisible to CFG, XFG, CET, and PAC, but raises an MTE tag-mismatch fault on the very first attacker-controlled dereference. This is the structural reason memory-tagging is the emerging frontier and the structural reason a Windows-on-ARM-with-MTE future would close attack classes the current per-process surface cannot reach.</Sidenote>

Stronger primitives exist on competing platforms. But Microsoft's per-process surface is the most extensively-deployed and most-frequently-extended in industry use. The *bypasses* are what tell us where the surface still leaks.

## 12. How attackers respond to a fully hardened process

Every generation of Windows mitigation has shipped with a named bypass within a year of its release. Here is the tradition, one named class per defensive generation.

**Signed-DLL injection.** Predates CIG. Find a Microsoft-signed DLL with a controllable side effect -- a DLL-search-order hijack against a signed Windows component, an Authenticode-padding write (CVE-2013-3900 family), or a signed driver with a known IOCTL privilege primitive. CIG sees a valid Microsoft signature and lets the DLL load. The mitigation is reactive: Microsoft's App Control / WDAC blocklist and the Driver Block List enumerate hundreds of banned-but-signed binaries; the list grows every quarter; the attacker's job is to find one not yet on it. This is one of the unsolved problems section 14 names.

**JIT spray as a CFG bypass (Theori, 2016).** The canonical writeup is Theori's *Chakra JIT CFG Bypass* [@theori-chakra-cfg-bypass]. The page itself states verbatim that the bypass targeted Microsoft Security Bulletin MS16-119 (October 2016) -- a Chakra fix that tightened the JIT's emit pattern. The technique: persuade the Chakra JIT to emit attacker-chosen byte sequences inside JIT-allocated code pages, at addresses the attacker has marked as valid CFG targets via the `SetProcessValidCallTargets` carve-out. The MS16-119 patch shrank the set of byte sequences a JavaScript program could induce the JIT to emit, but did not eliminate the technique structurally -- the structural fix was ACG (move the JIT out of process), section 8.

<Definition term="JIT spray">
An exploitation technique in which an attacker writes JavaScript (or another JIT-targeted language) that causes the runtime JIT compiler to emit a long sequence of executable bytes at predictable addresses, where some of those emitted bytes form a useful gadget chain when reinterpreted at an offset. The classic JIT spray (Dion Blazakis, BHDC 2010) used Adobe Flash's ActionScript JIT [@blazakis-bhdc-2010, @wiki-jit-spraying]. The 2016 Theori work generalised the idea to use the JIT to emit *CFG-valid* function-entry bytes [@theori-chakra-cfg-bypass].
</Definition>

**COOP -- code-reuse without a single CFG-invalid call.** Discussed in section 5; recapped here as the *first* bypass class against coarse-grained forward-edge CFI [@coop-ieeesecurity-pdf, @coop-ieeexplore]. The structural fix is fine-grained CFI: XFG, which Microsoft did not enforce by default and has since deprecated; LLVM's `-fsanitize=cfi-icall` and `-fsanitize=kcfi`; ARM PAC. The per-prototype hash check that XFG would have provided is exactly the property that closes COOP.

**Race-the-mitigation-window (Forshaw + Fratric, 2017).** Discussed in section 8; recapped here. The structural fix is `UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY)`, which installs mitigation policies *by the kernel* at `CreateProcess` time, before any user-mode code in the child runs. The race window between `CreateProcess` return and the child's `SetProcessMitigationPolicy` call is structurally closed. Documented in the Project Zero issue [@p0-issue-42450607] and the Exploit-DB mirror [@exploit-db-44467].

**The CET-bypass research direction (McGarr, 2025).** Connor McGarr's Black Hat USA 2025 deck *Out of Control* names the live research front: kCFG and kCET in the Windows kernel [@mcgarr-bhusa25]. The deck enumerates bypass classes that survive both kernel-mode CFG and kernel-mode CET: page-table modification of the kCFG bitmap (requires kernel write primitives the attacker may already have), abuse of unprotected global function-pointer arrays, structural limits of CET when the attacker is operating with kernel privileges in the first place. The user-mode mitigation surface is mature; the kernel-mode surface is where the live work happens. Hypervisor-Protected Code Integrity (HVCI) is what makes kCFG bitmap mutations harder -- the bitmap is in VTL1, and a VTL0 kernel write cannot touch it -- which is the cross-link to the VBS/Trustlets sibling article in this series.

**Cross-context PAC oracles (Apple).** Listed for comparative completeness. PAC's per-process key is forgeable if an attacker can call into a function that signs an attacker-controlled pointer with the per-process key and then read the result. This is a known research class on Apple platforms and has produced several CVEs against Safari and iOS over the past five years.

<Mermaid caption="The bypass tradition: one named class per defensive generation">
flowchart LR
    A[1996 stack smashing] -->|defended by| B[2004 DEP/NX]
    B -->|bypassed by| C[2007 ROP]
    C -->|defended by| D[2014 CFG]
    D -->|bypassed by| E[2015 COOP]
    D -->|bypassed by| F[2016 Theori<br/>JIT spray as<br/>CFG bypass]
    F -->|defended by| G[2017 ACG]
    G -->|bypassed by| H[2017 Forshaw<br/>Fratric race]
    H -->|defended by| I[UpdateProc-<br/>ThreadAttribute]
    G -->|defended by| J[2020 CET<br/>shadow stack]
    J -->|new front| K[2025 McGarr<br/>kCFG kCET<br/>research]
</Mermaid>

The honest summary is that three classes of bypass survive a fully-stacked user-mode process today:

1. Signed-but-vulnerable DLL hijack -- defeats CIG by definition (publisher check, not content check).
2. COOP-style chains where the prototypes match the call site -- defeats CFG (coarse-grained) and is not closed by CET because the call/return invariant holds.
3. Data-only attacks -- which never violate any control-flow invariant at all, because no control transfer is hijacked.

What is the theoretical limit on what process mitigations can do? That is the next section.

## 13. What process mitigations cannot do

The Abadi paper that founded CFI in 2005 [@msr-cfi] is also the paper that establishes CFI's structural ceiling. CFI is, by construction, a *control-flow* property. That is exactly the property a sophisticated attacker can avoid violating.

The formal claim from Abadi, Budiu, Erlingsson, and Ligatti: enforcement of CFI restricts an attacker to control-flow transfers that respect the static call graph. The paper *does not say* every reachable program behavior is benign. CFI says "the attacker's control flow stays inside the legal CFG." It does not say "the legal CFG is benign." Any attack that operates entirely within the legal CFG is invisible to any CFI variant, including CFG, XFG, CET, PAC, and kCFI.

The lower bound on what an attacker can do *while staying inside the legal CFG* is given by data-oriented programming. The canonical paper is *Data-Oriented Programming: On the Expressiveness of Non-Control Data Attacks* by Hong Hu, Shweta Shinde, Sendroiu Adrian, Zheng Leong Chua, Prateek Saxena, and Zhenkai Liang, all of the National University of Singapore Department of Computer Science [@dop-paper, @dop-ieeexplore]. The abstract is constructive and devastating: "such attacks are Turing-complete. We present a systematic technique called data-oriented programming (DOP) to construct expressive non-control data exploits."

<Definition term="Data-Oriented Programming (DOP)">
An exploitation technique in which the attacker corrupts non-control data -- authentication flags, length fields, function-table indices, loop bounds -- and lets the program's own legitimate, unmodified control flow execute the attacker's intended computation. Hu, Shinde, Adrian, Chua, Saxena, and Liang proved DOP is Turing-complete: any computation can be expressed as a chain of data-only corruptions in a sufficiently-large program [@dop-paper, @dop-ieeexplore]. No CFI variant -- CFG, XFG, CET shadow stack, ARM PAC, kCFI -- can detect a DOP attack, because no control flow is hijacked.
</Definition>

The mechanism: the attacker corrupts a `current_user.is_admin` flag rather than redirecting a function pointer. They corrupt a `buffer_len` field to enable a subsequent legitimate write past the allocation's intended end. They corrupt a `next_state` index to drive a state machine through an attacker-chosen path. The program's own logic, executing every instruction the compiler emitted and following every control transfer the static call graph allows, performs the attack. DOP is, in a precise sense, the program working as designed -- on data the attacker has chosen.

A second structural limit: process mitigations are *per-process*. The kernel has a parallel mitigation surface (kCFG, kCET, HVCI, Secure Kernel, the VBS/Trustlets stack) the per-process policies do not touch [@mcgarr-bhusa25]. The user-mode hardening recipe stops at the syscall boundary. Everything beyond is the kernel's job. A renderer that is fully hardened can still be the entry point for a kernel privilege escalation if a syscall takes attacker-controlled input and the kernel-side code path has its own bug.

The third structural limit is the most uncomfortable to state.

> **Key idea:** Process mitigations harden the exploit chain. They do not fix the bug. The C/C++ memory-safety bug is still there; mitigations just constrain what the attacker can do with it.

Matt Miller, then a senior security engineer at the Microsoft Security Response Center, said this in his Black Hat IL 2019 talk. The deck is on GitHub at the Microsoft MSRC Security Research repository, with the load-bearing slide preserved verbatim [@miller-bhil-pdf]:

<PullQuote>
"~70% of the vulnerabilities addressed through a security update each year continue to be memory safety issues." -- Matt Miller, BlueHat IL 2019 [@miller-bhil-pdf]
</PullQuote>

ZDNet's contemporaneous coverage extended the claim: "around 70 percent of all the vulnerabilities in Microsoft products addressed through a security update each year are memory safety issues; a Microsoft engineer revealed last week at a security conference; over the last 12 years, around 70 percent of all Microsoft patches were fixes for memory safety bugs" [@zdnet-70percent].

Seventy percent. For a decade. The mitigations in this article -- CFG, XFG, CET, ACG, CIG, every smaller policy in the enum -- exist precisely because that number was not going down. Each generation raises the cost of weaponizing a memory-safety bug into a working exploit. None of them reduces the rate at which memory-safety bugs are introduced into the codebase in the first place.

<Aside label="The kernel has its own parallel surface">
For the kernel-mode side -- kCFG, kCET, HVCI, and the Trustlets that execute in the Virtual Trust Level 1 (VTL1) Secure Kernel layer -- see the *VBS and Trustlets* sibling article in this series. The user-mode and kernel-mode mitigation surfaces are designed to compose: a renderer hardened to the canonical recipe in section 10, syscalling into a kernel hardened with kCFG and kCET, and protected by an HVCI hypervisor, is the layered defense Microsoft's strategic direction since 2014 has been building toward.
</Aside>

The only ceiling-breaker is to replace the *language* (so the bug never exists) or to replace the *memory model* (so the bug cannot be turned into a primitive). The two long-term answers are: memory-safe systems languages, principally Rust (Microsoft has been publicly committing to Rust in Windows since 2019 [@msrc-rust-2019]); and capability-hardware platforms like CHERI and ARM MTE, which catch the bug at the dereference rather than the chain.

Three things have to be true for mitigations to keep buying time:

1. Each new mitigation closes a specific attack class -- which means a specific bypass class becomes the next research front.
2. Each new bypass class must take an attacker longer to develop than it takes Microsoft to ship the next mitigation -- otherwise the curve goes the wrong way.
3. The fraction of memory-safety bugs in shipped code has to either stop rising or start falling -- otherwise no number of mitigations stacks fast enough.

Mitigations are a delaying action. The long-term answer is somewhere else. The reader's belief at this point is no longer "stack enough mitigations and we win." It is "mitigations have a structural ceiling, and the bug is still there." If process mitigations have a ceiling, what is Microsoft pivoting toward, and what is the open frontier?

## 14. Open problems

Six things are still unsolved -- or, more precisely, six things are partially solved in ways that are documented but visibly imperfect.

**1. Forward-edge CFI without recompilation.** Binary-rewriting CFI (BinCFI, Mocfi, Lockdown) is not production-grade on Windows. Microsoft's strategic answer is "recompile first-party code with `/guard:cf` and accept that legacy third-party binaries remain unguarded." That answer is a long-tail problem: the surface of legacy third-party DLLs that load into hardened Windows processes (drivers, COM components, accessibility tools) is large, slow to recompile, and outside Microsoft's direct control.

**2. Backward-edge protection on pre-CET hardware.** Microsoft's pre-CET internal experiment was Return Flow Guard (RFG), a software-implemented per-thread shadow stack maintained by the runtime rather than the CPU. Tencent Xuanwu Lab bypasses came faster than Microsoft could harden RFG [@wiki-cfi]; Microsoft pivoted to wait for Intel CET. Pre-Tiger-Lake (pre-September-2020) Intel hardware and pre-Zen-3 (pre-November-2020) AMD hardware remain unprotected on the backward edge. Enterprises that need backward-edge protection on older hardware have to sandbox in VBS-isolated VMs -- cross-link to the VBS/Trustlets sibling article.

**3. The JIT-engine compatibility tax under ACG.** Out-of-process JIT adds roughly a millisecond per JIT compilation for the IPC round-trip. For short-lived JavaScript (lots of small functions, one-shot pages, ad-network microservices), this is significant. Chrome's V8 sandbox project (active since 2023) confines the JIT process to a sandboxed memory range of the renderer's address space, which closes the IPC-level attack class but does not erase the perf cost [@v8-sandbox-blog]. Interpreter-only renderers for low-trust contexts (small pages, ad iframes) are the medium-term direction; the cost is the runtime perf gap to fully-jitted JS.

**4. ACG plus AV interoperability.** Defender's `MsMpEng.exe` cannot enable ACG. The scanner engine generates code at runtime: signature compilation routines, emulator bytecode, regex JITs. Migration to interpreted bytecode is partial. This is a permanent compatibility tension between W^X-as-process-invariant and runtime-generated-code-as-a-feature, and it shows up in every AV engine across every vendor (CrowdStrike Falcon, SentinelOne, Symantec), not just Defender.

**5. Signed-but-vulnerable Microsoft DLLs as universal CIG-bypass loaders.** The Microsoft-signed DLL surface is enormous and historically full of side-effect DLLs. The App Control / WDAC blocklist is reactive. The blocklist publishes quarterly. New signed-but-vulnerable DLLs are found every quarter. This is a permanent residual risk against CIG and the structural reason vendors with sensitive workloads sometimes run with `MitigationOptIn` plus a per-process allowlist rather than `MicrosoftSignedOnly` plus an unbounded universe.

**6. XFG default-on tradeoffs.** XFG's instrumentation is in the MSVC binaries; the dispatch thunks are in `ntdll.dll`. Enforcement-by-default never shipped. McGarr's BHUSA 2025 deck names XFG as "deprecated" [@mcgarr-bhusa25]; Microsoft's strategic direction is hardware-backed CFI (CET shadow stack for the backward edge) plus KCFG / KCET in the kernel. The unsolved question is whether the *forward edge* can ever get fine-grained protection without the compatibility cost that killed XFG. Apple's PAC suggests yes (because the cryptographic key approach has zero compatibility cost on cast); LLVM's `-fsanitize=cfi-icall` suggests yes for code built end-to-end with LTO. Neither has a Windows analog as of 2026.

<Aside label="Microsoft's strategic direction in one sentence">
Recompile first-party code with `/guard:cf /CETCOMPAT`. Push the kernel hardening (kCFG, kCET, HVCI) forward, since the user-mode surface is mature. Lean on hardware (Intel CET, AMD shadow stack, eventually MTE-on-Windows-on-ARM) rather than software heuristics. Accept that legacy unrecompiled binaries remain unguarded and quarantine them in lower-trust VBS-isolated contexts. That is the strategy McGarr's 2025 deck implies and that the Defender / Edge / Recall configurations in the section 10 matrix execute [@mcgarr-bhusa25].
</Aside>

Six open problems. The first four are engineering. The last two are structural. The structural ones suggest the next-decade answer is not a better mitigation, but a different memory model: Rust, CHERI, MTE.

## 15. Practical guide: ten steps to ship a hardened binary

Concrete. Ten steps. By the end of this checklist, your new sandboxed-parser binary is hardened to the canonical 2026 recipe.

1. Run `dumpbin /headers /loadconfig YourBinary.exe`. Verify the `Guard Flags` word is non-zero, that `FID Table present` is in the output, and that the `Guard CF Function Table` is non-empty [@ms-cfg-doc].
2. Compile and link with: `/guard:cf` `/guard:cfw` `/CETCOMPAT` `/DYNAMICBASE` `/HIGHENTROPYVA` `/NXCOMPAT`. The `/CETCOMPAT` flag requires Visual Studio 2019 or later and x64 only [@ms-guard-cf-compiler, @ms-guard-cf-linker, @ms-cetcompat].
3. Call `SetProcessMitigationPolicy` (or, better, `UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY)` for child processes) for: `ProcessDynamicCodePolicy`, `ProcessExtensionPointDisablePolicy`, `ProcessImageLoadPolicy` (with `NoRemoteImages` plus `NoLowMandatoryLabelImages` plus `PreferSystem32Images`), `ProcessStrictHandleCheckPolicy`, `ProcessSystemCallDisablePolicy` (if your process does not draw windows), and `ProcessUserShadowStackPolicy` (with `EnableUserShadowStack` and, for the most-hardened sandboxes, `BlockNonCetBinaries`) [@ms-setprocessmitigationpolicy, @ms-dynamic-code-policy, @ms-image-load-policy, @ms-user-shadow-stack-policy].
4. Use `UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY)` rather than post-`CreateProcess` policy installation for any child process. This is the single most important step on this list.
5. Audit with `Set-ProcessMitigation -PolicyFilePath` (Group Policy / Intune deployable XML). The schema and the cmdlet are documented in the Defender Exploit Protection reference [@ms-defender-exploit-protection].
6. For sandboxed parsers (PDF, image, video, font), enable `ProcessFontDisablePolicy`. Refuse non-system fonts at the per-process layer.
7. For signed-component-only processes, enable `ProcessSignaturePolicy(MicrosoftSignedOnly)`. Accept that some third-party DLLs will not load and document each gap in your threat model [@ms-binary-signature-policy].
8. For browser-class sandboxed children, prohibit child-process creation with `ProcessChildProcessPolicy`. Closes the renderer-to-`cmd.exe` pivot class.
9. Validate the rendered policy at runtime with `Get-ProcessMitigation -Name <binary>`. Spot-check that every flag you set in code is reflected in the cmdlet output [@ms-defender-exploit-protection].
10. For each policy you *cannot* enable, document the structural reason in your threat model. A binary that misses CIG because it depends on third-party COM add-ins is making a deliberate threat-model choice; that choice must be visible to the security review.

> **Note:** `UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY)` closes the race-the-mitigation-window class structurally (section 8, section 12). Every other step on this list is a useful addition. Step 4 is the load-bearing step that lets every other step work as designed. Without it, a peer process in the same security context can disable any of the others between `CreateProcess` and the child's first attempt to install its policies.

The composition of the policy bitfield itself is mechanical. Each policy is a small DWORD-sized structure; the mitigation-policy attribute for `UpdateProcThreadAttribute` packs the relevant flags into a 64-bit `MitigationOptions` value plus an optional 64-bit `MitigationAuditOptions` value.

<Spoiler kind="solution" label="Show the Get-ProcessMitigation command to verify a running binary">
Run this in an elevated PowerShell session, replacing `MicrosoftEdgeCP.exe` with the basename of your binary:

```
Get-ProcessMitigation -Name MicrosoftEdgeCP.exe |
  Format-List CFG, CETShadowStack, BinarySignature, DynamicCode,
              ExtensionPoint, ImageLoad, StrictHandle, SystemCall,
              ChildProcess, FontDisable, PayloadRestriction,
              SideChannelIsolation, ASLR, DEP
```

Each block in the output shows `Enable`, `Audit`, and the subordinate flag word with its individual boolean fields. Spot-check that every flag your code sets in `SetProcessMitigationPolicy` is reflected as `ON` in the cmdlet output, and that any `OFF` or `NOTSET` cell has a documented structural reason in your threat model [@ms-defender-exploit-protection].
</Spoiler>

<RunnableCode lang="js" title="Compose the PROCESS_MITIGATION flag word for UpdateProcThreadAttribute">{`
// Each name is documented in PROCESS_CREATION_MITIGATION_POLICY_* constants
// in winnt.h. The bit positions below match the Microsoft Learn reference.
const POL = {
  // First DWORD: legacy mitigations
  'DEP_ENABLE':                     0x01n << 0n,
  'DEP_ATL_THUNK_ENABLE':           0x01n << 1n,
  'SEHOP_ENABLE':                   0x01n << 2n,
  'FORCE_RELOCATE_IMAGES_ALWAYS_ON':0x01n << 8n,
  'HEAP_TERMINATE_ALWAYS_ON':       0x01n << 12n,
  'BOTTOM_UP_ASLR_ALWAYS_ON':       0x01n << 16n,
  'HIGH_ENTROPY_ASLR_ALWAYS_ON':    0x01n << 20n,
  // Second DWORD: modern mitigations (packed at +32)
  'STRICT_HANDLE_CHECKS_ALWAYS_ON': 0x01n << 32n,
  'WIN32K_SYSTEM_CALL_DISABLE_ALWAYS_ON': 0x01n << 36n,
  'EXTENSION_POINT_DISABLE_ALWAYS_ON':   0x01n << 40n,
  'PROHIBIT_DYNAMIC_CODE_ALWAYS_ON':     0x01n << 44n,
  'CONTROL_FLOW_GUARD_ALWAYS_ON':        0x01n << 48n,
  'BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON': 0x01n << 52n,
  'FONT_DISABLE_ALWAYS_ON':              0x01n << 56n,
  'IMAGE_LOAD_NO_REMOTE_ALWAYS_ON':      0x01n << 60n,
};

// Compose the recipe for a sandboxed PDF parser
const enabled = [
  'DEP_ENABLE',
  'BOTTOM_UP_ASLR_ALWAYS_ON',
  'HIGH_ENTROPY_ASLR_ALWAYS_ON',
  'STRICT_HANDLE_CHECKS_ALWAYS_ON',
  'WIN32K_SYSTEM_CALL_DISABLE_ALWAYS_ON',
  'EXTENSION_POINT_DISABLE_ALWAYS_ON',
  'PROHIBIT_DYNAMIC_CODE_ALWAYS_ON',
  'CONTROL_FLOW_GUARD_ALWAYS_ON',
  'BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON',
  'FONT_DISABLE_ALWAYS_ON',
  'IMAGE_LOAD_NO_REMOTE_ALWAYS_ON',
];

let options = 0n;
for (const name of enabled) options |= POL[name];
console.log('MitigationOptions = 0x' + options.toString(16).padStart(16, '0'));
console.log('Policies enabled: ' + enabled.length + ' of ' + Object.keys(POL).length);
`}</RunnableCode>

Stack the recipe. Document the gaps. Watch the FAQ below for the common misconceptions you will hit on the way.

## 16. Frequently asked questions

<FAQ title="Frequently asked questions">
<FAQItem question="Is DEP still a per-process mitigation, or is it always on?">
On x64 Windows, DEP is unconditionally on for all processes. `ProcessDEPPolicy` in `SetProcessMitigationPolicy` is a 32-bit-only vestigial knob, retained because some 32-bit legacy code is still in production [@ms-setprocessmitigationpolicy, @ms-dep]. For new code on x64, you do not need to touch the DEP policy; the only useful per-process refinement is `ProcessASLRPolicy` (specifically `ForceRelocateImages` and `HighEntropy`), to insist on high-entropy randomization even when third-party DLLs were built without `/DYNAMICBASE`.
</FAQItem>

<FAQItem question="ACG and CIG are the same thing, right?">
No. They attack different surfaces. CIG (`ProcessSignaturePolicy`) prohibits *loading unsigned images*. ACG (`ProcessDynamicCodePolicy`) prohibits *generating new executable code at runtime*. An attacker who finds a signed-but-vulnerable DLL bypasses CIG but does not bypass ACG. An attacker who finds a JIT-spray primitive in an in-process JIT bypasses ACG but does not bypass CIG (because they are not loading a new DLL). The two are orthogonal, and a hardened process needs both [@miller-acg-blog, @ms-binary-signature-policy, @ms-dynamic-code-policy].
</FAQItem>

<FAQItem question="Is XFG shipped by default in Windows 11?">
No. The MSVC `/guard:xfg` flag exists. The `__guard_dispatch_icall_fptr_xfg` thunk exists in `ntdll.dll`. The instrumentation is in some binaries. Enforcement-by-default never shipped, and Connor McGarr's Black Hat USA 2025 deck describes XFG as "deprecated" [@mcgarr-bhusa25]. Microsoft's strategic direction is hardware-backed CET shadow stack for the backward edge plus kCFG and kCET in the kernel; fine-grained forward-edge protection on Windows in 2026 means LLVM's `-fsanitize=cfi-icall` on opted-in builds, not XFG.
</FAQItem>

<FAQItem question="Does CET make ROP impossible?">
Only the return-edge variant. CET shadow stack catches any attempt to corrupt a return address on the regular stack and then return through it [@cet-techcommunity-wayback]. *Call-oriented programming* (COP, chains of `call`-terminated gadgets) and *jump-oriented programming* (JOP, chains of `jmp`-terminated gadgets) preserve the call/return invariant -- the gadgets do not return through corrupted stack frames -- so CET sees nothing. COOP (section 5) chains entire legitimate virtual function calls with matching call/return pairs; CET also sees nothing [@coop-ieeesecurity-pdf]. CET stops *classical* ROP. It does not stop code-reuse exploitation in general.
</FAQItem>

<FAQItem question="Why is the Microsoft Edge JIT in a separate process now?">
Because ACG, enabled in Edge in Windows 10 1703 (March 2017), made in-process JIT a `STATUS_DYNAMIC_CODE_BLOCKED` error [@miller-acg-blog]. The Chakra JIT (then later V8 when Edge moved to Chromium) was rearchitected to run in a separate JIT process that compiles JavaScript and ships the compiled code back to the renderer via an authenticated IPC channel plus a signed-section mapping. The renderer maps the signed section read-execute via `MapViewOfFile`; nothing in the renderer ever calls `VirtualAlloc(PAGE_EXECUTE_*)`. Section 8 walks the architecture in detail.
</FAQItem>

<FAQItem question="Are these mitigations enough to stop modern memory-corruption exploits?">
They constrain the exploit chain but do not fix the root-cause bug. Data-oriented attacks (DOP, section 13) are Turing-complete and survive every CFI variant because no control flow is ever hijacked [@dop-paper]. Signed-but-vulnerable DLLs survive CIG. ACG plus CIG closes the *code* dimension on a hardened process, but a sufficiently-determined attacker who finds a write-what-where primitive can still build a data-only exploit chain in any nontrivial program. The long-term answer is memory-safe languages; Microsoft has been publicly committing to Rust in Windows since 2019, and Matt Miller's BlueHat IL 2019 talk gave the structural justification: "~70% of the vulnerabilities addressed through a security update each year continue to be memory safety issues" [@miller-bhil-pdf]. The short-term answer is the recipe in section 15: stack the mitigations, document the gaps, and treat memory-safety as the limit you are working against.
</FAQItem>
</FAQ>

The bug is still there. The exploit is just much harder. The article ends where it began: a renderer process that survived an info-leak-plus-write-what-where chain because six per-process mitigations all held at once. That is what Windows process mitigation policies do.

<StudyGuide slug="windows-process-mitigation-policies" keyTerms={[
  { term: "Process Mitigation Policy", definition: "A per-process, opt-in security policy installed via SetProcessMitigationPolicy (or, more safely, via UpdateProcThreadAttribute before a child process executes its first user-mode instruction). The PROCESS_MITIGATION_POLICY enum lists twenty-one values (twenty actual policies plus the MaxProcessMitigationPolicy sentinel) as of Windows 11 24H2." },
  { term: "CFG (Control Flow Guard)", definition: "Forward-edge CFI. Compiler emits __guard_check_icall_fptr before every indirect call; linker emits a FID table of valid call targets; loader unions FID tables into a per-process bitmap; runtime validator checks the bitmap on every indirect call. /guard:cf requires /DYNAMICBASE." },
  { term: "XFG (eXtended Flow Guard)", definition: "Type-hashed forward-edge CFI. A 64-bit prototype hash placed 8 bytes before each function entry; the call site compares against the expected prototype hash. Closes COOP. /guard:xfg flag exists; enforcement-by-default never shipped; deprecated per McGarr BHUSA 2025." },
  { term: "CET shadow stack", definition: "Hardware-enforced backward-edge CFI. Every call writes the return address to both the regular stack and a CPU-protected shadow stack; every ret pops both and compares; mismatch raises #CP / STATUS_STACK_BUFFER_OVERRUN. Tiger Lake Sep 2020, AMD Zen 3 Nov 2020." },
  { term: "ACG (Arbitrary Code Guard)", definition: "W^X for the entire process. Prohibits VirtualAlloc(PAGE_EXECUTE_*), prohibits VirtualProtect that adds execute permission, requires MapViewOfSection-with-execute to be backed by signed image. Forced browser JITs out of process. Edge 1703 (March 2017)." },
  { term: "CIG (Code Integrity Guard)", definition: "Only signed images load. ProcessSignaturePolicy with MicrosoftSignedOnly, StoreSignedOnly, or MitigationOptIn. Implemented via User-Mode Code Integrity (UMCI); failed loads return STATUS_INVALID_IMAGE_HASH. Edge 1511 (Nov 2015)." },
  { term: "COOP (Counterfeit Object-Oriented Programming)", definition: "Schuster, Tendyck, Liebchen, Davi, Sadeghi, Holz, IEEE S&P 2015. Code-reuse attack chaining legitimate C++ virtual function calls via corrupted vtable pointers. First attack class to bypass coarse-grained CFG." },
  { term: "Data-Oriented Programming (DOP)", definition: "Hu, Shinde, Adrian, Chua, Saxena, Liang (NUS), IEEE S&P 2016. Turing-complete attack technique that corrupts non-control data (flags, lengths, indices) and lets the program's own legitimate control flow execute the attacker's computation. Invisible to every CFI variant." },
  { term: "UpdateProcThreadAttribute", definition: "Kernel-installed pre-process-start mitigation policy delivery. Closes the race-the-mitigation-window class (Forshaw + Fratric 2017) by installing policies before the child process executes its first user-mode instruction." }
]} questions={[
  { q: "Which two MSVC linker flags must both be set for CFG to actually work?", a: "/GUARD:CF and /DYNAMICBASE. Without /DYNAMICBASE, the linker omits the FID table and CFG is silently a no-op." },
  { q: "Which kind of control-flow transfer does CET shadow stack protect?", a: "The backward edge -- returns. It compares the shadow-stack return address against the regular stack on every ret instruction." },
  { q: "Name two mitigations that close orthogonal attack surfaces on the same process.", a: "ACG (prohibits dynamic code generation) and CIG (prohibits loading unsigned images). An attacker who solves one still has to solve the other." },
  { q: "What attack class did COOP introduce, and what was the structural answer?", a: "COOP chains legitimate C++ virtual function calls via corrupted vtable pointers. The structural answer is fine-grained CFI: XFG (deprecated), LLVM cfi-icall, or ARM PAC." },
  { q: "Why can Microsoft Defender not enable ACG?", a: "Defender's MsMpEng.exe generates scanner code at runtime -- signature compilation routines, emulator bytecode, regex JITs. Enabling ProhibitDynamicCode would crash the engine on its first compile." },
  { q: "What is the single most important step when launching a hardened child process?", a: "Use UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY) at CreateProcess time so the kernel installs mitigation policies before the child's first user-mode instruction runs. Closes the race-the-mitigation-window class." }
]} />
