67 min read

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.

Permalink

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?

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 [2, 1].

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.

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.

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 [3]. 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 [4]. 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 [5]. 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.

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.
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.

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 [6]. 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 [7, 8].

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.

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 [9]. 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 [10, 11, 12]. 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" [13]. 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 [14]. 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.

Ctrl + scroll to zoom
The 1996-2007 timeline: from stack smashing to system-wide DEP plus ASLR

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 [1].

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 [15, 16]. 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.

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 [16].

The follow-up Black Hat USA 2008 talk generalised the result to RISC architectures [17], 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.

"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 [13]

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?"

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.

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 [18]. Microsoft's stated successors are the ProcessMitigations PowerShell module and Windows Defender Exploit Guard -- i.e., the formal SetProcessMitigationPolicy surface this article catalogs [18].

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) [19]. 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.

Control-Flow Integrity (CFI)

A defensive property formalized by Abadi, Budiu, Erlingsson, and Ligatti in 2005 [19]: 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.

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" [20]. 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 [21].

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.

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 [22]. 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 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.

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.

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.

Ctrl + scroll to zoom
CFG's four phases: compile, link, load, runtime

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 [21]. 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:

JavaScript 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);
}

Press Run to execute.

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 [24, 25]. 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."

"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 [24]

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 [24, 25].

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) [26]. 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 [27].

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 [27]. 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.

Ctrl + scroll to zoom
XFG call site vs CFG call site: the prototype hash

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.

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 [28].

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:

"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 [29]

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.

The mechanism, drawn from Intel's CET specification and Microsoft's Windows enabling documents [29, 30, 31]:

  • 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 [31].

Tiger Lake shipped CET first, in September 2020. AMD followed with the same architectural spec in Zen 3 in November 2020 [30]. 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.

AMD Zen 3 was launched on November 5, 2020, two months after Tiger Lake [30]. Both vendors implement the Intel CET specification verbatim, so Microsoft's Windows enabling code is single-source.
Ctrl + scroll to zoom
Shadow-stack mechanics: every call writes both stacks; every ret compares

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 [32]. 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 [32].

`#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.

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

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) [33]. The canonical primary on its design is Matt Miller's February 2017 Edge blog Mitigating arbitrary native code execution in Microsoft Edge [33]. The corresponding policy in SetProcessMitigationPolicy is ProcessSignaturePolicy, with the bitfield PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY [34].

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 [33, 34].

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.

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.

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 [33, 34]. 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.

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") [35]. The structural-vulnerable-but-signed class has been operationally hard to retire for the same reason every backwards-compatibility constraint is hard to retire.

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.

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 [33, 36].

The PROCESS_MITIGATION_DYNAMIC_CODE_POLICY structure carries four flags [36]:

  • 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 [33, 36]:

  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 [33]. 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.

Ctrl + scroll to zoom
JIT architecture: pre-ACG single process vs post-ACG broker plus JIT process plus renderer

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 [37, 38]. The PoC is small enough to read in one paragraph.

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 [39]. 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 [2]. 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 [40]; the policy enum entry itself is in ms-setprocessmitigationpolicy [1]. 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 [28].

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 [1, 2]. 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 [1].

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 [41]. 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) [1]. 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

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" [43].

ProcessSideChannelIsolationPolicy

Two distinct sub-mitigations [1]:

  • 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 [32]. Listed here for enum completeness.

ProcessChildProcessPolicy

Refuses any CreateProcess call originating from the process [1]. 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 [44]: 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 [1]. 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 [33], and the policy-enum reference [2, 1].

PolicyEdge content (MicrosoftEdgeCP.exe)Chrome rendererOutlook (Office)Defender (MsMpEng.exe)Recall (Windows AI service)Notepad.exe
DEP / ASLR (system foundation)yesyesyesyesyesyes
CFGyesyesyesyesyesyes
CET shadow stackyes (strict)yespartialyesyes (strict)yes (default)
ACG (ProcessDynamicCodePolicy)yesyes (with OOP JIT)no -- COM/MAPI add-insno -- engine generates scanner code at runtimeyesn/a (no JIT)
CIG (ProcessSignaturePolicy)yes (MicrosoftSignedOnly)partial -- pluginsno -- third-party add-insyesyes (MicrosoftSignedOnly)n/a
Disable-Win32k (SystemCallDisable)yesyes (renderer process)n/a (GUI)yes (no GUI)yes (no GUI)n/a (GUI)
Disable-Extension-Pointsyesyespartialyesyesdefault
Image-Load (all three flags)yesyespartialyesyesdefault
StrictHandleCheckyesyesyesyesyesyes
ChildProcessyesyesno -- launches winword, etc.yes (no children)yes (no children)no
FontDisableyesyesn/a (renders fonts)n/an/an/a
RedirectionGuardyes (since 2025)yes (since 2025)partialyesyespartial
SideChannelIsolationoptionaloptionaloptionaloptionalyes (high-trust)optional
PayloadRestriction (EAF/IAF/ROP)yesyesyesyesyesn/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).

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.

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.

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 [45, 46]. 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 [47]; 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 [48]. 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) [49]. 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 [50]. 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 [51]. 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 [52]. 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 [53, 52]. 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.

PlatformForward-edgeBackward-edgeDynamic codeMemory safety
Windows (x64)CFG (coarse), XFG (deprecated)CET shadow stackACGnone 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 ARMnot a kernel issueRust-in-kernel pilot
AndroidPAC + BTI on supported SoCsBTI / shadow call stacksandboxed by selinux + seccompMTE on Pixel 8
Chromiumper-platform forward-edgeper-platform backward-edgeOOP JIT + V8 sandboxlayered

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.
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.

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 [54]. 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.

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 [55, 56]. The 2016 Theori work generalised the idea to use the JIT to emit CFG-valid function-entry bytes [54].

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 [24, 25]. 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 [37] and the Exploit-DB mirror [38].

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 [28]. 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.

Ctrl + scroll to zoom
The bypass tradition: one named class per defensive generation

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 [19] 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 [57, 58]. 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."

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 [57, 58]. No CFI variant -- CFG, XFG, CET shadow stack, ARM PAC, kCFI -- can detect a DOP attack, because no control flow is hijacked.

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 [28]. 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.

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 [59]:

"~70% of the vulnerabilities addressed through a security update each year continue to be memory safety issues." -- Matt Miller, BlueHat IL 2019 [59]

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" [60].

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.

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 [61]); 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 [62]; 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 [51]. 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" [28]; 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.

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 [21].
  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 [22, 23, 31].
  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) [1, 36, 41, 32].
  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 [44].
  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 [34].
  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 [44].
  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.

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.

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 [44].

JavaScript 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);

Press Run to execute.

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

16. Frequently asked questions

Frequently asked questions

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 [1, 10]. 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.

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 [33, 34, 36].

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" [28]. 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.

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 [29]. 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 [24]. CET stops classical ROP. It does not stop code-reuse exploitation in general.

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 [33]. 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.

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 [57]. 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" [59]. 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.

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.

Study guide

Key terms

Process Mitigation Policy
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.
CFG (Control Flow Guard)
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.
XFG (eXtended Flow Guard)
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.
CET shadow stack
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.
ACG (Arbitrary Code Guard)
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).
CIG (Code Integrity Guard)
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).
COOP (Counterfeit Object-Oriented Programming)
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.
Data-Oriented Programming (DOP)
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.
UpdateProcThreadAttribute
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.

Comprehension questions

  1. Which two MSVC linker flags must both be set for CFG to actually work?

    /GUARD:CF and /DYNAMICBASE. Without /DYNAMICBASE, the linker omits the FID table and CFG is silently a no-op.

  2. Which kind of control-flow transfer does CET shadow stack protect?

    The backward edge -- returns. It compares the shadow-stack return address against the regular stack on every ret instruction.

  3. Name two mitigations that close orthogonal attack surfaces on the same process.

    ACG (prohibits dynamic code generation) and CIG (prohibits loading unsigned images). An attacker who solves one still has to solve the other.

  4. What attack class did COOP introduce, and what was the structural answer?

    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.

  5. Why can Microsoft Defender not enable ACG?

    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.

  6. What is the single most important step when launching a hardened child process?

    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.

References

  1. SetProcessMitigationPolicy function. https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setprocessmitigationpolicy
  2. PROCESS_MITIGATION_POLICY enum. https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-process_mitigation_policy
  3. Aleph One (Elias Levy) (1996). Smashing The Stack For Fun And Profit. http://phrack.org/issues/49/14.html - Phrack 49, file 14. The founding pedagogical recipe for stack-based buffer overflow exploitation.
  4. Alexander Peslyak (Solar Designer) (1997). Return-into-libc overflow exploit + non-exec stack patch. https://seclists.org/bugtraq/1997/Aug/63 - BugTraq post, August 10, 1997. First public return-into-libc exploit, published alongside the Linux non-exec-stack kernel patch.
  5. Openwall Project Openwall Linux kernel patch README. https://www.openwall.com/linux/README
  6. PaX. https://en.wikipedia.org/wiki/PaX
  7. PaX Team (2003). Address Space Layout Randomization (design doc). https://pax.grsecurity.net/docs/aslr.txt
  8. Address space layout randomization. https://en.wikipedia.org/wiki/Address_space_layout_randomization
  9. NX bit. https://en.wikipedia.org/wiki/NX_bit
  10. Data Execution Prevention. https://learn.microsoft.com/en-us/windows/win32/memory/data-execution-prevention
  11. Data Execution Prevention (Wikipedia). https://en.wikipedia.org/wiki/Data_Execution_Prevention
  12. Windows XP Service Pack 2. https://en.wikipedia.org/wiki/Windows_XP_SP2
  13. Return-oriented programming. https://en.wikipedia.org/wiki/Return-oriented_programming
  14. Michael Howard (2006). Address Space Layout Randomization in Windows Vista. https://learn.microsoft.com/en-us/archive/blogs/michael_howard/address-space-layout-randomization-in-windows-vista
  15. Hovav Shacham (2007). The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls (on the x86). https://hovav.net/ucsd/papers/s07.html
  16. Hovav Shacham (2007). The Geometry of Innocent Flesh on the Bone (PDF). https://hovav.net/ucsd/dist/geometry.pdf
  17. Hovav Shacham (2008). Return-Oriented Programming: Systems, Languages, and Applications. https://hovav.net/ucsd/talks/blackhat08.html
  18. Enhanced Mitigation Experience Toolkit. https://en.wikipedia.org/wiki/Enhanced_Mitigation_Experience_Toolkit
  19. Martín Abadi, Mihai Budiu, Úlfar Erlingsson, & Jay Ligatti (2005). Control-Flow Integrity. https://www.microsoft.com/en-us/research/publication/control-flow-integrity/
  20. Yunhai Zhang (2015). Bypass Control Flow Guard Comprehensively. https://github.com/tpn/pdfs/raw/master/Bypass%20Control%20Flow%20Guard%20Comprehensively%20-%20Slides%20(2015).pdf
  21. Control Flow Guard. https://learn.microsoft.com/en-us/windows/win32/secbp/control-flow-guard
  22. /guard (Enable Control Flow Guard). https://learn.microsoft.com/en-us/cpp/build/reference/guard-enable-control-flow-guard?view=msvc-170
  23. /GUARD (Enable Guard Checks). https://learn.microsoft.com/en-us/cpp/build/reference/guard-enable-guard-checks?view=msvc-170
  24. Felix Schuster, Thomas Tendyck, Christopher Liebchen, Lucas Davi, Ahmad-Reza Sadeghi, & Thorsten Holz (2015). Counterfeit Object-oriented Programming. https://www.ieee-security.org/TC/SP2015/papers-archived/6949a745.pdf
  25. (2015). Counterfeit Object-oriented Programming (IEEE Xplore). https://ieeexplore.ieee.org/document/7163058
  26. David Weston (2019). Advancing Windows Security. https://github.com/dwizzzle/Presentations/raw/master/Bluehat%20Shanghai%20-%20Advancing%20Windows%20Security.pdf - BlueHat Shanghai 2019. Weston is identified on the title slide as Microsoft OS Security Group Manager.
  27. Connor McGarr (2020). Examining Xtended Flow Guard (XFG). https://connormcgarr.github.io/examining-xfg/
  28. Connor McGarr (2025). Out Of Control: How KCFG and KCET Redefine Control Flow Integrity in the Windows Kernel. https://i.blackhat.com/BH-USA-25/Presentations/USA-25-McGarr-Out-Of-Control-KCFG-And-KCET.pdf - Black Hat USA 2025. McGarr is listed as Software Engineer, Prelude Security.
  29. (2020). Understanding Hardware-enforced Stack Protection (Wayback). https://web.archive.org/web/20241119023959/https://techcommunity.microsoft.com/blog/windowsosplatform/understanding-hardware-enforced-stack-protection/1247815
  30. Intel CET. https://en.wikipedia.org/wiki/Intel_CET
  31. /CETCOMPAT linker option. https://learn.microsoft.com/en-us/cpp/build/reference/cetcompat?view=msvc-170
  32. PROCESS_MITIGATION_USER_SHADOW_STACK_POLICY. https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-process_mitigation_user_shadow_stack_policy
  33. Matt Miller (2017). Mitigating arbitrary native code execution in Microsoft Edge. https://blogs.windows.com/msedgedev/2017/02/23/mitigating-arbitrary-native-code-execution/
  34. PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY. https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-process_mitigation_binary_signature_policy
  35. (2013). CVE-2013-3900 -- WinVerifyTrust Signature Validation Vulnerability. https://nvd.nist.gov/vuln/detail/CVE-2013-3900 - NVD detail page. Carries the verbatim text "Microsoft does not plan to enforce the stricter verification behavior as a default functionality on supported releases of Microsoft Windows. This behavior remains available as an opt-in feature via reg key setting."
  36. PROCESS_MITIGATION_DYNAMIC_CODE_POLICY. https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-process_mitigation_dynamic_code_policy
  37. James Forshaw & Ivan Fratric (2018). Project Zero issue 42450607: Edge ACG OpenProcess race. https://project-zero.issues.chromium.org/issues/42450607
  38. (2018). Microsoft Edge Chakra - ACG OpenProcess Bypass (EDB 44467). https://www.exploit-db.com/exploits/44467
  39. Ivan Fratric (2018). Bypassing Mitigations by Attacking JIT Server in Microsoft Edge. https://projectzero.google/2018/05/bypassing-mitigations-by-attacking-jit.html
  40. Crispin Cowan (2017). Strengthening the Microsoft Edge Sandbox. https://blogs.windows.com/msedgedev/2017/03/23/strengthening-microsoft-edge-sandbox/
  41. PROCESS_MITIGATION_IMAGE_LOAD_POLICY. https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-process_mitigation_image_load_policy
  42. (2025). RedirectionGuard: Mitigating unsafe junction traversal in Windows. https://www.microsoft.com/en-us/msrc/blog/2025/06/redirectionguard-mitigating-unsafe-junction-traversal-in-windows/
  43. Exploit protection reference (Microsoft Defender). https://learn.microsoft.com/en-us/defender-endpoint/exploit-protection-reference
  44. Apple A12. https://en.wikipedia.org/wiki/Apple_A12 - Wikipedia infobox: "Launched: September 12, 2018"; "Instruction set: A64 -- ARMv8.3-A".
  45. Apple Inc. Apple Platform Security: Operating system integrity. https://support.apple.com/guide/security/operating-system-integrity-sec8b776536b/web - Apple Platform Security Guide, OS integrity chapter. Per-chip feature support table covers A10, A11, A12-A14, A15-A18, M1, M2-M4, A19, M5; PAC support tracks the ARMv8.3-A introduction at A12.
  46. AArch64 (ARMv8.3-A and Pointer Authentication). https://en.wikipedia.org/wiki/ARMv8.3-A - Wikipedia AArch64 article (the ARMv8.3-A slug redirects here). Documents the ARMv8.3-A Pointer Authentication PACIA / AUTIA / PACIB / AUTIB instructions and key registers.
  47. Apple Inc. Hardened Runtime. https://developer.apple.com/documentation/security/hardened_runtime - Apple developer documentation. Names "com.apple.security.cs.allow-jit" as the entitlement that grants a signed binary an in-process JIT carve-out.
  48. Jonathan Corbet (2022). Forward-edge control-flow integrity for the kernel. https://lwn.net/Articles/898040/
  49. Control Flow Integrity (Clang). https://clang.llvm.org/docs/ControlFlowIntegrity.html
  50. Samuel Gross (2024). The V8 Sandbox. https://v8.dev/blog/sandbox - V8 team blog post. Verbatim: "After almost three years since the initial design document and hundreds of CLs in the meantime, the V8 Sandbox -- a lightweight, in-process sandbox for V8 -- has now progressed to the point where it is no longer considered an experimental security feature."
  51. Arm Ltd. Memory safety: Arm Memory Tagging Extension. https://newsroom.arm.com/blog/memory-safety-arm-memory-tagging-extension - Arm newsroom post. Verbatim: "Arm's MTE was first introduced as part of the Armv8.5 instruction set in August 2019".
  52. Pixel 8. https://en.wikipedia.org/wiki/Pixel_8 - Wikipedia infobox: "First released October 12, 2023". Tensor G3 SoC; first consumer device with MTE default-on for the kernel and key system services.
  53. (2016). Chakra JIT CFG Bypass. https://theori.io/blog/chakra-jit-cfg-bypass
  54. Dion Blazakis (2010). Interpreter Exploitation: Pointer Inference and JIT Spraying. http://www.semantiscope.com/research/BHDC2010/BHDC-2010-Paper.pdf - Black Hat DC 2010. Canonical primary for JIT spraying, demonstrated against the Adobe Flash ActionScript JIT.
  55. JIT spraying. https://en.wikipedia.org/wiki/JIT_spraying - Wikipedia explicitly credits Dion Blazakis (BHDC 2010) as the originator of the JIT-spraying technique.
  56. Hong Hu, Shweta Shinde, Sendroiu Adrian, Zheng Leong Chua, Prateek Saxena, & Zhenkai Liang (2016). Data-Oriented Programming: On the Expressiveness of Non-Control Data Attacks. https://huhong789.github.io/papers/hu:dop.pdf
  57. (2016). Data-Oriented Programming (IEEE Xplore). https://ieeexplore.ieee.org/document/7546545
  58. Matt Miller (2019). Trends, challenges, and shifts in software vulnerability mitigation. https://github.com/microsoft/MSRC-Security-Research/raw/master/presentations/2019_02_BlueHatIL/2019_01%20-%20BlueHatIL%20-%20Trends%2C%20challenge%2C%20and%20shifts%20in%20software%20vulnerability%20mitigation.pdf
  59. Catalin Cimpanu (2019). Microsoft: 70 percent of all security bugs are memory safety issues. https://www.zdnet.com/article/microsoft-70-percent-of-all-security-bugs-are-memory-safety-issues/
  60. Adam Burch (2019). Using Rust in Windows. https://msrc.microsoft.com/blog/2019/11/using-rust-in-windows/ - Microsoft Security Response Center blog post, November 2019. The first public Microsoft commitment to evaluating Rust as a memory-safe systems language for Windows components.
  61. Control-flow integrity. https://en.wikipedia.org/wiki/Control-flow_integrity - Wikipedia CFI article. Documents Tencent Xuanwu Lab's RFG bypass research alongside primary CFI taxonomy and the Microsoft Return Flow Guard history.