<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Parag Mali - tag: windows-internals</title><description>Posts tagged windows-internals.</description><link>https://paragmali.com/</link><language>en-US</language><lastBuildDate>Sun, 07 Jun 2026 04:13:06 GMT</lastBuildDate><atom:link href="https://paragmali.com/tags/windows-internals/rss.xml" rel="self" type="application/rss+xml"/><item><title>The Same-Privilege Paradox: Twenty-One Years of Windows Kernel Self-Defense</title><link>https://paragmali.com/blog/the-same-privilege-paradox-twenty-one-years-of-windows-kerne/</link><guid isPermaLink="true">https://paragmali.com/blog/the-same-privilege-paradox-twenty-one-years-of-windows-kerne/</guid><description>PatchGuard, KASLR, KDP, and the Win32k Lockdown are four answers to one paradox -- a defense at the attacker&apos;s privilege cannot succeed in principle. The 2005-2026 trajectory is migration out of the kernel.</description><pubDate>Wed, 03 Jun 2026 00:00:00 GMT</pubDate><content:encoded>
Microsoft has spent twenty-one years defending the Windows kernel from itself. PatchGuard, KASLR, KDP, and the Win32k Lockdown are four answers to a single problem -- the **same-privilege paradox**, that a defense at the attacker&apos;s privilege level cannot succeed in principle. The trajectory is migration: from in-kernel obfuscation (PatchGuard, 2005), to address-space tricks (KASLR 2007, KVA Shadow 2018), to hypervisor-anchored isolation (KDP, 2020), and finally to attack-surface deletion (Win32k filter, 2017). Microsoft&apos;s own Security Servicing Criteria say PatchGuard is not a security boundary [@ms-servicing-criteria], and that admission is the load-bearing premise of every modern Windows kernel mitigation.
&lt;h2&gt;1. If the attacker is already in the kernel, what is left to defend?&lt;/h2&gt;
&lt;p&gt;For three years, a Russian-attributed espionage rootkit called Uroburos ran on Microsoft&apos;s most heavily defended kernel -- the 64-bit Windows kernel with PatchGuard active -- and PatchGuard never made a sound [@gdata-uroburos-blog]. The reason is the one the marketing copy will not tell you: PatchGuard is not, and was never designed to be, a security boundary; Microsoft says so in its own Security Servicing Criteria [@ms-servicing-criteria]. The twenty-one-year history of Windows kernel self-defense is the story of why the answer to &quot;the kernel cannot defend itself from itself&quot; turned out to be &quot;stop trying to defend it from inside.&quot;&lt;/p&gt;
&lt;p&gt;That sentence will read like editorial provocation until you see the architecture. Uroburos did not bypass PatchGuard. It side-stepped it. The rootkit shipped a signed-but-vulnerable copy of Oracle&apos;s &lt;code&gt;VBoxDrv.sys&lt;/code&gt;, used the vulnerability to flip the &lt;code&gt;g_CiEnabled&lt;/code&gt; flag that gates Driver Signature Enforcement, loaded its own unsigned kernel driver, and then operated alongside PatchGuard for three years (2011 -- 2014) without ever modifying anything PatchGuard checked [@gdata-uroburos-blog] [@stmxcsr-turla]. The Stage 2 evolution survey calls this the canonical refutation of the most common reader misconception about PatchGuard: not &quot;PatchGuard was broken&quot; but &quot;PatchGuard&apos;s protected-structure list is, by construction, narrower than the kernel-modification surface.&quot;&lt;/p&gt;

A defense that shares its CPU privilege level with the attacker can in principle always be subverted by an attacker at that privilege level, because every code path and data structure the defense relies on is, by construction, mutable by the attacker. The paradox is not a formal impossibility theorem in the cryptographic sense, but it is the de facto design constraint Microsoft has acknowledged in writing through its Security Servicing Criteria [@ms-servicing-criteria].

A Microsoft kernel feature that periodically verifies a fixed list of kernel structures -- the SSDT, IDT, GDT, syscall MSRs, the in-memory `nt` and `hal` images, and select processor control registers -- and bug-checks the system with stop code `CRITICAL_STRUCTURE_CORRUPTION` (0x109) on mismatch. Introduced April 25, 2005 in Windows XP Professional x64 Edition and Windows Server 2003 x64 Edition; never shipped on x86 [@ms-advisory-932596] [@ms-driver-x64-restrictions]. PatchGuard is an *engineering deterrent*, not a security boundary.
&lt;p&gt;This article covers four mitigations across twenty-one years -- April 25, 2005, when PatchGuard shipped with Windows XP Professional x64 Edition and Windows Server 2003 x64 Edition [@ms-advisory-932596], through June 2026, when kCET and the VTL1-anchored stack are the front line. The four mitigations are PatchGuard (KPP), KASLR (and its 2018 successor KVA Shadow), KDP (Kernel Data Protection), and the two-stage Win32k Lockdown that began in 2012 with &lt;code&gt;DisallowWin32kSystemCalls&lt;/code&gt; and resolved in 2017 with &lt;code&gt;Win32kSystemCallFilter&lt;/code&gt; [@ms-syscall-disable-policy] [@ms-syscall-filter-policy]. They do not look like they belong together until you notice the direction. Each generation moves the defense one step further away from where the attacker lives: from in-kernel obfuscation, to address-space tricks, to hypervisor-anchored isolation (VTL1), to attack-surface deletion.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Every meaningful Windows kernel mitigation since 2017 has moved the &lt;em&gt;enforcement&lt;/em&gt; to a privilege level the kernel-mode attacker cannot reach -- hypervisor (VTL1), CPU silicon (KTRR on Apple, kCET shadow stack hardware on Intel / AMD), or out of the syscall surface entirely. The reason is the same-privilege paradox: a defense that lives where the attacker lives cannot, in principle, succeed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Four misconceptions are worth retiring before we start. &lt;strong&gt;First&lt;/strong&gt;, &quot;PatchGuard is the load-bearing kernel-rootkit defense&quot;; in fact, Microsoft says it is not a security boundary at all, and Uroburos operated alongside it for three years. &lt;strong&gt;Second&lt;/strong&gt;, &quot;PatchGuard is x64-only&quot;; the documentation is x64-centric, but in 2026 PatchGuard also runs on 64-bit ARM Windows -- the one architectural truth in the framing is that PatchGuard never shipped on 32-bit Windows. &lt;strong&gt;Third&lt;/strong&gt;, &quot;KASLR is dead because entropy is the variable that matters&quot;; the Hund-Willems-Holz 2013 result and Gruss et al. 2017 generalization showed that &lt;em&gt;randomness&lt;/em&gt; was never the load-bearing defense -- structural unreachability is [@doi-hund-2013] [@gruss-kaiser-pdf]. &lt;strong&gt;Fourth&lt;/strong&gt;, &quot;Win32k Lockdown killed half the LPE class&quot;; the lockdown removes roughly the historically-vulnerable syscall surface &lt;em&gt;from sandboxed renderers specifically&lt;/em&gt;, not from the operating system in general [@pz-breaking-chain].&lt;/p&gt;
&lt;p&gt;To see why Microsoft has spent twenty-one years on a problem that, by their own admission, has no in-kernel answer, we have to go back to April 25, 2005 -- and to the architectural break that made the new contract politically possible.&lt;/p&gt;
&lt;h2&gt;2. Why Microsoft built PatchGuard at all (1998 -- 2005)&lt;/h2&gt;
&lt;p&gt;Before April 2005, the Windows kernel was a public hooking surface &lt;em&gt;by design&lt;/em&gt;. McAfee, Symantec, F-Secure, and Trend Micro patched the System Service Descriptor Table (SSDT), hooked the Interrupt Descriptor Table (IDT), and inline-patched &lt;code&gt;nt!Nt*&lt;/code&gt; system-service routines as legitimate engineering practice. The same primitives, applied with malicious intent, became the rootkit canon of the late 1990s and early 2000s: NTRootkit, FU, Hacker Defender. From the operating system&apos;s point of view, the defender and the attacker were architecturally indistinguishable.&lt;/p&gt;

A kernel data structure on Windows containing function pointers to every system service routine (the `Nt*` functions that implement system calls). On 32-bit Windows, anti-virus vendors routinely patched the SSDT to intercept system calls before the kernel processed them. On x64, modifying the SSDT is prohibited and PatchGuard treats it as a `CRITICAL_STRUCTURE_CORRUPTION` event [@ms-driver-x64-restrictions].
&lt;p&gt;The symmetry was awkward enough in normal operation. It became politically untenable in October 2005, when Mark Russinovich discovered that Sony BMG&apos;s XCP DRM software, shipped on tens of millions of audio CDs, installed an actual cloaking rootkit on consumer Windows machines.Russinovich&apos;s October 31, 2005 Sysinternals post &quot;Sony, Rootkits and Digital Rights Management Gone Too Far&quot; turned a niche kernel-internals topic into national news within a week. The lawsuit settlements and CD recall that followed established, in pop-culture terms, the symmetry between &quot;legitimate kernel hooking&quot; and &quot;malware kernel hooking&quot; that the security industry had been arguing about for years. The XCP code was structurally identical to malware -- it hid files whose names began with &lt;code&gt;$sys$&lt;/code&gt;, modified system calls, and resisted removal -- and it shipped under a Sony certificate.&lt;/p&gt;
&lt;p&gt;What Microsoft needed was an architectural break large enough that they could rewrite the kernel contract without having to honor the old one. They got it from AMD. The x64 architecture, productised as AMD64 and adopted by Intel as EM64T, was Microsoft&apos;s once-in-a-decade chance to publish a new contract incompatible with the old. Windows XP Professional x64 Edition and Windows Server 2003 x64 Edition shipped on April 25, 2005 [@ms-advisory-932596]. The new kernel-mode contract had two enforcement layers. &lt;strong&gt;PatchGuard&lt;/strong&gt; was the engineering enforcement -- the code that periodically inspected the kernel&apos;s most sensitive structures and bug-checked the system on mismatch. &lt;strong&gt;Kernel-Mode Code Signing (KMCS)&lt;/strong&gt; was the policy enforcement -- the rule that production x64 kernels would load only Authenticode-signed drivers.&lt;/p&gt;

The policy on 64-bit Windows that the kernel will load only Authenticode-signed kernel drivers in production (test-signing modes exist for development). KMCS shipped with the same April 2005 release as PatchGuard and is its policy counterpart -- KMCS controls what code enters the kernel; PatchGuard checks the kernel structures the loaded code is expected to leave alone [@ms-driver-x64-restrictions].
&lt;p&gt;The combination did exactly what the AV industry feared. Their entire detection methodology was, by the new contract, illegal on x64. McAfee bought a full-page ad in the &lt;em&gt;Financial Times&lt;/em&gt; in October 2006 to call Microsoft&apos;s behaviour anti-competitive. Symantec joined the EC complaint. The verbatim industry framing was delivered by Vincent Weafer, then Symantec&apos;s senior director of security response, in a &lt;em&gt;CRN&lt;/em&gt; report: &lt;em&gt;&quot;Either everybody has access to the kernel or nobody has access to the kernel -- and we believe in the latter&quot;&lt;/em&gt; [@crn-mcafee-symantec]. Microsoft declined to publish a signed bypass API. By the time the dust settled, the AV-vendor hooking pattern on Windows had been industrially ended.&lt;/p&gt;

Either everybody has access to the kernel or nobody has access to the kernel -- and we believe in the latter. -- Vincent Weafer, Symantec, quoted in CRN, September 25, 2006 [@crn-mcafee-symantec].

McAfee and Symantec argued that Vista x64 plus PatchGuard locked third-party security vendors out of the kernel while Microsoft&apos;s own Windows Defender remained free to ship integrations Microsoft had not exposed to anyone else. The EC investigation eventually closed without forcing Microsoft to expose a signed bypass API. The 2024 CrowdStrike Falcon outage -- where a single bad signature update propagated through a kernel driver and bricked an estimated 8.5 million Windows machines worldwide -- is now widely read, retroactively, as vindication of Microsoft&apos;s 2006 position. The argument that &quot;everybody or nobody&quot; has kernel access turned out to have a third answer: &quot;as few people as possible, with as small a kernel footprint as possible, mediated by user-mode brokers.&quot; That is the design move the rest of this article is about.
&lt;p&gt;The historical record has one quirk worth flagging. No primary 2005 PatchGuard launch document is preserved in Microsoft&apos;s current documentation surface; the earliest official primary is Microsoft Security Advisory 932596 from August 2007, which describes Kernel Patch Protection as protecting &quot;code and critical structures in the Windows kernel from modification by unknown code or data&quot; and announces an upcoming PatchGuard update [@ms-advisory-932596]. The technical detail of what PatchGuard checked was reverse-engineered by the offensive security community before Microsoft documented it.&lt;/p&gt;

gantt
    title Windows kernel self-defense, 2005-2026
    dateFormat YYYY-MM
    section Same-privilege (CPL=0)
    PatchGuard v1            :2005-04, 2008-02
    PatchGuard v2-v3         :2006-11, 2010-10
    PatchGuard v7-v8         :2012-08, 2026-06
    KASLR (8-bit entropy)    :2007-01, 2018-01
    section CPU mediated
    KVA Shadow               :2018-01, 2026-06
    kCET / shadow stack      :2022-09, 2026-06
    section VTL1 anchored
    HVCI                     :2015-07, 2026-06
    kCFG with VBS bitmap     :2017-04, 2026-06
    KDP static plus dynamic  :2020-05, 2026-06
    section Surface deletion
    DisallowWin32kSystemCalls:2012-08, 2017-10
    Win32kSystemCallFilter   :2017-10, 2026-06
&lt;p&gt;So the contract was published, the kernel was no longer a public hooking surface, and Microsoft shipped a feature called PatchGuard that ran inside the kernel and checked the kernel&apos;s most sensitive structures. The question Skywing and skape would publish nine months later was the question everybody in offensive security had been waiting for: &lt;em&gt;how do you defend a kernel from inside the kernel?&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;3. PatchGuard v1 and v2: obfuscation as defense (2005 -- 2008)&lt;/h2&gt;
&lt;p&gt;PatchGuard v1 was an engineering answer to a political problem. It worked exactly the way a defense works if you do not state out loud that the attacker is in the same address space: a periodic timer fired, a checksum was recomputed, a mismatch caused the machine to bug-check with stop code &lt;code&gt;CRITICAL_STRUCTURE_CORRUPTION&lt;/code&gt; (0x109), and the assumption was that the cost of figuring out which timer, which checksum, and which DPC handler was high enough to deter casual rootkit authors. And for nine months, that was the story.&lt;/p&gt;

The Windows bug-check stop code raised by PatchGuard when one of its periodic integrity checks detects an unexpected modification to a protected kernel structure. The bug-check call goes through `KeBugCheckEx`, which on later PatchGuard generations is itself a protected structure -- swallowing the bug-check from a hooked `KeBugCheckEx` was one of the four bypass classes Skywing and skape catalogued in 2005 [@uninformed-v3-archive].
&lt;p&gt;What does PatchGuard actually check? The protected-structure list has grown across generations, but the core, as Microsoft documents it for driver authors, has been remarkably stable [@ms-driver-x64-restrictions]:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The SSDT and &lt;code&gt;KeServiceDescriptorTable[Shadow]&lt;/code&gt; (the function-pointer tables that dispatch system calls)&lt;/li&gt;
&lt;li&gt;The Interrupt Descriptor Table (IDT), read from the CPU via &lt;code&gt;IDTR&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The Global Descriptor Table (GDT), read from the CPU via &lt;code&gt;GDTR&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The syscall-related model-specific registers: &lt;code&gt;IA32_LSTAR&lt;/code&gt;, &lt;code&gt;IA32_STAR&lt;/code&gt;, &lt;code&gt;IA32_CSTAR&lt;/code&gt;, and the &lt;code&gt;IA32_SYSENTER_*&lt;/code&gt; family&lt;/li&gt;
&lt;li&gt;The in-memory &lt;code&gt;nt&lt;/code&gt; and &lt;code&gt;hal&lt;/code&gt; kernel images (so you cannot inline-patch &lt;code&gt;nt!NtCreateFile&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KdpStub&lt;/code&gt;, &lt;code&gt;KeBugCheckCallbackHead&lt;/code&gt;, and other kernel call-back tables&lt;/li&gt;
&lt;li&gt;Select processor control registers and debug registers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mechanism: a context block built by &lt;code&gt;nt!KiInitializePatchGuard&lt;/code&gt; at boot, scattered across allocations, XOR-encrypted; a DPC-driven verifier routine that fires at randomized intervals; a per-fire recomputation of expected checksums; a &lt;code&gt;KeBugCheckEx(0x109, ...)&lt;/code&gt; call on any mismatch. The load-bearing property of the design -- the one that drives the rest of the story -- is that &lt;em&gt;the defense lives at CPL=0, alongside the attacker&lt;/em&gt;. The verifier, the keys, the schedule, the bug-check routine itself: all of it lives in the same address space as the rootkit it is meant to detect.&lt;/p&gt;

flowchart TD
    A[Timer fires at random interval] --&amp;gt; B[DPC routine dispatched]
    B --&amp;gt; C[Decrypt scattered context fragment]
    C --&amp;gt; D[Hash protected structures]
    D --&amp;gt; E{Hash matches expected}
    E -- yes --&amp;gt; F[Reschedule next check]
    E -- no --&amp;gt; G[Call KeBugCheckEx 0x109]
    G --&amp;gt; H[System bug-check CRITICAL_STRUCTURE_CORRUPTION]
    F --&amp;gt; A
&lt;p&gt;In December 2005, eight months after PatchGuard shipped, Skywing and skape published &quot;Bypassing PatchGuard on Windows x64&quot; in &lt;em&gt;Uninformed&lt;/em&gt; Volume 3 [@uninformed-v3-archive]. The paper enumerated four architectural bypass classes that would, with minor variations, survive every PatchGuard generation Microsoft has shipped since:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Patch the verifier timer.&lt;/strong&gt; If you control the DPC queue, you can prevent the check from ever firing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hook the verification callback.&lt;/strong&gt; Replace the function pointer the DPC routine is dispatched through.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replace the DPC routine.&lt;/strong&gt; Rewrite the bytes of &lt;code&gt;nt!KiPatchGuardCheckRoutine&lt;/code&gt; itself, before it executes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swallow the bug-check.&lt;/strong&gt; Hook &lt;code&gt;KeBugCheckEx&lt;/code&gt; so that the eventual mismatch call returns to the attacker&apos;s handler instead of crashing the system.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;code&gt;KiInitializePatchGuard&lt;/code&gt; initialization routine itself uses the &quot;scattered initialization&quot; tradition Microsoft inherited from Windows 2000 -- the context block is not allocated as a single contiguous structure but assembled from fragments at randomized offsets, each XOR-keyed against a derived value the verifier alone reconstructs at check time. The fragments are referenced through call-graph paths designed to be inaccessible to a static reader. This is exactly the &lt;em&gt;engineering cost&lt;/em&gt; layer that Skywing&apos;s 2005 paper would later identify as raising the cost of bypass without affecting any structural bypass class.&lt;/p&gt;
&lt;p&gt;The thesis the &lt;em&gt;Uninformed&lt;/em&gt; paper stated in its abstract was the framing Microsoft would not formally adopt in writing for another twelve years: &lt;em&gt;any&lt;/em&gt; defense at the same privilege as the attacker can be subverted in principle, because the attacker can do anything the defense can do -- including reading the obfuscation key and rewriting the check. The argument is structural, not empirical. Skywing&apos;s contribution was not &quot;we broke PatchGuard&quot;; it was &quot;PatchGuard&apos;s class of defense has a fixed structural ceiling, and the ceiling is below &apos;security boundary.&apos;&quot;&lt;/p&gt;

The biographical pattern that ran through this story is unusual and worth naming explicitly. Skape (Matt Miller) later joined Microsoft and became the lead on multiple mitigation features. Skywing (Ken Johnson) later wrote the bylined MSRC blog post that introduced KVA Shadow in 2018 [@ms-kva-shadow-blog]. Andrea Allievi, who reverse-engineered PatchGuard 8.1 at NoSuchCon 2014 [@allievi-nsc2014], later co-authored *Windows Internals 7e Part 2* and the 2020 KDP launch blog [@ms-kdp-blog]. The pattern is not random: the offensive-research community that proved the same-privilege paradox was the same community Microsoft eventually hired to design the cross-privilege answer.
&lt;p&gt;Microsoft did exactly what you would expect a serious engineering organisation to do when an obfuscation layer is partially peeled back: they added another. PatchGuard v2 shipped in 2006 servicing updates and was inherited by Vista x64 in November 2006. It introduced an XOR-encrypted-and-scattered context, decoy DPC routines, a generalised anti-hook framework that flagged modifications to additional kernel function tables, and randomized timer phase. In January 2007 Skywing published &quot;Subverting PatchGuard Version 2&quot; in &lt;em&gt;Uninformed&lt;/em&gt; Volume 6, walking through the v2 hardening in detail and demonstrating that the same four bypass classes survived [@uninformed-v6-archive]. The engineering cost was raised; the structural ceiling was not.&lt;/p&gt;
&lt;p&gt;It is worth seeing the integrity check as a teaching primitive. The real implementation is hardened with anti-disassembly and anti-debugging tricks that we will not reproduce; the underlying &lt;em&gt;control loop&lt;/em&gt; is plain.&lt;/p&gt;
&lt;p&gt;{`
// Conceptual demonstration only -- the real PatchGuard is far more obfuscated
const protectedStructures = {
  SSDT: &apos;eb2f4c1abe007f29d6c910a9c66e0b21&apos;,
  IDT:  &apos;7c4b48a39b22d5f0a1e4ecb0d80b1c2a&apos;,
  GDT:  &apos;0d1f3a72b9aa6d8a14e88f9d22cc66ab&apos;,
  KeBugCheckEx: &apos;6677aabbccdd0011223344556677ff88&apos;,
};
const expected = {...protectedStructures};&lt;/p&gt;
&lt;p&gt;function hashStructure(name) {
  // In real KPP this is a derived hash over current memory contents
  return protectedStructures[name];
}&lt;/p&gt;
&lt;p&gt;function patchguardCheck() {
  for (const name of Object.keys(expected)) {
    if (hashStructure(name) !== expected[name]) {
      // KeBugCheckEx(CRITICAL_STRUCTURE_CORRUPTION, ...)
      console.log(&apos;BUGCHECK 0x109 on&apos;, name);
      return;
    }
  }
  console.log(&apos;All structures intact -- reschedule&apos;);
}&lt;/p&gt;
&lt;p&gt;// Simulate one tick of the verifier
patchguardCheck();&lt;/p&gt;
&lt;p&gt;// Simulate an attacker modifying SSDT
protectedStructures.SSDT = &apos;ffffffffffffffffffffffffffffffff&apos;;
patchguardCheck();
`}&lt;/p&gt;
&lt;p&gt;The toy is honest about the shape: a verifier walks a fixed list, computes a hash, compares against a stored expected value, calls a bug-check on mismatch. Everything Skywing&apos;s bypass classes targeted -- the verifier&apos;s schedule, the verifier&apos;s code, the expected-hash store, the bug-check primitive -- is sitting in the address space the attacker also writes.&lt;/p&gt;
&lt;p&gt;By January 2007, the pattern was set. Microsoft adds an obfuscation layer; Skywing peels it back; Microsoft adds another. Both sides were right. Microsoft was right that the engineering cost mattered: the AV-vendor hooking pattern was being industrially ended, signed third-party kernel drivers were a much narrower entry point than the old free-for-all, and casual rootkit authors were locked out of the bypass class. Skywing was right that engineering cost is not a security boundary. The next decade would prove both.&lt;/p&gt;
&lt;h2&gt;4. The evolution, generation by generation (2008 -- 2016)&lt;/h2&gt;
&lt;p&gt;Twelve years of cat-and-mouse ran on two parallel tracks. PatchGuard added DPC-based checks in v3 (Vista SP1 / Server 2008, February 2008) [@uninformed-v8-archive], HAL function-table verification and stack-context randomisation in Windows 7 -- 8 (2009 -- 2012), and a context-block ring in Windows 8.1 (2013) -- which Andrea Allievi reverse-engineered at NoSuchCon 2014, again finding four independent bypass paths [@allievi-nsc2014]. Meanwhile, two quieter developments laid the groundwork for what was coming: KASLR shipped on Vista x64 in 2007 [@russinovich-vista-part3], and Jurczyk and Coldwind&apos;s Bochspwn project in 2013 falsified the industry&apos;s assumption that win32k LPE bugs were a tail of accidents [@j00ru-bochspwn-blog].&lt;/p&gt;
&lt;h3&gt;The PatchGuard generation ladder&lt;/h3&gt;
&lt;p&gt;Each generation tightened the engineering cost without changing the structural ceiling. The table below summarises the evolution; the right-most column lists the canonical reverse-engineering primary, which in every generation came from outside Microsoft.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Generation&lt;/th&gt;
&lt;th&gt;Year, OS first shipped&lt;/th&gt;
&lt;th&gt;Key delta&lt;/th&gt;
&lt;th&gt;Canonical reverse-engineering primary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;v1&lt;/td&gt;
&lt;td&gt;April 2005, XP x64 / Server 2003 SP1 x64&lt;/td&gt;
&lt;td&gt;Baseline -- single context block, fixed protected-structure list, single DPC&lt;/td&gt;
&lt;td&gt;Skywing &amp;amp; skape, &lt;em&gt;Uninformed&lt;/em&gt; v3, Dec 2005 [@uninformed-v3-archive]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2&lt;/td&gt;
&lt;td&gt;2006 servicing, inherited by Vista x64 Nov 2006&lt;/td&gt;
&lt;td&gt;XOR-encrypted scattered context, decoy DPCs, anti-hook framework&lt;/td&gt;
&lt;td&gt;Skywing, &lt;em&gt;Uninformed&lt;/em&gt; v6, Jan 2007 [@uninformed-v6-archive]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v3&lt;/td&gt;
&lt;td&gt;Vista SP1 / Server 2008, Feb 2008&lt;/td&gt;
&lt;td&gt;Multiple concurrent contexts, randomised timer phase, &lt;code&gt;KeBugCheckEx&lt;/code&gt; self-protection&lt;/td&gt;
&lt;td&gt;Skywing, &lt;em&gt;Uninformed&lt;/em&gt; v8, Sep 2007 [@uninformed-v8-archive]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v7 (Windows 7)&lt;/td&gt;
&lt;td&gt;2009 -- 2010&lt;/td&gt;
&lt;td&gt;HAL function-table verification, stack-context randomisation&lt;/td&gt;
&lt;td&gt;Community RE; no single canonical paper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v8 (Windows 8)&lt;/td&gt;
&lt;td&gt;2012&lt;/td&gt;
&lt;td&gt;&lt;code&gt;KeServiceDescriptorTableShadow&lt;/code&gt; added (now covers win32k syscall table), expanded MSR list&lt;/td&gt;
&lt;td&gt;Community RE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v8.1&lt;/td&gt;
&lt;td&gt;2013 (Windows 8.1)&lt;/td&gt;
&lt;td&gt;Single context block replaced by &lt;strong&gt;context-block ring&lt;/strong&gt;; atomic patching of every block required; 247 protected structures (vs ~26 on Vista x64)&lt;/td&gt;
&lt;td&gt;Andrea Allievi, NoSuchCon 2014 [@allievi-nsc2014]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Allievi&apos;s 2014 talk is the clearest single picture of what hardening looked like by the Windows 8.1 era. The single context block had become a singly-linked list (SLIST) of context blocks. The cryptographic self-integrity check now ran across the SLIST. The protected-structure set had grown from roughly twenty-six on Vista x64 to &lt;strong&gt;two hundred and forty-seven&lt;/strong&gt; by Windows 8.1, including &lt;code&gt;HalPrivateDispatchTable&lt;/code&gt; and &lt;code&gt;HalpInterruptController&lt;/code&gt; [@allievi-nsc2014]. And the four 2006 bypass classes still worked. The engineering cost of bypassing PatchGuard had risen by an order of magnitude; the architectural class of bypass had not changed.&lt;/p&gt;
&lt;h3&gt;KASLR on Vista, February -- April 2007&lt;/h3&gt;
&lt;p&gt;In parallel with the PatchGuard generation ladder, Microsoft shipped a different style of defense on the same kernel. Mark Russinovich&apos;s three-part &lt;em&gt;Inside the Windows Vista Kernel&lt;/em&gt; series in TechNet Magazine documented the new mitigation in April 2007 [@russinovich-vista-part3]: the kernel image base, instead of being constant, was selected at boot from a small space of possible offsets.&lt;/p&gt;

Randomising the kernel image base across boots, so that an attacker with a stale or guessed kernel address cannot use it as an absolute reference. On Vista x64 the implementation had roughly eight bits of entropy (256 possible kernel base addresses), selected at boot time by `winload.exe` [@russinovich-vista-part3]. The mitigation is *probabilistic* by construction: it raises the cost of an unprivileged information-leak, but cannot survive a deterministic side-channel attacker.
&lt;p&gt;The Vista bootloader, &lt;code&gt;winload.exe&lt;/code&gt;, was the component that picked the kernel image base at boot. The choice of selecting the offset early -- before the kernel proper executes -- was deliberate; KASLR after the kernel is mapped is harder to do because every kernel pointer recorded so far becomes invalid. The Vista bootloader was also the component PatchGuard&apos;s protected list depended on: an attacker with bootloader code execution simply chose their own offset.&lt;/p&gt;
&lt;p&gt;The probabilistic framing held until 2013. Hund, Willems, and Holz published &quot;Practical Timing Side Channel Attacks Against Kernel Space ASLR&quot; at IEEE S&amp;amp;P 2013 [@doi-hund-2013]. Their technique exploited the shared TLB and cache state between user mode and kernel mode on every x86 / x64 CPU then shipping: an unprivileged user-mode timer could measure differential cache behaviour when accessing addresses near where the kernel mapped its image, and recover the kernel base in seconds. Eight bits of entropy collapse fast under a side-channel that gives you one bit per probe. Gruss et al. generalised the argument in 2017 with a paper whose title was the thesis: &lt;em&gt;&quot;KASLR is Dead: Long Live KASLR&quot;&lt;/em&gt; [@gruss-kaiser-pdf]. The structural answer would have to be something other than entropy.&lt;/p&gt;
&lt;h3&gt;The 2012 Windows 8 attempt at attack-surface deletion&lt;/h3&gt;
&lt;p&gt;While KASLR&apos;s structural limits were being demonstrated in academia, Microsoft shipped a different style of mitigation in Windows 8: &lt;code&gt;DisallowWin32kSystemCalls&lt;/code&gt;, a process-level option enabling the kernel to refuse &lt;em&gt;every&lt;/em&gt; win32k system call from a process that opted in [@ms-syscall-disable-policy]. The semantics are all-or-nothing: a process either can call into &lt;code&gt;win32k.sys&lt;/code&gt; or it cannot. Useful for non-UI broker processes (where the answer is &quot;never&quot;). Structurally inadequate for browser renderers, which need to draw windows, render fonts, and dispatch input through a constrained-but-non-empty subset of the win32k surface. The mitigation languished for five years, waiting for the per-syscall version that arrived in 2017.&lt;/p&gt;
&lt;h3&gt;The Bochspwn empirical surprise&lt;/h3&gt;
&lt;p&gt;In 2013, Mateusz Jurczyk and Gynvael Coldwind presented Bochspwn at SyScan and at Black Hat USA [@j00ru-bochspwn-blog] [@j00ru-bhusa-pdf]. The methodology was a Bochs x86 emulator instrumented to trace every memory access made by the kernel during syscall handling. The instrumentation found classes of bugs -- specifically &lt;em&gt;double-fetch&lt;/em&gt; bugs, where the kernel reads the same user-controlled memory twice without re-validating between reads -- by tagging each user-pointer dereference and looking for repeats.&lt;/p&gt;

A double-fetch happens when kernel code reads a value from user-mode memory, validates it, and later reads the *same* address again expecting the value to be unchanged. A racing user-mode thread can flip the value between the two reads, defeating the validation. Detecting double-fetches statically is hard; detecting them by static analysis on a closed-source kernel is harder still. Bochspwn solved the detection problem at the emulator level: instrument the entire kernel under Bochs, log every memory read of every page table mapped writable from user mode, and post-process the trace for &quot;same address, same kernel function, two reads, no intervening synchronisation.&quot; The result: dozens of exploitable kernel race conditions across multiple Windows versions, the *majority* in `win32k.sys` [@j00ru-bochspwn-blog]. The win32k bug class was systemic, not accidental.
&lt;p&gt;Jurczyk&apos;s empirical finding mattered because it pre-dated the design of the eventual lockdown by four years. The community knew, by mid-2013, that &lt;em&gt;win32k.sys was a bug class, not a bug tail&lt;/em&gt;. Microsoft&apos;s eventual answer -- per-process filtering of the win32k syscall surface -- had a clean empirical motivation by the time it shipped.&lt;/p&gt;
&lt;p&gt;The pre-Bochspwn high-profile example was already in the literature: Bruce Dang and Peter Ferrie&apos;s December 2010 talk at the 27th Chaos Communication Congress (&quot;Adventures in Analyzing Stuxnet&quot;) had named CVE-2010-2743, a &lt;code&gt;win32k.sys&lt;/code&gt; &lt;code&gt;NtUserLoadKeyboardLayoutEx&lt;/code&gt; LPE that Stuxnet used to escalate from user to kernel on Windows XP [@nvd-cve-2010-2743]. Stuxnet placed one of the most consequential kernel-level malware operations on record on top of a single win32k vulnerability. Bochspwn explained why: the surface was structurally vulnerable, not accidentally so.&lt;/p&gt;
&lt;h3&gt;The intellectual surprise of this act -- Uroburos coexisted with PatchGuard&lt;/h3&gt;
&lt;p&gt;The cleanest demonstration that the same-privilege paradox is empirical, not theoretical, came in February 2014. G Data SecurityLabs published its analysis of Uroburos, a Russian-attributed espionage rootkit that had been operating in production for an estimated three years [@gdata-uroburos-blog]. Uroburos did not bypass PatchGuard. It loaded a copy of Oracle&apos;s &lt;code&gt;VBoxDrv.sys&lt;/code&gt; (a signed third-party driver shipped as part of VirtualBox), used a privilege-escalation vulnerability in that driver to flip the &lt;code&gt;g_CiEnabled&lt;/code&gt; flag (the gate for Driver Signature Enforcement), loaded its own unsigned rootkit driver, and then operated for three years in production &lt;em&gt;without ever modifying anything PatchGuard checked&lt;/em&gt; [@stmxcsr-turla].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The most-repeated misreading of PatchGuard&apos;s track record is &quot;Uroburos was a PatchGuard bypass.&quot; It was not. Uroburos was a Driver Signature Enforcement (DSE) bypass that operated &lt;em&gt;alongside&lt;/em&gt; PatchGuard for three years (2011 -- 2014) without modifying any PatchGuard-protected structure [@gdata-uroburos-blog] [@stmxcsr-turla]. The lesson is structural: PatchGuard&apos;s protected-structure list is, by construction, narrower than the kernel-modification surface, and a disciplined attacker simply stays outside the list. The corollary -- that no in-kernel integrity monitor can be wider than its protected-structure list, and any list narrower than &quot;all kernel memory&quot; leaves gaps -- is the empirical anchor for the same-privilege paradox.&lt;/p&gt;
&lt;/blockquote&gt;

The policy on 64-bit Windows that the kernel will load only Authenticode-signed drivers in production. DSE is gated by an in-memory flag (`nt!g_CiEnabled` historically, `nt!g_CiOptions` on later builds). An attacker with arbitrary kernel write can flip the flag and load unsigned drivers -- which is precisely how the BYOVD attack pattern works [@gdata-uroburos-blog] [@hfiref0x-upgdsed].
&lt;p&gt;Three insights converged from this act. From the side-channel KASLR literature: some defenses cannot succeed at CPL=0 because the &lt;em&gt;attack&lt;/em&gt; is below the operating system. From Allievi 2014 and Uroburos 2011 -- 2014: same-privilege obfuscation is permanently bounded by engineering cost, no matter how much engineering cost you pay. From Bochspwn: win32k is not a bug tail but a bug class -- the only structural answer is to delete the surface rather than defend it. The 2017 calendar year was about to land all three answers at once.&lt;/p&gt;
&lt;h2&gt;5. 2017&apos;s triple inflection&lt;/h2&gt;
&lt;p&gt;In a single calendar year, three mutually independent breakthroughs reshaped kernel self-defense. June 2017: CyberArk&apos;s Kasif Dekel published GhostHook, an Intel-PT-based PatchGuard bypass that forced Microsoft&apos;s first public statement that PatchGuard is not a security boundary [@cyberark-ghosthook]. July 2017: Gruss et al. published &quot;KASLR is Dead: Long Live KASLR&quot; at ESSoS, proposing kernel page-table isolation as the structural answer [@gruss-kaiser-pdf]. October 2017: Windows 10 1709 shipped &lt;code&gt;Win32kSystemCallFilter&lt;/code&gt;, the per-process, per-syscall allow-list designed for the Chrome and Edge renderer sandboxes [@ms-syscall-filter-policy]. Three teams, three mitigations, three facets of the same paradox.&lt;/p&gt;
&lt;h3&gt;Win32kSystemCallFilter (October 17, 2017)&lt;/h3&gt;
&lt;p&gt;The Windows 8 mitigation &lt;code&gt;DisallowWin32kSystemCalls&lt;/code&gt; had been the right idea applied as a meat-axe: an opted-in process loses access to &lt;em&gt;every&lt;/em&gt; win32k system call. Windows 10 1709 introduced the surgical version. &lt;code&gt;PROCESS_MITIGATION_SYSTEM_CALL_FILTER_POLICY&lt;/code&gt; registers a per-process bitmap of system-defined &lt;code&gt;FilterId&lt;/code&gt; values that the process is allowed to call; everything outside the bitmap is denied [@ms-syscall-filter-policy]. The filter is applied via &lt;code&gt;UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, ...)&lt;/code&gt; at &lt;code&gt;CreateProcess&lt;/code&gt; time -- not at runtime.&lt;/p&gt;

A Windows 10 1709+ process-mitigation policy (`PROCESS_MITIGATION_SYSTEM_CALL_FILTER_POLICY`, header `ntddk.h`) that registers a per-process bitmap of allowed system-defined `FilterId` values for win32k system calls. Calls outside the bitmap terminate the calling process. Used by the Chromium sandbox to constrain the win32k surface available to a renderer process [@ms-syscall-filter-policy] [@chromium-sandbox-doc].
&lt;p&gt;The &quot;at CreateProcess time, not at runtime&quot; detail is load-bearing. James Forshaw and Ivan Fratric&apos;s November 2016 Project Zero post &quot;Breaking the Chain&quot; documented how Edge&apos;s window-broker architecture, which applied syscall restrictions to a child process &lt;em&gt;after&lt;/em&gt; it had started, was subject to a window-of-opportunity race between the child&apos;s earliest syscall and the broker&apos;s policy application [@pz-breaking-chain]. If the policy is not in place by the time the first attacker-controlled syscall fires, the policy has not happened. The lesson the Windows 10 1709 design banked: mitigations belong on the &lt;code&gt;CreateProcess&lt;/code&gt; boundary, not on a later thread.&lt;/p&gt;

sequenceDiagram
    participant R as Renderer process (VTL0 user)
    participant SD as Syscall dispatcher (kernel)
    participant W as win32k handler
    participant EP as EPROCESS filter bitmap
    R-&amp;gt;&amp;gt;SD: NtUser/NtGdi syscall with FilterId N
    SD-&amp;gt;&amp;gt;EP: Consult per-process filter bitmap
    EP--&amp;gt;&amp;gt;SD: bit N set or unset
    alt FilterId allowed
        SD-&amp;gt;&amp;gt;W: Dispatch to win32k handler
        W--&amp;gt;&amp;gt;R: Return result
    else FilterId denied
        SD-&amp;gt;&amp;gt;R: Terminate process via fast-fail
    end
&lt;p&gt;The Forshaw / Fratric Edge race is a textbook case of why &quot;apply at runtime&quot; is a security anti-pattern for process mitigations. The Microsoft Edge of late 2016 used a sandbox model in which a renderer process started with limited restrictions and then upgraded itself to the full lockdown profile after initialisation. Forshaw and Fratric showed that an attacker who landed code execution before the upgrade completed -- a window of milliseconds -- could simply not upgrade. The lesson generalises beyond Edge: every per-process mitigation in modern Windows is applied at process creation time precisely so there is no window the attacker can race [@pz-breaking-chain].&lt;/p&gt;
&lt;p&gt;The cleanest way to see the two-mitigation contrast is side by side:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;&lt;code&gt;DisallowWin32kSystemCalls&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Win32kSystemCallFilter&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Chromium&apos;s actual choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;First shipped&lt;/td&gt;
&lt;td&gt;Windows 8, 2012 [@ms-syscall-disable-policy]&lt;/td&gt;
&lt;td&gt;Windows 10 1709, October 2017 [@ms-syscall-filter-policy]&lt;/td&gt;
&lt;td&gt;Both, in different process types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Granularity&lt;/td&gt;
&lt;td&gt;All-or-nothing&lt;/td&gt;
&lt;td&gt;Per-syscall allow-list&lt;/td&gt;
&lt;td&gt;Blanket-disable for non-UI; per-syscall for renderer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mitigation policy struct&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PROCESS_MITIGATION_SYSTEM_CALL_DISABLE_POLICY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PROCESS_MITIGATION_SYSTEM_CALL_FILTER_POLICY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Composes both with LPAC privilege reduction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use case&lt;/td&gt;
&lt;td&gt;Non-UI broker processes (GPU broker, network process)&lt;/td&gt;
&lt;td&gt;Renderer processes that draw windows&lt;/td&gt;
&lt;td&gt;The renderer needs a constrained-but-non-zero win32k subset [@chromium-sandbox-doc]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The Chromium sandbox composes the two mitigations with one more: the &lt;strong&gt;Less Privileged AppContainer&lt;/strong&gt; (LPAC). LPAC removes ambient access to user data, the network, and most named-object namespaces; &lt;code&gt;Win32kSystemCallFilter&lt;/code&gt; removes the syscall surface; &lt;code&gt;DisallowWin32kSystemCalls&lt;/code&gt; applies to processes that need no UI at all. Defense in depth at the surface level rather than the structural level.&lt;/p&gt;

A Windows AppContainer variant introduced in Windows 10 that further restricts the ambient capabilities available to the contained process -- no access to user files, no access to most named objects, restricted ability to enumerate the system. Combined with `Win32kSystemCallFilter`, LPAC gives the Chromium renderer a process model in which both *what the renderer can ask the kernel to do* and *what the renderer can see in user mode* are deliberately narrow [@chromium-sandbox-doc].
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;Win32kSystemCallFilter&lt;/code&gt; is the first mitigation in the 21-year arc that &lt;em&gt;deletes&lt;/em&gt; attack surface rather than defending it. PatchGuard and KASLR are kernel defenses: they live inside the kernel and protect kernel state. The win32k filter is a process-mitigation policy enforced by the kernel&apos;s system-call dispatcher at the syscall boundary. The protection is realised by &lt;em&gt;not letting the kernel be called&lt;/em&gt; rather than by checking the kernel&apos;s state afterwards. Once you see this shape, the rest of the modern Windows mitigation stack -- KDP, kCFG-with-VBS-bitmap, kCET -- becomes legible as variations on the same move: put the enforcement outside the attacker&apos;s reach.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;KAISER and the page-table split&lt;/h3&gt;
&lt;p&gt;In July 2017, Gruss et al. presented &quot;KASLR is Dead: Long Live KASLR&quot; at ESSoS [@gruss-kaiser-pdf]. The acronym was &lt;strong&gt;KAISER&lt;/strong&gt; -- Kernel Address Isolation to have Side-channels Efficiently Removed. The architecture is simple to describe, hard to engineer, and devastating to a side-channel attacker.&lt;/p&gt;
&lt;p&gt;A modern x64 kernel runs in the same virtual address space as the calling user process, distinguished by privilege bits in page-table entries. A syscall does not change the page tables; it only changes the privilege level. The TLB is therefore shared between user and kernel mappings, and side-channel attacks like Hund 2013 work by timing the resulting cache and TLB behaviour. KAISER&apos;s answer was to give each process &lt;em&gt;two&lt;/em&gt; sets of page tables: a &quot;user&quot; CR3 in which the kernel address space is &lt;em&gt;not mapped&lt;/em&gt;, and a &quot;kernel&quot; CR3 in which the full virtual address space is mapped. The syscall entry path switches from user CR3 to kernel CR3; the sysret path switches back. The kernel address space is not just unknown to a user-mode attacker -- it is structurally unreachable.&lt;/p&gt;

A design proposed by Gruss et al. (KAISER, ESSoS 2017) [@gruss-kaiser-pdf] in which each process has two page-table hierarchies: a user CR3 that does not map the kernel and a kernel CR3 that maps both. CR3 is switched on every syscall entry and exit. The kernel is no longer just *hard to find* (the KASLR posture); it is *unreachable* from user CR3 (the structural posture). Linux shipped KAISER as KPTI in early 2018; Microsoft shipped a re-engineered variant as KVA Shadow [@ms-kva-shadow-blog].

sequenceDiagram
    participant U as User-mode thread
    participant CPU as CPU CR3
    participant K as Kernel
    U-&amp;gt;&amp;gt;CPU: syscall (SYSCALL instruction)
    CPU-&amp;gt;&amp;gt;CPU: Switch CR3 from user to kernel
    CPU-&amp;gt;&amp;gt;K: Kernel now mapped, enter system service
    K-&amp;gt;&amp;gt;K: Handle request
    K-&amp;gt;&amp;gt;CPU: SYSRET
    CPU-&amp;gt;&amp;gt;CPU: Switch CR3 back to user
    CPU-&amp;gt;&amp;gt;U: Return to user mode, kernel unmapped
&lt;p&gt;The Gruss paper landed six months before anyone knew why it mattered. Then, on January 3, 2018, Jann Horn published &quot;Reading privileged memory with a side-channel&quot; on Project Zero [@pz-meltdown-post], the same day the academic teams (Lipp et al., independently) published the Meltdown disclosure [@usenix-lipp-meltdown]. Meltdown -- CVE-2017-5754, &quot;rogue data cache load&quot; -- exploited transient out-of-order execution on Intel CPUs to read kernel memory from user mode. The only structural fix was to ensure the kernel pages were not present in the user-mode page table. KAISER&apos;s design, drafted as a generic side-channel countermeasure, was suddenly Meltdown&apos;s required mitigation.&lt;/p&gt;
&lt;h3&gt;GhostHook and the formal admission&lt;/h3&gt;
&lt;p&gt;In June 2017, Kasif Dekel published GhostHook [@cyberark-ghosthook]. The mechanism is elegant. Intel Processor Trace (Intel PT) is a CPU feature for low-overhead recording of control flow, designed for performance analysis and debugging. The trace is written to a Table of Physical Addresses (ToPA), and when a configured ToPA region fills, the CPU raises a performance-monitoring interrupt (PMI). The OS&apos;s PMI handler is a function pointer. PMI handlers run in kernel mode, with full kernel privilege. GhostHook configured Intel PT with a tiny ToPA covering an address near &lt;code&gt;IA32_LSTAR&lt;/code&gt; (the syscall entry MSR), arranged for the buffer to fill immediately, and registered an attacker-controlled PMI handler. Every kernel transition fired the PMI; the attacker&apos;s handler ran first. PatchGuard does not enumerate Intel PT. By design.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s response, as reported in the CyberArk write-up, was the formal end of an eleven-year ambiguity. PatchGuard is &quot;considered an in-depth security feature&quot; but not a security boundary; the GhostHook bypass would &quot;be considered for a future version of Windows&quot; but did not warrant an out-of-band fix [@cyberark-ghosthook]. The Microsoft position aligns with the Security Servicing Criteria: admin-to-kernel is not a security boundary, and an attacker who has already reached kernel mode (the precondition for installing a GhostHook-style PMI handler) is outside the scope of what PatchGuard exists to prevent [@ms-servicing-criteria].&lt;/p&gt;

While the technique was found to bypass PatchGuard, Microsoft has graciously agreed to consider [the issue] for a future version of Windows. As such, no immediate risk exists for customers. -- Microsoft response to GhostHook, June 2017 [@cyberark-ghosthook].
&lt;p&gt;The three breakthroughs of 2017 were structurally aligned. &lt;code&gt;Win32kSystemCallFilter&lt;/code&gt; deleted the most-vulnerable syscall surface from sandboxed renderers. KAISER&apos;s page-table split made KASLR&apos;s probabilistic defense obsolete and structurally unreachable. GhostHook forced the public admission that the same-privilege class of defense has a ceiling Microsoft already knew about. And then, on the morning of January 3, 2018, the academic paper of six months earlier became an emergency engineering deliverable.&lt;/p&gt;
&lt;h2&gt;6. State of the art: KDP, KVA Shadow, kCFG, kCET, and the Secure Kernel shift (2018 -- 2026)&lt;/h2&gt;
&lt;p&gt;January 3, 2018: Meltdown&apos;s public disclosure forces every major operating system to ship page-table isolation within weeks [@pz-meltdown-post]. Microsoft&apos;s response, &lt;strong&gt;KVA Shadow&lt;/strong&gt;, ships in the Windows 10 1709 cumulative security update the same day. The engineering write-up is bylined to Ken Johnson of the Microsoft Security Response Center [@ms-kva-shadow-blog]. The same Ken Johnson who, twelve years earlier, co-authored &lt;em&gt;Bypassing PatchGuard on Windows x64&lt;/em&gt; under the name Skywing [@uninformed-v3-archive]. The offensive-research outsider had become the bylined Microsoft defender. The same loop was about to close on the architectural question: &lt;em&gt;where, exactly, does the defense live?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The Ken Johnson / Skywing trajectory -- offensive Uninformed paper in 2005, the bylined MSRC blog post in 2018, twelve years later -- is the cleanest single illustration of the offensive-research-to-Microsoft pattern. He is engineering credit attributed to Ken Johnson on the MSRC byline; the offensive identity is widely known but not asserted by Microsoft. Either reading of the byline is valid; the structural point is that the same person whose 2005 paper identified the architectural ceiling of CPL=0 obfuscation later shipped the cross-privilege answer for Meltdown [@uninformed-v3-archive] [@ms-kva-shadow-blog].&lt;/p&gt;
&lt;h3&gt;KVA Shadow: the productisation of KAISER&lt;/h3&gt;
&lt;p&gt;KVA Shadow is the Windows productisation of KAISER. Two CR3-loadable page tables per process: a user-mode shadow that does not map most of the kernel, and a kernel-mode page table that does. CR3 is switched on every syscall entry and exit. The kernel address space is unmapped from user CR3 [@ms-kva-shadow-blog]. The structural Meltdown fix is exact: a Meltdown-class transient read of a kernel address from user mode now hits an unmapped page-table entry and raises a fault before any cached side-channel evidence is produced.&lt;/p&gt;
&lt;p&gt;Two things to be precise about. First, KVA Shadow addresses &lt;strong&gt;Variant 3&lt;/strong&gt; (Meltdown, CVE-2017-5754) only. Spectre Variant 1 (CVE-2017-5753), Variant 2 (CVE-2017-5715), and Variant 4 (Speculative Store Bypass) require their own mitigations (microcode updates, retpoline, IBRS / IBPB, SSBD); KVA Shadow does nothing for them [@usenix-lipp-meltdown]. Second, the performance cost of the CR3-switch on every syscall is real -- Fortinet&apos;s analysis of the KVA Shadow build measured significant slowdowns for syscall-heavy workloads, mitigated on newer CPUs by Process-Context Identifiers (PCID) that keep TLB entries valid across CR3 switches [@fortinet-kva-shadow].&lt;/p&gt;
&lt;h3&gt;HVCI: the VTL1 enabler&lt;/h3&gt;
&lt;p&gt;Hypervisor-Protected Code Integrity (HVCI) is not, strictly, a kernel defense -- it is the foundation everything else in the modern stack stands on. HVCI uses Virtualization-Based Security (VBS) to run a small Secure Kernel in Virtual Trust Level 1 (VTL1), one privilege level above the NT kernel in VTL0. The Secure Kernel manages the Second-Level Address Translation (SLAT) page tables -- Intel EPT or AMD NPT -- that mediate physical memory access for the NT kernel. With HVCI on, kernel pages are managed W^X (writable XOR executable): a kernel-mode driver attempting to make a writable page executable triggers a SLAT fault that VTL1 catches.&lt;/p&gt;

A Windows architecture in which the hypervisor partitions the system into two Virtual Trust Levels. VTL0 hosts the normal NT kernel, drivers, and user-mode processes. VTL1 hosts a Secure Kernel and a small set of trustlets that enforce policy on VTL0. Cross-VTL transitions are mediated by the hypervisor; a VTL0 kernel-mode attacker cannot reach VTL1, even with arbitrary kernel write. VBS is the architectural primitive that makes HVCI, KDP, and kCFG-with-VBS-bitmap possible [@ms-kdp-blog].
&lt;p&gt;For this article HVCI is the cross-cutting dependency: it is what makes KDP and the VBS-protected kCFG bitmap work. Once you have a hypervisor enforcing SLAT on the NT kernel, every defense you want to anchor &lt;em&gt;outside&lt;/em&gt; the NT kernel has a home.&lt;/p&gt;
&lt;h3&gt;KDP: static and dynamic kernel data protection&lt;/h3&gt;
&lt;p&gt;Microsoft announced Kernel Data Protection on July 8, 2020, with Windows 10 version 2004 [@ms-kdp-blog]. Two flavours.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Static KDP&lt;/strong&gt; uses the &lt;code&gt;MmProtectDriverSection&lt;/code&gt; API, called from &lt;code&gt;DriverEntry&lt;/code&gt;, to mark a section of the driver&apos;s image as read-only for the rest of the kernel&apos;s lifetime. The intended use is for tables of policy data the driver expects never to modify after initialisation: function-pointer arrays, configuration constants, signed policy blobs. Once &lt;code&gt;MmProtectDriverSection&lt;/code&gt; returns, the section&apos;s pages are tagged read-only in the VTL1-managed SLAT; a VTL0 kernel-mode attempt to write them takes a hardware page fault that VTL0 has no way to relax.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dynamic KDP&lt;/strong&gt; is for runtime-allocated state. The canonical API is &lt;code&gt;ExAllocatePool3&lt;/code&gt;, called with a &lt;code&gt;POOL_EXTENDED_PARAMETER&lt;/code&gt; array containing a &lt;code&gt;POOL_EXTENDED_PARAMS_SECURE_POOL&lt;/code&gt; extended parameter [@ms-kdp-blog]. The flags &lt;code&gt;SECURE_POOL_FLAGS_FREEABLE&lt;/code&gt; (1) and &lt;code&gt;SECURE_POOL_FLAG_MODIFIABLE&lt;/code&gt; (2) control whether the allocation can later be freed and whether further protected modifications are permitted. The secure-pool extension routes the allocation through the Secure Kernel; the resulting memory is verified by VTL1 and protected by SLAT.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; KDP does &lt;em&gt;not&lt;/em&gt; automatically protect &quot;all kernel memory.&quot; It protects exactly the memory a driver author opts in to protect via &lt;code&gt;MmProtectDriverSection&lt;/code&gt; (static) or &lt;code&gt;ExAllocatePool3&lt;/code&gt; with the secure-pool extension (dynamic) [@ms-kdp-blog]. Memory allocated through the normal &lt;code&gt;ExAllocatePool2&lt;/code&gt; path is &lt;em&gt;not&lt;/em&gt; KDP-protected. A defender architecting around KDP must explicitly opt the data they care about into the secure pool; the protection is targeted, not blanket.&lt;/p&gt;
&lt;/blockquote&gt;

A Microsoft kernel-memory protection introduced with Windows 10 version 2004 (July 2020) that allows drivers to mark sections of kernel memory as read-only and have the protection enforced by the Secure Kernel in VTL1 via the SLAT page tables. Static KDP uses `MmProtectDriverSection`; Dynamic KDP uses `ExAllocatePool3` with a `POOL_EXTENDED_PARAMS_SECURE_POOL` extended parameter passed via `POOL_EXTENDED_PARAMETER`. The enforcement lives at a privilege level the VTL0 attacker cannot reach [@ms-kdp-blog].
&lt;p&gt;The Microsoft launch blog makes the architectural point in one sentence: &lt;em&gt;&quot;the memory managed by KDP is always verified by the secure kernel (VTL1) and protected using SLAT tables by the hypervisor&quot;&lt;/em&gt; [@ms-kdp-blog]. This is the first kernel self-defense mitigation in the Windows lineage whose enforcement is &lt;em&gt;structurally&lt;/em&gt; outside the NT kernel. A VTL0 attacker with arbitrary kernel write &lt;em&gt;cannot&lt;/em&gt; relax the SLAT entry that protects a KDP-tagged page, because the SLAT entry is managed by VTL1, and VTL1 is not in VTL0&apos;s address space.&lt;/p&gt;

flowchart TD
    A[VTL0 NT kernel plus attacker driver] --&amp;gt;|attempt write to KDP-protected page| B[CPU memory access]
    B --&amp;gt; C[SLAT page table consulted]
    C --&amp;gt; D{SLAT entry writable for VTL0}
    D -- no, RO by VTL1 --&amp;gt; E[Hardware EPT or NPT fault]
    D -- yes --&amp;gt; F[Write succeeds]
    E --&amp;gt; G[Secure Kernel in VTL1 receives fault]
    G --&amp;gt; H[VTL0 attacker has no path to relax SLAT entry]
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The canonical pre-boot PatchGuard bypass, EfiGuard, is a UEFI bootkit that patches the loaded kernel image to disable PatchGuard and DSE before the kernel runs [@mattiwatti-efiguard]. It works precisely because PatchGuard, DSE, and the kernel image all live in VTL0 -- a pre-boot agent has the same architectural reach. But once the system boots into a VBS-enabled configuration, the SLAT enforcement lives in VTL1, and the launching firmware does &lt;em&gt;not&lt;/em&gt; have VTL1&apos;s privileges. The same attacker that defeats PatchGuard at the kernel level cannot defeat HVCI from the same vantage. This is the cleanest cross-mitigation demonstration that the architectural-layer choice -- &quot;which privilege level does the defense live at?&quot; -- is the load-bearing variable.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;kCFG: forward-edge integrity&lt;/h3&gt;
&lt;p&gt;Control Flow Guard (CFG) is Microsoft&apos;s compiler-assisted forward-edge CFI. Every indirect call is replaced by a check against a bitmap of valid call targets; an invalid target raises a fast-fail [@ms-cfg]. The kernel variant -- &lt;strong&gt;kCFG&lt;/strong&gt; -- is enabled by &lt;code&gt;/guard:cf&lt;/code&gt; and protects indirect calls in &lt;code&gt;ntoskrnl&lt;/code&gt; and CFG-compiled drivers. With HVCI on, the CFG bitmap is stored in VTL1-protected memory; a VTL0 attacker who can write arbitrary kernel pages still cannot tamper with the bitmap. kCFG defeats jump-oriented and call-oriented programming (JOP / COP) against the forward edge. It does nothing for the backward edge.&lt;/p&gt;
&lt;h3&gt;kCET: backward-edge integrity in hardware&lt;/h3&gt;
&lt;p&gt;Kernel-mode hardware-enforced stack protection (informally &lt;strong&gt;kCET&lt;/strong&gt;, formally documented as &quot;Kernel Mode Hardware-enforced Stack Protection&quot;) closes the backward edge using the Intel CET and AMD Shadow Stack hardware features [@ms-kernel-mode-hsp]. A CPU-maintained shadow stack records every &lt;code&gt;CALL&lt;/code&gt; return address; every &lt;code&gt;RET&lt;/code&gt; validates the popped address against the shadow stack and fast-fails on mismatch. The shadow-stack pages are marked Shadow Stack in the kernel-mode PTE, which the CPU enforces directly; with VBS on, the Secure Kernel additionally locks the shadow-stack mappings against VTL0 write.&lt;/p&gt;
&lt;p&gt;kCET requires Intel 11th-generation Tiger Lake or later, or AMD Zen 3 or later, plus VBS and HVCI [@ms-kernel-mode-hsp]. It is off-by-default on Windows Server 2025 because enabling it system-wide requires every loaded driver to be compiled with the &lt;code&gt;/CETCOMPAT&lt;/code&gt; flag; a single non-&lt;code&gt;/CETCOMPAT&lt;/code&gt; driver disables kCET for the entire system at load time. As of June 2026, the rollout is gated on driver vendor adoption.&lt;/p&gt;
&lt;p&gt;An adjacent technique worth knowing about by name is &lt;strong&gt;eXtended Flow Guard (XFG)&lt;/strong&gt;. XFG augmented kCFG&apos;s bitmap-membership check with a per-function type-derived 64-bit hash compared at the call site -- a defense that detects not just &quot;is this target valid?&quot; but &quot;is this target the &lt;em&gt;right&lt;/em&gt; target for this call&apos;s signature?&quot; XFG was prototyped in MSVC and partially shipped on Windows 10 Insider builds, but the instrumentation never reached full inbox-kernel coverage and the feature is no longer Microsoft&apos;s strategic investment direction. The shipping equivalent on 2026 hardware is kCET for the backward edge plus kCFG for the forward edge.&lt;/p&gt;
&lt;p&gt;Connor McGarr&apos;s Black Hat USA 2025 deck, &quot;Out of Control: KCFG and KCET,&quot; documents the 2026 frontier of kCET bypasses -- an &lt;code&gt;iretq&lt;/code&gt;-frame corruption combined with a write-what-where primitive can pivot around the shadow stack [@mcgarr-bh25-blackhat] [@mcgarr-km-shadow] [@mcgarr-github]. The bypass requires the attacker to already control a kernel-mode write primitive and several CFG-clean targets, which is exactly the precondition KDP, kCFG, and HVCI are designed to make hard.&lt;/p&gt;
&lt;h3&gt;ARM64 Pointer Authentication&lt;/h3&gt;
&lt;p&gt;The recurring framing of PatchGuard as &quot;x64-only&quot; is documentation-accurate but deployment-incomplete. In 2026, PatchGuard, kCFG, and Pointer Authentication Codes (PAC) ship on 64-bit ARM Windows as well as x64. PAC is an ARMv8.3-A feature in which a tag computed over a pointer value and a per-process key is stored in the unused high bits of the pointer; the CPU validates the tag on dereference. PAC closes a different class of pointer-corruption attacks than kCFG/kCET. The structural point is that the kernel self-defense investment is fully cross-architecture, not x64-only.&lt;/p&gt;
&lt;h3&gt;The Microsoft Vulnerable Driver Blocklist&lt;/h3&gt;
&lt;p&gt;The reactive answer to BYOVD is the &lt;strong&gt;Microsoft Recommended Driver Block Rules&lt;/strong&gt; -- a list of known-vulnerable signed third-party drivers that Windows refuses to load when App Control for Business (formerly WDAC) is enabled [@ms-driver-block-rules]. The list is default-on with Memory Integrity, Smart App Control, and S-mode since Windows 11 22H2 and is updated through Windows Update. Verification on a modern system: &lt;code&gt;CiTool --list-policies&lt;/code&gt; and look for a policy whose friendly name is &lt;code&gt;Microsoft Windows Driver Policy&lt;/code&gt; and &lt;code&gt;Is Currently Enforced: true&lt;/code&gt;. The blocklist is the structural answer to the Uroburos pattern -- Microsoft cannot prevent any signed third-party driver from having a write-primitive bug, but they can refuse to load specific drivers known to have shipped such bugs.&lt;/p&gt;

The attack pattern in which an attacker, having reached administrator privilege, installs a *legitimate* signed third-party kernel driver known to contain a privilege-escalation vulnerability, then exploits that vulnerability to obtain arbitrary kernel-mode primitives. The Uroburos VBoxDrv abuse [@gdata-uroburos-blog] is the canonical 2011 example; the Microsoft Recommended Driver Block Rules are the 2024+ reactive answer [@ms-driver-block-rules].
&lt;h3&gt;Synthesis&lt;/h3&gt;
&lt;p&gt;By 2026, the Windows kernel self-defense stack is no longer a single mitigation; it is a &lt;em&gt;stack&lt;/em&gt; organised by where the defense actually runs. The 21-year trajectory now resolves into a single thesis: every generation has been a partial answer to the same-privilege paradox, and Microsoft&apos;s strategy has progressively migrated the defense out of the kernel -- first into instruction-level obfuscation, then into address-space tricks, then into VBS-anchored isolation, and finally into attack-surface deletion. Before we name that thesis formally, it is worth asking: what did the rest of the industry do?&lt;/p&gt;
&lt;h2&gt;7. What the rest of the industry did differently&lt;/h2&gt;
&lt;p&gt;The Microsoft answer to the same-privilege paradox -- twenty-one years of compounding investment in same-privilege deterrents while progressively shifting enforcement to VTL1 -- is not the only answer. Apple and the Linux mainline community took architecturally opposite paths, each correct for a different platform constraint.&lt;/p&gt;
&lt;h3&gt;Apple: push the defense into silicon&lt;/h3&gt;
&lt;p&gt;Apple&apos;s answer was to put enforcement &lt;em&gt;below&lt;/em&gt; the kernel, into hardware Apple controls end-to-end. On Apple Silicon, the Kernel Text Read-only Region (KTRR) is hardware-enforced via the AMCC (Apple Memory Cache Controller). At boot, after the kernel is mapped and before user code runs, the kernel text region is locked read-only at the memory-controller level. Once locked, no software running at &lt;em&gt;any&lt;/em&gt; privilege level can modify it -- not the kernel itself, not a kernel extension, not a hypothetical EL2 hypervisor [@siguza-ktrr].&lt;/p&gt;

Apple Silicon&apos;s hardware-enforced read-only kernel text region. After boot, the kernel image is locked via the AMCC memory controller; no software at any privilege level can write to the protected region for the lifetime of that boot [@siguza-ktrr]. Apple&apos;s architectural answer to the same-privilege paradox: push the defense *below* the kernel, into hardware Apple controls.
&lt;p&gt;The corollary is that Apple&apos;s hardware control allows them to make a software move Microsoft cannot. Apple deprecated third-party Kernel Extensions (KEXTs) in favour of user-mode DriverKit and Endpoint Security, structurally removing the BYOVD class from the platform.Apple&apos;s deprecation of third-party KEXTs began in macOS Catalina (2019) with a deprecation warning, escalated to &quot;system extensions&quot; requiring user approval and reduced kernel-mode footprint, and reached a near-complete migration target on Apple Silicon. The architectural cost is that legitimate device-driver vendors and EDR products had to rebuild their stacks on top of user-mode brokers and Apple-curated APIs; the architectural benefit is that a 2024-style CrowdStrike Falcon kernel-driver outage is structurally not possible on Apple Silicon, because the EDR product runs in user mode against an Endpoint Security framework that mediates the kernel for it.&lt;/p&gt;
&lt;h3&gt;Linux mainline: privilege reduction, not integrity monitoring&lt;/h3&gt;
&lt;p&gt;The mainline Linux community&apos;s strategy is structurally the opposite of Microsoft&apos;s: do not invest in same-privilege deterrents at all; invest in privilege reduction and surface isolation instead. LKRG (Linux Kernel Runtime Guard, maintained by Openwall) is the closest functional analogue to PatchGuard [@openwall-lkrg-page] [@openwall-lkrg-github]. Its own documentation describes it as &quot;bypassable by design&quot; -- an openly-acknowledged same-privilege paradox.LKRG&apos;s frank framing is unusual in the security tools space. The project explicitly tells operators that LKRG is a hardening layer that raises the engineering cost of common kernel rootkit techniques, not a security boundary, and that a determined kernel-mode attacker can defeat it. This is the same architectural truth Skywing made in 2005 and that Microsoft published in the Servicing Criteria a decade later, stated upfront in a project README.&lt;/p&gt;
&lt;p&gt;Beyond LKRG, the mainline mechanisms have a recurring structural shape. Each row of the table below is structurally a &lt;em&gt;privilege-reduction&lt;/em&gt; or &lt;em&gt;surface-removal&lt;/em&gt; mechanism rather than a same-privilege integrity check.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Linux mechanism&lt;/th&gt;
&lt;th&gt;Status (as of June 2026)&lt;/th&gt;
&lt;th&gt;What it protects&lt;/th&gt;
&lt;th&gt;Windows analogue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Lockdown LSM&lt;/td&gt;
&lt;td&gt;Mainline since 5.4 (2019)&lt;/td&gt;
&lt;td&gt;Restricts root&apos;s ability to modify the running kernel&lt;/td&gt;
&lt;td&gt;Driver Signature Enforcement plus HVCI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FG-KASLR&lt;/td&gt;
&lt;td&gt;Out-of-tree&lt;/td&gt;
&lt;td&gt;Per-function rather than per-image randomisation&lt;/td&gt;
&lt;td&gt;No direct analogue; closest is kASLR base randomisation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clang KCFI (&lt;code&gt;-fsanitize=kcfi&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Mainline since 6.1 (Dec 2022)&lt;/td&gt;
&lt;td&gt;Forward-edge CFI for the Linux kernel&lt;/td&gt;
&lt;td&gt;kCFG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shadow Call Stack (ARM64)&lt;/td&gt;
&lt;td&gt;Mainline since 5.8 (2020)&lt;/td&gt;
&lt;td&gt;Backward-edge integrity on ARM64&lt;/td&gt;
&lt;td&gt;kCET (on x64 / AMD), SCS on ARM64 Windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seccomp-bpf&lt;/td&gt;
&lt;td&gt;Mainline since 3.5 (2012)&lt;/td&gt;
&lt;td&gt;Caller-defined per-syscall filter for any process&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Win32kSystemCallFilter&lt;/code&gt; (system-defined IDs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eBPF kernel-mode restrictions&lt;/td&gt;
&lt;td&gt;Mainline since 5.8 (2020)&lt;/td&gt;
&lt;td&gt;Limits unprivileged users from loading eBPF programs that touch kernel state&lt;/td&gt;
&lt;td&gt;No direct Windows analogue&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The shared design move across all six is &lt;strong&gt;structural privilege reduction rather than same-privilege integrity monitoring&lt;/strong&gt;. seccomp-bpf is particularly instructive as a counterpoint to &lt;code&gt;Win32kSystemCallFilter&lt;/code&gt;. The Linux design is &lt;em&gt;caller-defined&lt;/em&gt;: any process can register a BPF program that filters its own syscalls. The Windows design is &lt;em&gt;system-defined&lt;/em&gt;: a process registers an opaque bitmap of &lt;code&gt;FilterId&lt;/code&gt; values whose semantics are decided by the kernel. The two are not interchangeable, but they answer the same architectural question -- &quot;how do you let a process tell the kernel which syscalls it does not want?&quot; -- with the same fundamental move: per-process surface deletion at the syscall boundary.&lt;/p&gt;
&lt;h3&gt;Hypervisor-anchored alternatives at the application level&lt;/h3&gt;
&lt;p&gt;The third philosophy applies the &quot;live at a different privilege than the attacker&quot; answer at the &lt;em&gt;application&lt;/em&gt; level rather than the kernel level. Bromium / HP Sure Click and Windows Defender Application Guard open every tab or document in its own micro-VM. The hypervisor is the protection boundary; the kernel inside the VM may be fully compromised without affecting the host. This is structurally the same move Microsoft makes with VBS / VTL1, applied one level up the stack.&lt;/p&gt;
&lt;h3&gt;Three philosophies, one shared admission&lt;/h3&gt;
&lt;p&gt;Three platforms, three philosophies, one shared admission: every architecture eventually had to admit that a defense at the same privilege as the attacker cannot succeed in principle. Apple put the defense in silicon. Linux invested in surface reduction instead of integrity monitoring. Microsoft built a same-privilege deterrent first, then migrated the load-bearing pieces of it to VTL1. The interesting disagreement is not whether the paradox exists -- it is where, exactly, to put the defense instead. That is a question with no single right answer, and to see why, we have to state the paradox formally.&lt;/p&gt;
&lt;h2&gt;8. The same-privilege paradox, formally&lt;/h2&gt;
&lt;p&gt;Now we can state the paradox in a sentence: &lt;em&gt;a defense that shares its CPU privilege level with the attacker can in principle always be subverted by an attacker at that privilege level, because every code path and data structure the defense relies on is, by construction, mutable by the attacker.&lt;/em&gt; It is not a formal impossibility theorem in the cryptographic sense -- there is no FLP-style no-go proof for kernel self-defense -- but it is the de facto design constraint Microsoft has acknowledged in writing.&lt;/p&gt;
&lt;h3&gt;Microsoft&apos;s formal admission&lt;/h3&gt;
&lt;p&gt;The Microsoft Security Servicing Criteria for Windows defines a &quot;security boundary&quot; as &lt;em&gt;&quot;a logical separation between the code and data of security domains with different levels of trust&quot;&lt;/em&gt;, with kernel-mode versus user-mode as the canonical example [@ms-servicing-criteria]. The document then enumerates which transitions Microsoft treats as security boundaries (kernel / user, hypervisor / kernel, VTL1 / VTL0, virtual machine / host, network), and explicitly &lt;em&gt;does not&lt;/em&gt; enumerate admin-to-kernel or kernel-to-kernel as boundaries. The exclusion is the cleanest possible architectural admission of the paradox: no defense at CPL=0 in the attacker&apos;s kernel can be a security boundary, no matter how cleverly engineered. PatchGuard, by Microsoft&apos;s own classification, is not a boundary and never has been.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The same-privilege paradox is, formally, the observation that the &lt;strong&gt;reference monitor&lt;/strong&gt; of a security policy must be tamper-resistant from the principals it monitors, and that &quot;tamper-resistant from a co-resident kernel-mode attacker&quot; is structurally unachievable in a single-address-space single-privilege design. Every modern Windows kernel mitigation either &lt;em&gt;raises the cost&lt;/em&gt; of tampering (the engineering-deterrent class: PatchGuard, KASLR, kASLR variants) or &lt;em&gt;moves the monitor outside CPL=0&lt;/em&gt; (the structural class: KDP, kCFG-with-VBS-bitmap, kCET, the entire VTL1-anchored stack). Only the second class can claim a security boundary.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The KASLR-specific bound&lt;/h3&gt;
&lt;p&gt;The cleanest mathematical version of the paradox lives in the KASLR side-channel literature. Suppose an x64 system has $n$ bits of entropy in its kernel base address; the probabilistic floor on guessing it from one shot is $2^{-n}$. The Hund-Willems-Holz 2013 result is that a co-resident user-mode attacker with access to a shared TLB or cache state can extract bits of the kernel base at a rate of one bit per probe, recovering the address in $O(n)$ probes -- a polynomial-time defeat of the probabilistic defense [@doi-hund-2013]. Increasing $n$ does not change the asymptotic; it only changes the constant. Gruss et al. 2017 generalised the argument across micro-architectural side channels and concluded that any operating system implementing user / kernel address-space sharing on a CPU with shared TLB / cache state must leak the kernel base address to an unprivileged user-mode timing observer [@gruss-kaiser-pdf]. The structural fix is not to add entropy: it is to remove the sharing. KVA Shadow / KPTI is the structural answer.&lt;/p&gt;
&lt;p&gt;The shape of the bound is general. Wherever a defense&apos;s correctness reduces to &lt;em&gt;the attacker not knowing X&lt;/em&gt;, and &lt;em&gt;X&lt;/em&gt; leaks across a shared micro-architectural channel, the defense is asymptotically defeated.&lt;/p&gt;
&lt;h3&gt;The proper formal anchor: Anderson 1972&lt;/h3&gt;
&lt;p&gt;The right formal anchor for the same-privilege paradox is the reference-monitor concept introduced in Anderson&apos;s 1972 &lt;em&gt;Computer Security Technology Planning Study&lt;/em&gt; for the US Air Force [@csrc-anderson-1972]. Anderson&apos;s &quot;reference monitor&quot; must satisfy three properties:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Always invoked.&lt;/strong&gt; Every reference of a subject to an object is mediated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tamper-resistant.&lt;/strong&gt; The reference monitor cannot be modified by the subjects it monitors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Small enough to be analysed.&lt;/strong&gt; The Trusted Computing Base (TCB) is small enough to be verified.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;PatchGuard fails property 2 by construction: it lives in the same address space as the subjects it monitors, and any subject with kernel-mode write can modify the verifier code, the verifier schedule, the expected-hash store, or the bug-check primitive. KDP, by contrast, satisfies property 2 because its enforcement lives in VTL1 and a VTL0 subject cannot reach VTL1.&lt;/p&gt;

A recurring confusion in the kernel-security literature is to anchor same-privilege-paradox arguments in the Bell-LaPadula or Biba multi-level security models (1973 / 1977). Those models formalise *information flow* across security domains -- which subjects may read or write which objects given their lattice levels. They are silent on the question of whether the policy *enforcement mechanism itself* can be tamper-resistant against a co-resident attacker. That is Anderson&apos;s reference-monitor property, formalised in the 1972 USAF report [@csrc-anderson-1972]. Bell-LaPadula assumes a tamper-resistant reference monitor as a precondition; Anderson&apos;s report is the document that *names* the precondition. For the same-privilege paradox, Anderson is the load-bearing anchor.
&lt;p&gt;The existence proof for what a minimal verifiable TCB looks like is seL4 (Klein et al., SOSP 2009): a roughly 8,700-line microkernel formally verified down to its C implementation against a high-level specification of access control. seL4 is the constructive counterpoint to the Microsoft-style mitigation stack: instead of adding integrity monitors to a large kernel, build a small kernel small enough to verify and put everything else in user-space servers. Windows&apos; VBS / VTL1 architecture is a partial gesture in the same direction -- the Secure Kernel is far smaller than the NT kernel and hosts only policy-enforcement trustlets -- but it is not a from-scratch redesign.&lt;/p&gt;
&lt;h3&gt;Upper and lower bounds, mitigation by mitigation&lt;/h3&gt;
&lt;p&gt;The 21-year story now lays out cleanly as a table of bounds.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mitigation&lt;/th&gt;
&lt;th&gt;Upper bound achieved&lt;/th&gt;
&lt;th&gt;Lower bound that remains&lt;/th&gt;
&lt;th&gt;Structural reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;PatchGuard&lt;/td&gt;
&lt;td&gt;Engineering-deterrent class; raises cost of casual kernel hooking&lt;/td&gt;
&lt;td&gt;Zero structural lower bound; same-privilege bypass class always exists [@uninformed-v3-archive] [@cyberark-ghosthook]&lt;/td&gt;
&lt;td&gt;Verifier lives at attacker&apos;s privilege&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KASLR (entropy alone)&lt;/td&gt;
&lt;td&gt;Probabilistic floor against blind-guess attacker&lt;/td&gt;
&lt;td&gt;Zero structural lower bound against side-channel attacker [@doi-hund-2013]&lt;/td&gt;
&lt;td&gt;TLB / cache shared between user and kernel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KVA Shadow / KPTI&lt;/td&gt;
&lt;td&gt;Structural Meltdown fix (Variant 3)&lt;/td&gt;
&lt;td&gt;Spectre Variants 1, 2, 4 require separate mitigations [@usenix-lipp-meltdown]&lt;/td&gt;
&lt;td&gt;Address-space split addresses only the user-to-kernel transient read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HVCI&lt;/td&gt;
&lt;td&gt;Structural W^X for kernel pages, enforced by VTL1&lt;/td&gt;
&lt;td&gt;VBS-coverage gap on systems that cannot run VBS [@ms-kdp-blog]&lt;/td&gt;
&lt;td&gt;Hypervisor is the protection boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KDP (static and dynamic)&lt;/td&gt;
&lt;td&gt;Structural read-only-after-init for explicitly-tagged kernel data&lt;/td&gt;
&lt;td&gt;Protects only what is explicitly opted in [@ms-kdp-blog]&lt;/td&gt;
&lt;td&gt;VTL1 enforces SLAT page tables outside VTL0 reach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kCFG (with HVCI)&lt;/td&gt;
&lt;td&gt;Structural forward-edge CFI; bitmap in VTL1-protected memory&lt;/td&gt;
&lt;td&gt;Backward edge unprotected; same-call-target overwrite via type confusion possible without XFG [@ms-cfg]&lt;/td&gt;
&lt;td&gt;Bitmap stored outside VTL0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kCET&lt;/td&gt;
&lt;td&gt;Structural backward-edge CFI in CPU hardware&lt;/td&gt;
&lt;td&gt;Off-by-default on Server 2025; gated on driver &lt;code&gt;/CETCOMPAT&lt;/code&gt; [@ms-kernel-mode-hsp] [@mcgarr-bh25-blackhat]&lt;/td&gt;
&lt;td&gt;Shadow stack hardware enforced in silicon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Win32kSystemCallFilter&lt;/td&gt;
&lt;td&gt;Structural surface deletion for sandboxed renderers&lt;/td&gt;
&lt;td&gt;Full lockdown not viable for UI-bearing processes [@ms-syscall-filter-policy]&lt;/td&gt;
&lt;td&gt;Per-process bitmap consulted by syscall dispatcher&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The gap between the same-privilege upper bound (PatchGuard, KASLR-alone -- structurally zero) and the cross-privilege upper bound (HVCI, KDP, kCET -- structurally meaningful) is exactly the gap Microsoft has spent twenty-one years migrating across. With the paradox stated formally, the rest of the article is a single question: where in the privilege hierarchy does the next problem live, and how is Microsoft positioned to answer it?&lt;/p&gt;
&lt;h2&gt;9. Open problems on the June 2026 frontier&lt;/h2&gt;
&lt;p&gt;The same-privilege paradox is in 2026 closer to architecturally resolved than at any prior point in Windows history -- the VTL1-anchored stack of HVCI / KDP / kCFG / kCET makes the cross-privilege answer real. But every structural mitigation has a practical residual, and five of them are large enough to be the article&apos;s frontier.&lt;/p&gt;
&lt;h3&gt;BYOVD: the dominant 2026 attacker path&lt;/h3&gt;
&lt;p&gt;Bring-Your-Own-Vulnerable-Driver is the dominant practical defeat of every structural mitigation in the 2026 stack. Uroburos&apos;s 2011 pattern is essentially what current attackers do: locate a signed third-party driver with a kernel-write primitive (an IOCTL that allows arbitrary physical memory read or write, or arbitrary MSR manipulation), install it through a legitimate driver-load path, exploit the primitive to obtain arbitrary kernel write, then flip the policy flags or hook the structures Microsoft thought were protected. Elastic Security Labs&apos; 2024 survey of in-the-wild Windows kernel LPE 0-days confirms that BYOVD remains a recurring subsystem of incidents [@elastic-lpe-survey], and the Project Zero &quot;0day In the Wild&quot; tracker continues to record Windows kernel-mode CVEs across DWM, win32k, and ALPC subsystems [@pz-0days-tracker]. Every structural mitigation collapses the moment an attacker reaches arbitrary kernel write through a legitimately-loaded driver: KDP-protected pages can be ignored if the attacker can install a new driver that simply does not allocate from the secure pool; kCFG can be bypassed by writing to memory that was not opted in; kCET can be bypassed via McGarr-style &lt;code&gt;iretq&lt;/code&gt; corruption [@mcgarr-km-shadow]; PatchGuard can be hooked from a coexisting driver.&lt;/p&gt;
&lt;p&gt;The Microsoft Recommended Driver Block List [@ms-driver-block-rules] is the reactive answer. The structural problem -- that signed third-party drivers with kernel-write primitives exist &lt;em&gt;at all&lt;/em&gt;, and that the third-party driver supply chain cannot be removed for compatibility reasons -- is unresolved.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A defender architecting around the 2026 Windows kernel mitigation stack must assume BYOVD as the dominant practical bypass. The structural mitigations -- KDP, kCFG, kCET, HVCI -- are sound against an attacker who is &lt;em&gt;constrained&lt;/em&gt; to operate within the inbox kernel. They are not sound against an attacker who can load any of the recurring vulnerable signed drivers the Microsoft Recommended Driver Block List exists to catalogue [@ms-driver-block-rules] [@elastic-lpe-survey]. Verify that the block list is enforced (&lt;code&gt;CiTool --list-policies&lt;/code&gt;), watch CodeIntegrity Event ID 3099, and treat BYOVD as the threat model that drives mitigation selection.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The VBS coverage gap&lt;/h3&gt;
&lt;p&gt;Every VTL1-anchored mitigation collapses on systems that cannot run VBS. Older silicon (pre-2015 Intel without VT-x / VT-d / EPT, AMD parts predating AMD-V / NPT), enterprise-imaged corporate fleets that disabled VBS for compatibility, ARM64 devices below a baseline, and any system without UEFI Secure Boot all fall back to the same-privilege defenses we just classified as structurally bounded. The defender&apos;s threat model is the worst case in the fleet, not the average case in the Microsoft launch announcement.&lt;/p&gt;
&lt;h3&gt;Win32k Lockdown coverage in UI-bearing processes&lt;/h3&gt;
&lt;p&gt;Office, browsers&apos; GPU and UI processes, and any application that draws windows cannot use the full &lt;code&gt;Win32kSystemCallFilter&lt;/code&gt; lockdown. Their allow-lists must cover composition, font rendering, and a substantial fraction of the GDI surface -- which is exactly the surface from which historical LPE bugs emerged. The 2016 &lt;code&gt;win32kbase.sys&lt;/code&gt; / &lt;code&gt;win32kfull.sys&lt;/code&gt; typeisolation refactor (Windows 10 v1607, build 14393) split &lt;code&gt;win32k.sys&lt;/code&gt; to make the surface more attributable, but per-app auto-tuning of the allow-list from observed-call traces remains an open product-engineering problem [@j00ru-syscalls-table]. Until UI-bearing processes can use a tight allow-list rather than a permissive one, the win32k surface remains the systemic LPE foothold Bochspwn identified in 2013 [@j00ru-bochspwn-blog].&lt;/p&gt;
&lt;h3&gt;Hypervisor escapes as the structural counter&lt;/h3&gt;
&lt;p&gt;Every VTL1-anchored mitigation assumes VTL1 is uncompromised. Hyper-V CVEs show that the hypervisor TCB hosts its own vulnerability surface. CVE-2024-38080 (Hyper-V SLAT vulnerability) is a 2024 example with Akamai write-up [@akamai-hyperv-cve]. Joanna Rutkowska&apos;s 2006 Blue Pill demonstration at Black Hat USA, &lt;em&gt;Subverting Vista Kernel for Fun and Profit&lt;/em&gt;, was the seminal academic primary for the hypervisor-rootkit class and remains the canonical &quot;Hyperjacking&quot; reference [@blackhat-rutkowska-bluepill]. Every step the Windows mitigation stack takes toward putting more enforcement in VTL1 raises the criticality of VTL1&apos;s own correctness. The Hyper-V code base is small relative to &lt;code&gt;ntoskrnl&lt;/code&gt; but is not zero, and the post-2018 trend of finding side-channel and architectural bugs in CPU hardware applies to VTL1 as much as it does to VTL0.&lt;/p&gt;
&lt;h3&gt;kCET deployment completion&lt;/h3&gt;
&lt;p&gt;kCET is shipping but off-by-default on Windows Server 2025, gated on driver &lt;code&gt;/CETCOMPAT&lt;/code&gt; compatibility [@ms-kernel-mode-hsp]. Until kCET is on-by-default across the inbox kernel and all loaded drivers, the backward-edge ROP class against the Windows kernel remains exploitable in practice. McGarr&apos;s 2025 Black Hat USA deck documents both the structural-bypass frontier and the operational gating problem [@mcgarr-bh25-blackhat] [@mcgarr-github] [@mcgarr-km-shadow].&lt;/p&gt;

On July 19, 2024, a faulty kernel-mode signature update from CrowdStrike Falcon triggered a Windows page fault in a CrowdStrike driver, crashing an estimated 8.5 million Windows endpoints worldwide and disrupting airline operations, hospital systems, payment processing, and emergency-services dispatch for hours to days. The post-incident discussion produced one architectural takeaway widely shared across the kernel-security community: a single signed third-party kernel driver, even one shipped by a defender, can take the operating system down -- and there is no in-kernel protection against it that does not also break legitimate EDR vendors. Microsoft&apos;s 2006 position that the right answer is &quot;as few third-party kernel drivers as possible, with as much functionality as possible mediated by user-mode brokers&quot; got eighteen years of pushback before being retroactively vindicated. The 2024-2026 product direction -- Microsoft&apos;s announcement of the Windows Endpoint Security Platform, a user-mode EDR API that lets vendors build without kernel drivers -- is the inheritor of that position.
&lt;h3&gt;Historical anchoring: the win32k LPE share&lt;/h3&gt;
&lt;p&gt;The &quot;win32k killed half of LPE&quot; framing in the article&apos;s subtitle deserves time-scoping. Pre-lockdown, win32k was the dominant Windows kernel LPE subsystem -- Stuxnet 2010 (CVE-2010-2743) is the historical anchor [@nvd-cve-2010-2743], Bochspwn 2013 documented the systemic shape [@j00ru-bochspwn-blog] [@j00ru-bhusa-pdf], Forshaw 2016 reports that the Chrome M54 lockdown &quot;blocked the sandbox escape of an exploit chain being used in the wild&quot; [@pz-breaking-chain], and Elastic Security Labs&apos; 2024 in-the-wild survey continues to name win32k among the recurring subsystems [@elastic-lpe-survey]. The Project Zero 0day tracker also confirms that win32k remains in the post-lockdown attacker mix [@pz-0days-tracker]. The lockdown removed roughly half the historically-vulnerable syscall surface &lt;em&gt;from sandboxed renderers specifically&lt;/em&gt;; both the fraction and the scope are time- and context-bounded, and a precise percentage cannot be cited to the Project Zero tracker because the tracker does not publish per-subsystem aggregates.&lt;/p&gt;

flowchart TD
    subgraph SD[&quot;Surface deletion (kernel system-call boundary)&quot;]
        SDF[&quot;Win32kSystemCallFilter per-process bitmap&quot;]
        SDD[&quot;DisallowWin32kSystemCalls all-or-nothing&quot;]
    end
    subgraph V1[&quot;VTL1 (Secure Kernel anchored)&quot;]
        V1H[&quot;HVCI (W^X SLAT for kernel pages)&quot;]
        V1K[&quot;KDP static and dynamic via SLAT RO&quot;]
        V1C[&quot;kCFG bitmap in VTL1-protected memory&quot;]
    end
    subgraph CPU[&quot;CPU mediated (hardware enforced)&quot;]
        CPUS[&quot;kCET shadow stack on Intel CET / AMD&quot;]
        CPUK[&quot;KVA Shadow CR3 switch&quot;]
    end
    subgraph V0[&quot;VTL0 same-privilege (CPL=0)&quot;]
        V0P[&quot;PatchGuard integrity checks&quot;]
        V0K[&quot;KASLR base-address randomisation&quot;]
    end
    SD --&amp;gt; V1
    V1 --&amp;gt; CPU
    CPU --&amp;gt; V0
&lt;p&gt;BYOVD is in 2026 what same-privilege bypass was in 2007 -- the dominant practical defeat of a mitigation stack whose individual pieces are each structurally sound. The next twenty-one years of Windows kernel self-defense will be substantially the story of what Microsoft does about it.&lt;/p&gt;
&lt;h2&gt;10. What a Windows defender or driver developer actually does today&lt;/h2&gt;
&lt;p&gt;The article&apos;s intellectual payoff has been made; the practical payoff is the rest of this section. Five concrete decision questions, in roughly the order a working practitioner would reason through them.&lt;/p&gt;
&lt;h3&gt;1. Is the system Secured-core or Windows 11 22H2+ with Memory Integrity on?&lt;/h3&gt;
&lt;p&gt;If yes, HVCI, KDP, kCFG, and the Microsoft Recommended Driver Block Rules are baseline [@ms-kdp-blog] [@ms-driver-block-rules]. Layer kCET if all loaded drivers are &lt;code&gt;/CETCOMPAT&lt;/code&gt; and the CPU is Intel 11th-gen Tiger Lake or later or AMD Zen 3 or later [@ms-kernel-mode-hsp]. The baseline gets you the structural mitigations the same-privilege paradox argues are required; everything else is layered on top.&lt;/p&gt;
&lt;h3&gt;2. Is the workload a sandboxed renderer or sandboxable child process?&lt;/h3&gt;
&lt;p&gt;Apply &lt;code&gt;Win32kSystemCallFilter&lt;/code&gt; (Windows 10 1709+) via &lt;code&gt;UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, ...)&lt;/code&gt; at &lt;code&gt;CreateProcess&lt;/code&gt; time, not at runtime [@ms-syscall-filter-policy]. The Forshaw / Fratric race-the-mitigation Edge demonstration is the empirical reason -- if the filter is applied after the child process has started, an attacker who races the policy application can simply not be filtered [@pz-breaking-chain]. The Chromium sandbox is the canonical consumer reference for what this composition looks like in a production browser [@chromium-sandbox-doc].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Every per-process mitigation in modern Windows -- &lt;code&gt;Win32kSystemCallFilter&lt;/code&gt;, &lt;code&gt;DisallowWin32kSystemCalls&lt;/code&gt;, ACG, CIG, Strict CIG, user-mode shadow stack, CFG -- belongs on the &lt;code&gt;CreateProcess&lt;/code&gt; boundary. The Forshaw / Fratric Project Zero finding on Edge&apos;s window-broker race [@pz-breaking-chain] is the empirical proof that mitigations applied to a running process leave a race window. The Windows API path is &lt;code&gt;STARTUPINFOEXW&lt;/code&gt; with a &lt;code&gt;PPROC_THREAD_ATTRIBUTE_LIST&lt;/code&gt; containing &lt;code&gt;PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY&lt;/code&gt;; the policy enums to set are documented in &lt;code&gt;ntddk.h&lt;/code&gt; for the filter [@ms-syscall-filter-policy] and in &lt;code&gt;winnt.h&lt;/code&gt; for the disable [@ms-syscall-disable-policy].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3. Is the workload UI-bearing?&lt;/h3&gt;
&lt;p&gt;Full lockdown is out of reach for processes that draw windows, render fonts, or dispatch input. The practical answer is the &lt;em&gt;adjacent&lt;/em&gt; mitigation set: Arbitrary Code Guard (ACG), Code Integrity Guard (CIG), Strict CIG, user-mode shadow stack, and CFG, plus PatchGuard, HVCI, and kCFG at the system level. The composition raises the cost of remote exploitation without requiring the renderer-style syscall-surface deletion.&lt;/p&gt;

For a sandboxed renderer-class process on Windows 11 22H2+:&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Win32kSystemCallFilter&lt;/code&gt;&lt;/strong&gt; -- &lt;code&gt;PROCESS_MITIGATION_SYSTEM_CALL_FILTER_POLICY&lt;/code&gt; with the bitmap permitting only the &lt;code&gt;FilterId&lt;/code&gt; values the renderer needs [@ms-syscall-filter-policy].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ACG (Arbitrary Code Guard)&lt;/strong&gt; -- forbid dynamic code generation in the process.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CIG / Strict CIG (Code Integrity Guard)&lt;/strong&gt; -- forbid loading non-Microsoft-signed DLLs (CIG), or non-Microsoft-signed-and-not-store-signed DLLs (Strict CIG).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User-mode shadow stack and CFG&lt;/strong&gt; -- backward and forward edge CFI in user mode.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All four are applied via &lt;code&gt;UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, ...)&lt;/code&gt; at &lt;code&gt;CreateProcess&lt;/code&gt; time, in the same call. The Chromium renderer is the canonical reference deployment [@chromium-sandbox-doc].
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;4. Are you a driver author?&lt;/h3&gt;
&lt;p&gt;Three things to do, in order:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mark RO-after-init data via Static KDP.&lt;/strong&gt; Call &lt;code&gt;MmProtectDriverSection&lt;/code&gt; from &lt;code&gt;DriverEntry&lt;/code&gt; on any image section that should be read-only for the rest of the driver&apos;s lifetime [@ms-kdp-blog].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Allocate runtime-protected state via Dynamic KDP.&lt;/strong&gt; Call &lt;code&gt;ExAllocatePool3&lt;/code&gt; with a &lt;code&gt;POOL_EXTENDED_PARAMETER&lt;/code&gt; array containing a &lt;code&gt;POOL_EXTENDED_PARAMS_SECURE_POOL&lt;/code&gt; extended parameter. Set &lt;code&gt;SECURE_POOL_FLAGS_FREEABLE&lt;/code&gt; if the allocation needs to be freeable; set &lt;code&gt;SECURE_POOL_FLAG_MODIFIABLE&lt;/code&gt; only if the allocation must be modifiable under further protected control [@ms-kdp-blog].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compile with &lt;code&gt;/guard:cf&lt;/code&gt; and &lt;code&gt;/CETCOMPAT&lt;/code&gt;.&lt;/strong&gt; The first enables CFG instrumentation across the driver image; the second tells the loader the driver is compatible with kernel-mode shadow stack [@ms-cfg] [@ms-kernel-mode-hsp].&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The driver-side KDP pattern is short enough to show in full:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// DriverEntry-time static KDP: mark a .rdata-like section as read-only
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject,
                     _In_ PUNICODE_STRING RegistryPath) {
    NTSTATUS status = MmProtectDriverSection(
        &amp;amp;g_PolicyTable,        // address of the section to protect
        sizeof(g_PolicyTable), // size in bytes
        0);                    // reserved
    if (!NT_SUCCESS(status)) return status;
    // ... rest of driver init
    return STATUS_SUCCESS;
}

// Runtime dynamic KDP allocation: a secure pool buffer
POOL_EXTENDED_PARAMETER params[2] = {0};
params[0].Type = PoolExtendedParameterSecurePool;
params[0].SecurePoolParams = &amp;amp;(POOL_EXTENDED_PARAMS_SECURE_POOL){
    .SecurePoolFlags = SECURE_POOL_FLAGS_FREEABLE,
    .SecurePoolBuffer = NULL,
    .Cookie = 0xC0FFEEDEADBEEFULL,
    .NoFill = FALSE,
};
params[1].Type = PoolExtendedParameterInvalidType;

PVOID secureBuffer = ExAllocatePool3(
    POOL_FLAG_NON_PAGED,    // pool flags
    bufferSize,             // size
    &apos;KDPx&apos;,                 // pool tag
    params,                 // extended parameters
    1);                     // count of extended parameters
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. Are you a defender on an existing fleet?&lt;/h3&gt;
&lt;p&gt;Verify that the Recommended Driver Block Rules are active via &lt;code&gt;CiTool --list-policies&lt;/code&gt;. Look for a policy whose &lt;code&gt;Friendly Name&lt;/code&gt; is &lt;code&gt;Microsoft Windows Driver Policy&lt;/code&gt; and &lt;code&gt;Is Currently Enforced&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt; [@ms-driver-block-rules]. Watch Event ID 3099 in the CodeIntegrity Operational log for block events. For verifying the broader VBS / HVCI state, the canonical PowerShell query is &lt;code&gt;Get-CimInstance Win32_DeviceGuard&lt;/code&gt; followed by selecting &lt;code&gt;VirtualizationBasedSecurityStatus&lt;/code&gt;, &lt;code&gt;SecurityServicesRunning&lt;/code&gt;, and &lt;code&gt;AvailableSecurityProperties&lt;/code&gt;. For KVA Shadow specifically, &lt;code&gt;Get-SpeculationControlSettings&lt;/code&gt; reports the state. For per-process mitigation policy, &lt;code&gt;Get-ProcessMitigation -System&lt;/code&gt; for the system policy and &lt;code&gt;Get-ProcessMitigation -Name &amp;lt;name&amp;gt;&lt;/code&gt; for a specific process; the Chromium internal page &lt;code&gt;chrome://sandbox&lt;/code&gt; shows the per-process filter state from inside the browser.&lt;/p&gt;
&lt;p&gt;A reader who wants to play with the field-decoding logic can do it in a browser. The Python below mirrors what the PowerShell pipeline does -- enumerate the bits, decode by name. The real Windows API surface is bigger, but the decoding shape is the same.&lt;/p&gt;
&lt;p&gt;{`&lt;/p&gt;
Conceptual decoder for Win32_DeviceGuard fields
Real PowerShell: Get-CimInstance Win32_DeviceGuard | Select VirtualizationBasedSecurityStatus,
SecurityServicesRunning, AvailableSecurityProperties
&lt;p&gt;VBS_STATUS = {
    0: &quot;VBS not enabled&quot;,
    1: &quot;VBS enabled but not running&quot;,
    2: &quot;VBS enabled and running&quot;,
}&lt;/p&gt;
&lt;p&gt;SECURITY_SERVICES = {
    0: &quot;None&quot;,
    1: &quot;Credential Guard&quot;,
    2: &quot;HVCI&quot;,
    3: &quot;System Guard Secure Launch&quot;,
    4: &quot;SMM Firmware Measurement&quot;,
    7: &quot;Kernel Mode Hardware-enforced Stack Protection (kCET)&quot;,
    8: &quot;Hypervisor-Protected Code Integrity (HVCI legacy)&quot;,
}&lt;/p&gt;
&lt;p&gt;AVAILABLE_PROPERTIES = {
    1: &quot;Base virtualization support&quot;,
    2: &quot;Secure boot&quot;,
    3: &quot;DMA protection&quot;,
    4: &quot;Secure memory overwrite&quot;,
    5: &quot;UEFI code readonly&quot;,
    6: &quot;SMM security mitigations&quot;,
    7: &quot;Mode-based execute control for HVCI&quot;,
    8: &quot;APIC virtualization&quot;,
}&lt;/p&gt;
&lt;p&gt;def decode(field_name, value, table):
    if isinstance(value, list):
        names = [table.get(v, f&quot;unknown({v})&quot;) for v in value]
        print(f&quot;  {field_name}: {names}&quot;)
    else:
        print(f&quot;  {field_name}: {table.get(value, f&apos;unknown({value})&apos;)}&quot;)&lt;/p&gt;
Simulated CIM response from a Secured-core PC
&lt;p&gt;sample = {
    &quot;VirtualizationBasedSecurityStatus&quot;: 2,
    &quot;SecurityServicesRunning&quot;: [1, 2, 7],
    &quot;AvailableSecurityProperties&quot;: [1, 2, 3, 5, 7],
}&lt;/p&gt;
&lt;p&gt;print(&quot;Win32_DeviceGuard decoded:&quot;)
decode(&quot;VirtualizationBasedSecurityStatus&quot;,
       sample[&quot;VirtualizationBasedSecurityStatus&quot;], VBS_STATUS)
decode(&quot;SecurityServicesRunning&quot;,
       sample[&quot;SecurityServicesRunning&quot;], SECURITY_SERVICES)
decode(&quot;AvailableSecurityProperties&quot;,
       sample[&quot;AvailableSecurityProperties&quot;], AVAILABLE_PROPERTIES)
`}&lt;/p&gt;
&lt;h3&gt;Common pitfalls&lt;/h3&gt;
&lt;p&gt;A short reference list of mistakes that recur in real-world reviews:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Apply mitigations at &lt;code&gt;CreateProcess&lt;/code&gt;, not at runtime.&lt;/strong&gt; The Forshaw / Fratric race is the cited example [@pz-breaking-chain].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do not assume &lt;code&gt;DisallowWin32kSystemCalls&lt;/code&gt; is the modern lockdown.&lt;/strong&gt; It is the Windows 8 ancestor of &lt;code&gt;Win32kSystemCallFilter&lt;/code&gt; and is structurally distinct -- different mitigation enum, different policy struct [@ms-syscall-disable-policy] [@ms-syscall-filter-policy].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do not use &lt;code&gt;MmAllocateNodePagesForMdlEx&lt;/code&gt; for Dynamic KDP.&lt;/strong&gt; The canonical API is &lt;code&gt;ExAllocatePool3&lt;/code&gt; with the secure-pool extended parameter; the NUMA-MDL API is a different API for a different purpose [@ms-kdp-blog].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;kCET disables system-wide on a non-&lt;code&gt;/CETCOMPAT&lt;/code&gt; driver.&lt;/strong&gt; A single non-compat driver in the inbox set turns it off [@ms-kernel-mode-hsp].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PatchGuard is not a security boundary.&lt;/strong&gt; Do not architect a defense whose security argument rests on it; Microsoft&apos;s own Servicing Criteria say so [@ms-servicing-criteria].&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these decisions makes the kernel a security boundary; together they make the kernel as hard to defeat as today&apos;s stack allows. The remaining questions are FAQs.&lt;/p&gt;
&lt;h2&gt;11. Frequently asked questions&lt;/h2&gt;

No. Microsoft&apos;s own *Security Servicing Criteria for Windows* explicitly does not enumerate admin-to-kernel or kernel-to-kernel as a security boundary; PatchGuard is an *engineering deterrent*, not a security boundary [@ms-servicing-criteria]. The most empirically grounded refutation is Uroburos&apos;s 2011 -- 2014 operational coexistence with PatchGuard on production Windows systems [@gdata-uroburos-blog]. PatchGuard raises the cost of a class of attacks; it does not eliminate any class of attacks.

No. PatchGuard shipped on April 25, 2005, with Windows XP Professional x64 Edition and Windows Server 2003 x64 Edition [@ms-advisory-932596]. Vista x64 (November 2006) inherited PatchGuard v2 from the 2005 release; the x86 editions of Vista never received PatchGuard. The &quot;Vista first&quot; misreading conflates PatchGuard&apos;s first widely-publicised release with its first shipping release.

No. Uroburos was a Driver Signature Enforcement (DSE) bypass that coexisted with PatchGuard for three years (2011 -- 2014) without modifying any PatchGuard-protected structure. It loaded a signed-but-vulnerable copy of Oracle&apos;s `VBoxDrv.sys`, used the vulnerability to flip the `g_CiEnabled` DSE-gating flag, loaded its own unsigned rootkit driver, then operated alongside PatchGuard [@gdata-uroburos-blog] [@stmxcsr-turla]. The canonical PatchGuard *bypass* is GhostHook (Kasif Dekel, CyberArk, June 2017), which uses an Intel-PT-buffer-fill PMI to redirect execution without touching any structure PatchGuard enumerates [@cyberark-ghosthook].

No. They are distinct `SetProcessMitigationPolicy` enums with distinct semantics. `DisallowWin32kSystemCalls` shipped in Windows 8 (2012) as a `PROCESS_MITIGATION_SYSTEM_CALL_DISABLE_POLICY` and is all-or-nothing [@ms-syscall-disable-policy]. `Win32kSystemCallFilter` shipped in Windows 10 1709 (October 2017) as a `PROCESS_MITIGATION_SYSTEM_CALL_FILTER_POLICY` and is a per-syscall allow-list driven by a bitmap of system-defined `FilterId` values [@ms-syscall-filter-policy]. Chromium uses *both* in different process types -- the blanket-disable for processes that need no UI, the per-syscall filter for the renderer [@chromium-sandbox-doc].

Microsoft&apos;s documentation still calls it an x64 feature [@ms-driver-x64-restrictions], but in deployment it is also enforced on 64-bit ARM Windows in 2026. It has never shipped on x86 -- the precise framing is &quot;64-bit Windows only, both x64 and ARM64.&quot; The &quot;x64 only&quot; framing is documentation-accurate but deployment-incomplete.

Mostly no. KDP is a VBS-backed (Secure Kernel / VTL1) mitigation that *protects* kernel memory but is *enforced* outside the kernel. The Microsoft launch blog states the architecture directly: &quot;the memory managed by KDP is always verified by the secure kernel (VTL1) and protected using SLAT tables by the hypervisor&quot; [@ms-kdp-blog]. KDP is the canonical example of the same-privilege paradox resolved by structural means: the enforcement lives at a privilege level the VTL0 attacker cannot reach.

Title hyperbole, time-scoped. Pre-lockdown, win32k was the dominant Windows kernel LPE subsystem -- Stuxnet 2010 used a `win32k.sys` keyboard-layout LPE [@nvd-cve-2010-2743]; Bochspwn 2013 documented the systemic shape [@j00ru-bochspwn-blog]; Forshaw reports that Chrome&apos;s M54 win32k lockdown &quot;blocked the sandbox escape of an exploit chain being used in the wild&quot; [@pz-breaking-chain]. Elastic Security Labs&apos; 2024 in-the-wild survey continues to name win32k among the recurring subsystems [@elastic-lpe-survey]. The lockdown removed roughly half the historically-vulnerable syscall surface *from sandboxed renderers specifically* -- both the fraction and the scope are time- and context-bounded.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; PatchGuard, KASLR, KDP, &lt;code&gt;Win32kSystemCallFilter&lt;/code&gt; -- four answers, twenty-one years, one paradox. The arc resolves: every meaningful kernel defense in modern Windows ultimately lives at a privilege level the attacker does not have, because the alternative -- defending the kernel from inside the kernel -- is the one thing the architecture cannot do.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;kernel-self-defense-in-windows-patchguard-kaslr-kdp-and-the-win32k-lockdown-that&quot; keyTerms={[
  { term: &quot;Same-Privilege Paradox&quot;, definition: &quot;A defense at the attacker&apos;s privilege level cannot in principle succeed; the de facto design constraint Microsoft has acknowledged in writing through the Security Servicing Criteria.&quot; },
  { term: &quot;PatchGuard (KPP)&quot;, definition: &quot;Microsoft kernel feature that periodically verifies a fixed list of kernel structures and bug-checks the system on mismatch with stop code 0x109; not a security boundary.&quot; },
  { term: &quot;SSDT&quot;, definition: &quot;System Service Descriptor Table; the kernel function-pointer table that dispatches system calls. Pre-PatchGuard, the canonical AV hooking surface; post-PatchGuard, a protected structure.&quot; },
  { term: &quot;KMCS&quot;, definition: &quot;Kernel-Mode Code Signing; the 64-bit Windows policy that the kernel will load only Authenticode-signed drivers in production.&quot; },
  { term: &quot;CRITICAL_STRUCTURE_CORRUPTION (0x109)&quot;, definition: &quot;The bug-check stop code PatchGuard raises on detecting an unexpected modification to a protected kernel structure.&quot; },
  { term: &quot;KASLR&quot;, definition: &quot;Kernel Address Space Layout Randomisation; probabilistic defense by randomising kernel base address; defeated by side-channel attackers on systems with shared TLB/cache state.&quot; },
  { term: &quot;DSE&quot;, definition: &quot;Driver Signature Enforcement; the policy gate that loads only signed drivers in production. The g_CiEnabled flag is the in-memory gate; flipping it is the canonical BYOVD operation.&quot; },
  { term: &quot;Win32kSystemCallFilter&quot;, definition: &quot;Windows 10 1709+ process-mitigation policy registering a per-process allow-list of win32k system calls; the canonical &apos;attack-surface deletion&apos; mitigation.&quot; },
  { term: &quot;KAISER / KPTI&quot;, definition: &quot;Kernel Page-Table Isolation; the two-CR3 page-table architecture that makes the kernel address space unreachable from user CR3; Linux shipped KPTI in 2018, Microsoft shipped KVA Shadow.&quot; },
  { term: &quot;LPAC&quot;, definition: &quot;Less Privileged AppContainer; a Windows process model that further restricts ambient capabilities. Used by the Chromium renderer in composition with Win32kSystemCallFilter.&quot; },
  { term: &quot;KDP&quot;, definition: &quot;Kernel Data Protection; static via MmProtectDriverSection, dynamic via ExAllocatePool3 with POOL_EXTENDED_PARAMS_SECURE_POOL. Enforced by the Secure Kernel (VTL1) via SLAT.&quot; },
  { term: &quot;VBS / VTL1&quot;, definition: &quot;Virtualization-Based Security; the hypervisor-partitioned architecture in which a Secure Kernel runs at Virtual Trust Level 1, above the NT kernel in VTL0.&quot; },
  { term: &quot;BYOVD&quot;, definition: &quot;Bring-Your-Own-Vulnerable-Driver; the dominant 2026 attacker pattern of installing a signed third-party driver with a kernel-write primitive to obtain arbitrary kernel-mode access.&quot; },
  { term: &quot;KTRR&quot;, definition: &quot;Kernel Text Read-only Region; Apple Silicon&apos;s hardware-enforced read-only kernel text region, locked at boot at the AMCC memory-controller level.&quot; },
  { term: &quot;Reference Monitor (Anderson 1972)&quot;, definition: &quot;The formal anchor for the same-privilege paradox: a security policy must be enforced by a monitor that is always invoked, tamper-resistant from its subjects, and small enough to be analysed.&quot; },
  { term: &quot;HVCI&quot;, definition: &quot;Hypervisor-Protected Code Integrity; the VTL1-anchored W^X enforcement for kernel pages that underpins KDP, kCFG, and kCET.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>windows-internals</category><category>kernel-security</category><category>patchguard</category><category>kaslr</category><category>kdp</category><category>win32k-lockdown</category><category>vbs-hvci</category><category>same-privilege-paradox</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>A Mitigation That Became a Primitive: The Story of SeImpersonatePrivilege</title><link>https://paragmali.com/blog/a-mitigation-that-became-a-primitive-the-story-of-seimperson/</link><guid isPermaLink="true">https://paragmali.com/blog/a-mitigation-that-became-a-primitive-the-story-of-seimperson/</guid><description>How a 2003 backward-compatibility privilege became the most-abused Windows service primitive, and why every Microsoft closure path breaks something shipped.</description><pubDate>Tue, 02 Jun 2026 00:00:00 GMT</pubDate><content:encoded>
Any Windows process running as `IIS APPPOOL\...`, `MSSQLSERVER`, or any other LOCAL SERVICE or NETWORK SERVICE-derived account holds one privilege -- `SeImpersonatePrivilege` -- that is sufficient, given any token-source primitive, to become `NT AUTHORITY\SYSTEM`. The privilege was introduced in Windows Server 2003 as a *mitigation*, so that lower-privileged service accounts could keep impersonating their RPC clients after Microsoft moved services off `SYSTEM`. Eighteen years of named-exploit lineage -- Token Kidnapping (2008), HotPotato (2016), RottenPotato, JuicyPotato, PrintSpoofer, GodPotato, LocalPotato, SilverPotato -- all ride on the same three-piece system: the privilege, the `ImpersonateNamedPipeClient` API, and Microsoft&apos;s documented decision to treat Windows Service Hardening as a *safety* boundary rather than a *security* boundary. This article explains why every closure path Microsoft has shipped narrows the surface without closing it, and why the primitive is structurally undefeated in 2026.
&lt;h2&gt;1. The One Line in &lt;code&gt;whoami /priv&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Open a shell inside any IIS application pool worker, any SQL Server service-step process, or any Exchange worker on a fully patched Windows 11 24H2 or Server 2025 box in 2026, and type &lt;code&gt;whoami /priv&lt;/code&gt;. One line will read:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SeImpersonatePrivilege  Impersonate a client after authentication  Enabled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That single line is sufficient, given the right coercion primitive, to become &lt;code&gt;NT AUTHORITY\SYSTEM&lt;/code&gt; in under a second. Microsoft has known this on the record since April 2009 [@msrc-blog-2009-04-token-kidnapping]. The privilege has not moved.&lt;/p&gt;

A Windows user right that lets a process call any of the kernel&apos;s token-substitution APIs on a token it has received from another principal. The right is enumerated as the constant `SE_IMPERSONATE_NAME` [@ms-learn-privilege-constants]. It is assigned by default to `LOCAL SERVICE`, `NETWORK SERVICE`, the local Administrators group, and every Windows service that runs under one of those accounts [@ms-learn-impersonate-policy].

Two well-known Windows accounts introduced in Windows Server 2003 / XP SP2 as a hardening alternative to running services under `NT AUTHORITY\SYSTEM`. The Microsoft Learn account documentation lists each account&apos;s default privilege set; in both cases `SE_IMPERSONATE_NAME` appears with the marker `(enabled)` [@ms-learn-localservice; @ms-learn-networkservice].
&lt;p&gt;The Microsoft Learn pages list this assignment as a default. &quot;Enabled&quot; is a token-state distinction with operational weight. Most privileges in a service-account token are &lt;em&gt;present but disabled&lt;/em&gt;: the process can call &lt;code&gt;AdjustTokenPrivileges&lt;/code&gt; to turn them on, but until that happens the kernel treats the privilege as absent during access checks. &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; on a NETWORK SERVICE token is shipped &lt;em&gt;enabled&lt;/em&gt;. The process can call &lt;code&gt;CreateProcessWithTokenW&lt;/code&gt; immediately, on first instruction.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; There is a real semantic difference between a privilege that is present-but-disabled and a privilege that is enabled. The kernel checks the &lt;em&gt;enabled&lt;/em&gt; bit during access decisions. A NETWORK SERVICE process does not need to elevate the privilege before using it; the token already has it in the active state. This is the reason a freshly spawned IIS worker is one well-aimed coercion away from SYSTEM, with no preparatory steps.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Andrea Pierini, the researcher who has spent more time with this primitive than anyone outside Microsoft, put the operational fact in eleven words: &quot;if you have SeAssignPrimaryToken or SeImpersonate privilege, you are SYSTEM&quot; [@labro-2020-printspoofer-post]. Clement Labro, quoting him, added the qualifier: &quot;a deliberately provocative shortcut obviously, but it&apos;s not far from the truth.&quot; The aphorism gets repeated in every PrintSpoofer-era writeup for a reason.&lt;/p&gt;
&lt;p&gt;Here is the article&apos;s load-bearing claim, stated up front and re-argued through every section that follows:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Microsoft gave every NETWORK SERVICE a privilege that, in the wrong hands, is equivalent to SYSTEM. They knew. They could not change it without breaking the service model. Roughly eighteen years after Cerrudo first put that fact on the record -- and ten years after HotPotato made it pushbutton -- they still have not.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The figure &quot;roughly eighteen years&quot; anchors to Cesar Cerrudo&apos;s March 2008 disclosure at Hack In The Box Dubai [@cerrudo-2008-pdf]. The privilege itself shipped earlier, in Server 2003 / XP SP2 (2003-2004), and the operational-pushbutton anchor is Stephen Breen&apos;s HotPotato (January 16, 2016) [@breen-2016-hot-potato]. Three different dates, three different anchors for &quot;how long has this been true.&quot; The article uses the Cerrudo date because that is when the fact entered the offensive-research public record.&lt;/p&gt;
&lt;p&gt;From here, this article traces the privilege from a 2003 backward-compatibility concession to a 2024 Troopers articulation by Pierini and Cocomazzi, and explains why every closure path Microsoft has shipped narrows the surface without closing it.&lt;/p&gt;
&lt;p&gt;{`
// On a Windows service account, this is the line that matters:
const tokenPrivileges = [
  { name: &apos;SeAssignPrimaryTokenPrivilege&apos;, state: &apos;Disabled&apos; },
  { name: &apos;SeIncreaseQuotaPrivilege&apos;,      state: &apos;Disabled&apos; },
  { name: &apos;SeAuditPrivilege&apos;,              state: &apos;Disabled&apos; },
  { name: &apos;SeChangeNotifyPrivilege&apos;,       state: &apos;Enabled&apos;  },
  { name: &apos;SeImpersonatePrivilege&apos;,        state: &apos;Enabled&apos;  },  // &amp;lt;-- the gate
  { name: &apos;SeCreateGlobalPrivilege&apos;,       state: &apos;Enabled&apos;  },
];&lt;/p&gt;
&lt;p&gt;const gateOpen = tokenPrivileges.some(
  p =&amp;gt; p.name === &apos;SeImpersonatePrivilege&apos; &amp;amp;&amp;amp; p.state === &apos;Enabled&apos;
);
console.log(gateOpen ? &apos;Gate is open. Token-source primitive is the only missing piece.&apos; : &apos;Gate is closed.&apos;);
`}&lt;/p&gt;
&lt;p&gt;If one line in &lt;code&gt;whoami /priv&lt;/code&gt; is sufficient to become SYSTEM, why does Microsoft ship that line as the default for every IIS application pool, every SQL Server service step, and every Exchange worker process on every shipping Windows release? The answer is not a mistake. It is a decision -- and to understand it we need to go back to a Tymshare FORTRAN compiler in the late 1970s, around 1977 by Hardy&apos;s own &quot;about eleven years ago&quot; dating from his 1988 paper.&lt;/p&gt;
&lt;h2&gt;2. Hardy&apos;s Deputy and the 2003 Service-Hardening Pivot&lt;/h2&gt;
&lt;p&gt;In the late 1970s, around 1977, a Tymshare engineer named Norm Hardy watched a FORTRAN compiler with &quot;home files license&quot; overwrite the system billing file &lt;code&gt;(SYSX)BILL&lt;/code&gt; because some user had passed that path as the compiler&apos;s debug-output target. The compiler had two authorities -- its own (to read system libraries) and the caller&apos;s (to write the caller&apos;s files) -- and no way to keep them separate when serving a request. The compiler was, in Hardy&apos;s later phrasing, &lt;em&gt;confused&lt;/em&gt; about which authority to use [@hardy-1988].&lt;/p&gt;

A program that holds authority on behalf of two or more principals at once and has no architectural way to keep those authorities separate when acting on a request. Hardy&apos;s 1988 paper [@hardy-1988] argues that any identity-and-ACL system in which a server holds more authority than its clients and acts on client requests has a confused-deputy attack surface by construction. The only complete defence, Hardy argues, is capability-based access control.
&lt;p&gt;Hardy&apos;s argument generalises: as long as authority flows ambiently with identity rather than being passed explicitly with each request, a server cannot reliably tell whose authority a given request should run under. This is not a bug class. It is a structural property of the access-matrix model Lampson formalised in 1971 [@lampson-1971]. Windows is an instance of that model. A NETWORK SERVICE process holding &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; is &lt;em&gt;Hardy&apos;s deputy&lt;/em&gt;: it carries two authorities at once (its own modest service identity and whatever caller just connected to its named pipe), and Windows has no in-architecture way to keep them apart.&lt;/p&gt;

Capability systems -- EROS, Coyotos, seL4 -- bind authority to operations rather than to running identities. A capability is an unforgeable token that names both an object and the rights you have on it; you cannot exercise authority you were not handed. In a capability system, Hardy&apos;s compiler would have been handed a capability only for the file the caller actually wanted opened, and the bill-overwrite would have been mechanically prevented. Windows shipped the alternative design in 1993 -- identity-and-ACL with kernel tokens carrying ambient authority -- and the rest of this article is, in a precise sense, the story of what that design costs eighteen years on. Section 8 returns to this thread.
&lt;h3&gt;2.1 The kernel object Cutler&apos;s team shipped in 1993&lt;/h3&gt;
&lt;p&gt;Dave Cutler&apos;s NT 3.1 team chose the identity-and-ACL model and built a kernel object to carry it. The &lt;em&gt;access token&lt;/em&gt; is what an NT thread or process holds; it enumerates the user SID, the group SIDs, and the privileges currently associated with the running code. Every access check the kernel performs reduces to &quot;does this token, evaluated against this object&apos;s ACL, grant the requested rights?&quot; The standard reference is &lt;em&gt;Windows Internals&lt;/em&gt;, Part 1, chapter on security [@ms-learn-windows-internals].&lt;/p&gt;

A kernel object the Windows security subsystem creates at logon (and clones on demand). It carries the user SID, group SIDs, privileges, integrity level, and impersonation level for a running thread or process. Tokens come in two flavours: *primary* (attached to a process at creation) and *impersonation* (attached to a thread to make it temporarily act as another identity).
&lt;p&gt;NT 3.1 also shipped two structural distinctions that the rest of this article depends on. First, &lt;em&gt;primary&lt;/em&gt; versus &lt;em&gt;impersonation&lt;/em&gt; tokens -- a primary token is what a process is born with; an impersonation token is what a thread can wear temporarily to act on behalf of someone else. Second, the four &lt;em&gt;impersonation levels&lt;/em&gt; (Anonymous, Identification, Impersonation, Delegation), each granting progressively more authority to act under the borrowed identity. Both distinctions exist because servers need to act on client requests under the client&apos;s authority -- and both distinctions are the surface every Potato variant operates on.&lt;/p&gt;
&lt;p&gt;The Tymshare anecdote that Hardy uses in the 1988 paper -- the FORTRAN compiler that overwrote &lt;code&gt;(SYSX)BILL&lt;/code&gt; -- is worth recounting in full because it is structurally identical to the Windows scenario. A user invoked the compiler with the billing information file as the debug-output target. The compiler had write access to system files (it was a &quot;home files license&quot; service). The compiler dutifully opened the user-supplied path under its own authority and wrote debug output to it, destroying the bill. The compiler was not malicious; it had no way to ask the OS to scope its write to &quot;only files the caller could write.&quot; Hardy&apos;s own dating in the paper is &quot;about eleven years ago&quot; from 1988 -- so the events sit in the late 1970s, not the early ones.&lt;/p&gt;
&lt;h3&gt;2.2 Why the privilege exists: the 2003 service-hardening pivot&lt;/h3&gt;
&lt;p&gt;Through the 1990s, Windows services almost universally ran under &lt;code&gt;NT AUTHORITY\SYSTEM&lt;/code&gt;. The convenience was operational: SYSTEM is the local-machine principal and holds every right the kernel knows about, so a service running as SYSTEM never needed an explicit privilege grant. The cost became visible in 2001-2003 as the first generation of service-borne worms hit production: Code Red and Nimda (2001) walked IIS; SQL Slammer and MSBlast (2003) walked SQL Server and the DCOM RPC endpoint [@wikipedia-timeline-worms]. Every successful remote code execution against a service became a SYSTEM compromise of the host, because the service &lt;em&gt;was&lt;/em&gt; SYSTEM.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s response was a structural retreat. Two new well-known accounts shipped in Windows Server 2003 (and reached desktop with XP SP2 in 2004): &lt;code&gt;NT AUTHORITY\LOCAL SERVICE&lt;/code&gt; (no network credentials) and &lt;code&gt;NT AUTHORITY\NETWORK SERVICE&lt;/code&gt; (machine-account credentials when authenticating off-box). The two account documentation pages enumerate the default privileges the SCM assigns when a service is configured to run under either account [@ms-learn-localservice; @ms-learn-networkservice]. Most of the SYSTEM-only privileges -- &lt;code&gt;SeTcbPrivilege&lt;/code&gt;, &lt;code&gt;SeLoadDriverPrivilege&lt;/code&gt;, &lt;code&gt;SeRestorePrivilege&lt;/code&gt; -- are absent from the enumerated default sets [@ms-learn-localservice; @ms-learn-networkservice]. The intent was clear: a worm-popped IIS worker should land as a low-privileged process, not as SYSTEM.&lt;/p&gt;
&lt;p&gt;But the new accounts could not lose &lt;em&gt;every&lt;/em&gt; SYSTEM authority. Pre-2003 services routinely impersonated their clients to make access checks against per-user resources -- IIS reading a user&apos;s home directory under the user&apos;s identity, SQL Server enforcing per-login row security, the SMB server returning per-user file lists. That entire pattern depended on the service being able to call &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; (or &lt;code&gt;RpcImpersonateClient&lt;/code&gt;, or one of the LSA-side APIs) and then act under the caller&apos;s token. If LOCAL SERVICE and NETWORK SERVICE could not impersonate, the entire RPC server population would break.&lt;/p&gt;
&lt;p&gt;So Microsoft introduced &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; -- a new named user right gating the impersonation APIs -- and assigned it by default to LOCAL SERVICE, NETWORK SERVICE, the local Administrators group, and (via the SCM&apos;s auto-grant logic) every service configured to run under one of those accounts [@ms-learn-impersonate-policy]. The policy-setting page is explicit about the intent: &quot;If this user right is required for this type of impersonation, an unauthorized user cannot cause a client to connect (for example, by remote procedure call (RPC) or named pipes) to a service that they have created to impersonate that client&quot; [@ms-learn-impersonate-policy].&lt;/p&gt;
&lt;p&gt;The privilege, in other words, was created &lt;em&gt;as a mitigation&lt;/em&gt;. Its purpose was to keep impersonation working for legitimate service-account RPC servers while denying it to ordinary user processes. That decision -- to gate impersonation on an explicit named right rather than to forbid impersonation outright -- is the architectural pivot the rest of this article re-examines from every angle.&lt;/p&gt;

flowchart TD
    Client[&quot;Low-privileged caller&quot;] -- &quot;Connects to attacker pipe&quot; --&amp;gt; NS[&quot;NETWORK SERVICE process&quot;]
    NS -- &quot;Holds its own modest authority&quot; --&amp;gt; A1[&quot;Authority 1, service identity&quot;]
    NS -- &quot;Holds SeImpersonatePrivilege&quot; --&amp;gt; A2[&quot;Authority 2, any token it receives&quot;]
    SYSPROC[&quot;Privileged caller, SYSTEM&quot;] -- &quot;Coerced to authenticate to the pipe&quot; --&amp;gt; NS
    NS -- &quot;Impersonate caller token, then act&quot; --&amp;gt; Action[&quot;Action runs under SYSTEM&quot;]
&lt;p&gt;Microsoft did not introduce &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; to enable an exploit. They introduced it as a backward-compatibility concession. So why did the privilege become the dominant lineage of service-to-SYSTEM elevation for nearly two decades? The answer starts with the API surface.&lt;/p&gt;
&lt;h2&gt;3. The Token API Surface&lt;/h2&gt;
&lt;p&gt;There is no single &quot;impersonate&quot; API on Windows. There are four substitution APIs that put a token on a thread or a new process, and one coercion API that supplies the token in the first place. The Potato family lives at the intersection of all five.&lt;/p&gt;
&lt;h3&gt;3.1 Primary versus impersonation tokens&lt;/h3&gt;
&lt;p&gt;The kernel distinguishes &lt;code&gt;TOKEN_PRIMARY&lt;/code&gt; from &lt;code&gt;TOKEN_IMPERSONATION&lt;/code&gt;. A primary token is what a process is created with; an impersonation token can be attached only to a thread. The distinction matters operationally because only an impersonation token at level &lt;code&gt;SecurityImpersonation&lt;/code&gt; or &lt;code&gt;SecurityDelegation&lt;/code&gt; lets you take real action under the borrowed identity. An &lt;code&gt;Identification&lt;/code&gt;-level token can be checked against ACLs but cannot be used to open kernel objects under the new identity, and an &lt;code&gt;Anonymous&lt;/code&gt;-level token is useless for almost everything [@ms-learn-windows-internals; @ms-learn-impersonateloggedonuser].&lt;/p&gt;

A *primary token* is created at logon and attached to a process for its lifetime; the kernel uses it for every access check the process makes by default. An *impersonation token* is attached to an individual thread by `SetThreadToken` (or by an impersonation API that calls it internally) and overrides the primary token for that thread only. The kernel reserves the right to demote impersonation tokens to `Identification` level in cross-machine RPC scenarios where delegation has not been explicitly negotiated.

A four-value enum -- `SecurityAnonymous`, `SecurityIdentification`, `SecurityImpersonation`, `SecurityDelegation` -- carried on every impersonation token. It limits what the impersonating thread can do under the borrowed identity. `SecurityImpersonation` is the level a service can act under for local access checks; `SecurityDelegation` extends that to off-box authentication and is the level the LocalPotato class occasionally reaches.
&lt;p&gt;The Potato lineage navigates these four levels with care. &lt;code&gt;Identification&lt;/code&gt; is harmless because it cannot spawn a process under the borrowed identity; &lt;code&gt;Impersonation&lt;/code&gt; is the level a service can act under for any local kernel object; &lt;code&gt;Delegation&lt;/code&gt; is what cross-host variants such as SilverPotato sometimes need.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;SecurityIdentification&lt;/code&gt; versus &lt;code&gt;SecurityImpersonation&lt;/code&gt; distinction is the gate that makes many naive coercion attempts fail. If the attacker controls only an RPC interface that performs an &lt;code&gt;ImpersonateClient&lt;/code&gt; call without the right SQOS (Security Quality of Service) negotiation, the resulting token may land at &lt;code&gt;SecurityIdentification&lt;/code&gt; -- usable for &lt;code&gt;AccessCheck&lt;/code&gt;, useless for &lt;code&gt;CreateProcessWithTokenW&lt;/code&gt;. Each Potato variant either chooses a coercion primitive that arrives at &lt;code&gt;SecurityImpersonation&lt;/code&gt; or upgrades the token via a subsequent &lt;code&gt;DuplicateTokenEx&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;3.2 The substitution primitives&lt;/h3&gt;
&lt;p&gt;Four APIs move tokens around the system. None of them produces a token from nothing; all of them assume the caller already has a handle to one.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SetThreadToken&lt;/code&gt; -- attach an impersonation token to a thread [@ms-learn-setthreadtoken]. The thread now runs under the borrowed identity for every subsequent access check.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ImpersonateLoggedOnUser&lt;/code&gt; -- the thread-level convenience wrapper [@ms-learn-impersonateloggedonuser]. Same effect as &lt;code&gt;SetThreadToken&lt;/code&gt;, with simpler arguments.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DuplicateTokenEx&lt;/code&gt; -- create a new token from an existing one, with adjustable type (primary vs impersonation) and level (the four-value enum above) [@ms-learn-duplicatetokenex]. The Potato lineage uses this to convert an impersonation token into a primary one before launching a process.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CreateProcessWithTokenW&lt;/code&gt; -- spawn a new process under an arbitrary primary token [@ms-learn-createprocesswithtokenw]. The Microsoft Learn documentation is explicit about the gate: &quot;The process that calls &lt;strong&gt;CreateProcessWithTokenW&lt;/strong&gt; must have the &lt;code&gt;SE_IMPERSONATE_NAME&lt;/code&gt; privilege.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last sentence is the keystone. &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; is not just &quot;the right to impersonate.&quot; It is the right to convert an impersonated identity into a fresh process that owns the desktop, the registry, the file system, and every other kernel object the borrowed identity has authority over. Without the privilege, the attacker has a thread temporarily wearing SYSTEM&apos;s hat; with it, the attacker has &lt;code&gt;cmd.exe&lt;/code&gt; running as SYSTEM until the system reboots.&lt;/p&gt;
&lt;h3&gt;3.3 The coercion primitive&lt;/h3&gt;
&lt;p&gt;The three substitution primitives are inert without a token to substitute. The dominant token source on Windows is the named-pipe server primitive &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt;, shipped since Windows XP / Server 2003 [@ms-learn-impersonatenamedpipeclient]. Any process that owns a named pipe can call this API after a client connects; the impersonating thread then wears the caller&apos;s token at whatever impersonation level the caller&apos;s SQOS negotiated.&lt;/p&gt;

A Win32 API that copies the connected client&apos;s access token onto the calling thread, after which the thread acts under the client&apos;s identity until `RevertToSelf` is called. The API has shipped since Windows XP / Server 2003 [@ms-learn-impersonatenamedpipeclient]. It is the load-bearing token source for every Potato variant from HotPotato through GodPotato. Calling the API at higher than `SecurityIdentification` requires `SeImpersonatePrivilege` on the caller.
&lt;p&gt;This is the four-step chain every Potato operator runs, as enumerated in Forshaw&apos;s 2021 Project Zero retrospective on the lineage [@forshaw-2021-10-relaying-dcom-pz]:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;CreateNamedPipe(&quot;\\.\pipe\&amp;lt;attacker_name&amp;gt;&quot;)&lt;/code&gt; -- a service-account process opens a pipe it controls.&lt;/li&gt;
&lt;li&gt;Induce some privileged Windows component to authenticate to that pipe.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; -- the impersonating thread now wears the caller&apos;s token.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DuplicateTokenEx&lt;/code&gt; to primary; &lt;code&gt;CreateProcessWithTokenW(cmd.exe)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

sequenceDiagram
    participant Atk as Attacker, service account
    participant Pipe as Named pipe attacker controls
    participant Sys as Privileged caller, SYSTEM-context
    Atk-&amp;gt;&amp;gt;Pipe: CreateNamedPipe and listen
    Atk-&amp;gt;&amp;gt;Sys: Trigger coercion primitive
    Sys-&amp;gt;&amp;gt;Pipe: Authenticate to the pipe
    Atk-&amp;gt;&amp;gt;Pipe: ImpersonateNamedPipeClient
    Atk-&amp;gt;&amp;gt;Atk: DuplicateTokenEx, impersonation to primary
    Atk-&amp;gt;&amp;gt;Atk: CreateProcessWithTokenW cmd.exe
    Note over Atk: cmd.exe now running as SYSTEM
&lt;p&gt;Step three depends on step two. Step two is the open question every generation of Potato has answered differently -- and that Microsoft has patched, one token source at a time, for nearly two decades.&lt;/p&gt;
&lt;p&gt;{`
// Pseudocode showing the four-step Potato chain.
// Privilege checks shown as comments where the kernel enforces them.&lt;/p&gt;
&lt;p&gt;function impersonationChain(coercionTrigger) {
  const pipe = createNamedPipe(&quot;\\.\pipe\demo&quot;);            // no privilege required
  coercionTrigger(pipe);                                          // induce SYSTEM to connect
  pipe.waitForConnect();&lt;/p&gt;
&lt;p&gt;  // kernel allows SecurityImpersonation only if caller has SeImpersonatePrivilege:
  const callerToken = pipe.impersonateNamedPipeClient();&lt;/p&gt;
&lt;p&gt;  const primary = duplicateTokenEx(callerToken, &quot;primary&quot;,
                                   &quot;SecurityImpersonation&quot;);      // no privilege required&lt;/p&gt;
&lt;p&gt;  // kernel gate: requires SE_IMPERSONATE_NAME on the calling process:
  return createProcessWithTokenW(primary, &quot;cmd.exe&quot;);
}
`}&lt;/p&gt;
&lt;h3&gt;3.4 The privilege next to it&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;CreateProcessWithTokenW&lt;/code&gt; is gated on &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt;. Its sibling &lt;code&gt;CreateProcessAsUser&lt;/code&gt; is gated on a &lt;em&gt;different&lt;/em&gt; pair of privileges -- &lt;code&gt;SeAssignPrimaryTokenPrivilege&lt;/code&gt; (constant name &lt;code&gt;SE_ASSIGNPRIMARYTOKEN_NAME&lt;/code&gt;) when the supplied token is not assignable by the caller, plus &lt;code&gt;SeIncreaseQuotaPrivilege&lt;/code&gt; (&lt;code&gt;SE_INCREASE_QUOTA_NAME&lt;/code&gt;) in all cases. Both are enumerated separately in the privilege-constants table [@ms-learn-privilege-constants]. On a NETWORK SERVICE or LOCAL SERVICE token, &lt;code&gt;SE_ASSIGNPRIMARYTOKEN_NAME&lt;/code&gt; and &lt;code&gt;SE_INCREASE_QUOTA_NAME&lt;/code&gt; are both &lt;em&gt;present but disabled&lt;/em&gt; [@ms-learn-localservice; @ms-learn-networkservice]: a service-account process must call &lt;code&gt;AdjustTokenPrivileges&lt;/code&gt; to enable them before &lt;code&gt;CreateProcessAsUser&lt;/code&gt; will succeed, whereas &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; is shipped &lt;em&gt;enabled&lt;/em&gt; and &lt;code&gt;CreateProcessWithTokenW&lt;/code&gt; works on the first instruction. Pierini&apos;s aphorism quoted in section 1 names both privileges because either one independently makes the same chain runnable -- but on a vanilla NETWORK SERVICE token, only &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; is enabled, and the rest of this article treats it as the privilege that matters in practice.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Privilege required&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none for &lt;code&gt;SecurityIdentification&lt;/code&gt; or &lt;code&gt;SecurityAnonymous&lt;/code&gt;; for higher levels, either &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt;, or the token was created with explicit credentials via &lt;code&gt;LogonUser&lt;/code&gt;/&lt;code&gt;LsaLogonUser&lt;/code&gt; from within the caller&apos;s logon session, or the authenticated identity is the same as the caller (see [@ms-learn-impersonatenamedpipeclient])&lt;/td&gt;
&lt;td&gt;connected pipe handle&lt;/td&gt;
&lt;td&gt;impersonation token on thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ImpersonateLoggedOnUser&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none (caller must already hold the token)&lt;/td&gt;
&lt;td&gt;token handle&lt;/td&gt;
&lt;td&gt;impersonation token on thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SetThreadToken&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;depends on token level&lt;/td&gt;
&lt;td&gt;token handle&lt;/td&gt;
&lt;td&gt;impersonation token on thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DuplicateTokenEx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;source token&lt;/td&gt;
&lt;td&gt;new token, type/level adjustable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CreateProcessWithTokenW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;SeImpersonatePrivilege&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;primary token + command line&lt;/td&gt;
&lt;td&gt;new process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CreateProcessAsUser&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;SeAssignPrimaryTokenPrivilege&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;primary token + command line&lt;/td&gt;
&lt;td&gt;new process&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

flowchart LR
    Process[&quot;Process, holds primary token&quot;]
    Thread[&quot;Thread, optional impersonation token&quot;]
    NewProc[&quot;New process, spawned with chosen primary token&quot;]
    Process -- &quot;OpenProcessToken, read&quot; --&amp;gt; TH[&quot;Token handle&quot;]
    TH -- &quot;SetThreadToken or ImpersonateLoggedOnUser&quot; --&amp;gt; Thread
    Thread -- &quot;GetThreadToken&quot; --&amp;gt; TH
    TH -- &quot;DuplicateTokenEx, impersonation to primary&quot; --&amp;gt; PT[&quot;Primary token handle&quot;]
    PT -- &quot;CreateProcessWithTokenW, gated on SeImpersonatePrivilege&quot; --&amp;gt; NewProc
    Pipe[&quot;Connected named pipe&quot;] -- &quot;ImpersonateNamedPipeClient, gated on SeImpersonatePrivilege beyond SecurityIdentification&quot; --&amp;gt; Thread
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The five-API surface decomposes cleanly into two halves. &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; is the kernel-side &lt;em&gt;gate&lt;/em&gt; that decides whether a process can substitute an arbitrary primary token into a new process. &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; is the user-mode &lt;em&gt;source&lt;/em&gt; that provides the token in the first place. Closing one half closes the surface. Closing neither half is the choice Microsoft has shipped for twenty years.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So how do you get a SYSTEM-context Windows process to authenticate to a pipe you control? Cesar Cerrudo asked that question in 2008 -- and his answer was just the first of five.&lt;/p&gt;
&lt;h2&gt;4. Five Generations of Token Sources, One Constant Privilege&lt;/h2&gt;
&lt;p&gt;Cesar Cerrudo had the privilege figured out in March 2008. So why did it take until January 2016 for HotPotato to make the chain pushbutton, until July 2018 for JuicyPotato to industrialise it, and until December 2022 for GodPotato to bypass the most aggressive DCOM hardening Microsoft has shipped? Because every generation answered the same question -- &lt;em&gt;where do the tokens come from?&lt;/em&gt; -- differently, and Microsoft patched each token source one at a time.&lt;/p&gt;
&lt;p&gt;This section is &lt;em&gt;generation-level&lt;/em&gt;. The variant-by-variant chronology of every named Potato lives in the &lt;a href=&quot;https://paragmali.com/blog/system-in-ten-seconds-how-the-potato-family-survived-every-m/&quot; rel=&quot;noopener&quot;&gt;sibling Potato Family article&lt;/a&gt; (2026-05-31); here, variants appear only as evidence for claims about the primitive.&lt;/p&gt;
&lt;h3&gt;4.1 Generation 1, direct token theft (2008-2010)&lt;/h3&gt;
&lt;p&gt;Cerrudo&apos;s HITB Dubai 2008 paper, &lt;em&gt;Token Kidnapping&lt;/em&gt;, named the privilege and named the technique [@cerrudo-2008-pdf]. The chain ran inside an MSSQL or IIS process and looked like this: enumerate processes the service account could open; find a thread that was already impersonating a higher-privileged token (typically leaked by some service-startup path); &lt;code&gt;DuplicateTokenEx&lt;/code&gt; that token; &lt;code&gt;CreateProcessWithTokenW&lt;/code&gt; to spawn &lt;code&gt;cmd.exe&lt;/code&gt; under the new identity. Two years later, at DEF CON 18, Cerrudo presented &lt;em&gt;Token Kidnapping&apos;s Revenge&lt;/em&gt; with fresh examples and a community-canonical title for the technique [@cerrudo-2010-defcon].&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s response was MS09-012 in April 2009 (community-known as the &lt;em&gt;Chimichurri&lt;/em&gt; fix, after Cesar Cerrudo&apos;s PoC of the same name shipped by Argeniss alongside the disclosure [@webarchive-argeniss-chimichurri; @forshaw-2020-01-empirical-wsh]). The MSRC blog post announcing the bulletin is unusually clear about what it closed and what it deliberately did not:&lt;/p&gt;

An attacker can escalate their privileges on a system if they can control the SeImpersonatePrivilege token. An attacker would need to be executing code in the context of a Windows service to use this exploit. -- MSRC blog, April 14, 2009 [@msrc-blog-2009-04-token-kidnapping]
&lt;p&gt;The MSRC text continues: &quot;the first update addresses service isolation, while the second addresses processes running as service accounts&quot; [@msrc-blog-2009-04-token-kidnapping]. &lt;em&gt;Service isolation&lt;/em&gt;, not the privilege itself. The bulletin closed the specific handle-leak surface Cerrudo had used -- it did not revoke &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; from NETWORK SERVICE, did not modify &lt;code&gt;CreateProcessWithTokenW&lt;/code&gt;, did not modify &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt;. The MSRC acknowledged on the record that the privilege was sufficient for the escalation and elected to fix the &lt;em&gt;symptom&lt;/em&gt; (the leak surface), not the &lt;em&gt;gate&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;This is the supersession pattern that every subsequent generation follows: Microsoft patches the current token source; the next generation finds a new one within months.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Chimichurri&lt;/em&gt; (sometimes &lt;code&gt;Chimichurri.exe&lt;/code&gt;) is not a Microsoft codename. It is the name Cesar Cerrudo gave to the PoC exploit Argeniss released alongside the MS09-012 bulletin, hosted at the time at &lt;code&gt;argeniss.com/research/Chimichurri_CesarCerrudo.zip&lt;/code&gt; and preserved in the Internet Archive [@webarchive-argeniss-chimichurri]. Microsoft&apos;s own naming for the bulletin is simply MS09-012 / KB959454. Offensive-research convention has used &quot;Chimichurri&quot; as shorthand for the Cerrudo PoC ever since -- never for a Microsoft internal codename. Forshaw&apos;s January 2020 service-hardening retrospective references the same Cerrudo / Argeniss lineage [@forshaw-2020-01-empirical-wsh].&lt;/p&gt;
&lt;p&gt;Cerrudo presented the 2008 paper under his Argeniss affiliation and the 2010 DEF CON talk under IOActive [@cerrudo-2008-pdf; @cerrudo-2010-defcon]. The affiliation change occasionally trips up archival cross-referencing -- the work is the same lineage.&lt;/p&gt;
&lt;h3&gt;4.2 Generation 2, local NTLM cross-protocol reflection (2014-2016)&lt;/h3&gt;
&lt;p&gt;In December 2014, James Forshaw filed Project Zero Issue 222 -- a WebDAV-to-SMB local NTLM reflection that turned the Windows authentication redirector into a self-service token source. Stephen Breen&apos;s &lt;em&gt;HotPotato&lt;/em&gt; (January 16, 2016) used a related local-NTLM-relay primitive to deliver the first end-to-end service-account-to-SYSTEM chain that did not depend on finding a leaked token handle [@breen-2016-hot-potato]. Breen credits the genealogy openly: &quot;If this sounds vaguely familiar, it&apos;s because a similar technique was disclosed by the guys at Google Project Zero . . . In fact, some of our code was shamelessly borrowed from their PoC and expanded upon&quot; [@breen-2016-hot-potato].&lt;/p&gt;
&lt;p&gt;The conceptual leap is the one every subsequent generation depends on. Cerrudo&apos;s G1 had to &lt;em&gt;find&lt;/em&gt; a high-privileged token leaked into the local process tree; Breen&apos;s G2 &lt;em&gt;makes the system hand you one&lt;/em&gt; by coercing it to authenticate. The system itself becomes the token source. Forshaw articulated this generalisation explicitly in the 2021 Project Zero retrospective on the entire lineage [@forshaw-2021-10-relaying-dcom-pz].&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s response was MS16-075 (the SMB-side fix) and a handful of WPAD-hardening rollups. The chain became fragile and stopped being pushbutton -- but, again, none of these changes touched &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; or &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;4.3 Generation 3, local DCOM activation (2016-2018)&lt;/h3&gt;
&lt;p&gt;Within months of HotPotato, the community converged on a more reliable coercion primitive: a forged DCOM &lt;code&gt;OBJREF&lt;/code&gt; marshalled with an attacker-chosen OXID resolver. The trick induces a SYSTEM-context COM server to authenticate to a named pipe the attacker controls. Forshaw had reported the underlying primitive at Project Zero in 2015 as Issue 325, fixed as CVE-2015-2370 [@nvd-cve-2015-2370], but as his 2021 retrospective notes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The technique to locally relay authentication for DCOM was something I originally reported back in 2015 (issue 325). This issue was fixed as CVE-2015-2370, however the underlying authentication relay using DCOM remained. This was repurposed and expanded upon by various others for local and remote privilege escalation in the RottenPotato series of exploits, the latest in that line being RemotePotato which is currently unpatched as of October 2021.&quot; [@forshaw-2021-10-relaying-dcom-pz]&lt;/p&gt;
&lt;/blockquote&gt;

The DCOM service that maps an OXID (Object Exporter Identifier) to the RPC binding string a client uses to call methods on a marshalled COM object. The &quot;Rotten&quot; and &quot;Juicy&quot; Potato families forge `OBJREF` marshalled blobs in which the OXID resolver field points back at an attacker-controlled endpoint, causing the SYSTEM-context RPCSS to authenticate to the attacker&apos;s pipe when it tries to resolve the OXID.
&lt;p&gt;RottenPotato (September 26, 2016) demonstrated the chain [@foxglove-2016-09-rotten-potato]; JuicyPotato (July 2018) industrialised it with a configurable CLSID table and reliable pipe handling. The canonical mirror for the JuicyPotato repository is the &lt;code&gt;ohpe/juicy-potato&lt;/code&gt; GitHub project [@ohpe-juicy-potato-repo]. Crucially, the load-bearing API was still &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; -- the DCOM trick is just the &lt;em&gt;vehicle&lt;/em&gt; that delivers a SYSTEM-context authentication to the attacker&apos;s pipe.&lt;/p&gt;
&lt;h3&gt;4.4 Generation 4, coercion APIs beyond DCOM (2020-2024)&lt;/h3&gt;
&lt;p&gt;Clement Labro (itm4n) shipped PrintSpoofer on April 28, 2020 [@labro-2020-printspoofer-post; @itm4n-printspoofer-repo]. The coercion primitive was MS-RPRN&apos;s &lt;code&gt;RpcRemoteFindFirstPrinterChangeNotificationEx&lt;/code&gt; -- an RPC method on the Print Spooler that takes an attacker-supplied UNC-like notification target and authenticates to it under the Spooler&apos;s SYSTEM identity. PrintSpoofer needed neither DCOM nor any leaked handle; the coercion primitive lived inside a always-running Windows service.&lt;/p&gt;
&lt;p&gt;PrintSpoofer generalised. Researchers quickly mapped a family of Windows RPC interfaces with the same shape -- an RPC method that takes an attacker-supplied path and resolves it server-side under a privileged identity. MS-EFSR (the Encrypting File System remote protocol) gave EfsPotato and SharpEfsPotato -- the canonical fork is &lt;code&gt;bugch3ck/SharpEfsPotato&lt;/code&gt; [@bugch3ck-sharpefspotato-repo], not the &lt;code&gt;ly4k&lt;/code&gt; mirror. MS-FSRVP, MS-DFSNM, and a long tail followed. CoercedPotato&apos;s &lt;code&gt;--interface {ms-rprn, ms-efsr}&lt;/code&gt; switch operationalises the enumeration in a single tool [@prepouce-coercedpotato-repo]; the project&apos;s MS-EFSR catalogue alone lists fourteen entry points (indices 0-13, with two marked NOT WORKING).&lt;/p&gt;
&lt;p&gt;The pattern is clear at this point: the privilege is the constant; the coercion primitive is interchangeable. Microsoft has shipped per-CVE patches for individual coercion APIs (the &lt;a href=&quot;https://paragmali.com/blog/three-years-of-printnightmare-how-the-oldest-windows-service/&quot; rel=&quot;noopener&quot;&gt;PrintNightmare cluster&lt;/a&gt; around MS-RPRN, anchored by CVE-2021-34527 [@nvd-cve-2021-34527]; targeted MS-EFSR fixes), but no commitment to enumerate or class-close the surface.&lt;/p&gt;
&lt;h3&gt;4.5 Generation 5, into RPCSS itself (2022-2024)&lt;/h3&gt;
&lt;p&gt;In December 2022, the researcher who goes by BeichenDream published GodPotato, with a README that names the structural defect plainly:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Based on the history of Potato privilege escalation for 6 years, from the beginning of RottenPotato to the end of JuicyPotatoNG, I discovered a new technology by researching DCOM, which enables privilege escalation in Windows 2012 - Windows 2022, now as long as you have &lt;code&gt;ImpersonatePrivilege&lt;/code&gt; permission. Then you are &lt;code&gt;NT AUTHORITY\SYSTEM&lt;/code&gt; . . . There are some defects in rpcss when dealing with oxid, and rpcss is a service that must be opened by the system.&quot; [@beichendream-godpotato-readme]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;GodPotato survives every phase of CVE-2021-26414 (the three-phase DCOM hardening, rolled out 2021-06-08, 2022-06-14, 2023-03-14) [@nvd-cve-2021-26414] because the defect is in RPCSS&apos;s OXID &lt;em&gt;handling&lt;/em&gt;, not in DCOM &lt;em&gt;activation&lt;/em&gt;. The other structural half of the defect is documented by Forshaw in April 2020: &quot;When LSASS creates a Token for a new Logon session it stores that Token for later retrieval . . . in this case it does matter as it means that the negotiated Token on the server, which is the same machine, will actually be the session&apos;s Token, not the caller&apos;s Token&quot; [@forshaw-2020-04-sharing-logon-session]. Together those two structural properties keep GodPotato functional across the README&apos;s tested matrix -- Server 2012 through Server 2022, Windows 8 through Windows 11 -- and no public Microsoft patch has been issued for the underlying defect through mid-2026 [@beichendream-godpotato-readme].&lt;/p&gt;
&lt;p&gt;LocalPotato (February 2023) is the parallel branch: Antonio Cocomazzi and Andrea Pierini discovered that the NTLM Type-2 &quot;Reserved&quot; field could be used to swap context handles during local authentication, escalating from an &lt;em&gt;unprivileged&lt;/em&gt; user -- the first variant in the lineage that does not require &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; to start [@cocomazzi-pierini-2023-localpotato-post]. Microsoft fixed it as CVE-2023-21746 [@nvd-cve-2023-21746], but the conceptual proof remains: the local NTLM stack itself is an attacker-controllable token source.&lt;/p&gt;
&lt;p&gt;SilverPotato (April 24, 2024) extended the family across hosts [@pierini-2024-silverpotato-post]. Members of the Distributed COM Users or Performance Log Users groups trigger remote activation of the &lt;code&gt;sppui&lt;/code&gt; DCOM application (CLSID &lt;code&gt;{F87B28F1-DA9A-4F35-8EC0-800EFCF26B83}&lt;/code&gt;) on a target server. The coerced Domain Admin authentication is then chained through SMB relay to the &lt;a href=&quot;https://paragmali.com/blog/certified-pre-owned-ad-cs-and-active-directorys-second-trust/&quot; rel=&quot;noopener&quot;&gt;ADCS host&lt;/a&gt;, SAM dump, &lt;a href=&quot;https://paragmali.com/blog/pass-the-hash-to-pass-the-prt-twenty-nine-years-of-windows-c/&quot; rel=&quot;noopener&quot;&gt;Pass-the-Hash&lt;/a&gt;, CA private key extraction, and ForgeCert to mint a Domain Admin certificate. Microsoft fixed SilverPotato as CVE-2024-38061 in the July 2024 Patch Tuesday [@nvd-cve-2024-38061]; the original researcher&apos;s credit was subsequently removed after a second-reporter overlap and an MSRC severity re-grading from &lt;em&gt;moderate&lt;/em&gt; to &lt;em&gt;important&lt;/em&gt; [@pierini-2024-silverpotato-post]. The structural primitive the chain exploits -- DCOM cross-session activation gated on Distributed COM Users / Performance Log Users group membership chained into a cross-host NTLM relay -- remains a per-CVE rather than a class-level close.&lt;/p&gt;
&lt;p&gt;FakePotato (CVE-2024-38100, July 2024 KB5040434) closed the ShellWindows DCOM activation path that Pierini disclosed; the patch shipped about a month &lt;em&gt;before&lt;/em&gt; the public disclosure [@nvd-cve-2024-38100; @pierini-2024-fakepotato-post].&lt;/p&gt;

James Forshaw&apos;s writing is, by some margin, the single most-cited body on the impersonation primitive in the offensive-research community. Four single-author primaries underpin most of this article: *The Art of Becoming TrustedInstaller* (2017-08) on Service-SID derivation [@forshaw-2017-08-trustedinstaller]; *Empirically Assessing Windows Service Hardening* (2020-01), the canonical empirical assessment of what the WSH stack actually closes and what it does not [@forshaw-2020-01-empirical-wsh]; *Sharing a Logon Session a Little Too Much* (2020-04), which documents the LSASS cached-token defect that GodPotato later weaponised [@forshaw-2020-04-sharing-logon-session]; and *Windows Exploitation Tricks: Relaying DCOM Authentication* (2021-10), the Project Zero retrospective that names the genealogy from Issue 325 to RemotePotato [@forshaw-2021-10-relaying-dcom-pz]. Forshaw&apos;s 2020-01 opening sentence is the line every defender quotes back: &quot;In the past few years there&apos;s been numerous exploits for service to system privilege escalation. Primarily they revolve around the fact that system services typically have impersonation privilege&quot; [@forshaw-2020-01-empirical-wsh].

flowchart TD
    G1[&quot;G1, 2008-2010, Cerrudo Token Kidnapping, leaked impersonation handles&quot;]
    G2[&quot;G2, 2014-2016, HotPotato, local NTLM WPAD reflection&quot;]
    G3[&quot;G3, 2016-2018, RottenPotato, JuicyPotato, DCOM OXID activation&quot;]
    G4[&quot;G4, 2020-2024, PrintSpoofer, CoercedPotato, non-DCOM RPC coercion&quot;]
    G5[&quot;G5, 2022-2024, GodPotato, LocalPotato, SilverPotato, RPCSS OXID and NTLM-loopback defects&quot;]
    Constant[&quot;SeImpersonatePrivilege plus ImpersonateNamedPipeClient, unchanged 2003 through 2026&quot;]
    G1 -- &quot;MS09-012, Cerrudo Chimichurri PoC&quot; --&amp;gt; G2
    G2 -- &quot;MS16-075 plus WPAD hardening&quot; --&amp;gt; G3
    G3 -- &quot;Win10 1809 OXID hardening, then CVE-2021-26414 three phases&quot; --&amp;gt; G4
    G4 -- &quot;Per-CVE coercion-API patches, PrintNightmare cluster&quot; --&amp;gt; G5
    G5 -- &quot;GodPotato unpatched, SilverPotato patched CVE-2024-38061, LocalPotato patched CVE-2023-21746, FakePotato patched CVE-2024-38100&quot; --&amp;gt; Open[&quot;Mid-2026 state, still functional via GodPotato and the coercion-API long tail&quot;]
    G1 --- Constant
    G2 --- Constant
    G3 --- Constant
    G4 --- Constant
    G5 --- Constant
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Generation&lt;/th&gt;
&lt;th&gt;Years&lt;/th&gt;
&lt;th&gt;Token source&lt;/th&gt;
&lt;th&gt;Microsoft response&lt;/th&gt;
&lt;th&gt;Still works in 2026?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;G1 Direct Token Theft (Cerrudo)&lt;/td&gt;
&lt;td&gt;2008-2010&lt;/td&gt;
&lt;td&gt;Leaked impersonation handles&lt;/td&gt;
&lt;td&gt;MS09-012 (Cerrudo &lt;em&gt;Chimichurri&lt;/em&gt; PoC)&lt;/td&gt;
&lt;td&gt;No (handle leaks closed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G2 Local NTLM Reflection (HotPotato)&lt;/td&gt;
&lt;td&gt;2014-2016&lt;/td&gt;
&lt;td&gt;WPAD + HTTP-to-SMB reflection&lt;/td&gt;
&lt;td&gt;MS16-075 + WPAD hardening&lt;/td&gt;
&lt;td&gt;No (chain too fragile)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G3 DCOM Activation (Rotten/Juicy)&lt;/td&gt;
&lt;td&gt;2016-2018&lt;/td&gt;
&lt;td&gt;Coerced DCOM auth to attacker pipe&lt;/td&gt;
&lt;td&gt;Win10 1809 OXID + CVE-2021-26414&lt;/td&gt;
&lt;td&gt;Partial (some LTSC pins)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G4 Non-DCOM RPC Coercion (PrintSpoofer/Coerced)&lt;/td&gt;
&lt;td&gt;2020-2024&lt;/td&gt;
&lt;td&gt;MS-RPRN / MS-EFSR / MS-FSRVP coercion&lt;/td&gt;
&lt;td&gt;Per-CVE patches&lt;/td&gt;
&lt;td&gt;Yes (long tail)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G5 RPCSS OXID + NTLM-Loopback (GodPotato/Local/Silver)&lt;/td&gt;
&lt;td&gt;2022-2024&lt;/td&gt;
&lt;td&gt;RPCSS handling defect + cross-host NTLM relay&lt;/td&gt;
&lt;td&gt;None for GodPotato; CVE-2023-21746 for LocalPotato; CVE-2024-38061 for SilverPotato (July 2024)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt; (GodPotato unaddressed)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

Microsoft&apos;s umbrella term for the post-2003 stack of mitigations around the service-account population: Service SIDs, restricted tokens, write-restricted tokens, integrity levels for services, the SCM&apos;s per-service required-privileges list, and the LPAC variants for select Windows components. The hardening is real, but as section 7 establishes, Microsoft has elected not to treat WSH as a *security* boundary.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Eighteen years. Five generations. One privilege. The variable is the token source; the constant is the gate.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Each generation tells a story of an MSRC bulletin that closed a specific token source and a researcher who found a new one within months. But every generation also leaves the same three components in place: the privilege, the named-pipe coercion API, and Microsoft&apos;s choice not to close the family at its root. What if those three components, taken together, form a closed system?&lt;/p&gt;
&lt;h2&gt;5. The Three-Piece Theorem&lt;/h2&gt;
&lt;p&gt;The Potato lineage is not a collection of bugs. It is the consequence of a single architectural identity:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; + &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; + the MSRC servicing-criteria carve-out = service-account-to-SYSTEM.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Each summand is individually documented. Each is individually shipped by Microsoft. Each is individually justified by a real engineering or product requirement. &lt;em&gt;Together they form a closed system that no point fix can break, because removing any one of them breaks a documented Windows behaviour shipped applications depend on.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is the article&apos;s main contribution: re-frame the eighteen-year named-exploit lineage as the consequence of a documented three-piece architectural decision rather than as a series of bugs.&lt;/p&gt;
&lt;h3&gt;Component 1: the privilege&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; is enumerated in the privilege-constants table as &lt;code&gt;SE_IMPERSONATE_NAME&lt;/code&gt; [@ms-learn-privilege-constants] and is the subject of a dedicated security-policy page that lists default assignments [@ms-learn-impersonate-policy]. The LOCAL SERVICE and NETWORK SERVICE account documentation each enumerate it as &lt;code&gt;(enabled)&lt;/code&gt; in the default privilege set [@ms-learn-localservice; @ms-learn-networkservice].&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Cost of removal:&lt;/em&gt; every shipping RPC server that impersonates clients breaks; §7.1 walks through the production-Windows surface this affects in detail.&lt;/p&gt;
&lt;h3&gt;Component 2: the coercion API&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; has shipped since Windows XP / Server 2003 [@ms-learn-impersonatenamedpipeclient]. It is the standard mechanism by which a Win32 RPC server picks up the identity of a connecting client to make per-user access checks. Deprecating it is not a question of swapping one API for another -- the Microsoft-recommended impersonation APIs (&lt;code&gt;RpcImpersonateClient&lt;/code&gt;, the LSA-side variants) ultimately compose into the same kernel-side token-substitution call.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Cost of removal:&lt;/em&gt; the named-pipe RPC server population that pre-dates the modern impersonation APIs breaks; §7.3 details the SMB-redirector, Print-Spooler, EFS-RPC, and broader Win32 ABI migration cost.&lt;/p&gt;
&lt;h3&gt;Component 3: the carve-out&lt;/h3&gt;

Microsoft&apos;s public policy document defining what counts as a security boundary, a security feature, and a defence-in-depth feature for servicing purposes. The two-question test is direct: &quot;Does the vulnerability violate the goal or intent of a security boundary or a security feature? Does the severity of the vulnerability meet the bar for servicing?&quot; If either answer is no, &quot;the vulnerability will be considered for the next version or release of Windows but will not be addressed through a security update or guidance&quot; [@msrc-windows-security-servicing-criteria].
&lt;p&gt;The &lt;a href=&quot;https://paragmali.com/blog/windows-security-boundaries-the-document-that-decides-what-g/&quot; rel=&quot;noopener&quot;&gt;MSRC Windows Security Servicing Criteria&lt;/a&gt; document [@msrc-windows-security-servicing-criteria] is the policy-level anchor. The operational articulation came at Troopers 24 from Pierini and Cocomazzi, who named the doctrine in three sentences anchored on the WSH-as-safety-not-security distinction [@pierini-cocomazzi-troopers24-talk]. §7 opens with the full quote and walks through its implications; for the three-piece theorem here, what matters is that the carve-out is &lt;em&gt;documented&lt;/em&gt; and &lt;em&gt;Microsoft-position-as-stated&lt;/em&gt;, not inferred from per-CVE behaviour.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Cost of removal:&lt;/em&gt; Microsoft commits to the per-CVE cadence becoming a structural-close cadence -- servicing every coercion API in the long tail, every NTLM-loopback edge case, every cross-session token confusion, on the same SLAs as kernel RCEs. The MSRC has explicitly declined to take on that workload [@msrc-windows-security-servicing-criteria].&lt;/p&gt;

&quot;if you have SeAssignPrimaryToken or SeImpersonate privilege, you are SYSTEM&quot; -- Andrea Pierini; &quot;a deliberately provocative shortcut obviously, but it&apos;s not far from the truth&quot; -- Clement Labro&apos;s gloss on the same line [@labro-2020-printspoofer-post]

flowchart TB
    Priv[&quot;SeImpersonatePrivilege, default-assigned to LOCAL SERVICE and NETWORK SERVICE.  Removing this breaks every service that impersonates clients.&quot;]
    API[&quot;ImpersonateNamedPipeClient, shipped since XP/Server 2003.  Removing this breaks every named-pipe RPC server.&quot;]
    Doctrine[&quot;MSRC servicing criteria: WSH is a safety boundary, not a security boundary.  Changing this commits Microsoft to a structural-close servicing cadence.&quot;]
    Center[&quot;Service-account to SYSTEM&quot;]
    Priv --&amp;gt; Center
    API --&amp;gt; Center
    Doctrine --&amp;gt; Center
&lt;p&gt;The original focus paragraph that seeded this article mentioned &quot;RBAC for services&quot; as one of Microsoft&apos;s mitigations. The Stage 0a focus-premise audit found this phrase to be non-standard Windows terminology and explicitly retracted it; Microsoft has never shipped a Windows-side RBAC architecture for services. Azure RBAC and Microsoft Entra RBAC are cloud-side authorisation systems and do not gate the local &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; at all. Section 6.6 returns to this retraction in full.&lt;/p&gt;
&lt;p&gt;If the primitive is a closed three-piece system, what has Microsoft actually shipped in the eighteen years since Cerrudo? Five containment mitigations -- each of which narrows the surface around the primitive without closing it.&lt;/p&gt;
&lt;h2&gt;6. Five Mitigations and the Surface None of Them Closes&lt;/h2&gt;
&lt;p&gt;Microsoft has not been idle. Over nineteen years of service hardening they have shipped Service SIDs, restricted tokens, the Less-Privileged AppContainer model, group Managed Service Accounts, and the three-phase DCOM hardening of CVE-2021-26414. Each closes a real surface. None of them closes the primitive. The pattern is too consistent to be accidental.&lt;/p&gt;
&lt;h3&gt;6.1 Service SID isolation (Vista, 2007)&lt;/h3&gt;
&lt;p&gt;Vista shipped per-service SIDs of the form &lt;code&gt;NT SERVICE\&amp;lt;name&amp;gt;&lt;/code&gt; -- a SID generated on the fly from the service&apos;s name and attached to the service-process token. Forshaw&apos;s &lt;em&gt;The Art of Becoming TrustedInstaller&lt;/em&gt; is the canonical reference for the derivation: &quot;The SID itself is generated on the fly as the SHA1 hash of the uppercase version of the service name&quot; [@forshaw-2017-08-trustedinstaller]. Service SIDs are also documented as part of the SCM service-security model [@ms-learn-service-security].&lt;/p&gt;

A SID of the form `NT SERVICE\` derived as the SHA1 hash of the uppercased service name. Service SIDs let an ACL grant access to a specific service without granting access to every service running under the same account. When `SERVICE_SID_TYPE_UNRESTRICTED` is configured, the Service SID is added to the service-process token as a regular group SID.
&lt;p&gt;&lt;em&gt;Closes:&lt;/em&gt; lateral movement between services sharing an account. A NETWORK SERVICE process for service A cannot, by Service SID alone, open files ACL&apos;d to NETWORK SERVICE for service B.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Does NOT close:&lt;/em&gt; vertical movement to SYSTEM via NETWORK SERVICE. Forshaw&apos;s April 2020 &lt;em&gt;Sharing a Logon Session a Little Too Much&lt;/em&gt; documents the LSASS cached-token defect that underpins GodPotato: even with Service SIDs in place, the local logon session that LSASS retrieves for a same-machine authentication is the &lt;em&gt;session&apos;s&lt;/em&gt; token, not the &lt;em&gt;caller&apos;s&lt;/em&gt; token, which is exactly the structural property GodPotato weaponises [@forshaw-2020-04-sharing-logon-session].&lt;/p&gt;
&lt;h3&gt;6.2 Restricted and write-restricted service tokens (Vista 2007, backport via MS09-012)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;SERVICE_SID_TYPE_RESTRICTED&lt;/code&gt; and &lt;code&gt;WRITE_RESTRICTED&lt;/code&gt; are SCM configuration values that wrap the service-process token in a restricting-SID set; the kernel performs every access check twice (once against the regular group SIDs, once against the restricting set) and grants only the intersection. Forshaw&apos;s January 2020 empirical assessment is the canonical study of what these settings actually accomplish: &quot;In the past few years there&apos;s been numerous exploits for service to system privilege escalation. Primarily they revolve around the fact that system services typically have impersonation privilege&quot; [@forshaw-2020-01-empirical-wsh].&lt;/p&gt;

A token marked with a *restricting SID* set in addition to its regular group SIDs. The kernel grants access only when both sets satisfy the ACL. Configured per-service via `SERVICE_SID_TYPE_RESTRICTED` (or `WRITE_RESTRICTED`, which restricts only write access). The intent is to prevent a compromised service from touching arbitrary objects outside an explicit allow-list of restricting SIDs.
&lt;p&gt;&lt;em&gt;Closes:&lt;/em&gt; the compromised service&apos;s ability to write to (or read, depending on configuration) arbitrary objects outside its restricting-SID set.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Does NOT close:&lt;/em&gt; &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; is not revoked. A restricted token can still call &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; and &lt;code&gt;CreateProcessWithTokenW&lt;/code&gt;. The privilege gate is orthogonal to the restricting-SID gate.&lt;/p&gt;
&lt;h3&gt;6.3 LPAC (Less-Privileged AppContainer) for select services (Windows 10+)&lt;/h3&gt;
&lt;p&gt;Some Microsoft components opt into the AppContainer model with the Less-Privileged variant: the Edge browser broker, certain Defender child processes, parts of the DNS Client and Web Account Manager stacks. Inside an LPAC, the process runs with a deny-all token capabilities profile and must declare every Win32 capability it intends to use. The sibling &lt;a href=&quot;https://paragmali.com/blog/appcontainer-and-lowbox-tokens-windowss-capability-sandbox/&quot; rel=&quot;noopener&quot;&gt;AppContainer and LowBox Tokens&lt;/a&gt; article (2026-05-12) covers the model in depth.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Closes:&lt;/em&gt; the attack surface of a few specific Microsoft-shipped contained services.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Does NOT close:&lt;/em&gt; the LOCAL SERVICE and NETWORK SERVICE population this article is about is &lt;strong&gt;not&lt;/strong&gt; LPAC-contained by default. Declaring an LPAC service requires rewriting the service to operate inside an AppContainer, which most product teams do not undertake.&lt;/p&gt;

Building an LPAC service is not a configuration flag; it is an architectural commitment. The service must declare every Win32 capability it uses, must be packaged through the modern app installer pipeline, and must accept the deny-by-default file-system view that the LPAC sandbox enforces. The cost is real for legacy code -- file paths and registry keys the service has historically reached without scrutiny become inaccessible, and IPC patterns that assumed a normal token need to be re-engineered through capability-mediated brokers. Even Microsoft uses LPAC narrowly. Third-party adoption among independent software vendors that ship NETWORK SERVICE workloads is essentially nil. The mitigation that *would* containerise the impersonation surface is technically available; in practice almost nobody uses it.
&lt;h3&gt;6.4 group Managed Service Accounts (gMSA, Server 2012+)&lt;/h3&gt;
&lt;p&gt;gMSA is Microsoft&apos;s solution to the credential-hygiene problem for service accounts: a domain-managed identity whose 240-byte password is rotated automatically by the KDS Root Key, retrieved by authorised hosts via Group Policy, and never typed by a human [@ms-learn-gmsa-overview].&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Closes:&lt;/em&gt; domain-credential exposure for service accounts. A service no longer has a memorable password an admin will reuse; the credential lives in AD and is rotated on a schedule.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Does NOT close:&lt;/em&gt; anything to do with &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; on the local box. gMSA is a credential-hygiene mitigation, not a privilege-escape mitigation. A service running under a gMSA still holds the same default service-account privileges, and the SilverPotato-class cross-host coerce-and-relay flow [@pierini-2024-silverpotato-post; @nvd-cve-2024-38061] directly exploits a chain that gMSA does not protect against (per-variant patches like CVE-2024-38061 close instances, not the class).&lt;/p&gt;
&lt;h3&gt;6.5 CVE-2021-26414 three-phase DCOM hardening&lt;/h3&gt;
&lt;p&gt;CVE-2021-26414 raised the minimum DCOM client authentication level to &lt;code&gt;RPC_C_AUTHN_LEVEL_PKT_INTEGRITY&lt;/code&gt;. The rollout was deliberately gradual: phase 1 (2021-06-08) opt-in via registry, phase 2 (2022-06-14) opt-out via registry, phase 3 (2023-03-14) enforced with no opt-out [@nvd-cve-2021-26414].&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Closes:&lt;/em&gt; the original RottenPotato and JuicyPotato OBJREF-with-attacker-OXID chain on phase-3-enforced builds. The DCOM activation surface those variants depended on is meaningfully harder after phase 3.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Does NOT close:&lt;/em&gt; anything that does not depend on DCOM activation. &lt;strong&gt;GodPotato&lt;/strong&gt; (RPCSS OXID handling, not DCOM activation) remains functional [@beichendream-godpotato-readme]; &lt;strong&gt;PrintSpoofer / CoercedPotato&lt;/strong&gt; (non-DCOM RPC coercion) remain functional [@labro-2020-printspoofer-post; @prepouce-coercedpotato-repo]; &lt;strong&gt;JuicyPotatoNG&lt;/strong&gt; found a same-quarter bypass on the DCOM side via the PrintNotify CLSID &lt;code&gt;{854A20FB-2D44-457D-992F-EF13785D2B51}&lt;/code&gt; [@antoniococo-juicypotatong-repo]; &lt;strong&gt;SilverPotato&lt;/strong&gt; used a different CLSID and a cross-host relay until Microsoft fixed it as CVE-2024-38061 in July 2024 [@pierini-2024-silverpotato-post; @nvd-cve-2024-38061] -- a per-variant fix that illustrates exactly why CVE-2021-26414 does not address the cross-host coerce-and-relay class as a whole.&lt;/p&gt;
&lt;h3&gt;6.6 The mitigation that does not exist: &quot;RBAC for services&quot;&lt;/h3&gt;
&lt;p&gt;Windows has shipped no unified RBAC architecture for local services. The SCM provides per-service SDDL controls, the file system and registry provide per-resource ACLs everywhere, and Service SIDs let ACLs name a specific service identity -- but &quot;RBAC for services&quot; as a single named mechanism is non-standard Windows terminology. Azure RBAC and Microsoft Entra RBAC are cloud-side authorisation systems and do not gate the local &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; at all. The §5 Sidenote on the Stage 0a focus-premise retraction covers the audit-trail framing; this subsection states the reader-facing point.&lt;/p&gt;

flowchart TB
    M1[&quot;Service SID Isolation, Vista 2007&quot;]
    M2[&quot;Restricted and Write-Restricted Tokens, Vista 2007 plus MS09-012 backport&quot;]
    M3[&quot;LPAC for select services, Windows 10 plus&quot;]
    M4[&quot;gMSA, Server 2012 plus&quot;]
    M5[&quot;CVE-2021-26414 three-phase DCOM hardening, 2021-2023&quot;]
    Surface1[&quot;Closes lateral movement between same-account services&quot;]
    Surface2[&quot;Closes write access outside restricting-SID set&quot;]
    Surface3[&quot;Closes blast radius of select Microsoft-shipped services&quot;]
    Surface4[&quot;Closes domain-credential exposure&quot;]
    Surface5[&quot;Closes DCOM activation chain, Rotten and Juicy&quot;]
    Core[&quot;Service-account-to-SYSTEM, primitive remains open&quot;]
    M1 --&amp;gt; Surface1
    M2 --&amp;gt; Surface2
    M3 --&amp;gt; Surface3
    M4 --&amp;gt; Surface4
    M5 --&amp;gt; Surface5
    Surface1 -. &quot;does not reach&quot; .-&amp;gt; Core
    Surface2 -. &quot;does not reach&quot; .-&amp;gt; Core
    Surface3 -. &quot;does not reach&quot; .-&amp;gt; Core
    Surface4 -. &quot;does not reach&quot; .-&amp;gt; Core
    Surface5 -. &quot;does not reach&quot; .-&amp;gt; Core
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mitigation&lt;/th&gt;
&lt;th&gt;What it closes&lt;/th&gt;
&lt;th&gt;What it does NOT close&lt;/th&gt;
&lt;th&gt;Primary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Service SID Isolation (Vista 2007)&lt;/td&gt;
&lt;td&gt;Lateral movement between services sharing an account&lt;/td&gt;
&lt;td&gt;Vertical SYSTEM via NETWORK SERVICE LSASS-cached-token defect&lt;/td&gt;
&lt;td&gt;[@forshaw-2017-08-trustedinstaller; @forshaw-2020-04-sharing-logon-session]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restricted / Write-Restricted Tokens&lt;/td&gt;
&lt;td&gt;Write access to non-restricting-SID objects&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; still present; &lt;code&gt;CreateProcessWithTokenW&lt;/code&gt; still works&lt;/td&gt;
&lt;td&gt;[@forshaw-2020-01-empirical-wsh]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LPAC (Windows 10+)&lt;/td&gt;
&lt;td&gt;Select-services blast radius&lt;/td&gt;
&lt;td&gt;NETWORK / LOCAL SERVICE population not LPAC-contained by default&lt;/td&gt;
&lt;td&gt;sibling AppContainer article&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gMSA (Server 2012+)&lt;/td&gt;
&lt;td&gt;Domain-credential exposure&lt;/td&gt;
&lt;td&gt;Local &lt;code&gt;SeImpersonate&lt;/code&gt;; SilverPotato-class cross-host relay&lt;/td&gt;
&lt;td&gt;[@ms-learn-gmsa-overview]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2021-26414 phase 3 (2023-03-14)&lt;/td&gt;
&lt;td&gt;DCOM activation chain (Rotten/Juicy)&lt;/td&gt;
&lt;td&gt;GodPotato (RPCSS), PrintSpoofer (non-DCOM), JuicyPotatoNG (same-quarter bypass)&lt;/td&gt;
&lt;td&gt;[@nvd-cve-2021-26414]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; None of this section is an indictment of the mitigations. Each one closes a meaningful surface, and a NETWORK SERVICE host with all five active is materially harder to attack than a host without them. But the surface they collectively leave open -- the &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; plus &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; plus coercion-API combination -- is the surface that every shipping Potato variant lives on. The gap is not a missing patch. The gap is the design.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Microsoft has shipped five mitigations in nineteen years. Every one narrows the surface around the primitive. None of them closes it. The pattern is too consistent to be accidental. So what is the policy that produces this pattern?&lt;/p&gt;
&lt;h2&gt;7. The MSRC Servicing-Criteria Carve-Out&lt;/h2&gt;

Most of these exploits allow an attacker to break the WSH (Windows Service Hardening) boundary, enabling privilege escalation from a limited service to SYSTEM: a common scenario when dealing with web services like IIS or MSSQL. Interestingly, Microsoft does not consider WSH a security boundary but rather a safety boundary; for this reason, many Potato exploits work (and have been working) on fully updated Windows systems. -- Andrea Pierini and Antonio Cocomazzi, Troopers 24 [@pierini-cocomazzi-troopers24-talk]
&lt;p&gt;This is the Microsoft-position-as-stated-to-researchers anchor for the entire article. The MSRC Windows Security Servicing Criteria page [@msrc-windows-security-servicing-criteria] is the policy-document anchor with the same content: the two-question test &quot;Does the vulnerability violate the goal or intent of a security boundary or a security feature? Does the severity of the vulnerability meet the bar for servicing?&quot; If either answer is no, the vulnerability is considered for the next version of Windows but is not addressed through a security update.&lt;/p&gt;
&lt;p&gt;Service-to-SYSTEM escalation across the Windows Service Hardening boundary is not a violation of a &lt;em&gt;security boundary&lt;/em&gt;. It is a violation of a &lt;em&gt;safety boundary&lt;/em&gt;. The distinction is doctrinal and explicit. Microsoft will fix specific token-source primitives -- LocalPotato got CVE-2023-21746, FakePotato got CVE-2024-38100 -- but the class is, on the record, not within scope for security servicing [@nvd-cve-2023-21746; @nvd-cve-2024-38100].&lt;/p&gt;
&lt;p&gt;Why? Walk through each of the three closure paths Microsoft could in principle take, and the cost of each.&lt;/p&gt;
&lt;h3&gt;7.1 Revoke &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; from NETWORK SERVICE and LOCAL SERVICE&lt;/h3&gt;
&lt;p&gt;The cleanest fix in the model: drop the privilege from the default-assignment list documented on the LOCAL SERVICE and NETWORK SERVICE account pages [@ms-learn-localservice; @ms-learn-networkservice]. Every Potato variant that ends in &lt;code&gt;CreateProcessWithTokenW&lt;/code&gt; fails immediately.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Cost.&lt;/em&gt; Every RPC server, web server, database server, and Office service that needs to act on a client&apos;s behalf breaks. The privilege exists &lt;em&gt;because&lt;/em&gt; services need it. IIS application pools cannot impersonate authenticated users; SQL Server cannot enforce per-login row security; Exchange cannot operate on mailboxes under the connected user&apos;s identity; the print spooler cannot enforce per-user printer ACLs; the file server cannot enforce per-user file ACLs. The 2003 service-hardening pivot would be reversed -- services would have to run as SYSTEM again to do the work they need to do, which is precisely the worm-target population Microsoft spent the early 2000s migrating away from.&lt;/p&gt;
&lt;h3&gt;7.2 Declare local DCOM activation a security boundary and service it&lt;/h3&gt;
&lt;p&gt;This was the partial path Microsoft did take with CVE-2021-26414 [@nvd-cve-2021-26414]: tighten the DCOM activation surface and ship the change in three phases over twenty-one months. But declaring &lt;em&gt;all&lt;/em&gt; local DCOM activation a security boundary requires a serviceable-CVE pipeline for every cross-session COM activation, every cross-integrity-level activation, every weakly-authenticated marshalled OBJREF.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Cost.&lt;/em&gt; MSRC has declined to take on that workload. The on-the-record case is RemotePotato0 [@antoniococo-remotepotato0-repo], which was classified &quot;Won&apos;t Fix&quot; by MSRC as the first explicit declination in the lineage -- documented in Forshaw&apos;s 2021 retrospective as still unpatched at the time of writing [@forshaw-2021-10-relaying-dcom-pz]. RemotePotato0 is the empirical evidence that Microsoft has chosen to live with a known cross-session DCOM relay rather than commit to a structural close.&lt;/p&gt;
&lt;h3&gt;7.3 Deprecate &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Remove the named-pipe-server impersonation API from the Win32 surface. Mark it deprecated. Stop callers from using it. Provide a replacement that requires explicit per-request token plumbing.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Cost.&lt;/em&gt; Most Win32 RPC servers stop being able to impersonate their callers. The SMB redirector, the Print Spooler, the EFS RPC server, and a long tail of named-pipe RPC servers depend on this specific API; their alternatives all compose into the same kernel-side call. The replacement -- a per-request capability handle threading through every RPC binding -- would be a multi-year ABI change with no clean migration path for legacy binaries.&lt;/p&gt;

flowchart LR
    Start[&quot;Closure path&quot;]
    A[&quot;A. Revoke SeImpersonatePrivilege from NETWORK SERVICE and LOCAL SERVICE&quot;]
    B[&quot;B. Declare local DCOM activation a security boundary, service every CVE&quot;]
    C[&quot;C. Deprecate ImpersonateNamedPipeClient&quot;]
    Cost1[&quot;Breaks IIS, Exchange, MSSQL, Office services&quot;]
    Cost2[&quot;Per-CVE servicing pipeline for every cross-session COM activation, MSRC has declined&quot;]
    Cost3[&quot;Breaks SMB redirector, Print Spooler, EFS, every named-pipe RPC server that impersonates&quot;]
    Converge[&quot;Compatibility cost Microsoft has not accepted&quot;]
    Start --&amp;gt; A
    Start --&amp;gt; B
    Start --&amp;gt; C
    A --&amp;gt; Cost1 --&amp;gt; Converge
    B --&amp;gt; Cost2 --&amp;gt; Converge
    C --&amp;gt; Cost3 --&amp;gt; Converge
&lt;p&gt;RemotePotato0 [@antoniococo-remotepotato0-repo] holds a particular place in the lineage because it is the first variant for which MSRC&apos;s &quot;Won&apos;t Fix&quot; classification became public on the record. Forshaw&apos;s 2021 Project Zero retrospective notes the variant as &quot;currently unpatched as of October 2021&quot; [@forshaw-2021-10-relaying-dcom-pz], and Microsoft did not subsequently issue a CVE for it. The Stage 5 outline cross-references the sibling Potato Family article (2026-05-31) for variant detail; in this article RemotePotato0 functions as the empirical proof that the carve-out is not a hypothetical preference but a shipped policy choice.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Eighteen years. Five mitigations. Three closure paths Microsoft has explicitly declined to take. The primitive is not unpatched. It is documented-as-policy not to be patched.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Microsoft has chosen, on the record, to treat this boundary as a safety boundary rather than a security boundary. Is that an architectural failure -- or is it a rational policy choice under a deeper structural constraint? Hardy 1988 has an answer.&lt;/p&gt;
&lt;h2&gt;8. The Hardy Ceiling&lt;/h2&gt;
&lt;p&gt;Norm Hardy named the class in 1988. Forty years later, Windows is still demonstrating it. The confused-deputy attack surface is not a Microsoft mistake; it is the predictable behaviour of any identity-and-ACL system in which a server holds more authority than its clients and acts on client requests [@hardy-1988].&lt;/p&gt;
&lt;p&gt;The argument generalises beyond Windows. Any system that lets a process inherit ambient authority from its identity, and then lets that process act on requests from less-authorised principals, has a confused-deputy surface by construction. The only complete defence is capability discipline: bind authority to operations rather than to running identities, and never let a process exercise authority it was not explicitly handed [@hardy-1988]. Lampson&apos;s 1971 access-matrix paper is the formal substrate the argument depends on [@lampson-1971].&lt;/p&gt;
&lt;p&gt;Windows is not a capability system. It is an identity-and-ACL system, as Cutler&apos;s NT 3.1 team chose in 1993 [@ms-learn-windows-internals]. As long as that remains true, &lt;em&gt;some&lt;/em&gt; version of &quot;service-account to higher-privileged identity&quot; is reachable, and the only question is which specific token-source primitive is currently in play. Microsoft&apos;s eighteen-year per-CVE response cadence is consistent with that ceiling. Each individual token source is fixable; the class is not.&lt;/p&gt;

The capability-systems lineage -- KeyKOS, EROS, Coyotos, seL4 -- spent four decades demonstrating that the confused-deputy class is closeable in principle. In a capability system, when Hardy&apos;s user passed the FORTRAN compiler the path to the billing file as a debug-output target, the OS would have handed the compiler a write capability only for the file the *user* could write -- not for `(SYSX)BILL`. The compiler could not have damaged the bill even if it tried. seL4 has a machine-checked proof of this property. But none of those systems is the Windows service-compatibility envelope, and porting Windows to a capability substrate is not on any public roadmap. The road exists; Microsoft has not taken it.
&lt;p&gt;The closest in-architecture approximations Windows has shipped are narrow: AppContainer and LowBox tokens (the sibling AppContainer article 2026-05-12) bind a subset of authority to declared capabilities for select Microsoft components; the &lt;a href=&quot;https://paragmali.com/blog/adminless-how-windows-finally-made-elevation-a-security-boun/&quot; rel=&quot;noopener&quot;&gt;Adminless / Administrator Protection feature&lt;/a&gt; (sibling Adminless article 2026-05-10) binds elevation authority to per-action prompts for interactive admins. Both are partial applications of the capability principle within an otherwise identity-and-ACL system. Neither extends to the service-account population this article is about.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Windows is an identity-and-ACL system. As long as it remains one, the confused-deputy class is structurally present, and the Potato lineage is its Windows-specific instantiation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If the ceiling is structural and Microsoft has chosen the doctrine to match, what is the offensive-research community working on next? And what should defenders be doing in the meantime?&lt;/p&gt;
&lt;h2&gt;9. Open Problems&lt;/h2&gt;
&lt;p&gt;The closure of LocalPotato in 2023, SilverPotato (CVE-2024-38061) in July 2024, and FakePotato (CVE-2024-38100) in July 2024 did not slow the lineage. GodPotato remains functional. The supply of coercion APIs is structurally large. Microsoft has shipped no policy change. The four open questions below define what the lineage looks like through the rest of the decade.&lt;/p&gt;
&lt;h3&gt;9.1 The coercion-API treadmill&lt;/h3&gt;
&lt;p&gt;Generation 4 demonstrated that any Windows RPC interface accepting an attacker-supplied path or endpoint and resolving it server-side under a privileged identity is a viable token source. CoercedPotato&apos;s MS-EFSR catalogue alone lists fourteen entry points (two marked NOT WORKING) [@prepouce-coercedpotato-repo], with additional protocols (MS-RPRN, MS-FSRVP, MS-DFSNM) in the same family. Microsoft patches per CVE -- PrintNightmare cluster around MS-RPRN, targeted MS-EFSR fixes -- but the supply is not exhausted, and there is no public Microsoft commitment to exhaustive enumeration or class-level closure.&lt;/p&gt;
&lt;h3&gt;9.2 GodPotato&apos;s RPCSS OXID path&lt;/h3&gt;
&lt;p&gt;Three years after the three-phase CVE-2021-26414 DCOM hardening completed [@nvd-cve-2021-26414], GodPotato remains functional across the README&apos;s tested Windows matrix (Server 2012-2022 / Windows 8-11) [@beichendream-godpotato-readme]. No public Microsoft patch has been issued for the underlying defect through mid-2026. The architectural question -- is RPCSS itself the right place to harden, or is the LSASS cached-token defect Forshaw documented in April 2020 [@forshaw-2020-04-sharing-logon-session] the right place -- remains open. Microsoft has assigned no CVE.&lt;/p&gt;
&lt;h3&gt;9.3 Credential Guard does not stop this&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://paragmali.com/blog/the-empty-hash-credential-guard-the-lsaiso-trustlet-and-the-/&quot; rel=&quot;noopener&quot;&gt;Credential Guard&lt;/a&gt; protects the &lt;em&gt;NTLM hash and Kerberos TGT&lt;/em&gt; in the LSASS Isolated User Mode trustlet. It does &lt;strong&gt;not&lt;/strong&gt; protect against runtime impersonation of an already-issued token. The boundary between credential-theft mitigations and impersonation mitigations is frequently confused.&lt;/p&gt;
&lt;p&gt;Credential Guard&apos;s actual scope is narrower than its name suggests. The mitigation moves long-term authenticators -- the NT hash, the Kerberos TGT, and certain ticket-granting material -- into an isolated user-mode trustlet whose memory the regular kernel cannot read. None of that touches the runtime token plumbing the Potato lineage exercises. The token you receive from &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; is not a credential and is not held in LSASS-isolated memory; Credential Guard cannot see it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Practitioners frequently treat Credential Guard and Virtualisation-Based Security as a generic answer to &quot;Windows privilege-escalation risk.&quot; For the Potato family they are not. A Credential-Guard-enabled host that runs IIS as NETWORK SERVICE is as vulnerable to PrintSpoofer / CoercedPotato / GodPotato as a host without VBS. The category error matters operationally: a security team that buys Credential Guard expecting it to mitigate this primitive is misallocating defensive budget.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;9.4 The &quot;service boundary&quot; re-definition Microsoft has quietly avoided&lt;/h3&gt;
&lt;p&gt;Adminless / Administrator Protection -- the 2024-2025 feature that re-frames local admin identity as a per-action consent surface [@ms-learn-admin-protection] (covered in the sibling Adminless article 2026-05-10) -- explicitly excludes services from its new boundary.&lt;/p&gt;
&lt;p&gt;The Adminless documentation scopes the feature to interactive administrator accounts on a device [@ms-learn-admin-protection]; services, MSAs, gMSAs, and virtual accounts are out of scope by construction because none of them is an interactive admin account. The new boundary applies to elevation-prompt consent for interactive admins, not to service-account workloads. The open question is whether Microsoft will ever extend the Adminless boundary to include service accounts. As of mid-2026, the answer is &lt;em&gt;not on the public roadmap&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;9.5 Generation-6 candidates&lt;/h3&gt;
&lt;p&gt;Three candidate paths for the next generation of the lineage, none with a pushbutton PoC on the scale of HotPotato / JuicyPotato / PrintSpoofer / GodPotato as of mid-2026:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Kerberos-only loopback coercion.&lt;/em&gt; The existing NTLM-reflection mitigations target NTLM specifically; a coercion primitive that lands as a Kerberos AP-REQ to the same loopback endpoint would sidestep them.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Virtual-account / gMSA token-state defects.&lt;/em&gt; Forshaw&apos;s April 2020 analysis [@forshaw-2020-04-sharing-logon-session] established that the LSASS cached-token logic has surprising behaviours under same-machine authentication; the gMSA-account variant of those edge cases has not been publicly explored.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Cross-host extensions beyond ADCS.&lt;/em&gt; SilverPotato&apos;s coerce-and-relay chain into ADCS infrastructure [@pierini-2024-silverpotato-post] -- patched as CVE-2024-38061 in July 2024 [@nvd-cve-2024-38061] but exemplifying an open class -- is the strongest current exemplar for the &quot;Generation 6&quot; archetype: cross-host coerce-and-relay attacks that combine the existing local impersonation primitive with off-box authentication targets. LDAP, WinRM, and MSSQL-with-cert-auth are obvious next targets for the same class; what matters for taxonomy is the cross-host shape, not the patched-or-unpatched status of any specific variant.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the lineage is not closing, what should a defender actually do today?&lt;/p&gt;
&lt;h2&gt;10. Defending, Detecting, and (Carefully) Removing the Privilege&lt;/h2&gt;
&lt;p&gt;Three operational questions: which accounts hold the privilege on your box, can you remove it, and how do you detect when someone is actually using it?&lt;/p&gt;
&lt;h3&gt;10.1 Auditing which accounts hold &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The first defensive action is enumeration -- not removal. Concrete commands, in increasing order of detail:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;whoami /priv&lt;/code&gt; -- per-process self-check from any shell. Reports the token&apos;s privileges in the form the article opens with.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;secedit /export /cfg secpol.cfg&lt;/code&gt; -- full local-policy export. Grep the output for &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; to see every SID the local policy grants it to.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;accesschk.exe -a SeImpersonatePrivilege&lt;/code&gt; -- the Sysinternals AccessChk tool [@ms-learn-accesschk] enumerates the effective holders directly from the LSA policy database.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Get-NtTokenPrivileges&lt;/code&gt; from James Forshaw&apos;s NtObjectManager PowerShell module [@forshaw-ntobjectmanager-repo] -- the same data, scriptable, with the broader NtObjectManager surface available for follow-up (named-pipe enumeration, token-handle leak search, kernel-object introspection).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Invoke-PrivescCheck&lt;/code&gt; from Clement Labro&apos;s PrivescCheck module [@labro-privesccheck-repo] -- the canonical local-privesc check-list. The output includes &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; presence as one of approximately forty enumerated checks.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Author&lt;/th&gt;
&lt;th&gt;What it reports&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;AccessChk (Sysinternals)&lt;/td&gt;
&lt;td&gt;Mark Russinovich&lt;/td&gt;
&lt;td&gt;Effective permissions, account-privilege enumeration via &lt;code&gt;-a&lt;/code&gt; [@ms-learn-accesschk]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NtObjectManager&lt;/td&gt;
&lt;td&gt;James Forshaw&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Get-NtTokenPrivileges&lt;/code&gt;, named-pipe enumeration, token-handle leak search [@forshaw-ntobjectmanager-repo]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PrivescCheck&lt;/td&gt;
&lt;td&gt;Clement Labro&lt;/td&gt;
&lt;td&gt;Canonical local-privesc check-list incl. &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; presence [@labro-privesccheck-repo]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;{`
// Logic of: secedit /export /cfg secpol.cfg ; grep SeImpersonate
const secpol = readPolicyExport();              // produced by secedit
const holders = secpol[&apos;SeImpersonatePrivilege&apos;] || [];&lt;/p&gt;
&lt;p&gt;console.log(&apos;SIDs holding SeImpersonatePrivilege:&apos;);
for (const sid of holders) {
  console.log(&apos;  &apos; + sid);
}&lt;/p&gt;
&lt;p&gt;// Typical default on a server-style install:
//   S-1-5-19   (NT AUTHORITY\LOCAL SERVICE)
//   S-1-5-20   (NT AUTHORITY\NETWORK SERVICE)
//   S-1-5-32-544 (BUILTIN\Administrators)
//   S-1-5-6    (NT AUTHORITY\SERVICE)
`}&lt;/p&gt;
&lt;h3&gt;10.2 Removing the privilege where you can&lt;/h3&gt;
&lt;p&gt;The policy path is documented: &lt;code&gt;Computer Configuration -&amp;gt; Windows Settings -&amp;gt; Security Settings -&amp;gt; Local Policies -&amp;gt; User Rights Assignment -&amp;gt; Impersonate a client after authentication&lt;/code&gt; [@ms-learn-impersonate-policy]. The temptation, especially after reading an article like this one, is to remove &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; from NETWORK SERVICE wholesale.&lt;/p&gt;
&lt;p&gt;Do not do that. It will break IIS, Exchange, SQL Server, and most other Windows server products -- the same set the 2003 service-hardening pivot was designed to support. The realistic defensive approach is narrower: &lt;em&gt;audit first&lt;/em&gt;, &lt;em&gt;understand the dependency surface&lt;/em&gt;, then &lt;em&gt;narrow the assignment to the specific service accounts that need it&lt;/em&gt; on the specific hosts where they run. On hosts that do not run an RPC-impersonating workload (jump boxes, build agents, certain hardened-management hosts), the privilege can sometimes be removed safely from the unused well-known accounts.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The single most common mistake after reading any Potato writeup is to remove the privilege from NETWORK SERVICE on a production host. Doing so breaks IIS (per-user authentication fails), Exchange (mailbox impersonation fails), SQL Server (per-login row security fails), the SMB redirector (file-server impersonation fails), the Print Spooler (per-user printer ACLs fail), and most third-party Win32 service products. The privilege exists because services need it. Audit before you remove. Remove only after you have positively identified which production services on this host depend on the privilege and confirmed none of them does.&lt;/p&gt;
&lt;/blockquote&gt;

*Hidden behind a spoiler intentionally, so a skimming reader does not accidentally remove the privilege from production NETWORK SERVICE.* Open `gpedit.msc` (or the Group Policy Management Console for a domain-joined host). Navigate Computer Configuration -&amp;gt; Windows Settings -&amp;gt; Security Settings -&amp;gt; Local Policies -&amp;gt; User Rights Assignment -&amp;gt; Impersonate a client after authentication. The right-hand pane lists the SIDs holding the privilege. Note the current list. Do not change it. Compare it against the audit output from Section 10.1. If the local list and the AccessChk output disagree, you have a domain-pushed policy override worth tracing. If they agree and you have a documented business reason to remove a specific account, change the policy for that specific account only, and confirm on a non-production host that the dependent services still function.
&lt;h3&gt;10.3 Detection signatures&lt;/h3&gt;
&lt;p&gt;Detection in this space breaks into two abstractions: &lt;em&gt;primitive-level&lt;/em&gt; rules that match the named-pipe pattern every Potato variant generates, and &lt;em&gt;named-tool&lt;/em&gt; rules that match a specific binary&apos;s fingerprint.&lt;/p&gt;
&lt;p&gt;The primitive-level open-source reference is the Elastic detection rule &lt;code&gt;Privilege Escalation via Rogue Named Pipe&lt;/code&gt; [@elastic-rogue-named-pipe-rule] (as of June 2026; the cited URL pins to the master HEAD), rule_id &lt;code&gt;76ddb638-abf7-42d5-be22-4a70b0bf7241&lt;/code&gt;. The EQL queries Sysmon Event ID 17 (pipe-creation events) and matches paths in which a &lt;code&gt;\pipe\&lt;/code&gt; token appears after another path segment -- the canonical PrintSpoofer-style relay endpoint fingerprint. Because the rule looks for the pattern every Potato variant produces (a service-account process creating a named pipe whose path embeds a coercion-API hint), it survives binary rename, source-recompile, and most CLI variation.&lt;/p&gt;
&lt;p&gt;The named-tool reference is the SigmaHQ LocalPotato rule [@sigmahq-localpotato-rule] (as of June 2026; the cited URL pins to the master HEAD), rule &lt;code&gt;id 6bd75993-9888-4f91-9404-e1e4e4e34b77&lt;/code&gt;. Three OR-joined selectors: image path ending in &lt;code&gt;\LocalPotato.exe&lt;/code&gt;; CLI fingerprint &lt;code&gt;-i C:\&lt;/code&gt; paired with &lt;code&gt;-o Windows\&lt;/code&gt;; specific IMPHASH selectors &lt;code&gt;E1742EE971D6549E8D4D81115F88F1FC&lt;/code&gt; and &lt;code&gt;DD82066EFBA94D7556EF582F247C8BB5&lt;/code&gt;. Useful as a low-noise IOC tripwire; trivially evaded by binary rename or recompilation.&lt;/p&gt;

The Sigma LocalPotato rule is a perfectly competent detection rule for *the LocalPotato binary distributed at a specific commit*. It is essentially useless against the *technique*. An attacker recompiling LocalPotato from source breaks the IMPHASH selectors; renaming the output binary breaks the image-path selector; rewriting the CLI argument parsing breaks the third selector. The rule is brittle by construction, and the brittleness is structural to named-tool detection. The same point this article makes about Microsoft&apos;s per-CVE patches applies one level down: closing this binary does not close the technique; closing this technique does not close the primitive.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Invest detection budget in the Elastic primitive-level rule (or equivalent) and accept the higher false-positive rate that comes with it. The named-tool rules are a useful low-noise tripwire but should not be the primary signal. The same logic that makes the privilege durable against per-CVE patches makes the named-tool rules ephemeral against re-tooling.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We have walked the eighteen-year history, named the three-piece system, surveyed the mitigations, articulated the Microsoft policy, hit the Hardy ceiling, scanned the open problems, and listed the operational tools. One thing remains: the eight misconceptions practitioners hold about this primitive that the article must explicitly correct.&lt;/p&gt;
&lt;h2&gt;11. FAQ -- Eight Misconceptions That Will Not Die&lt;/h2&gt;

No. UAC (User Account Control) is an interactive-token consent surface for desktop logons; it gates whether an interactive admin can elevate to a full administrator token at consent-prompt time. Service accounts have no interactive logon and never see a UAC prompt. NETWORK SERVICE and LOCAL SERVICE inherit `SeImpersonatePrivilege` in their default token regardless of UAC settings [@ms-learn-localservice; @ms-learn-networkservice]; the Potato chain runs entirely under the service token without ever touching the interactive consent surface.

No. Credential Guard protects long-term credentials (the NTLM hash, the Kerberos TGT) in an isolated user-mode trustlet whose memory the regular kernel cannot read. The Potato lineage does not steal a credential and does not call into LSASS-isolated memory -- see §9.3 for the architectural detail. The operational takeaway: Credential Guard and VBS are orthogonal to runtime token impersonation, and a security team buying VBS in response to Potato writeups is misallocating defensive budget.

Not if the account holds `SeImpersonatePrivilege`. LOCAL SERVICE and NETWORK SERVICE both hold it by default and have it enabled in their default tokens [@ms-learn-localservice; @ms-learn-networkservice]. The gate is the privilege, not the account name. A service that has been &quot;hardened&quot; by moving from SYSTEM to NETWORK SERVICE still has the gate open. Real hardening requires either removing the privilege from the account on that specific host (with the compatibility risks Section 10.2 describes) or running the service under a custom account that does not get the privilege auto-granted.

No. Microsoft has shipped CVEs for specific token-source primitives -- LocalPotato as CVE-2023-21746 [@nvd-cve-2023-21746], SilverPotato as CVE-2024-38061 [@nvd-cve-2024-38061], FakePotato as CVE-2024-38100 [@nvd-cve-2024-38100], the three-phase DCOM hardening as CVE-2021-26414 [@nvd-cve-2021-26414] -- but the underlying impersonation surface is documented-as-policy not to be addressed as a security boundary [@msrc-windows-security-servicing-criteria; @pierini-cocomazzi-troopers24-talk]. GodPotato remains functional across its tested README matrix (Server 2012-2022 / Windows 8-11) with no public Microsoft patch through mid-2026 [@beichendream-godpotato-readme]. PrintSpoofer and CoercedPotato variants remain functional on most hosts [@labro-2020-printspoofer-post; @prepouce-coercedpotato-repo]. The pattern is per-CVE closure of individual variants while the underlying privilege + coercion-API geometry remains in place.

Both, but the architectural responsibility is Windows&apos;s. The privilege is a Windows design decision; the coerced-authentication primitives are Windows components (RPCSS, Print Spooler, EFS RPC server). A service developer cannot opt out of `SeImpersonatePrivilege` by writing better code -- the SCM grants the privilege as part of the account setup, not at the developer&apos;s request. A service developer *can* run under a custom account configured without the privilege, but most service code paths assume impersonation works (especially Win32-era code, where `RpcImpersonateClient` is the standard idiom) and break in subtle ways without it.

Yes. IIS application pools cannot perform Windows-authenticated user impersonation; Exchange cannot run mailbox operations under the connecting user&apos;s identity; SQL Server cannot enforce per-login row security under Windows authentication; the SMB and EFS RPC servers cannot impersonate their callers [@ms-learn-impersonate-policy; @ms-learn-impersonatenamedpipeclient]. The MSRC policy text on the impersonation-policy page is explicit that the privilege is required for legitimate impersonation [@ms-learn-impersonate-policy]. Audit before you remove.

No. The Adminless / Administrator Protection feature is a per-action consent surface for interactive administrators [@ms-learn-admin-protection]. Service accounts (services, MSAs, gMSAs, virtual accounts) are out of scope by construction because none of them is an interactive admin account. The new boundary does not apply to the service-account population this article is about. There is no public Microsoft roadmap to extend it.

Because the named-pipe RPC server population (the SMB redirector, the Print Spooler, the EFS RPC server, and the long tail of pre-modern Win32 services) depends on this specific API, and the Microsoft-recommended alternatives (`RpcImpersonateClient`, the LSA-side variants) ultimately compose into the same kernel-side call -- §7.3 walks through the full ABI migration cost. The MSRC servicing carve-out [@msrc-windows-security-servicing-criteria] is the policy-level acknowledgement that the cost is not on the table.
&lt;h2&gt;12. The Line, Re-read&lt;/h2&gt;
&lt;p&gt;Bring the reader back to where this started: one line in &lt;code&gt;whoami /priv&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SeImpersonatePrivilege  Impersonate a client after authentication  Enabled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you know what it means. The line ships in the default token of every IIS application pool worker, every SQL Server service step, every Exchange worker process, and every other LOCAL SERVICE / NETWORK SERVICE-derived account on every shipping Windows release. The line gates &lt;code&gt;CreateProcessWithTokenW&lt;/code&gt;. The kernel-level token-substitution surface sits behind that gate. The named-pipe coercion API on the other side of the gate has shipped since Windows XP / Server 2003 and remains the dominant token source on the platform. Microsoft has shipped five containment mitigations in nineteen years -- each closes a real surface; none closes this primitive. The doctrinal articulation came at Troopers 24: Windows Service Hardening is a &lt;em&gt;safety&lt;/em&gt; boundary, not a &lt;em&gt;security&lt;/em&gt; boundary [@pierini-cocomazzi-troopers24-talk]. The 1988 ceiling that explains why is older than the operating system.&lt;/p&gt;

Microsoft gave every NETWORK SERVICE a privilege that, in the wrong hands, is equivalent to SYSTEM. They knew -- the MSRC said as much in April 2009 [@msrc-blog-2009-04-token-kidnapping]. They could not change it without breaking the service model: every closure path carries a documented compatibility cost they have explicitly declined to accept [@msrc-windows-security-servicing-criteria]. Pierini and Cocomazzi made the doctrine quotable at Troopers 24 [@pierini-cocomazzi-troopers24-talk]: WSH is a safety boundary, not a security boundary. Roughly eighteen years after Cerrudo first put that fact on the record [@cerrudo-2008-pdf], ten years after HotPotato made it pushbutton [@breen-2016-hot-potato], and three years after GodPotato survived the most aggressive DCOM hardening Microsoft has shipped [@beichendream-godpotato-readme; @nvd-cve-2021-26414], the primitive is still in place. It is not unpatched. It is documented-as-policy not to be patched.
&lt;p&gt;For the variant-by-variant chronology this article deliberately deferred -- HotPotato, RottenPotato, JuicyPotato, JuicyPotatoNG, PrintSpoofer, EfsPotato, CoercedPotato, RoguePotato, RemotePotato0, GodPotato, LocalPotato, SilverPotato, FakePotato -- see the sibling Potato Family article (2026-05-31). That article catalogues each named tool&apos;s CLSID, coercion primitive, and patch state. This one was about why the family exists at all.&lt;/p&gt;
&lt;p&gt;The one line in &lt;code&gt;whoami /priv&lt;/code&gt; is not a bug. It is the decision.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;seimpersonateprivilege-and-the-service-account-attack-surface&quot; keyTerms={[
  { term: &quot;SeImpersonatePrivilege&quot;, definition: &quot;Windows user right (constant SE_IMPERSONATE_NAME) that gates CreateProcessWithTokenW and the higher-level forms of ImpersonateNamedPipeClient; default-assigned and enabled on LOCAL SERVICE, NETWORK SERVICE, and Administrators.&quot; },
  { term: &quot;ImpersonateNamedPipeClient&quot;, definition: &quot;Win32 API by which a named-pipe-server thread receives the connected client&apos;s access token; shipped since Windows XP / Server 2003; the dominant token-source primitive on the platform.&quot; },
  { term: &quot;Confused Deputy&quot;, definition: &quot;Norm Hardy&apos;s 1988 name for the structural attack class in which a server holds more authority than its clients and acts on client requests, with no architectural way to keep the two authorities apart. The Potato lineage is the Windows-specific instantiation.&quot; },
  { term: &quot;Primary Token vs Impersonation Token&quot;, definition: &quot;Two flavours of the Windows access-token kernel object: primary tokens attach to processes for the process lifetime; impersonation tokens attach to individual threads and override the primary token for the thread&apos;s access checks.&quot; },
  { term: &quot;Impersonation Level&quot;, definition: &quot;Four-value enum (Anonymous, Identification, Impersonation, Delegation) carried on every impersonation token. Only Impersonation and Delegation tokens can be used to spawn a process under the borrowed identity.&quot; },
  { term: &quot;OXID Resolver&quot;, definition: &quot;The DCOM service that maps an OXID (Object Exporter Identifier) to the RPC binding string for a marshalled COM object. The Rotten/Juicy Potato chain forges OBJREF blobs with attacker-controlled OXID resolver fields.&quot; },
  { term: &quot;Windows Service Hardening (WSH)&quot;, definition: &quot;Microsoft&apos;s umbrella term for the post-2003 service-account mitigation stack (Service SIDs, restricted tokens, integrity levels, LPAC variants). Documented-as-policy a safety boundary, not a security boundary.&quot; },
  { term: &quot;Service SID&quot;, definition: &quot;A SID of the form NT SERVICE\\, generated as the SHA1 hash of the uppercased service name, attached to a service-process token to permit per-service ACLs without granting them to every service sharing the account.&quot; },
  { term: &quot;Restricted Token&quot;, definition: &quot;A token carrying a restricting-SID set in addition to its regular group SIDs; the kernel grants access only when both sets satisfy the ACL. Used to limit a compromised service&apos;s write surface.&quot; },
  { term: &quot;MSRC Servicing Criteria&quot;, definition: &quot;Microsoft&apos;s public policy document defining what counts as a security boundary for servicing purposes. The two-question test gates whether a vulnerability is addressed via a security update or merely considered for a future release.&quot; }
]} questions={[
  { q: &quot;Why does NETWORK SERVICE hold SeImpersonatePrivilege by default?&quot;, a: &quot;Because the 2003 service-hardening pivot moved services off NT AUTHORITY\\SYSTEM, but those services still needed to impersonate their RPC clients to enforce per-user access. The privilege was created as the named user right that lets the new low-privileged accounts keep doing what SYSTEM had implicitly done.&quot; },
  { q: &quot;What three components combine into the three-piece theorem of section 5?&quot;, a: &quot;(1) SeImpersonatePrivilege default-assigned to LOCAL SERVICE and NETWORK SERVICE; (2) the ImpersonateNamedPipeClient coercion API shipped since Windows XP / Server 2003; (3) the MSRC servicing-criteria carve-out treating WSH as a safety boundary rather than a security boundary.&quot; },
  { q: &quot;Why did MS09-012 not close the Potato family?&quot;, a: &quot;Because MS09-012 (the bulletin behind Cerrudo&apos;s &lt;em&gt;Chimichurri&lt;/em&gt; PoC) closed the specific handle-leak surface Cerrudo&apos;s 2008 disclosure used. It did not revoke SeImpersonatePrivilege, did not modify CreateProcessWithTokenW, and did not modify ImpersonateNamedPipeClient. The MSRC blog explicitly acknowledged on the record that the privilege was sufficient for the escalation but elected to fix the symptom, not the gate.&quot; },
  { q: &quot;What is the difference between primitive-level and named-tool detection, and why does it matter?&quot;, a: &quot;Primitive-level detection (e.g., the Elastic rogue-named-pipe rule) matches the pattern every Potato variant generates regardless of binary identity; named-tool detection (e.g., the Sigma LocalPotato rule) matches a specific binary&apos;s fingerprint via IMPHASH and CLI selectors. Named-tool detection is trivially evaded by rename or recompile; primitive-level detection survives re-tooling at the cost of a higher false-positive rate.&quot; },
  { q: &quot;If GodPotato is patchable in principle, why has Microsoft not patched it?&quot;, a: &quot;Because patching GodPotato requires changing either RPCSS&apos;s OXID-handling logic or the LSASS cached-token logic Forshaw documented in April 2020 -- both structural properties whose modification would cascade through dozens of dependent components. The MSRC servicing-criteria carve-out frames the broader class as a safety boundary, so individual variants in that class do not receive security-update servicing. GodPotato sits squarely in the carved-out region.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>windows-internals</category><category>privilege-escalation</category><category>access-tokens</category><category>service-hardening</category><category>potato-family</category><category>msrc</category><category>confused-deputy</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>Three Years of PrintNightmare: How the Oldest Windows Service Survived Four Patch Waves</title><link>https://paragmali.com/blog/three-years-of-printnightmare-how-the-oldest-windows-service/</link><guid isPermaLink="true">https://paragmali.com/blog/three-years-of-printnightmare-how-the-oldest-windows-service/</guid><description>How the Windows Print Spooler produced nine SYSTEM-execution primitives in 2010-2024 and why Microsoft answered with two parallel architectures, not one.</description><pubDate>Tue, 02 Jun 2026 00:00:00 GMT</pubDate><content:encoded>
Between June 2021 and August 2024, Microsoft patched the Windows Print Spooler four times for what the press collectively called PrintNightmare. The patches did not converge. Each wave revealed the last one as a behavior restriction rather than an architectural change. By October 2024 Microsoft had shipped two parallel architectural answers: Windows Protected Print Mode (WPP), an opt-in driverless local stack with a lower-privilege Spooler Worker process; and Universal Print, a cloud-hosted replacement. Two answers, because the local SYSTEM-context driver-loading primitive the spooler was built around in the early 1990s cannot be sandboxed without breaking the printer install base that depends on it. This article traces nine related Spooler EoP and RCE primitives from 2010 to 2024, the architectural concession that ended the patch cycle, and why no single 2026 configuration is the full answer.
&lt;h2&gt;1. June 29, 2021: The Repository That Should Not Have Existed&lt;/h2&gt;
&lt;p&gt;On June 29, 2021, three researchers from Sangfor Technology -- Zhiniang Peng, Xuefeng Li, and Lewis Lee -- pushed a GitHub repository named &lt;code&gt;afwu/PrintNightmare&lt;/code&gt; containing a working proof-of-concept exploit against the Windows Print Spooler service. The repository had been prepared for their upcoming Black Hat USA 2021 briefing, &quot;Diving Into Spooler: Discovering LPE and RCE Vulnerabilities in Windows Printer&quot; [@infocondb-bh2021-sangfor]. The team believed Microsoft&apos;s June 8 Patch Tuesday update had fixed the vulnerability they were about to demonstrate.&lt;/p&gt;
&lt;p&gt;Within hours the repository was deleted. By then it had already been mirrored on multiple GitHub accounts and was spreading [@hackernews-printnightmare-poc-leak]. By the end of the day, the internet had a new name for the bug class: PrintNightmare. And by the end of the week, Microsoft, CERT/CC, and CISA had each independently confirmed what the Sangfor team realized about an hour after the deletion: the June 8 patch did not actually fix the vulnerability they had reported, and now the world had a working exploit for it [@cert-vu-383432] [@bleepingcomputer-domain-takeover].&lt;/p&gt;
&lt;p&gt;The Wayback Machine preserves the original README. Below the technical description, the Sangfor team explained why they had thought it was safe to publish: Microsoft&apos;s June 8 advisory had marked CVE-2021-1675 as a local &quot;Privilege Escalation&quot; with a CVSS v3.1 base score of 7.8 [@nvd-cve-2021-1675]. The bug Sangfor had separately reported and analyzed was, they believed, a different bug -- a remote code execution against the same service. They were correct. Nobody knew it yet.Microsoft silently reclassified CVE-2021-1675 from &quot;Elevation of Privilege&quot; to &quot;Remote Code Execution&quot; on June 21, 2021, after community analysis demonstrated the remote primitive. The reclassification appears in the NVD entry&apos;s revision history [@nvd-cve-2021-1675] and was reported the same week by BleepingComputer [@bleepingcomputer-domain-takeover]. The Sangfor team&apos;s confusion was reasonable: the advisory they were reading on June 28 still said EoP.&lt;/p&gt;
&lt;p&gt;The README&apos;s most striking line is an apology. &quot;CVE-2021-1675 is a remote code execution in Windows Print Spooler,&quot; it begins. Then, two paragraphs in: &quot;We also found this bug before and hope to keep it secret to participate Tianfu Cup&quot; [@afwu-wayback-snapshot]. The Sangfor team had discovered the same primitive months earlier, planned to use it for the Tianfu Cup capture-the-flag prize money, and reasoned that Microsoft&apos;s June 8 patch had now closed it.The Tianfu Cup is a Chinese-government-organized exploit competition. Chinese researchers are restricted from foreign competitions like Pwn2Own by a 2018 directive and instead route their work through Tianfu. Holding a bug secret to maximize Tianfu prize money is a known practice; what is unusual here is the public admission of the practice in an apology README.&lt;/p&gt;
&lt;p&gt;The rest of this article is about two questions. First: why does a single Windows service produce, on the public record, nine independently classed SYSTEM-code-execution primitives across fifteen years? Second: why does the answer Microsoft eventually shipped in 2024 take the form of two parallel architectures rather than one patch? We will not tell you which configuration to deploy. We will tell you why neither one alone is the full answer, and why that is the only honest place to land.&lt;/p&gt;
&lt;p&gt;To understand why one Windows service can leak a SYSTEM-execution primitive to anyone who can reach an RPC named pipe on a domain controller, we have to understand what the service is for.&lt;/p&gt;
&lt;h2&gt;2. The Artifact: What &lt;code&gt;spoolsv.exe&lt;/code&gt; Is and Why It Was Built This Way&lt;/h2&gt;
&lt;p&gt;The Windows Print Spooler service has been part of Windows continuously since the Windows NT era of the early 1990s.The &quot;Windows NT 3.1, July 1993&quot; attribution often cited for the first Print Spooler service is folk knowledge. Microsoft&apos;s own Learn documentation anchors the spooler architecture to &quot;Microsoft Windows 2000 and later&quot; [@ms-print-spooler-architecture], and the Windows Internals team writes that the spooler is &quot;largely unchanged since Windows NT 4&quot; [@windows-internals-printdemon]. The early-1990s framing is the safe one. Same name today (&lt;code&gt;spoolsv.exe&lt;/code&gt;), same security context (LocalSystem), same RPC interface family, same in-process third-party DLLs (Print Providers, Print Processors, driver components). The interesting question is not why the spooler still has bugs. It is why a service designed before &lt;a href=&quot;https://paragmali.com/blog/appcontainer-and-lowbox-tokens-windowss-capability-sandbox/&quot; rel=&quot;noopener&quot;&gt;AppContainer&lt;/a&gt;, before &lt;a href=&quot;https://paragmali.com/blog/the-integrity-level-stack-mic-uipi-and-twenty-years-of-uacs-/&quot; rel=&quot;noopener&quot;&gt;Mandatory Integrity Control&lt;/a&gt;, before &lt;a href=&quot;https://paragmali.com/blog/amsi-the-pre-execution-window-defender/&quot; rel=&quot;noopener&quot;&gt;AMSI&lt;/a&gt;, before &lt;a href=&quot;https://paragmali.com/blog/windows-kernel-code-integrity-2006-2026/&quot; rel=&quot;noopener&quot;&gt;Driver Signature Enforcement&lt;/a&gt; -- before the entire modern Windows security architecture existed -- still occupies the same SYSTEM-context process slot it did in 1996.&lt;/p&gt;
&lt;h3&gt;2.1 Anatomy&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;spoolsv.exe&lt;/code&gt; is, in Microsoft&apos;s own words, &quot;the spooler&apos;s API server&quot; [@ms-intro-spooler-components]. The Service Control Manager starts it at boot under the LocalSystem account. Inside the process, the router DLL &lt;code&gt;spoolss.dll&lt;/code&gt; dispatches incoming API calls to one of three Print Provider DLLs [@ms-print-spooler-architecture].&lt;/p&gt;

The Windows service that mediates between print clients and printer drivers. It runs continuously as LocalSystem, exposes an RPC interface over the `\PIPE\spoolss` named pipe, and loads third-party Print Provider, Print Processor, and printer driver DLLs into its address space [@ms-intro-spooler-components]. Almost every named Print Spooler vulnerability since 2010 has cashed out as SYSTEM-context code execution inside this process.
&lt;p&gt;The three Print Providers handle three kinds of printer connections. The Local Print Provider &lt;code&gt;localspl.dll&lt;/code&gt; handles printers attached or shared on the local machine. The Remote Print Provider &lt;code&gt;win32spl.dll&lt;/code&gt; handles printers reached via Windows networking. The HTTP / IPP Print Provider &lt;code&gt;inetpp.dll&lt;/code&gt; handles printers exposed over the Internet Printing Protocol [@ms-print-spooler-architecture] [@ms-intro-spooler-components].&lt;/p&gt;

The three router-loaded DLLs that dispatch print operations to the appropriate transport. `localspl.dll` (Local Print Provider) handles local and SMB-shared printers; `win32spl.dll` (Remote Print Provider) handles Windows-network remote printers; `inetpp.dll` (HTTP / IPP Print Provider) handles IPP printers reached over HTTP [@ms-print-spooler-architecture]. The chain is often confused with the Print Processor layer (a different layer entirely; see below).
&lt;p&gt;Once a print job is accepted, a separate component decides how to render it. That component is the Print Processor. The default Print Processor is &lt;code&gt;winprint.dll&lt;/code&gt;. It is a sibling layer to the Print Providers, not a member of the chain.&lt;/p&gt;

The component that interprets the spool file format (EMF, XPS, RAW, TEXT) and renders pages for a specific printer. `winprint.dll` is the default Print Processor that ships with Windows. Vendor-supplied Print Processors can be installed alongside it. A common pre-research misclassification names `winprint.dll` as a Print Provider; it is not. The Print Providers handle which printer; the Print Processor handles how to render the page [@ms-print-spooler-architecture].
&lt;p&gt;Clients of &lt;code&gt;spoolsv.exe&lt;/code&gt; are &lt;code&gt;winspool.drv&lt;/code&gt; locally and &lt;code&gt;win32spl.dll&lt;/code&gt; remotely [@ms-intro-spooler-components]. A user-mode application that calls a Win32 print API (&lt;code&gt;OpenPrinter&lt;/code&gt;, &lt;code&gt;EnumPrinters&lt;/code&gt;, &lt;code&gt;AddPrinter&lt;/code&gt;, &lt;code&gt;AddPrinterDriverEx&lt;/code&gt;) is, under the covers, sending an RPC request to &lt;code&gt;spoolsv.exe&lt;/code&gt; through one of these client libraries.&lt;/p&gt;

flowchart TD
    SCM[&quot;Service Control Manager&quot;] --&amp;gt; SPOOLSV[&quot;spoolsv.exe&lt;br /&gt;LocalSystem&quot;]
    SPOOLSV --&amp;gt; ROUTER[&quot;spoolss.dll&lt;br /&gt;(router)&quot;]
    ROUTER --&amp;gt; LOCALSPL[&quot;localspl.dll&lt;br /&gt;Local Print Provider&quot;]
    ROUTER --&amp;gt; WIN32SPL[&quot;win32spl.dll&lt;br /&gt;Remote Print Provider&quot;]
    ROUTER --&amp;gt; INETPP[&quot;inetpp.dll&lt;br /&gt;HTTP / IPP Print Provider&quot;]
    ROUTER --&amp;gt; WINPRINT[&quot;winprint.dll&lt;br /&gt;Print Processor&quot;]
    PIPE[&quot;\PIPE\spoolss&lt;br /&gt;(named pipe / ncacn_np)&quot;] --&amp;gt; SPOOLSV
    WINSPOOL[&quot;winspool.drv&lt;br /&gt;local clients&quot;] --&amp;gt; PIPE
    REMOTE[&quot;win32spl.dll&lt;br /&gt;remote clients&quot;] --&amp;gt; PIPE
    SPOOLSV -. opt-in INF .-&amp;gt; PIH[&quot;PrintIsolationHost.exe&lt;br /&gt;(sibling, LocalSystem)&quot;]
    PIH --&amp;gt; VDRIVER[&quot;vendor driver DLLs&quot;]
&lt;h3&gt;2.2 The RPC Surface&lt;/h3&gt;
&lt;p&gt;The Print Spooler exposes two RPC interface families. MS-RPRN is the synchronous Print System Remote Protocol. MS-PAR is its asynchronous counterpart. Both bind to the same named pipe.&lt;/p&gt;

Microsoft&apos;s two open-specification RPC protocols for remote print management. MS-RPRN is synchronous; MS-PAR is asynchronous. The MS-RPRN specification states that &quot;The RPC Protocol Sequence MUST be `ncacn_np`. The RPC Protocol Sequence Endpoint MUST be `\PIPE\spoolss`&quot; [@ms-rprn-spec]. Both interfaces expose driver-installation entry points: `RpcAddPrinterDriverEx` in MS-RPRN [@ms-rprn-rpcaddprinterdriverex] and `RpcAsyncAddPrinterDriver` in MS-PAR [@ms-par-rpcasyncaddprinterdriver]. MS-PAR&apos;s documentation states verbatim that the latter is &quot;The counterpart of this method in the Print System Remote Protocol.&quot;
&lt;p&gt;Two symmetric entry points are the architectural seed of the entire PrintNightmare patch tree. &lt;code&gt;RpcAddPrinterDriverEx&lt;/code&gt; (MS-RPRN section 3.1.4.4.8, Opnum 89) &quot;installs a printer driver on the print server&quot; [@ms-rprn-rpcaddprinterdriverex]. &lt;code&gt;RpcAsyncAddPrinterDriver&lt;/code&gt; (MS-PAR section 3.1.4.1, Opnum 39) does the same thing through the asynchronous interface [@ms-par-rpcasyncaddprinterdriver]. When the June 8, 2021 patch tightened access checks on the first entry point, the second one remained as the obvious next bypass target. We will come back to this.&lt;/p&gt;
&lt;p&gt;The authentication boundary is the part most worth dwelling on, because the answer is structurally surprising. &lt;strong&gt;MS-RPRN does no authentication at the protocol layer.&lt;/strong&gt; The MS-RPRN Transport section states this verbatim: &quot;The client MUST use no authentication, and the server MUST accept connections without authentication&quot; [@ms-rprn-transport]. The initialization section adds that the binding handle &quot;MUST specify an &lt;code&gt;ImpersonationLevel&lt;/code&gt; of 2 (Impersonation)&quot; against the SMB2 transport [@ms-rprn-initialization]. The RPC layer trusts whatever caller identity SMB hands it.&lt;/p&gt;
&lt;p&gt;This means the practical authentication boundary on &lt;code&gt;\PIPE\spoolss&lt;/code&gt; is the SMB named-pipe access control surface, not the RPC server. Two security policy settings govern that surface. The first, &lt;strong&gt;Network access: Restrict anonymous access to Named Pipes and Shares&lt;/strong&gt; (the &lt;code&gt;RestrictNullSessAccess&lt;/code&gt; registry value under &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Services\LanManServer\Parameters&lt;/code&gt;), has shipped at value &lt;code&gt;1&lt;/code&gt; -- enforced -- by default since Windows Vista; its effective default is &quot;Enabled&quot; on stand-alone servers, domain controllers, member servers, and client computers [@ms-restrict-anonymous-named-pipes]. The second, &lt;strong&gt;Network access: Named Pipes that can be accessed anonymously&lt;/strong&gt; (the &lt;code&gt;NullSessionPipes&lt;/code&gt; list), enumerates the small set of pipes that an unauthenticated caller is allowed to touch even when the first policy is enforced. &lt;code&gt;spoolss&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; on the default &lt;code&gt;NullSessionPipes&lt;/code&gt; list [@ms-named-pipes-anonymous].The combination of these two settings is what makes a default modern Windows host immune to anonymous-SMB reachability of &lt;code&gt;\PIPE\spoolss&lt;/code&gt;. The MS-RPRN spec&apos;s &quot;MUST use no authentication&quot; sentence [@ms-rprn-transport] reads like a security failure in isolation; combined with &lt;code&gt;RestrictNullSessAccess=1&lt;/code&gt; and the absence of &lt;code&gt;spoolss&lt;/code&gt; from &lt;code&gt;NullSessionPipes&lt;/code&gt; [@ms-restrict-anonymous-named-pipes] [@ms-named-pipes-anonymous], it becomes a deliberate division of labour: RPC does not authenticate; SMB does. The architectural cost is that the boundary is administered through two settings on a different policy surface than the spooler itself.&lt;/p&gt;
&lt;p&gt;On a default Windows 11 24H2 host with the Print Spooler running, then: an unauthenticated remote attacker on the network cannot reach &lt;code&gt;\PIPE\spoolss&lt;/code&gt;. A &lt;em&gt;domain&lt;/em&gt; user authenticated to the same Active Directory forest can. That is the practical reachability boundary that CERT/CC and CISA had in mind when they called PrintNightmare a &quot;domain takeover&quot; primitive [@bleepingcomputer-domain-takeover] [@cisa-ed-21-04]: any domain user reaches the spooler on a domain controller; the spooler executes attacker-supplied code as LocalSystem; that LocalSystem code now runs on a host that owns the domain. The &quot;domain user can reach it&quot; half is true because SMB authenticates the user and the RPC layer accepts whatever SMB says; the &quot;executes attacker-supplied code as LocalSystem&quot; half is the architectural primitive section 2.3 will name.&lt;/p&gt;
&lt;h3&gt;2.3 The Back-Compat Constraint&lt;/h3&gt;
&lt;p&gt;Why has the architecture not been replaced? Because essentially every Windows-compatible printer manufactured since 1993 ships a third-party driver DLL that expects to be loaded into &lt;code&gt;spoolsv.exe&lt;/code&gt; as LocalSystem.&lt;/p&gt;
&lt;p&gt;The v3 driver model -- introduced with Windows 2000 -- loads driver render code into the spooler process by default [@ms-print-spooler-architecture]. The v4 driver model, introduced with Windows 8, was a simpler XPS-based alternative meant to package drivers in a way that worked across multiple Windows form factors [@ms-print-spooler-architecture]. It did not replace v3. The two coexisted for more than a decade. The IPP class driver [@ms-modern-print-platform], which lets Windows print to any Mopria-certified printer without any vendor-specific driver at all, was not even an option for the first twenty years of the spooler&apos;s life [@mopria-certified-products].&lt;/p&gt;
&lt;p&gt;What this means in practice: the installed base of printers in 2021 was overwhelmingly v3 drivers, signed by vendors, packaged for LocalSystem load. A naive &quot;sandbox the spooler&quot; change that broke that loading model would break printing for every one of those printers. Microsoft has spent twenty years trying not to make printing not work. That constraint is the protagonist of the rest of the article.&lt;/p&gt;
&lt;h3&gt;2.4 Point and Print and Why It Is Its Own Constraint&lt;/h3&gt;
&lt;p&gt;Point and Print is the SMB-fetch-and-install-driver-on-print behavior introduced with Windows NT 4.0. When a client first prints to a shared printer, the spooler downloads the driver package from the print server and installs it locally. The user does not have to be an administrator.&lt;/p&gt;

A Windows print-client behavior in which a non-administrator user, on first use of a shared printer, causes their machine&apos;s spooler to download and install the printer&apos;s driver package from the print server. Two Group Policy registry values govern whether the user is warned and whether elevation is suppressed: `NoWarningNoElevationOnInstall` (suppress install-time elevation) and `NoWarningNoElevationOnUpdate` (suppress update-time elevation) [@kb-5005010-topic] [@kb-5005652-topic]. The Microsoft-supplied &quot;fix&quot; to this design surface is a third registry value, `RestrictDriverInstallationToAdministrators`, which overrides both.
&lt;p&gt;Bake &quot;any authenticated user can cause a driver DLL to be downloaded and registered&quot; into a protocol and you have, by construction, a low-privilege code-installation path. The two relevant Group Policy levers (&lt;code&gt;NoWarningNoElevationOnInstall&lt;/code&gt; and &lt;code&gt;NoWarningNoElevationOnUpdate&lt;/code&gt;) and the registry override (&lt;code&gt;RestrictDriverInstallationToAdministrators&lt;/code&gt;) all existed before PrintNightmare. All three defaulted to the permissive position. The June 2021 disclosure made the permissive defaults visible.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Three of the four Print Spooler design choices -- LocalSystem context, third-party DLL loading, and a low-privilege RPC entry point -- form the architectural primitive. The rest of this article is the story of what happens when the security community discovers, again and again, that any single primitive of that shape produces a SYSTEM-execution bug by construction.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;3. Pre-history: Stuxnet, PrintDemon, and the Bug Class That Already Had a Decade Behind It&lt;/h2&gt;
&lt;p&gt;PrintNightmare is the name the press gave to a 2021 disclosure event. The bug class behind that event is older. The first weaponized Print Spooler privilege-escalation primitive in the public record is from 2010, and it is famous. It was one of the four zero-days Stuxnet chained to reach centrifuge controllers in Natanz.&lt;/p&gt;
&lt;h3&gt;3.1 CVE-2010-2729 (Stuxnet, MS10-061)&lt;/h3&gt;
&lt;p&gt;In September 2010, Microsoft shipped MS10-061 to patch a Print Spooler Service Impersonation Vulnerability that &quot;could allow remote code execution if an attacker sends a specially crafted print request to a vulnerable system that has a print spooler interface exposed over RPC&quot; [@ms-bulletin-ms10-061]. The NVD entry classifies it as a CWE-20 Improper Input Validation in the Print Spooler service that, &quot;when printer sharing is enabled, does not properly validate spooler access permissions&quot; [@nvd-cve-2010-2729]. NVD records publication on September 15, 2010 [@nvd-cve-2010-2729].&lt;/p&gt;

The Symantec dossier on Stuxnet [@symantec-stuxnet-dossier-broadcom] is the canonical technical history of the Iran-Natanz campaign and is out of scope here. What matters for the Print Spooler story is the architectural pattern Stuxnet&apos;s operators noticed. A low-privilege caller could reach a SYSTEM-context RPC service, get the service to do something on the caller&apos;s behalf (write a file, load a DLL, validate a credential), and turn that operation into SYSTEM-context code execution. That pattern is the same one every later PrintNightmare-family bug exploits. The 2010 case is not the first instance of the pattern in Windows. It is the first instance of the pattern in the Windows Print Spooler in the public record.
&lt;h3&gt;3.2 CVE-2020-1048 (PrintDemon, May 2020)&lt;/h3&gt;
&lt;p&gt;Ten years later, in May 2020, two independent research teams published essentially the same Print Spooler bug. Peleg Hadar and Tomer Bar at SafeBreach Labs presented their work at DEF CON Safe Mode 2020 [@defcon-28-hadar-bar-pdf]. Yarden Shafir and Alex Ionescu at Windows Internals wrote it up under the name PrintDemon [@windows-internals-printdemon].The co-discovery pattern is the norm for high-value Windows-internals research. Two well-resourced teams looked at the same architectural primitive and arrived at the same vulnerability within weeks of each other. The May 2020 Microsoft Security Response Center acknowledgments credit both groups. The vulnerability was assigned CVE-2020-1048.&lt;/p&gt;
&lt;p&gt;The mechanism: &lt;code&gt;spoolsv.exe&lt;/code&gt; accepts a Win32 print API call to set a printer port. The port string can be a file path. The spooler, running as LocalSystem, then writes spool data to that file path. A low-privilege user can therefore cause SYSTEM-context arbitrary writes to anywhere on the filesystem. NVD classifies the bug as CWE-669 Incorrect Resource Transfer Between Spheres [@nvd-cve-2020-1048].&lt;/p&gt;
&lt;p&gt;The Shafir-Ionescu writeup is the source of the line that most concisely captures the spooler&apos;s long arc:&lt;/p&gt;

The Print Spooler continues to be one of the oldest Windows components that still has not gotten much scrutiny, even though it is largely unchanged since Windows NT 4, and was even famously abused by Stuxnet. -- Yarden Shafir and Alex Ionescu, May 2020 [@windows-internals-printdemon]
&lt;h3&gt;3.3 CVE-2020-1337 (PrintDemon Redux, August 2020)&lt;/h3&gt;
&lt;p&gt;Microsoft patched CVE-2020-1048 on May 12, 2020. Three months later, on August 11, 2020 Patch Tuesday, Microsoft patched CVE-2020-1337. Paolo Stagno (VoidSec) had demonstrated that the May patch was bypassable through an NTFS junction race [@voidsec-cve-2020-1337]. NVD classes the bypass as a CWE-367 TOCTOU [@nvd-cve-2020-1337].&lt;/p&gt;
&lt;p&gt;The mechanism is the canonical pattern for path-validation patches. Microsoft&apos;s May fix resolved the printer port file path, validated it as benign, then re-resolved it during the actual spool write. Between check and use, a non-administrator could substitute a reparse point that redirected the write to a SYSTEM-writable target. The patch had moved the security check; the architectural primitive (SYSTEM-context filesystem operation on a caller-controlled path) was unchanged.&lt;/p&gt;
&lt;p&gt;The detail to file away: the exact same primitive, NTFS reparse points racing a spooler-side resolve-validate-use sequence, would resurface eighteen months later in SpoolFool. Same primitive, different entry point.&lt;/p&gt;
&lt;h3&gt;3.4 The Pattern Nobody Had Yet Named&lt;/h3&gt;
&lt;p&gt;Three independent research efforts (the Microsoft analysis post-Stuxnet, the SafeBreach and Windows Internals work in 2020, the Sangfor work that would surface in 2021) each rediscovered variants of the same architectural primitive. The frustration the §1 hook left implicit is now nameable. The security community had documented this primitive twice before PrintNightmare became a news event.&lt;/p&gt;
&lt;p&gt;Will Dormann&apos;s CERT/CC advisory VU#383432 (issued June 30, 2021) was not, strictly speaking, about the bug. It was about the disclosure-norms failure that turned an internal bug into an internet-mirrored zero-day inside twenty-four hours. Dormann wrote in plain language:&lt;/p&gt;

CVE-2021-34527 is similar but distinct from the vulnerability that is assigned CVE-2021-1675, which addresses a different vulnerability in `RpcAddPrinterDriverEx()`. The attack vector is different as well. -- Will Dormann, CERT/CC VU#383432, June 30, 2021 [@cert-vu-383432]
&lt;p&gt;The sentence is unusual for a CERT advisory because it concedes mid-disclosure that the June 8 patch had named one CVE and the public exploits were targeting another. CERT/CC&apos;s explicit &quot;does NOT protect&quot; framing -- which we quote verbatim in section 4.1 at the point in the patch-cascade narrative where it lands hardest -- followed in the same advisory and made the gap unmistakable.&lt;/p&gt;
&lt;p&gt;PrintNightmare is not the name of a CVE. It is the name a panic gave, in the last week of June 2021, to a class of Print Spooler EoP and RCE primitives that had already been exploited in production eleven years earlier and rediscovered by independent researchers fourteen months earlier. The 2021 event made the class famous. It did not invent the class.&lt;/p&gt;
&lt;p&gt;The next section is what happened when Microsoft and the security community spent three years trying to patch the class out of existence one entry point at a time.&lt;/p&gt;
&lt;h2&gt;4. The Patch Cascade: Four Generations of PrintNightmare&lt;/h2&gt;
&lt;p&gt;Between June 8, 2021, and August 13, 2024, Microsoft shipped four named patch waves targeting the PrintNightmare bug class. None of the first three converged. The fourth was issued for an unrelated-looking CVE (CVE-2024-38198) that turned out to be exploitable against a primitive the September 2021 wave had already documented as residual.&lt;/p&gt;
&lt;p&gt;The Mermaid gantt below sets the spine of the timeline. It runs from Stuxnet through the announced third-party-driver end-of-servicing milestones in 2027. Every later subsection of this article maps to a bar in this chart.&lt;/p&gt;

gantt
    title Print Spooler hardening timeline 2010-2027
    dateFormat YYYY-MM-DD
    axisFormat %Y
    section Bugs
    CVE-2010-2729 Stuxnet           :crit, 2010-09-15, 60d
    CVE-2020-1048 PrintDemon        :crit, 2020-05-12, 60d
    CVE-2020-1337 PrintDemon redux  :crit, 2020-08-11, 60d
    CVE-2021-1675                   :crit, 2021-06-08, 30d
    CVE-2021-34527 PrintNightmare   :crit, 2021-07-01, 30d
    CVE-2021-34481                  :crit, 2021-07-15, 30d
    CVE-2021-36958                  :crit, 2021-09-14, 30d
    CVE-2022-21999 SpoolFool        :crit, 2022-02-08, 30d
    CVE-2024-38198                  :crit, 2024-08-13, 30d
    section Patches
    MS10-061                        :active, 2010-09-14, 30d
    KB5004945 emergency             :active, 2021-07-06, 30d
    KB5005010 default flip          :active, 2021-08-10, 30d
    KB5005652 policy rewrite        :active, 2021-09-14, 30d
    SpoolFool fix                   :active, 2022-02-08, 30d
    Redirection Guard               :active, 2023-12-01, 60d
    section Architecture
    WPP announced                   :done, 2023-12-13, 30d
    WPP ships opt-in 24H2           :done, 2024-10-01, 30d
    No new 3p drivers WU            :2026-01-15, 30d
    IPP class preferred             :2026-07-01, 30d
    3p driver servicing ends        :2027-07-01, 30d
&lt;h3&gt;4.1 Generation 1: The June 8 Patch and the Sangfor Disclosure Failure&lt;/h3&gt;
&lt;p&gt;On June 8, 2021 Patch Tuesday, Microsoft fixed CVE-2021-1675, a Windows Print Spooler Elevation of Privilege Vulnerability rated CVSS v3.1 7.8 local EoP [@nvd-cve-2021-1675]. The fix added an authorization check to &lt;code&gt;RpcAddPrinterDriverEx&lt;/code&gt; (MS-RPRN section 3.1.4.4.8) so that a low-privilege user could no longer install an arbitrary printer driver into the spooler process via that synchronous entry point [@ms-rprn-rpcaddprinterdriverex]. Microsoft credited Zhipeng Huo (Tencent Security Xuanwu Lab), Piotr Madej (AFINE), and Yunhai Zhang (NSFOCUS Security Team), as recorded in the Wayback snapshot of the Sangfor README [@afwu-wayback-snapshot]. Three reporters. No Victor Mata. Mata enters this story later, in section 4.3.&lt;/p&gt;
&lt;p&gt;On June 21, 2021, Microsoft silently reclassified CVE-2021-1675 from EoP to RCE [@nvd-cve-2021-1675]. BleepingComputer&apos;s June 30 article documents the reclassification and the subsequent confusion it caused [@bleepingcomputer-domain-takeover]. The Sangfor team had been working from the June 8 advisory&apos;s EoP framing; by the time they noticed the reclassification, their PoC was already mirrored across the internet.&lt;/p&gt;
&lt;p&gt;The chaos compressed into seventy-two hours. June 29: Sangfor pushes &lt;code&gt;afwu/PrintNightmare&lt;/code&gt;, then deletes the repository on realizing the RCE was unpatched. June 30: public mirrors propagate across multiple GitHub accounts; CERT/CC publishes VU#383432 [@cert-vu-383432]; Sergiu Gatlan files the BleepingComputer &quot;domain takeover&quot; story [@bleepingcomputer-domain-takeover]. July 1: Microsoft assigns CVE-2021-34527 as a separate-bulletin entity covering the unpatched RCE primitive [@nvd-cve-2021-34527] [@msrc-cve-2021-34527]. CERT/CC documents the CVE pair as &quot;similar but distinct&quot; with the qualifier that &quot;the attack vector is different as well&quot; [@cert-vu-383432]. They are not the same bug; the new CVE is not simply the &quot;remote&quot; version of the old one.&lt;/p&gt;

This update does NOT protect against public exploits that may refer to PrintNightmare or CVE-2021-1675. -- Will Dormann, CERT/CC VU#383432 [@cert-vu-383432]
&lt;p&gt;CERT/CC&apos;s only available mitigation, in the window between July 1 and the emergency patch, was to stop and disable the Spooler service entirely [@cert-vu-383432]. The runnable block below models the PowerShell logic in JavaScript (the blog runtime supports JS, not PowerShell). The semantics are the same: turn the service off, verify it stays off across reboot.&lt;/p&gt;
&lt;p&gt;{&lt;code&gt;// Original PowerShell from CERT/CC VU#383432: //   Stop-Service -Name Spooler -Force //   Set-Service -Name Spooler -StartupType Disabled //   Get-Service -Name Spooler // // The probe below models the resulting state machine so you can // see what &quot;safe&quot; looks like for a domain controller under CISA // Emergency Directive 21-04. const spoolerState = {   status: &apos;Stopped&apos;,   startupType: &apos;Disabled&apos; }; const isCertSafe = spoolerState.status === &apos;Stopped&apos;   &amp;amp;&amp;amp; spoolerState.startupType === &apos;Disabled&apos;; console.log(isCertSafe   ? &apos;OK: spooler stopped and disabled (CERT/CC mitigation in force)&apos;   : &apos;WARN: spooler running or set to auto-start (vulnerable surface present)&apos;);&lt;/code&gt;}&lt;/p&gt;
&lt;p&gt;On July 6 and July 7, 2021, Microsoft shipped KB5004945 out-of-band. The NVD entry for CVE-2021-34527 records both shipping dates verbatim: &quot;UPDATE July 7, 2021: The security update for Windows Server 2012, Windows Server 2016 and Windows 10, Version 1607 have been released&quot; [@nvd-cve-2021-34527]. KB5004945&apos;s summary line is unambiguous: &quot;Updates a remote code execution exploit in the Windows Print Spooler service, known as PrintNightmare, as documented in CVE-2021-34527&quot; [@kb-5004945-help].KB5004945&apos;s SKU fan-out was unusually wide. Microsoft shipped the patch for Windows 10 across multiple feature updates, for Windows 11 (just-released at the time), for Windows Server 2016/2019/2022, and for ESU-only SKUs back through Windows 7 SP1 and Windows Server 2008 R2 [@kb-5004945-help]. The fan-out signals how broadly the vulnerable surface had spread across the supported install base, which is most of the reason the press could describe the bug as fleet-wide.&lt;/p&gt;
&lt;p&gt;The patch had two parts. The first closed the immediate RCE. The second added a new Group Policy registry value: &lt;code&gt;RestrictDriverInstallationToAdministrators&lt;/code&gt;. KB5004945 shipped this value as OFF by default. KB5005010 (released August 10, 2021) records the timeline of the default flip verbatim: &quot;Updates released July 6, 2021 or later have a default of 0 (disabled) until updates released August 10, 2021. Updates released August 10, 2021 or later have a default of 1 (enabled)&quot; [@kb-5005010-topic].&lt;/p&gt;
&lt;p&gt;The patch had a switch. The switch was off by default. The press named the bug PrintNightmare. By the end of the first week, the patch had not, in practice, been applied to most of the installed base.&lt;/p&gt;
&lt;h3&gt;4.2 Generation 2: &lt;code&gt;@cube0x0&lt;/code&gt;, MS-PAR, and the Asynchronous-Variant Patch Bypass&lt;/h3&gt;
&lt;p&gt;After KB5004945 closed the synchronous &lt;code&gt;RpcAddPrinterDriverEx&lt;/code&gt; entry point on MS-RPRN, a researcher under the handle &lt;code&gt;@cube0x0&lt;/code&gt; updated his repository to target the symmetric asynchronous entry point in MS-PAR. Different protocol family. Same primitive. No patch.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RpcAsyncAddPrinterDriver&lt;/code&gt; (MS-PAR section 3.1.4.1, Opnum 39) is, in Microsoft&apos;s own words, &quot;The counterpart of this method in the Print System Remote Protocol&quot; [@ms-par-rpcasyncaddprinterdriver]. CERT/CC&apos;s updated VU#383432 names the bypass explicitly:&lt;/p&gt;

While original exploit code relied on the `RpcAddPrinterDriverEx` to achieve code execution, an updated version of the exploit uses `RpcAsyncAddPrinterDriver` to achieve the same goal. -- Will Dormann, CERT/CC VU#383432 update [@cert-vu-383432]
&lt;p&gt;The &lt;code&gt;@cube0x0&lt;/code&gt; GitHub repository carries the artifact of the rename-mid-disclosure chaos in its very name. The repository is called &lt;code&gt;cube0x0/CVE-2021-1675&lt;/code&gt;. The vulnerability it actually exploits is CVE-2021-34527. The README&apos;s first paragraph clarifies: &quot;Impacket implementation of the PrintNightmare PoC originally created by Zhiniang Peng (@edwardzpeng) and Xuefeng Li (@lxf02942370). Tested on a fully patched 2019 Domain Controller&quot; [@cube0x0-cve-2021-1675].The repository-name-versus-CVE mismatch is a small artifact of the disclosure chaos, but it caused real downstream confusion. Detection rule authors had to handle both names. SigmaHQ&apos;s Zeek-on-the-wire rule for the wire-level driver-install primitive lists both &lt;code&gt;RpcAddPrinterDriverEx&lt;/code&gt; and &lt;code&gt;RpcAsyncAddPrinterDriver&lt;/code&gt; precisely because the entry point split between the two CVEs [@sigma-cve-2021-1675-zeek].&lt;/p&gt;
&lt;p&gt;The July 6 emergency patch (KB5004945) added the access check to MS-PAR&apos;s &lt;code&gt;RpcAsyncAddPrinterDriver&lt;/code&gt; in addition to MS-RPRN&apos;s &lt;code&gt;RpcAddPrinterDriverEx&lt;/code&gt;. Microsoft&apos;s NVD entry for CVE-2021-34527 records the residual configuration risk verbatim:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &quot;Having &lt;code&gt;NoWarningNoElevationOnInstall&lt;/code&gt; set to 1 makes your system vulnerable by design.&quot; -- Microsoft, NVD entry for CVE-2021-34527 [@nvd-cve-2021-34527]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;CISA&apos;s response was Emergency Directive 21-04, issued July 13, 2021. The directive mandated that federal civilian agencies disable the Print Spooler service on all Microsoft Active Directory domain controllers by 11:59 PM EDT on Wednesday, July 14, 2021 [@cisa-ed-21-04]. The framing in CISA&apos;s own words was direct: &quot;exploitation of the vulnerability allows an attacker to remotely execute code with system level privileges enabling a threat actor to quickly compromise the entire identity infrastructure of a targeted organization&quot; [@cisa-ed-21-04].&lt;/p&gt;

ED 21-04 is narrower than the press summaries suggest. It applies only to Active Directory domain controllers. It does not require disabling Spooler on every Windows endpoint in a federal agency, only on the hosts where the bug&apos;s domain-takeover impact is largest. CISA closed ED 21-04 in January 2026 and folded its required actions into BOD 22-01 (the Known Exploited Vulnerabilities catalogue), but the operational guidance survived intact: disable Spooler on DCs, patch elsewhere. The DC-disabled baseline is still the federal civilian default for agencies that have not migrated to Universal Print [@cisa-ed-21-04]. We come back to this in section 10.4.
&lt;p&gt;The June 8 patch covered one RPC entry point. The July 6 patch covered the other. Neither patch changed the architectural primitive. Within five weeks, a third primitive (not an RPC entry point but a registry default) was already failing.&lt;/p&gt;
&lt;h3&gt;4.3 Generation 3: CVE-2021-34481, KB5005010, KB5005652, and the September Policy Rewrite&lt;/h3&gt;
&lt;p&gt;On August 10, 2021, Microsoft shipped the cumulative update that flipped the &lt;code&gt;RestrictDriverInstallationToAdministrators&lt;/code&gt; default from 0 to 1. On September 14, 2021, it shipped the knowledge-base article that documented why the previous defaults could not be saved.&lt;/p&gt;
&lt;p&gt;CVE-2021-34481 had already been published as a Print Spooler local EoP on July 15, 2021, classified by NVD as CWE-269 Improper Privilege Management [@nvd-cve-2021-34481]. The August 10 KB5005010 / KB5005033 cumulative updates closed it and flipped the default value for the &lt;code&gt;HKLM\Software\Policies\Microsoft\Windows NT\Printers\PointAndPrint\RestrictDriverInstallationToAdministrators&lt;/code&gt; registry value from 0 to 1 [@kb-5005010-topic]. NVD&apos;s entry for CVE-2021-34481 carries the cross-reference verbatim: &quot;UPDATE August 10, 2021: Microsoft has completed the investigation and has released security updates to address this vulnerability... This security update changes the Point and Print default behavior; please see KB5005652&quot; [@nvd-cve-2021-34481].&lt;/p&gt;
&lt;p&gt;The five-week opt-in window between July 6 and August 10, 2021, is the most interesting failure in the entire patch cascade. Hosts that received KB5004945 but had no Group Policy push for the new value were still exploitable through Point and Print elevation suppression even with the emergency patch applied. The lesson is structural. Opt-in safe defaults do not protect a real installed base.&lt;/p&gt;
&lt;p&gt;On September 14, 2021, KB5005652 shipped. The article&apos;s title spells out its scope: &quot;Manage new Point and Print default driver installation behavior (CVE-2021-34481)&quot; [@kb-5005652-topic]. The article&apos;s most-quoted sentence is the most consequential one Microsoft has shipped about Print Spooler:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; KB5005652 says, in a customer-facing knowledge-base article, that there is no settings-tweak combination that gives you the same protection as flipping the new admin-only switch. That is Microsoft, in its own voice, naming the configuration surface as insufficient.&lt;/p&gt;
&lt;/blockquote&gt;

There is no combination of mitigations that is equivalent to setting `RestrictDriverInstallationToAdministrators` to 1. -- Microsoft, KB5005652, September 14, 2021 [@kb-5005652-topic] [@kb-5005652-help]
&lt;p&gt;Read that sentence twice. Microsoft, in its own knowledge-base voice, said that no combination of the previously available configuration knobs added up to the protection the new admin-only restriction provided. The implication is that for the entire period from Windows NT 4 through August 10, 2021 (roughly twenty-three years), the configuration surface for Point and Print did not contain a setting that made the bug class go away. Tightening individual knobs got you somewhere short of the architectural answer. That is the verbatim concession the September article makes.&lt;/p&gt;
&lt;p&gt;The same Patch Tuesday (September 14, 2021), Microsoft also patched CVE-2021-36958, another Print Spooler RCE in the same family [@nvd-cve-2021-36958].The reporter attribution for CVE-2021-36958 remains disputed in the public record. Public consensus credits Victor Mata (Accenture Security FusionX) for the formal MSRC acknowledgment. Benjamin Delpy demonstrated public bypasses of the existing PrintNightmare mitigations through August 2021 that are most often cited as the immediate motivation for the September fix. We have not located a Microsoft-primary source that resolves the question, and we cite both names rather than collapse them.&lt;/p&gt;
&lt;p&gt;Three patch waves into PrintNightmare, Microsoft had written down, in a customer-facing knowledge-base article, that no configuration-surface response was equivalent to the architectural fix. The architectural fix did not yet exist. SpoolFool was four months away.&lt;/p&gt;
&lt;h3&gt;4.4 Generation 4: CVE-2022-21999 (SpoolFool, February 8, 2022)&lt;/h3&gt;
&lt;p&gt;On February 8, 2022, Oliver Lyak (handle &lt;code&gt;@ly4k_&lt;/code&gt;, trailing underscore) of SafeBreach Labs published SpoolFool. The exploit is a Print Spooler local privilege escalation that abuses the &lt;code&gt;SpoolDirectory&lt;/code&gt; registry value plus an NTFS junction [@ly4k-spoolfool]. The primitive was the same one Shafir and Ionescu had described eighteen months earlier in CVE-2020-1337. The patch surface had moved. The architectural primitive had not.&lt;/p&gt;
&lt;p&gt;The mechanism, walked through carefully: each per-printer registry key under &lt;code&gt;HKLM\System\CurrentControlSet\Control\Print\Printers\&amp;lt;printer-name&amp;gt;&lt;/code&gt; has a &lt;code&gt;SpoolDirectory&lt;/code&gt; value. The SYSTEM-context spooler reads that value, calls &lt;code&gt;CreateDirectory&lt;/code&gt; on the path, and then writes spool files into the resulting directory. The &lt;code&gt;SpoolDirectory&lt;/code&gt; value is writable by an authenticated user. The exploit therefore composes three steps: (1) set &lt;code&gt;SpoolDirectory&lt;/code&gt; to an attacker-chosen path, (2) plant an NTFS junction or symbolic link at that path pointing into a SYSTEM-writable directory, (3) trigger a printer reload to cause the SYSTEM-context spooler to create the destination directory and drop attacker-controlled files there [@ly4k-spoolfool]. NVD classifies the bug as CWE-59 Link Following [@nvd-cve-2022-21999].&lt;/p&gt;
&lt;p&gt;Anti-regression note for readers familiar with the early coverage: SpoolFool is a Print Spooler arbitrary-file-write LPE. It is not a Win32k integrity-level bypass. Win32k is the GUI subsystem and is uninvolved in this bug class. The researcher handle is &lt;code&gt;@ly4k_&lt;/code&gt; (Oliver Lyak), not &lt;code&gt;@jonas_lyk&lt;/code&gt; (a distinct security researcher).&lt;/p&gt;
&lt;p&gt;From arbitrary file write to SYSTEM code execution is the next step. Lyak&apos;s repository demonstrates a DLL-drop into a path that a SYSTEM-context process will load on next start, then a service restart, then SYSTEM-context execution of the attacker&apos;s DLL [@ly4k-spoolfool]. The end-to-end primitive is the same shape as the post-PrintDemon exploit chain from August 2020.&lt;/p&gt;
&lt;p&gt;The architectural moral: patching &lt;code&gt;RpcAddPrinterDriverEx&lt;/code&gt;, patching &lt;code&gt;RpcAsyncAddPrinterDriver&lt;/code&gt;, and flipping the Point and Print elevation default did not change the fact that the spooler runs as SYSTEM and operates on user-controlled filesystem paths. SpoolFool is the bug-fixing-bug exhibit for the section 5 architectural-concession argument. Four patches into the cycle, the same TOCTOU primitive that the August 2020 PrintDemon bypass had used was still exploitable eighteen months later, against a different callsite, against the same SYSTEM-context spooler.&lt;/p&gt;
&lt;h3&gt;4.5 Generation 5: CVE-2024-38198 (August 13, 2024 Patch Tuesday)&lt;/h3&gt;
&lt;p&gt;On August 13, 2024 Patch Tuesday, Microsoft patched a Windows Print Spooler Elevation of Privilege Vulnerability. The CVSS v3.1 base score was 7.5, with vector &lt;code&gt;AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H&lt;/code&gt; [@wiz-cve-2024-38198] [@rapid7-cve-2024-38198]. The CWE class was 345, Insufficient Verification of Data Authenticity [@wiz-cve-2024-38198]. The exploit primitive required winning a race condition. No public researcher attribution exists for it. There is, as of mid-2026, no public PoC.&lt;/p&gt;
&lt;p&gt;The framing here has to be precise. CWE-345 is &quot;Insufficient Verification of Data Authenticity&quot;; CWE-362 is &quot;Race Condition.&quot; These are two different classes. The exploit happens to require winning a race condition to be exploitable; that is a statement about how hard it is to exploit, not a statement about the underlying bug class. Microsoft (per Wiz, citing the MSRC advisory) classified the underlying defect as CWE-345.The CWE-345 attribution for CVE-2024-38198 is INFERRED via Wiz&apos;s vulnerability database, which states verbatim that &quot;the vulnerability has been classified under CWE-345 (Insufficient Verification of Data Authenticity) by Microsoft Corporation&quot; [@wiz-cve-2024-38198]. The MSRC update-guide page is a JavaScript single-page application, so verification of the CWE attribution by automated tools like &lt;code&gt;web_fetch&lt;/code&gt; runs through Wiz&apos;s vulnerability database as the one-step intermediary; a reader with a browser can confirm the same classification directly on the MSRC page. Rapid7&apos;s vulnerability database carries the per-SKU KB list and confirms the August 13, 2024 publication date [@rapid7-cve-2024-38198].&lt;/p&gt;
&lt;p&gt;Why this CVE matters for the section 5 argument: it is the empirical proof point that the spooler was still producing novel-class EoP primitives three years after PrintNightmare, eight months after Microsoft announced WPP in the December 2023 MORSE blog [@ms-blog-secure-print-experience-4002645], and seven weeks before WPP shipped opt-in.&lt;/p&gt;
&lt;p&gt;Four patch waves across three years. Five named CVEs in the patch tree, plus four more in the pre-history. Nine independently classed Print Spooler SYSTEM-code-execution primitives in fifteen years. The next section is about why Microsoft did not, and could not, ship a tenth patch that closed the class.&lt;/p&gt;
&lt;h3&gt;4.6 The Four-Generation Patch Tree, in One Diagram&lt;/h3&gt;
&lt;p&gt;The patch tree, mapped to the entry point each generation closed and the bypass each generation enabled:&lt;/p&gt;

flowchart TD
    G1[&quot;G1: June 8, 2021&lt;br /&gt;RpcAddPrinterDriverEx&lt;br /&gt;auth check added&quot;]
    G2[&quot;G2: July 6, 2021&lt;br /&gt;KB5004945 emergency&lt;br /&gt;RpcAsyncAddPrinterDriver patched&quot;]
    G3[&quot;G3: August 10, 2021&lt;br /&gt;KB5005010 / KB5005033&lt;br /&gt;RestrictDriverInstallationToAdmins default 0 to 1&quot;]
    G3b[&quot;G3b: September 14, 2021&lt;br /&gt;KB5005652&lt;br /&gt;no settings combination is equivalent&quot;]
    G4[&quot;G4: February 8, 2022&lt;br /&gt;CVE-2022-21999 SpoolFool&lt;br /&gt;SpoolDirectory + NTFS junction CWE-59&quot;]
    G5[&quot;G5: August 13, 2024&lt;br /&gt;CVE-2024-38198&lt;br /&gt;CWE-345 race-condition-exploitable&quot;]
    G1 --&amp;gt; G2
    G2 --&amp;gt; G3
    G3 --&amp;gt; G3b
    G3b --&amp;gt; G4
    G4 --&amp;gt; G5
    G5 --&amp;gt; EXIT[&quot;Architectural exit:&lt;br /&gt;WPP / Universal Print&lt;br /&gt;(section 5)&quot;]
&lt;p&gt;And the four-fix-strategy comparison matrix in scannable form:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Generation&lt;/th&gt;
&lt;th&gt;CVE&lt;/th&gt;
&lt;th&gt;Patch artifact&lt;/th&gt;
&lt;th&gt;Attack surface closed&lt;/th&gt;
&lt;th&gt;Attack surface left open&lt;/th&gt;
&lt;th&gt;Time to documented bypass&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;G1&lt;/td&gt;
&lt;td&gt;CVE-2021-1675&lt;/td&gt;
&lt;td&gt;June 8 monthly patch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RpcAddPrinterDriverEx&lt;/code&gt; (MS-RPRN) low-priv path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RpcAsyncAddPrinterDriver&lt;/code&gt; (MS-PAR) low-priv path&lt;/td&gt;
&lt;td&gt;~3 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G2&lt;/td&gt;
&lt;td&gt;CVE-2021-34527&lt;/td&gt;
&lt;td&gt;KB5004945 (July 6-7 OOB)&lt;/td&gt;
&lt;td&gt;Both RPC entry points&lt;/td&gt;
&lt;td&gt;Point-and-Print elevation suppression (&lt;code&gt;NoWarningNoElevationOnInstall=1&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;~5 weeks (config-not-yet-flipped)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G3&lt;/td&gt;
&lt;td&gt;CVE-2021-34481&lt;/td&gt;
&lt;td&gt;KB5005010 / KB5005033 / KB5005652&lt;/td&gt;
&lt;td&gt;Admin-only default for new printer driver install&lt;/td&gt;
&lt;td&gt;Spool directory filesystem operations&lt;/td&gt;
&lt;td&gt;~21 weeks (SpoolFool)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G4&lt;/td&gt;
&lt;td&gt;CVE-2022-21999&lt;/td&gt;
&lt;td&gt;February 2022 patch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SpoolDirectory&lt;/code&gt; reparse-point race&lt;/td&gt;
&lt;td&gt;Other spooler filesystem operations and authenticity checks&lt;/td&gt;
&lt;td&gt;~30 months (CVE-2024-38198)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G5&lt;/td&gt;
&lt;td&gt;CVE-2024-38198&lt;/td&gt;
&lt;td&gt;August 13, 2024 patch&lt;/td&gt;
&lt;td&gt;CWE-345 authenticity gap (race-condition exploitable)&lt;/td&gt;
&lt;td&gt;Architectural primitive itself&lt;/td&gt;
&lt;td&gt;(no public bypass as of June 2026)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Five subsections. Five entry points. One architectural primitive. The patches do not converge because they cannot.&lt;/p&gt;
&lt;h2&gt;5. The Architectural Concession: Why Microsoft Cannot Sandbox &lt;code&gt;spoolsv.exe&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;An obvious question reading section 4 is: why does Microsoft not just sandbox &lt;code&gt;spoolsv.exe&lt;/code&gt;? AppContainer exists. Win32 has had constrained-token processes since Windows 8. The Microsoft Office suite runs in low-trust containers. Why is the Print Spooler the exception?&lt;/p&gt;
&lt;h3&gt;5.1 The Naive Sandbox Proposal&lt;/h3&gt;
&lt;p&gt;The naive proposal is to run &lt;code&gt;spoolsv.exe&lt;/code&gt; in an AppContainer with no SYSTEM token. The proposal fails for two reasons. The first is engineering. The spooler must register with the Service Control Manager, must coordinate with kernel-mode print components, and must accept inbound RPC over a system named pipe -- operations a fully constrained token does not permit. That problem is solvable; it costs engineering effort, but it has obvious answers (broker process, careful capability grants, custom token).&lt;/p&gt;

A Windows process sandboxing primitive introduced for the Universal Windows Platform that runs a process with a custom integrity level, a restricted token, and a set of explicitly granted capabilities. AppContainer-restricted processes cannot make network connections, read user files, or invoke APIs outside their capability set without explicit permission. Microsoft Edge content processes and many Windows Store apps run in AppContainers; legacy Win32 services typically do not.
&lt;p&gt;The second reason is the back-compat constraint from section 2.3. The third-party driver DLLs in the installed base are signed and packaged to expect LocalSystem context. They use Win32 APIs that a constrained token cannot call. They write to filesystem locations a constrained token cannot reach. They register printer ports through interfaces that a fully sandboxed spooler could not host. The cost of the constrained-token migration is not the cost of changing one Microsoft binary. It is the cost of breaking, in the worst case, every Windows-compatible printer manufactured before 2024.Microsoft has never published a statement that AppContainer was explicitly evaluated and rejected for &lt;code&gt;spoolsv.exe&lt;/code&gt;. The argument above is INFERRED from the absence of any constrained-token Spooler in any shipped Windows release, and from the MORSE blog&apos;s repeated framing of the third-party driver install base as the binding constraint [@ms-wpp-more-info]. The inference is well grounded but not directly stated.&lt;/p&gt;
&lt;h3&gt;5.2 PrintIsolationHost.exe as Partial Answer&lt;/h3&gt;
&lt;p&gt;Microsoft&apos;s first attempt to break the &quot;DLL loaded inside &lt;code&gt;spoolsv.exe&lt;/code&gt;&quot; conjunct shipped with Windows 7 and Windows Server 2008 R2 (October 22, 2009) [@ms-print-spooler-architecture] [@ms-previous-versions-server-2008-R2]. It was called Printer Driver Isolation. The mechanism: third-party driver code can run in a sibling process called &lt;code&gt;PrintIsolationHost.exe&lt;/code&gt;. The spooler talks to that process over IPC instead of loading the driver DLL into its own address space.&lt;/p&gt;

A sibling host process introduced in Windows 7 / Server 2008 R2 (October 22, 2009) that can load third-party printer driver code outside of `spoolsv.exe`. Drivers opt in via the `DriverIsolation` directive in their INF file: Microsoft&apos;s documentation enumerates two values, `2` (&quot;the driver supports driver isolation&quot;) and `0` (&quot;the driver does not support driver isolation&quot;; the same effect as omitting the keyword) [@ms-printer-driver-isolation]. By default, `PrintIsolationHost.exe` runs as LocalSystem [@ms-printer-driver-isolation] [@ms-print-spooler-architecture]. The isolation is process isolation, not privilege isolation.
&lt;p&gt;Three details matter for the section 5 argument. First, the isolation is process isolation, not privilege isolation: &lt;code&gt;PrintIsolationHost.exe&lt;/code&gt; itself runs as LocalSystem. A bug in &lt;code&gt;PrintIsolationHost.exe&lt;/code&gt; is still a SYSTEM bug, just in a different process. Second, the opt-in is the driver vendor&apos;s responsibility, set in the INF file&apos;s &lt;code&gt;DriverIsolation&lt;/code&gt; directive [@ms-printer-driver-isolation]. By default, if the INF does not opt in, the spooler loads the driver in-process. Third, and most importantly: &lt;code&gt;PrintIsolationHost.exe&lt;/code&gt; only hosts driver code at print time. It does not move the RPC server, the driver-installation flow (&lt;code&gt;RpcAddPrinterDriverEx&lt;/code&gt; and &lt;code&gt;RpcAsyncAddPrinterDriver&lt;/code&gt;), or the spool directory filesystem operations out of &lt;code&gt;spoolsv.exe&lt;/code&gt;. The PrintNightmare entry points are all in code paths Printer Driver Isolation does not touch.&lt;/p&gt;
&lt;p&gt;So Printer Driver Isolation existed for twelve years before PrintNightmare. It did not help. It addresses a different attack surface.&lt;/p&gt;
&lt;h3&gt;5.3 The MORSE Framing&lt;/h3&gt;
&lt;p&gt;In December 2023, the Microsoft Offensive Research and Security Engineering (MORSE) team and the Print team co-authored a Microsoft Security Blog post announcing what would become Windows Protected Print Mode. The blog and its companion Microsoft Learn pages contain two sentences that are load-bearing for the rest of this article.&lt;/p&gt;
&lt;p&gt;The first sentence sets the cadence empirically:&lt;/p&gt;

Print bugs accounted for 9% of all cases reported to the Microsoft Security Response Center (MSRC) over the past three years. -- Microsoft, December 2023 [@ms-wpp-more-info] [@ms-blog-secure-print-experience-4002645]
&lt;p&gt;The &quot;over the past three years&quot; qualifier matters. The 9% is a baseline measurement for 2020 through 2023, not a long-term steady-state rate. Without the qualifier, the number reads as a stable structural fact about Windows. With the qualifier, it reads as what it actually is: a measurement of the period during which the patch cascade documented in section 4 was running.&lt;/p&gt;
&lt;p&gt;The second sentence is more consequential. Microsoft, in its own voice, names the architectural answer:&lt;/p&gt;

The ideal solution would be to remove drivers entirely and move the Spooler to a least privilege security model. -- Microsoft, MORSE / Print team [@ms-wpp-more-info]
&lt;p&gt;Read that sentence in the context of the section 4 patch cascade. Microsoft is saying that the architectural answer to the bug class is not a better authorization check, not a tighter Point and Print policy, not a more aggressive default flip. It is to remove third-party drivers entirely and to move the spooler off LocalSystem. The enterprise version of the same document spells out the coverage expectation:&lt;/p&gt;

Windows protected print mode would mitigate over half of past reported security issues for Windows print. -- Microsoft, Windows Protected Print Mode for Enterprises [@ms-wpp-enterprises]
&lt;p&gt;&quot;Past reported security issues for Windows print&quot; is a class. &quot;Would mitigate over half&quot; is a coverage statement at the class level, not at the bug level. WPP is a class mitigation; it is the architectural answer the patch cascade could not produce.&lt;/p&gt;
&lt;h3&gt;5.4 The Forced Parallel-Stack Answer&lt;/h3&gt;
&lt;p&gt;Here is where the argument turns. Microsoft did not ship one architectural answer. It shipped two. The reason is that neither one alone covers the back-compat envelope.&lt;/p&gt;
&lt;p&gt;Universal Print is the cloud-hosted answer. It removes the local print queue, removes the local SYSTEM-context Spooler from the workflow entirely, and centralizes the print fan-out in Microsoft 365 [@ms-universal-print-whatis]. On a Universal-Print-only endpoint with the local Spooler service disabled, there is no &lt;code&gt;\PIPE\spoolss&lt;/code&gt; exposed to a low-privilege user. The architectural primitive&apos;s conjunct (a) -- the low-privilege RPC entry -- simply does not exist on that host.&lt;/p&gt;
&lt;p&gt;Windows Protected Print Mode is the local-stack answer. It keeps the local Spooler service but restructures it: most operations are deferred to a Spooler Worker process with a restricted token, and the spooler refuses to load any driver DLL that is not Microsoft-signed [@ms-wpp-more-info] [@ms-wpp-canonical]. The architectural primitive&apos;s conjuncts (b) (caller-influenced DLL load) and partially (c) (SYSTEM context, for per-user operations) are broken.&lt;/p&gt;
&lt;p&gt;Neither answer covers the union of constraints that a real Windows fleet faces. Universal Print requires cloud connectivity, Microsoft 365 / Entra ID licensing, and per-printer service costs. It does not work offline. It does not work for specialty printers (industrial label printers, healthcare imaging printers, secure check printers) that have no IPP-class-compatible firmware. WPP requires Mopria-certified printers or the small set of Microsoft-signed drivers that ship inbox. It does not work for the same specialty-printer category. The two answers cover different threat models, different licensing models, and different operational realities.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Windows Protected Print Mode and Universal Print are not redundant. They break different conjuncts of the architectural primitive, and together they cover what neither covers alone. The 2024 Windows print stack is a deliberate parallel architecture, not a transition state.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The WPP FAQ confirms the parallel-stack reading. When asked &quot;Will Windows protected print mode ever be enabled by default?&quot; the page answers verbatim: &quot;Windows protected print mode will be enabled by default at a future date&quot; [@ms-wpp-faq].The &quot;future date&quot; phrasing in the WPP FAQ is preserved verbatim because it carries the entire commitment. Microsoft has published deprecation milestones for third-party drivers (January 15, 2026; July 1, 2026; July 1, 2027) [@ms-end-of-servicing], but it has not committed to a date for WPP-on-by-default. As of June 2026, &quot;at a future date&quot; is still the only formal commitment.&lt;/p&gt;
&lt;h3&gt;5.5 The Conjunct Framing as Lead-in to Section 8&lt;/h3&gt;
&lt;p&gt;We can state the architectural argument compactly now and we will return to it formally in section 8. The architectural primitive has three conjuncts. (a) The service accepts low-privilege RPC. (b) It loads caller-influenced third-party DLLs. (c) It runs at SYSTEM. Any service of that shape produces a SYSTEM-execution primitive by construction. Microsoft&apos;s three shipped approaches each break exactly one conjunct:&lt;/p&gt;

flowchart LR
    PRIM[&quot;Architectural primitive&lt;br /&gt;(a) low-priv RPC entry&lt;br /&gt;(b) caller-influenced DLL load&lt;br /&gt;(c) SYSTEM context&quot;]
    EXITA[&quot;Break (a) low-priv RPC entry&quot;]
    EXITB[&quot;Break (b) caller-influenced DLL load&quot;]
    EXITC[&quot;Break (c) SYSTEM context&quot;]
    PRIM --&amp;gt; EXITA
    PRIM --&amp;gt; EXITB
    PRIM --&amp;gt; EXITC
    EXITA --&amp;gt; UP[&quot;Universal Print (2021)&lt;br /&gt;no local pipe spoolss&quot;]
    EXITA --&amp;gt; CERT[&quot;Stop Spooler service&lt;br /&gt;(CERT/CC 2021)&quot;]
    EXITB --&amp;gt; WPPMOD[&quot;WPP module blocking (2024)&lt;br /&gt;only Microsoft-signed drivers&quot;]
    EXITC --&amp;gt; PIH[&quot;PrintIsolationHost (2009)&lt;br /&gt;partial: still LocalSystem&quot;]
    EXITC --&amp;gt; WPPWORKER[&quot;WPP Spooler Worker (2024)&lt;br /&gt;restricted token, below SYSTEM IL&quot;]
&lt;p&gt;The remaining sections are about the design space Microsoft chose to occupy in 2024, why it occupies two points rather than one, and what is still missing in 2026 -- including, candidly, a satisfying answer for environments that cannot adopt either architectural exit.&lt;/p&gt;
&lt;h2&gt;6. State of the Art: Windows Protected Print Mode in 24H2&lt;/h2&gt;
&lt;p&gt;Windows Protected Print Mode shipped to Windows 11 24H2 on October 1, 2024 as an opt-in feature [@computerweekly-quocirca-wpp] [@ms-wpp-canonical]. As of June 2026 it is still opt-in. The WPP FAQ uses the verbatim phrase &quot;at a future date&quot; for when the default-on flip will happen [@ms-wpp-faq]. No date has been committed.&lt;/p&gt;

An opt-in Windows print stack introduced with Windows 11 24H2 (October 1, 2024) that exclusively uses the modern print stack, blocks all third-party printer drivers, runs normal spooler operations in a Spooler Worker process with a restricted token below SYSTEM integrity, and falls back to the inbox Microsoft IPP Class Driver for printer communication [@ms-wpp-canonical] [@ms-wpp-more-info]. Activation is by Group Policy (&quot;Configure Windows protected print&quot;), Intune (`./Device/Vendor/MSFT/Policy/Config/Printers/ConfigureWindowsProtectedPrint` via the Policy CSP for Printers [@ms-policy-csp-printers]), or registry [@ms-wpp-enterprises].
&lt;h3&gt;6.1 What WPP Changes&lt;/h3&gt;
&lt;p&gt;Microsoft&apos;s MORSE / Print team blog enumerates six concurrent changes [@ms-wpp-more-info]. Each one is interesting on its own; together they constitute the architectural exit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spooler Worker process with restricted token.&lt;/strong&gt; Normal &lt;code&gt;spoolsv.exe&lt;/code&gt; operations are deferred to a new Spooler Worker process. The worker runs with a restricted token that drops &lt;code&gt;SeTcbPrivilege&lt;/code&gt; and &lt;code&gt;SeAssignPrimaryTokenPrivilege&lt;/code&gt; and runs below SYSTEM integrity level. This is the operational form of &quot;move the Spooler to a least privilege security model&quot; from the MORSE quote.&lt;/p&gt;

The new Spooler Worker process has a new restricted token that removes many privileges such as SeTcbPrivilege, SeAssignPrimaryTokenPrivilege, and no longer runs at SYSTEM IL. -- Microsoft Learn, More information on Windows Protected Print Mode [@ms-wpp-more-info]
&lt;p&gt;That sentence, taken verbatim from Microsoft&apos;s own architecture documentation [@ms-wpp-more-info] [@ms-wpp-more-info-wayback], is the most concrete claim Microsoft has shipped about how WPP breaks conjunct (c). Two privileges enumerated, one integrity level reduced. The legacy &lt;code&gt;spoolsv.exe&lt;/code&gt; process is still SYSTEM; the &lt;em&gt;worker&lt;/em&gt; that does the per-job work is not.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Module blocking.&lt;/strong&gt; APIs that previously allowed third-party module loading (&lt;code&gt;AddPrintProviderW&lt;/code&gt; and similar) are gated by a module-blocking policy. The MORSE document states the new policy verbatim: &quot;only Microsoft Signed binaries required for IPP are loaded&quot; [@ms-wpp-more-info].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;XPS rendering per-user.&lt;/strong&gt; XPS rendering, historically a source of memory-corruption bugs in &lt;code&gt;PrintFilterPipelineSvc&lt;/code&gt;, runs per-user instead of as SYSTEM. A memory-corruption bug in the XPS parser now compromises a user, not the machine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Process hardening on the Spooler Worker.&lt;/strong&gt; The Spooler Worker process is built with &lt;a href=&quot;https://paragmali.com/blog/process-mitigation-policies-cfg-acg-cig-and-the-layer-betwee/&quot; rel=&quot;noopener&quot;&gt;Control Flow Guard&lt;/a&gt;, Control Flow Enforcement Technology (Intel CET shadow stack), Arbitrary Code Guard, Child Process Creation Disabled, and Redirection Guard enabled [@ms-wpp-more-info] [@msrc-redirectionguard-blog]. The MORSE blog explicitly says why the legacy spooler could not enable these mitigations: &quot;many print drivers are decades old and are incompatible with modern security mitigations&quot; [@ms-wpp-more-info].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Point and Print restricted.&lt;/strong&gt; Point and Print can configure an IPP printer but cannot install a third-party driver. The MORSE document is verbatim: &quot;Windows protected print mode prevents Point and Print from ever installing third-party drivers&quot; [@ms-wpp-more-info]. That sentence is the architectural answer to the Generation 3 patch wave from section 4.3.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fallback to inbox IPP class driver.&lt;/strong&gt; Printing falls back to the Microsoft IPP Class Driver that ships with Windows. The driver works with Mopria-certified printers and with the Microsoft-signed driver subset [@mopria-certified-products] [@ms-modern-print-platform].&lt;/p&gt;
&lt;h3&gt;6.2 Mapping WPP to the Three Conjuncts&lt;/h3&gt;
&lt;p&gt;WPP breaks conjunct (b) by refusing to load anything that is not Microsoft-signed. It weakens conjunct (c) by moving the bulk of operations into a Spooler Worker with a restricted token below SYSTEM integrity. The low-privilege RPC entry (conjunct a) is preserved by design: the RPC interface still exists, clients still talk to it, but what they can ask the service to do is reduced.&lt;/p&gt;
&lt;p&gt;That last asymmetry matters. WPP does not delete the &lt;code&gt;\PIPE\spoolss&lt;/code&gt; endpoint. A WPP-enabled host still answers &lt;code&gt;RpcAddPrinterDriverEx&lt;/code&gt; calls; it just refuses to load an unsigned driver in response. Detection rules that watched for the RPC call itself (the SigmaHQ Zeek-on-the-wire rule, for instance [@sigma-cve-2021-1675-zeek]) still see traffic on WPP hosts; rules that watched for the resulting unsigned DLL load (the SigmaHQ image-load rule [@sigma-cve-2021-1675-win-spooler]) should see audit events instead.&lt;/p&gt;
&lt;h3&gt;6.3 The Compatibility Envelope&lt;/h3&gt;
&lt;p&gt;WPP requires either a printer that the inbox IPP class driver can drive (a Mopria-certified printer in practice) or one of the small set of Microsoft-signed drivers. The Mopria Alliance certified-products directory lists a multi-vendor catalog of printers across Brother, Canon, HP, Epson, Lexmark, Xerox, and others [@mopria-certified-products]. The installed base of Mopria-certified printers is large.The Mopria Alliance does not publish a single official total install-base count. The certified-products directory is the canonical inventory [@mopria-certified-products], and the industry-analyst framing in the December 2023 MORSE blog points to a multi-vendor catalog &quot;covering many of the most common printer brands sold worldwide&quot; [@ms-blog-secure-print-experience-4002645]. We report the order of magnitude (industry-wide) rather than a brittle exact count.&lt;/p&gt;
&lt;p&gt;Printers that require vendor-specific v3 drivers are not WPP-compatible by default. Industrial label printers (Zebra, Honeywell, SATO, TSC, Dymo) are the painful case. Their command languages (ZPL, EPL) are not part of the IPP class driver&apos;s repertoire [@ezeep-label-printers-wpp]. ezeep&apos;s June 2026 writeup is blunt: &quot;Most thermal label printers... are not Mopria-certified, so they stop working when Windows Protected Print Mode is enforced. ZPL and EPL are not part of the IPP spec the IPP class driver speaks&quot; [@ezeep-label-printers-wpp]. Three paths are open: keep WPP disabled on label workstations via GPO, refresh hardware to IPP-capable models, or use a cloud-rendered alternative.&lt;/p&gt;
&lt;p&gt;Vendors that want WPP compatibility without a full IPP firmware conversion can ship Print Support Apps. Brother is one of the first vendors to publish a PSA [@brother-print-support-app]. Lexmark&apos;s vendor primary on the WPP transition documents the same path [@lexmark-wpp-support].&lt;/p&gt;

The Microsoft-supplied inbox driver that uses the Internet Printing Protocol (IPP) to communicate with printers that implement the Mopria-Alliance-certified IPP everywhere subset. WPP-enforced clients use this driver instead of a vendor-specific driver. Printers must be Mopria-certified (or implement Mopria-compatible IPP) for the inbox driver to drive them [@mopria-certified-products] [@ms-modern-print-platform].

The two pre-WPP Windows printer driver packaging models. v3 (Windows 2000 era) loads driver render code into the spooler process by default. v4 (Windows 8 era) is XPS-based, packaged for portability across architectures, and has a more limited print processor model. WPP deprecates both in favor of the inbox IPP class driver (or, transitionally, vendor Print Support Apps) [@ms-print-spooler-architecture] [@ms-end-of-servicing].
&lt;h3&gt;6.4 Deployment Surfaces and Detection Signals&lt;/h3&gt;
&lt;p&gt;WPP&apos;s enable / disable control is a binary two-state CSP. The Policy CSP page documents &lt;code&gt;Printers/ConfigureWindowsProtectedPrint&lt;/code&gt; as accepting &lt;code&gt;0&lt;/code&gt; (disabled, the 2026 default) or &lt;code&gt;1&lt;/code&gt; (enabled), with no audit / monitor intermediate enum [@ms-policy-csp-printers]. The corresponding Group Policy path is &quot;Computer Configuration &amp;gt; Administrative Templates &amp;gt; Printers &amp;gt; Configure Windows protected print&quot; [@ms-wpp-enterprises]. CIS Benchmarks v5.0.1 (Windows 11) and v1.0.0 (Server 2025) treat the setting as a Level-2 hardening recommendation with the same binary registry value [@tenable-cis-w11-l2] [@tenable-cis-server-2025-l2].&lt;/p&gt;
&lt;p&gt;This is an important correction to a piece of folk wisdom about WPP. The Windows kernel and AppLocker have audit / enforce modes; AppControl for Business has audit / enforce modes; AMSI has logging tiers. WPP does not. Microsoft did not ship an &quot;audit&quot; enum on &lt;code&gt;ConfigureWindowsProtectedPrint&lt;/code&gt;. Administrators who want pre-enforcement telemetry have to instrument it themselves, either by reading the existing &lt;code&gt;Microsoft-Windows-PrintService/Admin&lt;/code&gt; event log (which carries Point and Print failures and module-load refusals regardless of whether WPP is on) or by deploying WPP to a pilot ring and watching the same log on those pilot machines. The deployment pattern is rollout rings, not an in-product audit mode.&lt;/p&gt;
&lt;p&gt;Because there is no in-product audit mode, the pre-enforcement signal is the existing print-services event log. The &lt;code&gt;Microsoft-Windows-PrintService/Admin&lt;/code&gt; channel records driver-load failures, Point and Print restrictions, and plug-in load failures. Splunk Research&apos;s &lt;code&gt;spoolsv.exe&lt;/code&gt; rule pack covers PrintService Admin Event ID 808 (plug-in load failure) paired with security log Event ID 4909 [@splunk-research-spoolsv-plugin-fail], and Event ID 316 for driver-add operations [@splunk-printnightmare-story] [@splunk-research-printnightmare-driver]. Redirection Guard mitigation events land in &lt;code&gt;Microsoft-Windows-Security-Mitigations/Operational&lt;/code&gt; [@msrc-redirectionguard-blog]. The diagnostic Event ID 4098 (in the Application log) is the workhorse signal for Point and Print restrictions and predates WPP [@ms-event-ids-point-print].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;ConfigureWindowsProtectedPrint&lt;/code&gt; CSP has two states: &lt;code&gt;0&lt;/code&gt; (disabled) and &lt;code&gt;1&lt;/code&gt; (enabled). There is no in-product audit / monitor mode. The right deployment pattern is rings: pilot, broad-pilot, production. Pilot a small ring of representative endpoints with WPP enforced and watch &lt;code&gt;Microsoft-Windows-PrintService/Admin&lt;/code&gt; events 316, 808, and 4098 for failed driver loads and Point and Print restrictions. Identify the printers that would fail. Decide between a fleet hardware refresh, a transitional Print Support App, or an exclusion list. Then expand the ring.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The probe below models a WPP-state PowerShell script in JavaScript for the runtime. It pretends the four signals (WPP policy state, Redirection Guard, recent PrintService Admin events, IPP class driver availability) are already retrieved; in production the values come from the Group Policy resultant set, &lt;code&gt;Get-ProcessMitigation&lt;/code&gt;, &lt;code&gt;Get-WinEvent&lt;/code&gt;, and &lt;code&gt;Get-PrinterDriver&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;{`
// Original PowerShell equivalents:
//   $wppPolicy = (Get-ItemProperty &apos;HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers&apos;)
//                .ConfigureWindowsProtectedPrint  # 0 = disabled, 1 = enabled
//   $rg        = (Get-ProcessMitigation -Name spoolsv.exe).RedirectionTrust
//   $events    = Get-WinEvent -LogName &apos;Microsoft-Windows-PrintService/Admin&apos; &lt;br /&gt;//                  -MaxEvents 200 | Where-Object { $&lt;em&gt;.Id -in 316,808,4098 -and &lt;br /&gt;//                  $&lt;/em&gt;.TimeCreated -ge (Get-Date).AddDays(-7) }
//   $ipp       = (Get-PrinterDriver -Name &apos;Microsoft IPP Class Driver&apos;) -ne $null&lt;/p&gt;
&lt;p&gt;const state = {
  wppPolicy: 0,                   // 0 = disabled, 1 = enabled (binary CSP)
  redirectionGuard: &apos;Enabled&apos;,    // &apos;Disabled&apos; | &apos;Audit&apos; | &apos;Enabled&apos;
  recentPrintServiceFailures: 14, // count of EventID 316/808/4098 in last 7d
  inboxIppDriverPresent: true,
  deploymentRing: &apos;pilot&apos;         // &apos;pilot&apos; | &apos;broad-pilot&apos; | &apos;production&apos;
};&lt;/p&gt;
&lt;p&gt;function classify(s) {
  if (s.wppPolicy === 0) {
    return s.deploymentRing === &apos;pilot&apos;
      ? &apos;NotProtected: enable WPP on pilot ring and monitor PrintService/Admin&apos;
      : &apos;NotProtected: WPP is disabled in ring &apos; + s.deploymentRing;
  }
  // wppPolicy === 1 means enforced; there is no audit/monitor intermediate
  const supportingMitigations =
    s.redirectionGuard === &apos;Enabled&apos; &amp;amp;&amp;amp; s.inboxIppDriverPresent;
  if (s.recentPrintServiceFailures &amp;gt; 0) {
    return &apos;Enforced: &apos; + s.recentPrintServiceFailures
      + &apos; PrintService/Admin failures in 7d. Investigate before expanding ring.&apos;;
  }
  return supportingMitigations
    ? &apos;Protected: WPP enforced, Redirection Guard on, IPP driver present&apos;
    : &apos;Enforced: review Redirection Guard / IPP driver gaps&apos;;
}&lt;/p&gt;
&lt;p&gt;console.log(classify(state));
`}&lt;/p&gt;
&lt;h3&gt;6.5 Redirection Guard&lt;/h3&gt;
&lt;p&gt;Redirection Guard is an independent process mitigation that ships separately from WPP but composes with it. It first arrived in Windows 11 22H2 in late 2023 and was the subject of a June 2025 MSRC blog post that documents its design [@msrc-redirectionguard-blog]. The mitigation is documented in the &lt;code&gt;PROCESS_MITIGATION_REDIRECTION_TRUST_POLICY&lt;/code&gt; Win32 API structure [@ms-redirection-trust-policy] and is invoked through &lt;code&gt;Set-ProcessMitigation -Name spoolsv.exe -Enable RedirectionGuard&lt;/code&gt; [@ms-set-processmitigation].&lt;/p&gt;
&lt;p&gt;The mechanism: a process opted into Redirection Guard refuses to follow filesystem junctions or symbolic links created by non-administrator users. The MSRC blog frames the scope plainly: &quot;Junctions remain the biggest existing gap. Outside of a sandbox, they can be created by standard users and target any folder on the system&quot; [@msrc-redirectionguard-blog]. The Risky Business bulletin on the launch documents the empirical impact: of forty-two filesystem-path-redirection CVEs Microsoft patched in 2024, thirty-two used attacker-created junctions and could have been blocked by Redirection Guard had it been in place [@risky-biz-redirectionguard].&lt;/p&gt;
&lt;p&gt;Redirection Guard is the closest thing to a post-SpoolFool architectural fix in the legacy stack. WPP composes with it; a WPP-enabled host has both Redirection Guard on the legacy &lt;code&gt;spoolsv.exe&lt;/code&gt; process and the additional CFG / CET / ACG / Child Process Creation Disabled / Redirection Guard set on the Spooler Worker [@ms-wpp-more-info].&lt;/p&gt;
&lt;h3&gt;6.6 A Failed PrintNightmare Attempt Against a WPP-Enabled Host&lt;/h3&gt;
&lt;p&gt;The sequence below shows what happens when a low-privilege user attempts the Generation 1 PrintNightmare exploit against a WPP-enabled host. The RPC entry point is still answered; the module load is refused; the audit log captures the attempt; the elevation does not happen.&lt;/p&gt;

sequenceDiagram
    participant U as Low-priv user
    participant P as PIPE spoolss endpoint
    participant S as spoolsv.exe (parent)
    participant W as Spooler Worker (restricted token)
    participant L as Module loader
    participant V as Signature check
    participant E as PrintService Admin log
    U-&amp;gt;&amp;gt;P: RpcAddPrinterDriverEx (unsigned DLL)
    P-&amp;gt;&amp;gt;S: dispatch RPC call
    S-&amp;gt;&amp;gt;W: forward driver-install to worker
    W-&amp;gt;&amp;gt;L: load requested driver DLL
    L-&amp;gt;&amp;gt;V: verify signature
    V--&amp;gt;&amp;gt;L: reject (not Microsoft-signed)
    L--&amp;gt;&amp;gt;W: load refused
    W-&amp;gt;&amp;gt;E: write audit event 4098 (Point and Print failure)
    W--&amp;gt;&amp;gt;S: return access-denied
    S--&amp;gt;&amp;gt;U: STATUS_ACCESS_DENIED
    Note over U,W: No code runs as SYSTEM. Defender sees attempt in PrintService Admin.
&lt;p&gt;WPP is a partial answer that covers a large fraction of the threat model and a smaller fraction of the printer install base. The size of that smaller fraction -- specialty printers without IPP-class compatibility -- is the largest open practical problem in 2026 Print Spooler security.&lt;/p&gt;
&lt;h2&gt;7. Competing Answers: Universal Print versus Windows Protected Print Mode&lt;/h2&gt;
&lt;p&gt;Microsoft did not ship one architectural answer to Print Spooler. It shipped two. They are not redundant. They cover different threat models and different operational realities, and they are designed to coexist.&lt;/p&gt;
&lt;h3&gt;7.1 Universal Print at One Glance&lt;/h3&gt;
&lt;p&gt;Universal Print became generally available on March 2, 2021 [@ms-365-blog-universal-print-2212333] [@ms-universal-print-fundamentals].The exact March 2, 2021 GA date is industry knowledge anchored to Microsoft Ignite Spring 2021. The contemporaneous Microsoft 365 blog post [@ms-365-blog-universal-print-2212333] covers the wave but does not contain the verbatim date string. The Microsoft Learn fundamentals page documents the program&apos;s original &lt;code&gt;ms.date&lt;/code&gt; of March 2, 2020 (one year before GA) [@ms-universal-print-fundamentals]. We cite both because each one supports a different facet of the same date. The service moves the print queue to Microsoft 365 / Entra ID, removes the on-premises print server entirely, and removes the need for client-side third-party drivers. An optional on-prem connector lets the cloud service drive printers that are not directly cloud-aware [@ms-universal-print-whatis].&lt;/p&gt;

Microsoft&apos;s cloud-hosted print service. Universal Print eliminates print servers like OneDrive eliminates file servers [@ms-universal-print-whatis]. The architectural exit it takes is breaking conjunct (a): a Universal-Print-only endpoint with the local Spooler service disabled has no `\PIPE\spoolss` exposed to a low-privilege user. Universal Print became generally available on March 2, 2021 [@ms-365-blog-universal-print-2212333] and reached GCC / GCC High in October 2023 [@ms-universal-print-government].
&lt;p&gt;The architectural exit it takes is the one section 5 labelled (a): there is no &lt;code&gt;\PIPE\spoolss&lt;/code&gt; endpoint exposed on a Universal-Print-only host. The endpoint is a Microsoft 365 service called MPSIPPService that runs at &lt;code&gt;https://print.print.microsoft.com/&lt;/code&gt; [@ms-universal-print-getting-started]. Authentication is Entra ID OAuth2 / OIDC [@ms-universal-print-getting-started]. The threat model it removes is the local SMB-reachable low-privilege caller; the threat model it introduces is the cloud-account compromise.&lt;/p&gt;
&lt;h3&gt;7.2 The Cost of Universal Print&lt;/h3&gt;
&lt;p&gt;Universal Print is not free. It requires a Microsoft 365 / Entra ID license that includes the Universal Print entitlement. It requires network connectivity to print (the optional on-prem connector mitigates this for cached jobs; pure offline printing without the connector is not supported) [@ms-universal-print-getting-started]. It is per-user / per-printer in cost. The compatibility envelope is the IPP class driver plus the connector&apos;s translation surface; vendor-specific drivers are not part of the cloud service.&lt;/p&gt;
&lt;p&gt;Universal Print is available in commercial Microsoft 365 tenants and, as of October 2, 2023, in the GCC and GCC High government clouds. The fundamentals page records &quot;Universal Print is FedRamp certified by Office 365 and is now available in GCC, GCC High, and DoD environments&quot; [@ms-universal-print-government].&lt;/p&gt;
&lt;p&gt;The threat model Universal Print does not cover: an attacker who can reach Microsoft 365 / Entra ID tokens has cloud-side access, not local-spooler access. The PrintNightmare-class attack is moved off the endpoint; a different attack class (cloud-token compromise, mailbox compromise, OAuth phishing) takes its place. Universal Print does not, on its own, harden the surface; it relocates the surface to a cloud the customer outsources.&lt;/p&gt;
&lt;h3&gt;7.3 Head-to-Head&lt;/h3&gt;
&lt;p&gt;The trade-offs are easiest to compare in scannable form:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Universal Print&lt;/th&gt;
&lt;th&gt;Windows Protected Print Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Architectural exit&lt;/td&gt;
&lt;td&gt;Breaks conjunct (a): no local pipe spoolss&lt;/td&gt;
&lt;td&gt;Breaks (b) and partially (c): no third-party drivers, Spooler Worker below SYSTEM IL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment model&lt;/td&gt;
&lt;td&gt;Cloud-hosted M365 service; optional on-prem connector&lt;/td&gt;
&lt;td&gt;Local Windows feature, GPO / Intune toggle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Driver requirement&lt;/td&gt;
&lt;td&gt;None on client; connector translates server-side&lt;/td&gt;
&lt;td&gt;Microsoft IPP class driver or Microsoft-signed driver; Print Support Apps as transitional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline support&lt;/td&gt;
&lt;td&gt;None native; on-prem connector required&lt;/td&gt;
&lt;td&gt;Yes (local printing continues)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License requirement&lt;/td&gt;
&lt;td&gt;M365 / Entra ID with Universal Print entitlement&lt;/td&gt;
&lt;td&gt;None beyond Windows 11 24H2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threat model covered&lt;/td&gt;
&lt;td&gt;Removes the architectural primitive from the local host&lt;/td&gt;
&lt;td&gt;Removes third-party-driver and SYSTEM-context surfaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threat model NOT covered&lt;/td&gt;
&lt;td&gt;Cloud-side token / account compromise&lt;/td&gt;
&lt;td&gt;The RPC entry point still exists; specialty printers still require legacy stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default state in 2026&lt;/td&gt;
&lt;td&gt;Opt-in (license-gated)&lt;/td&gt;
&lt;td&gt;Opt-in (Group Policy off by default)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;7.4 The Composition Pattern&lt;/h3&gt;
&lt;p&gt;WPP and Universal Print can run on the same client. A managed enterprise endpoint can use Universal Print for its enrolled shared printers (cloud-mediated) and WPP for its locally-discovered printers (driverless local stack). Microsoft&apos;s documented stance is that this composition is the long-term direction. The WPP FAQ&apos;s &quot;at a future date&quot; language about default-on [@ms-wpp-faq] and the third-party-driver end-of-servicing milestones [@ms-end-of-servicing] together sketch a 2027-and-after world: WPP locally, Universal Print for cloud-enrolled printers, legacy stack restricted to specialty hosts that explicitly opt out.&lt;/p&gt;

A complete migration to Universal Print would force every Windows user to require Microsoft 365 entitlements and continuous network connectivity to print. That is a price Microsoft has not been willing to ask the global Windows install base to pay. WPP is the answer for endpoints that print locally; Universal Print is the answer for endpoints that print to enrolled shared printers; the parallel-stack architecture is the answer to the union. As of June 2026, no Microsoft document announces a date at which the local stack will be removed.
&lt;p&gt;The composed architecture in one picture:&lt;/p&gt;

flowchart LR
    subgraph EP[&quot;Managed Windows endpoint&quot;]
        APP[&quot;User application&quot;]
        WIN[&quot;winspool.drv&quot;]
        SPL[&quot;spoolsv.exe&quot;]
        WORKER[&quot;Spooler Worker&lt;br /&gt;restricted token&quot;]
        UPCLI[&quot;Universal Print client&quot;]
    end
    subgraph CLD[&quot;Microsoft 365 cloud&quot;]
        UPSVC[&quot;Universal Print service&lt;br /&gt;MPSIPPService&quot;]
        CONN[&quot;Optional on-prem connector&quot;]
    end
    subgraph LOC[&quot;Locally discovered printers&quot;]
        IPP[&quot;Mopria / IPP printer&quot;]
        SPEC[&quot;Specialty printer&lt;br /&gt;(opt-out path)&quot;]
    end
    APP --&amp;gt; WIN
    WIN --&amp;gt; SPL
    SPL --&amp;gt; WORKER
    WORKER --&amp;gt; IPP
    SPL -. opt-out .-&amp;gt; SPEC
    APP --&amp;gt; UPCLI
    UPCLI --&amp;gt; UPSVC
    UPSVC --&amp;gt; CONN
    CONN --&amp;gt; IPP
&lt;p&gt;Two answers, deliberately. We promised in section 1 that we would not tell you which one to deploy. We are keeping that promise. The next section is about why no third answer covers the gap.&lt;/p&gt;
&lt;h2&gt;8. Theoretical Limits: The Architectural Impossibility Argument&lt;/h2&gt;
&lt;p&gt;We can state the architectural-impossibility claim formally now. It is bounded, it has been bounded for fifteen years on this artifact, and it is sharp enough to act on.&lt;/p&gt;
&lt;h3&gt;8.1 The Three-Conjunct Primitive&lt;/h3&gt;
&lt;p&gt;Any local service that simultaneously satisfies three conditions exposes a SYSTEM code-execution primitive by construction:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(a) accepts low-privilege RPC,&lt;/li&gt;
&lt;li&gt;(b) loads caller-influenced third-party DLLs as part of those requests, and&lt;/li&gt;
&lt;li&gt;(c) runs at SYSTEM context.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The primitive is independent of any particular implementation bug. Particular implementation bugs are how the primitive is exercised. The primitive itself is what makes those bugs exploitable.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Any local service that simultaneously accepts low-privilege RPC, loads caller-influenced DLLs, and runs at SYSTEM context exposes a SYSTEM code-execution primitive by construction. No patch on individual entry points can close the class. The class is closed only by breaking one of the three conjuncts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The argument is not an empirical generalization. It is a structural one. Given (a), (b), and (c), the attacker&apos;s path to SYSTEM-execution is a finite search problem: enumerate the entry points that load DLLs, find one whose DLL-load arguments the attacker can steer, supply an attacker-supplied DLL. The defender&apos;s only options are to remove one of the conjuncts. Patching individual entry points moves the search problem; it does not eliminate it. The 2021-2024 patch cascade is the empirical record of that move-but-not-eliminate dynamic.&lt;/p&gt;
&lt;h3&gt;8.2 The Three Exits&lt;/h3&gt;
&lt;p&gt;Each shipped architectural approach breaks exactly one conjunct.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Break (c), the SYSTEM context.&lt;/strong&gt; PrintIsolationHost.exe shipped in 2009 as a partial answer: drivers can run in a sibling process, but that sibling process is itself LocalSystem by default [@ms-printer-driver-isolation]. WPP&apos;s Spooler Worker (2024) is more complete: a restricted token, below SYSTEM integrity level, for the bulk of per-user spooler operations [@ms-wpp-more-info].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Break (b), the caller-influenced DLL load.&lt;/strong&gt; WPP module blocking (2024) refuses to load anything except Microsoft-signed binaries required for IPP [@ms-wpp-more-info]. The conjunct is no longer &quot;loads caller-influenced DLLs&quot;; it is &quot;loads only Microsoft-signed DLLs the OS shipped.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Break (a), the low-privilege RPC entry.&lt;/strong&gt; Universal Print (2021) removes the local &lt;code&gt;\PIPE\spoolss&lt;/code&gt; endpoint from the endpoint&apos;s surface [@ms-universal-print-whatis]. The CERT/CC 2021 mitigation -- stop and disable the Spooler service -- is the same architectural exit with larger collateral damage (no local printing at all) [@cert-vu-383432].&lt;/p&gt;
&lt;h3&gt;8.3 What No Exit Covers&lt;/h3&gt;
&lt;p&gt;The intersection of constraints that no shipped exit covers: specialty printers that require v3 or v4 drivers, on a host that needs offline printing, on a non-managed endpoint, in an environment that cannot adopt cloud printing. Industrial label printers, secure check printers, and healthcare imaging devices are the canonical examples [@ezeep-label-printers-wpp]. This intersection is the empirical gap that justifies the parallel-stack answer in 2026 and the absence of a default-on commitment for WPP [@ms-wpp-faq].&lt;/p&gt;
&lt;h3&gt;8.4 The Argument as a Lower Bound&lt;/h3&gt;
&lt;p&gt;The three-conjunct argument is a &lt;em&gt;lower bound on bug class&lt;/em&gt;, not a &lt;em&gt;security analysis&lt;/em&gt;. It says the architectural primitive cannot be made safe without breaking one of the conjuncts. It does not say that a specific implementation of an exit is itself secure. WPP could ship a bug. The Microsoft-signed module loader could have a parser vulnerability. The Spooler Worker process could be coerced into elevation through some intermediate IPC channel; that channel is itself a research question we return to in section 9.4. The architectural argument bounds what &lt;em&gt;kind&lt;/em&gt; of bugs are still possible. It does not promise that no bugs will be.&lt;/p&gt;

The &quot;service that loads caller-influenced code in a privileged context produces a privilege-escalation primitive by construction&quot; pattern predates Windows. The capability-systems literature of the 1970s -- Hydra, KeyKOS, and the related work that gave us Mandatory Integrity Control as a Windows feature decades later -- worked through the same argument in different language. Confused-deputy attacks (the Hardy formulation) are exactly the case where a privileged process performs an operation on behalf of a less-privileged caller and the operation cashes out at the privileged process&apos;s authority. PrintNightmare is a confused-deputy primitive on `spoolsv.exe`. The architectural exits in section 5 are confused-deputy mitigations: revoke the deputy&apos;s authority (Universal Print breaks delegation entirely), confine what the deputy is willing to do (WPP module blocking), or split the deputy into a privileged broker and an unprivileged worker (WPP Spooler Worker).
&lt;p&gt;Fifteen years of Print Spooler CVEs have produced a single argument with three corollaries. It is not new. It is not Microsoft&apos;s. It has been latent in the academic literature on capability systems since the 1970s. What is new in 2024 is that it shipped, in two flavors, on consumer Windows.&lt;/p&gt;
&lt;h2&gt;9. Open Problems&lt;/h2&gt;
&lt;p&gt;Three years after Microsoft shipped the architectural answer, the Print Spooler security story is not complete. We end with five open problems, presented without recommendation.&lt;/p&gt;
&lt;h3&gt;9.1 WPP Adoption Velocity Through the Opt-In Tail&lt;/h3&gt;
&lt;p&gt;No default-on commitment exists. The WPP FAQ uses the verbatim phrase &quot;at a future date&quot; for the default-on flip [@ms-wpp-faq]. As of June 2026, opt-in adoption is reported only anecdotally; Microsoft has not published telemetry. The three published deprecation milestones are real and dated -- January 15, 2026 (no new third-party drivers via Windows Update), July 1, 2026 (Windows IPP class driver preferred over third-party drivers for new printer installs), July 1, 2027 (third-party servicing ends except for security fixes) [@ms-end-of-servicing] -- but they do not equal &quot;WPP is on by default.&quot;&lt;/p&gt;
&lt;p&gt;The Lexmark vendor primary on the WPP transition spells out the operational reading from the printer-OEM perspective: &quot;WPP is disabled by default until 2027... January 2026: no new third-party drivers published via Windows Update; July 2026: Windows defaults to IPP Class Driver when adding devices; July 2027: no updates for third-party drivers except security fixes&quot; [@lexmark-wpp-support]. The OEMs are reading the milestones as a 2027 horizon for the default-on flip. Microsoft has not, in writing, confirmed that reading.&lt;/p&gt;
&lt;p&gt;A negative-search finding sharpens the gap. The trade press that tracks Microsoft security launches (BleepingComputer&apos;s unveil coverage [@bleepingcomputer-wpp-unveil] and its dedicated WPP tag page [@bleepingcomputer-tag-wpp], BornCity&apos;s April 2026 Patch Tuesday print-issues report [@borncity-april-2026-patchday]), the Microsoft Tech Community discussion threads (the 2024 WPP intro discussion [@techcommunity-discuss-msec-print-4008206] and the Ignite 2024 Windows-security companion [@techcommunity-discuss-ignite-2024-4304464]), analyst output (the MPSA member eBook [@mpsa-wpp-ebook], Quocirca&apos;s vendor-published commentary [@computerweekly-quocirca-wpp]) -- none of these surface a quantitative WPP adoption number. Microsoft has not published telemetry, third-party analysts have not estimated it, and OEM disclosures cover hardware compatibility, not enterprise enablement rates. The gap is not a measurement difficulty; it is an absence in the public record.&lt;/p&gt;
&lt;h3&gt;9.2 The Specialty-Printer Gap&lt;/h3&gt;
&lt;p&gt;v3 / v4 driver printers without IPP-class compatibility still exist in production. Industrial label printers, healthcare imaging printers, secure check printers, line-printer holdouts. The honest answer is that these endpoints cannot adopt WPP and cannot adopt Universal Print and they will continue to run a legacy spooler. The defense for them is segmentation, not patching.&lt;/p&gt;
&lt;p&gt;Print Support Apps help bridge some categories. The PSA design guide is the canonical specification [@ms-print-support-app-design-guide]. A walk through the Microsoft Store [@apps-microsoft-store-root] surfaces a sampled (not exhaustive) roster of vendor PSAs available as of June 2026: Brother&apos;s PSA was one of the first to ship [@brother-print-support-app] [@brother-support-page]; Canon Print Assistant covers Canon&apos;s IPP-everywhere subset [@canon-print-assistant-psa]; HP Smart bridges HP&apos;s IPP-everywhere set [@hp-smart-psa]; Konica Minolta&apos;s bizhub PSA covers the bizhub series [@konica-bizhub-psa]; Xerox and Lexmark co-publish a joint PSA [@xerox-lexmark-psa] [@lexmark-wpp-support]. The cloud-print intermediaries ezeep document the operational reality for the categories the PSA model does not cover: industrial label printers (Zebra, Honeywell, SATO, TSC, Dymo) speaking ZPL / EPL are absent from the Mopria-certified IPP-everywhere catalogue and from the Microsoft-Store PSA roster as of June 2026 [@ezeep-label-printers-wpp]. For those vendors the operational guidance is to keep WPP disabled on the affected workstations and to segment them off the production network.&lt;/p&gt;
&lt;h3&gt;9.3 CVE-2024-38198: Attribution and PoC Gap&lt;/h3&gt;
&lt;p&gt;No public researcher is named in any primary source for CVE-2024-38198. No public PoC exists [@wiz-cve-2024-38198] [@rapid7-cve-2024-38198]. The bug was found, patched, and remained unattributed. This is not necessarily a problem -- silent fixes are normal in vendor patch flow -- but it is a data point: the bug class is still being mined three years after the disclosure event, and the public-research apparatus has not surfaced the next finding.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; CVE-2024-38198, patched on August 13, 2024, has no public researcher attribution and no public PoC as of June 2026 [@wiz-cve-2024-38198] [@rapid7-cve-2024-38198]. It is the most recent named Print Spooler EoP in the public record. Its existence is the empirical proof point that the legacy spooler is still producing novel CWE-class bugs three years after PrintNightmare.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;9.4 The spoolsv-to-Spooler-Worker IPC Primitive&lt;/h3&gt;
&lt;p&gt;WPP&apos;s per-user worker model introduces an IPC channel between the parent &lt;code&gt;spoolsv.exe&lt;/code&gt; service and the Spooler Worker process [@ms-wpp-more-info]. Microsoft documents the worker&apos;s restricted token in detail (see the verbatim quote in section 6.1: &quot;no longer runs at SYSTEM IL&quot; [@ms-wpp-more-info] [@ms-wpp-more-info-wayback]) but does not, in public, document the IPC primitive itself. The absence is the load-bearing finding.&lt;/p&gt;
&lt;p&gt;The Windows kernel offers at least four plausible IPC mechanisms that a service like the spooler could use to dispatch work to a per-user worker: an &lt;a href=&quot;https://paragmali.com/blog/every-uac-prompt-is-an-alpc-handshake-a-field-guide-to-windo/&quot; rel=&quot;noopener&quot;&gt;Advanced Local Procedure Call (ALPC)&lt;/a&gt; port, a named pipe (the same family &lt;code&gt;\PIPE\spoolss&lt;/code&gt; is from), a COM activation under RPC, or a shared-memory section with notification. Each has a different attack surface. ALPC ports are not directly named in the filesystem but are reachable through documented APIs; named pipes inherit the SMB and named-pipe-anonymous policy plane [@ms-named-pipes-anonymous] [@ms-restrict-anonymous-named-pipes]; COM-RPC inherits the COM permission DACL surface; shared-memory sections inherit the section-object DACL surface. Per-user services in Windows (the per-user-services framework Microsoft introduced in 1709) typically use ALPC or named pipes for parent / worker dispatch [@ms-per-user-services]. Which mechanism WPP uses, and what authentication the parent demands of the worker (and vice versa), is the specific research question. As of June 2026 it is unanswered in the public record.&lt;/p&gt;
&lt;p&gt;If that channel is itself coercible (TOCTOU on the IPC, redirection-style attacks on a worker named pipe), WPP may exhibit a SpoolFool-class bug at a different layer. Redirection Guard partially answers the obvious junction-following attack on the worker [@msrc-redirectionguard-blog] [@ms-redirection-trust-policy], but the worker has other IPC handles, and the worker&apos;s restricted token still has authority over operations the parent has delegated to it. No public research has surfaced an IPC-channel exploit as of June 2026. The research surface here is real and only loosely mapped.&lt;/p&gt;
&lt;h3&gt;9.5 Detection Signal Coverage for the Post-WPP Era&lt;/h3&gt;
&lt;p&gt;SigmaHQ, Splunk Security Content, Elastic, and Microsoft Defender XDR all ship rules for the PrintNightmare-era event signatures. SigmaHQ&apos;s PrintNightmare rule pack covers the PoC DLL load pattern (&lt;code&gt;win_exploit_cve_2021_1675_printspooler.yml&lt;/code&gt;, rule ID &lt;code&gt;4e64668a-4da1-49f5-a8df-9e2d5b866718&lt;/code&gt;) [@sigma-cve-2021-1675-win-spooler]. The Zeek-on-the-wire DCE-RPC rule (ID &lt;code&gt;7b33baef-2a75-4ca3-9da4-34f9a15382d8&lt;/code&gt;) watches both MS-RPRN&apos;s &lt;code&gt;RpcAddPrinterDriverEx&lt;/code&gt; and MS-PAR&apos;s &lt;code&gt;RpcAsyncAddPrinterDriver&lt;/code&gt; [@sigma-cve-2021-1675-zeek]. Splunk&apos;s research-team detection on &lt;code&gt;Microsoft-Windows-PrintService/Admin&lt;/code&gt; event code 316 (driver-add) carries the rule ID &lt;code&gt;313681a2-da8e-11eb-adad-acde48001122&lt;/code&gt; and maps to MITRE ATT&amp;amp;CK technique T1547.012 (Print Processors) [@splunk-research-printnightmare-driver] [@splunk-printnightmare-story] [@attack-mitre-t1547-012]. Splunk&apos;s &lt;code&gt;spoolsv.exe&lt;/code&gt;-focused rule pack adds: plug-in loading failure detection (&lt;code&gt;1adc9548-da7c-11eb-8f13-acde48001122&lt;/code&gt;, PrintService Admin Event 808 and security log Event 4909) [@splunk-research-spoolsv-plugin-fail]; &lt;a href=&quot;https://paragmali.com/blog/from-cmdexe-to-a-kusto-row-in-90-seconds-how-sysmon-and-defe/&quot; rel=&quot;noopener&quot;&gt;Sysmon&lt;/a&gt; Event ID 11 spool-folder DLL writes (&lt;code&gt;347fd388-da87-11eb-836d-acde48001122&lt;/code&gt;) [@splunk-research-spoolsv-dll-sysmon]; Sysmon Event ID 7 loaded-modules signal on &lt;code&gt;spoolsv.exe&lt;/code&gt; (&lt;code&gt;a5e451f8-da81-11eb-b245-acde48001122&lt;/code&gt;) [@splunk-research-spoolsv-loaded-modules]; Sysmon Event ID 10 process-access signal on &lt;code&gt;spoolsv.exe&lt;/code&gt; (&lt;code&gt;799b606e-da81-11eb-93f8-acde48001122&lt;/code&gt;) [@splunk-research-spoolsv-process-access]. Elastic&apos;s prebuilt rule &quot;Unusual Print Spooler Child Process&quot; catches the post-exploit child-process spawn pattern (risk score 47) [@elastic-unusual-printspooler-child]. Azure Sentinel&apos;s KQL hunting query for PrintNightmare watches file creations in the print-spooler drivers folder (&lt;code&gt;C:\WINDOWS\SYSTEM32\SPOOL\drivers&lt;/code&gt;) [@azure-sentinel-printnightmare-yaml].&lt;/p&gt;
&lt;p&gt;Coverage for the WPP era is sparser, and the gap has a specific shape: because WPP has &lt;strong&gt;no in-product audit mode&lt;/strong&gt; -- the &lt;code&gt;ConfigureWindowsProtectedPrint&lt;/code&gt; CSP is the binary two-state setting documented in section 6.4 [@ms-policy-csp-printers] [@tenable-cis-w11-l2] -- pre-enforcement detection has to be synthesized from the existing PrintService Admin and Sysmon event signals (Event 316 driver-adds, 808 / 4909 plug-in failures, Sysmon 7 / 10 / 11 on &lt;code&gt;spoolsv.exe&lt;/code&gt;) plus SCM service-state events (System log Event ID 7036 records spooler service start / stop transitions). Redirection Guard mitigation events appear in &lt;code&gt;Microsoft-Windows-Security-Mitigations/Operational&lt;/code&gt; [@msrc-redirectionguard-blog]. IPC-related signals on the Spooler Worker do not have public detection content as of June 2026. The audit-without-audit-mode pattern is well understood by detection engineers running PrintNightmare content already; the synthesis work to compose it into a WPP rollout-ring playbook is the gap detection content vendors have not yet closed.&lt;/p&gt;
&lt;p&gt;Five open problems. None of them are emergencies. All of them are reasons that a 2026 security program for Print Spooler is still a security program for Print Spooler, not an absence.&lt;/p&gt;
&lt;h2&gt;10. Practical Guide: What a Defender Does in 2026&lt;/h2&gt;
&lt;p&gt;We end with what a Windows administrator with print infrastructure should actually do in 2026. Four tiers, each with its own action list, none of them long.&lt;/p&gt;
&lt;h3&gt;10.1 Tier 1: Managed Enterprise with Cloud Workflows&lt;/h3&gt;
&lt;p&gt;For organizations already on Microsoft 365 with Entra-joined endpoints and cloud-friendly printers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Adopt Universal Print for shared printers [@ms-universal-print-whatis] [@ms-universal-print-getting-started].&lt;/li&gt;
&lt;li&gt;Adopt WPP on a pilot ring of managed endpoints (&lt;code&gt;ConfigureWindowsProtectedPrint = 1&lt;/code&gt;); WPP has no in-product audit mode, so the deployment pattern is rings, not audit-then-enforce [@ms-wpp-enterprises] [@ms-policy-csp-printers] [@tenable-cis-w11-l2].&lt;/li&gt;
&lt;li&gt;Verify Redirection Guard is enabled on &lt;code&gt;spoolsv.exe&lt;/code&gt; [@ms-set-processmitigation] [@ms-redirection-trust-policy].&lt;/li&gt;
&lt;li&gt;Verify the September 2021 default Point-and-Print policy is in force: &lt;code&gt;RestrictDriverInstallationToAdministrators=1&lt;/code&gt; [@kb-5005652-topic].&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;10.2 Tier 2: Managed Enterprise Without Cloud Workflows&lt;/h3&gt;
&lt;p&gt;For organizations with on-prem print infrastructure and no Universal Print appetite:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Deploy WPP to a pilot ring of managed endpoints (&lt;code&gt;ConfigureWindowsProtectedPrint = 1&lt;/code&gt;) and watch &lt;code&gt;Microsoft-Windows-PrintService/Admin&lt;/code&gt; for 30 or more days [@ms-wpp-enterprises] [@ms-policy-csp-printers] [@tenable-cis-server-2025-l2].&lt;/li&gt;
&lt;li&gt;After the pilot, expand the ring to the subset of endpoints whose printers are Mopria-certified [@mopria-certified-products].&lt;/li&gt;
&lt;li&gt;For non-Mopria printers, segment to dedicated print VLANs and enforce the September 2021 admin-only default [@kb-5005652-topic].&lt;/li&gt;
&lt;li&gt;Verify Redirection Guard on &lt;code&gt;spoolsv.exe&lt;/code&gt; on all spooler-bearing hosts [@msrc-redirectionguard-blog].&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;10.3 Tier 3: Specialty, Industrial, Regulated&lt;/h3&gt;
&lt;p&gt;For organizations whose print fleet includes specialty hardware (label printers, secure check printers, healthcare imaging):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Segment Spooler-bearing endpoints onto dedicated VLANs with restricted inbound RPC reachability [@ms-windows-firewall-overview].&lt;/li&gt;
&lt;li&gt;Where possible, enforce the CERT/CC 2021 guidance on domain controllers (Spooler disabled); CISA&apos;s required actions for the same hosts now flow through BOD 22-01 KEV remediation after the January 2026 closure of ED 21-04, but the DC-disabled baseline is unchanged [@cert-vu-383432] [@cisa-ed-21-04].&lt;/li&gt;
&lt;li&gt;Apply the September 2021 admin-only Point and Print default on every host [@kb-5005652-topic].&lt;/li&gt;
&lt;li&gt;Subscribe to MSRC notifications for the affected SKUs [@msrc-cve-2021-34527].&lt;/li&gt;
&lt;li&gt;Plan a multi-year IPP / PSA migration path; track vendor PSA availability [@brother-print-support-app] [@canon-print-assistant-psa] [@hp-smart-psa] [@konica-bizhub-psa] [@xerox-lexmark-psa] [@lexmark-wpp-support] [@ms-print-support-app-design-guide].&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;10.4 Tier 4: Print Server or Domain Controller Specifically&lt;/h3&gt;
&lt;p&gt;For hosts that are themselves print servers or domain controllers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Spooler off where possible. CERT/CC&apos;s 2021 guidance remains in force; CISA closed ED 21-04 in January 2026 and folded its requirements into BOD 22-01 (KEV-catalog remediation), but the practical effect on a domain controller is unchanged [@cert-vu-383432] [@cisa-ed-21-04]. SCM service state-changes appear in the System event log under Event ID 7036 (service start / stop transitions); alert on unexpected &lt;code&gt;Print Spooler&lt;/code&gt; Event 7036 entries on hosts where the service should remain stopped.&lt;/li&gt;
&lt;li&gt;Where Spooler-off is impossible, isolate the host, restrict &lt;code&gt;\PIPE\spoolss&lt;/code&gt; exposure at the firewall, and harden the named-pipe-anonymous policies (&lt;code&gt;RestrictNullSessAccess = 1&lt;/code&gt;; &lt;code&gt;spoolss&lt;/code&gt; absent from &lt;code&gt;NullSessionPipes&lt;/code&gt;) [@ms-restrict-anonymous-named-pipes] [@ms-named-pipes-anonymous] [@ms-ad-firewall-ports].&lt;/li&gt;
&lt;li&gt;Log MS-RPRN and MS-PAR calls; alert on &lt;code&gt;RpcAddPrinterDriverEx&lt;/code&gt; and &lt;code&gt;RpcAsyncAddPrinterDriver&lt;/code&gt; invocations from non-administrator SIDs [@sigma-cve-2021-1675-zeek]. The canonical event-log signals to instrument are: PrintService Admin Event ID 316 (driver-add) [@splunk-research-printnightmare-driver]; PrintService Admin Event ID 808 (spooler plug-in load failure) paired with security log Event ID 4909 [@splunk-research-spoolsv-plugin-fail]; Sysmon Event ID 7 (loaded modules on &lt;code&gt;spoolsv.exe&lt;/code&gt;) [@splunk-research-spoolsv-loaded-modules]; Sysmon Event ID 10 (process access on &lt;code&gt;spoolsv.exe&lt;/code&gt;) [@splunk-research-spoolsv-process-access]; Sysmon Event ID 11 (spool-folder DLL writes under &lt;code&gt;C:\WINDOWS\SYSTEM32\SPOOL\drivers&lt;/code&gt;) [@splunk-research-spoolsv-dll-sysmon] [@azure-sentinel-printnightmare-yaml].&lt;/li&gt;
&lt;li&gt;Confirm Redirection Guard is enabled on &lt;code&gt;spoolsv.exe&lt;/code&gt; and watch &lt;code&gt;Microsoft-Windows-Security-Mitigations/Operational&lt;/code&gt; for mitigation events [@msrc-redirectionguard-blog].&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; CISA Emergency Directive 21-04, issued July 13, 2021, mandated that federal civilian agencies stop and disable the Print Spooler service on Active Directory domain controllers [@cisa-ed-21-04]. CISA closed ED 21-04 in January 2026 and transitioned its required actions to BOD 22-01 (Reducing the Significant Risk of Known Exploited Vulnerabilities). The compliance vehicle changed; the operational outcome did not. Agencies that have not adopted Universal Print on their DC infrastructure should still keep Spooler stopped and disabled on every DC.&lt;/p&gt;
&lt;/blockquote&gt;

For detection engineers, the named-rule packs to start from are: SigmaHQ `4e64668a-4da1-49f5-a8df-9e2d5b866718` (PrintService Admin Event 808 PoC DLL-load failure) [@sigma-cve-2021-1675-win-spooler]; SigmaHQ `7b33baef-2a75-4ca3-9da4-34f9a15382d8` (Zeek DCE-RPC wire-level driver install) [@sigma-cve-2021-1675-zeek]; Splunk story `fd79470a-da88-11eb-b803-acde48001122` (PrintNightmare analytic story, production status) [@splunk-printnightmare-story]; Splunk research `313681a2-da8e-11eb-adad-acde48001122` (PrintService Admin Event Code 316 driver-add) [@splunk-research-printnightmare-driver]; Elastic prebuilt rule &quot;Unusual Print Spooler Child Process&quot; (EQL, risk 47) [@elastic-unusual-printspooler-child]; Azure Sentinel hunting query `8f404352-c4ff-44d1-8d70-c50ee2fad8f8` (DeviceFileEvents in spool drivers folder) [@azure-sentinel-printnightmare-yaml]. Jacob Baines&apos;s DEF CON 29 &quot;Bring Your Own Print Driver Vulnerability&quot; [@defcon-29-baines-pdf] and the companion `concealed_position` repository [@baines-concealed-position] are the canonical reference for the BYOV attack class, which detection rule packs for installed-driver behavior also need to model.
&lt;p&gt;The unifying pattern across the tiers: enforce the September 2021 default, enable Redirection Guard, audit WPP on the way to enforcement, and segment what cannot be migrated. The architectural answer to PrintNightmare exists. The operational answer is to use it.&lt;/p&gt;
&lt;h2&gt;11. Frequently Asked Questions&lt;/h2&gt;

No. The press attached the name to a sequence. CVE-2021-1675 (June 8, 2021) was originally classed as a local EoP, then silently reclassified to RCE on June 21 [@nvd-cve-2021-1675] [@bleepingcomputer-domain-takeover]. CVE-2021-34527 (July 1, 2021) was the separate-bulletin out-of-band assignment for the RCE primitive Sangfor&apos;s PoC actually exploited [@nvd-cve-2021-34527] [@cert-vu-383432]. CVE-2021-34481 (July 15, 2021) was a related local EoP fixed in KB5005652 [@nvd-cve-2021-34481] [@kb-5005652-topic]. CVE-2021-36958 (September 14, 2021) was the next-cycle Print Spooler RCE [@nvd-cve-2021-36958]. Several adjacent bugs (CVE-2022-21999 SpoolFool, CVE-2024-38198) are often called &quot;PrintNightmare-class&quot; without being assigned the name themselves [@nvd-cve-2022-21999] [@wiz-cve-2024-38198].

The proof-of-concept that triggered the disclosure event on June 29, 2021 was written by Zhiniang Peng, Xuefeng Li, and Lewis Lee at Sangfor Technology for their Black Hat USA 2021 briefing &quot;Diving Into Spooler&quot; [@infocondb-bh2021-sangfor] [@afwu-wayback-snapshot]. They published it briefly believing the bug had been patched on June 8; the patch turned out to cover only the synchronous MS-RPRN entry point [@nvd-cve-2021-1675]. A second variant against the asynchronous MS-PAR `RpcAsyncAddPrinterDriver` was published shortly after by the researcher `@cube0x0` [@cube0x0-cve-2021-1675] [@cert-vu-383432]. The CERT/CC disclosure-norms advisory VU#383432 was a separate document by Will Dormann about the disclosure failure itself, not the bug [@cert-vu-383432].

No. SpoolFool (CVE-2022-21999, disclosed February 8, 2022 by Oliver Lyak / `@ly4k_` of SafeBreach Labs) is a Print Spooler local privilege escalation that abuses the printer `SpoolDirectory` registry value and NTFS reparse points, classified as CWE-59 (Link Following) [@nvd-cve-2022-21999] [@ly4k-spoolfool]. Win32k is the GUI subsystem and is uninvolved. The researcher handle is `@ly4k_` with a trailing underscore; `@jonas_lyk` is a distinct researcher.

No, and it is not from March 2024 either. CVE-2024-38198 (August 13, 2024 Patch Tuesday) is a Print Spooler Elevation of Privilege Vulnerability classified as CWE-345 Insufficient Verification of Data Authenticity [@wiz-cve-2024-38198] [@rapid7-cve-2024-38198]. Exploitation requires winning a race, but the CWE is 345, not 362, and Microsoft did not name Point and Print as the affected component. CVSS v3.1 base 7.5 (`AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H`) [@wiz-cve-2024-38198]. No public PoC and no public researcher attribution exist as of June 2026.

Because the third-party printer-driver install base assumes drivers are loaded into a LocalSystem-context process. Sandboxing the spooler would break compatibility with the v3 and v4 driver model that the entire pre-2024 printer install base ships against [@ms-printer-driver-isolation] [@ms-print-spooler-architecture]. Microsoft&apos;s chosen architectural exits (Windows Protected Print Mode and Universal Print) sidestep the constraint by either restricting which DLLs the spooler will load (WPP module blocking plus the lower-privilege Spooler Worker) or removing the local spooler from the workflow entirely (Universal Print) [@ms-wpp-more-info] [@ms-wpp-canonical] [@ms-universal-print-whatis].

For endpoints that print only through Universal Print and where the local Spooler service is disabled, yes. The `\PIPE\spoolss` RPC entry point is not exposed and the architectural primitive is broken [@ms-universal-print-whatis] [@ms-universal-print-getting-started]. Most enterprise deployments are mixed (Universal Print for some workflows, local Spooler for others), in which case the PrintNightmare risk surface is reduced but not eliminated. Universal Print does not automatically disable the local Spooler.

We can find no record of one. The Sangfor &quot;Diving Into Spooler&quot; talk on August 4, 2021 at Black Hat USA 2021 is the canonical primary-source talk for the technique [@infocondb-bh2021-sangfor]. Jacob Baines&apos;s DEF CON 29 (August 2021) talk &quot;Bring Your Own Print Driver Vulnerability&quot; is a related contemporary talk worth citing if you have heard the Giakouminakis attribution and are trying to track down its source [@defcon-29-baines-pdf] [@baines-concealed-position].
The Sangfor Black Hat USA 2021 session record (presenters, time, abstract) is preserved on InfoconDB at `infocondb.org/con/black-hat/black-hat-usa-2021/diving-into-spooler-discovering-lpe-and-rce-vulnerabilities-in-windows-printer` [@infocondb-bh2021-sangfor]. Jacob Baines&apos;s DEF CON 29 slides are mirrored at `media.defcon.org/DEF CON 29/DEF CON 29 presentations/Jacob Baines - Bring Your Own Print Driver Vulnerability.pdf` [@defcon-29-baines-pdf], and the companion `concealed_position` GitHub repository documents the four-CVE driver exploit set (ACIDDAMAGE / RADIANTDAMAGE / POISONDAMAGE / SLASHINGDAMAGE) [@baines-concealed-position].

&lt;p&gt;&amp;lt;StudyGuide slug=&quot;print-spooler-three-generations-of-printnightmare&quot; keyTerms={[
  { term: &quot;spoolsv.exe&quot;, definition: &quot;The Windows Print Spooler API server, running as LocalSystem, that loads third-party Print Provider, Print Processor, and printer driver DLLs into its address space. The architectural protagonist of every named Print Spooler CVE 2010-2024.&quot; },
  { term: &quot;Print Provider DLL chain&quot;, definition: &quot;The three router-loaded DLLs that dispatch print operations to the appropriate transport: localspl.dll (Local), win32spl.dll (Remote), inetpp.dll (HTTP/IPP). Often confused with the Print Processor layer; the Provider handles which printer, the Processor handles how to render the page.&quot; },
  { term: &quot;Print Processor (winprint.dll)&quot;, definition: &quot;The component that interprets the spool file format (EMF, XPS, RAW, TEXT) and renders pages for a specific printer. winprint.dll is the default. A separate layer from the Print Providers.&quot; },
  { term: &quot;MS-RPRN and MS-PAR&quot;, definition: &quot;Microsoft&apos;s synchronous (MS-RPRN) and asynchronous (MS-PAR) print-system RPC protocols. Both bind to the named pipe PIPE spoolss. The MS-PAR spec verbatim describes RpcAsyncAddPrinterDriver as the counterpart of MS-RPRN&apos;s RpcAddPrinterDriverEx.&quot; },
  { term: &quot;Point and Print&quot;, definition: &quot;Windows behavior in which a non-admin user causes their machine to download and install a printer driver from a print server on first use. Governed by the registry values NoWarningNoElevationOnInstall, NoWarningNoElevationOnUpdate, and overridden by RestrictDriverInstallationToAdministrators.&quot; },
  { term: &quot;PrintIsolationHost.exe&quot;, definition: &quot;Sibling host process introduced in Windows 7 / Server 2008 R2 (October 22, 2009) that can load third-party printer driver code outside spoolsv.exe. The isolation is process isolation, not privilege isolation; the host runs as LocalSystem by default.&quot; },
  { term: &quot;AppContainer&quot;, definition: &quot;A Windows process sandboxing primitive with a custom integrity level, a restricted token, and a set of explicitly granted capabilities. Microsoft has not deployed AppContainer to spoolsv.exe because of the back-compat constraint with v3/v4 drivers.&quot; },
  { term: &quot;Windows Protected Print Mode (WPP)&quot;, definition: &quot;An opt-in Windows print stack introduced with Windows 11 24H2 (October 1, 2024) that blocks all third-party drivers, runs normal operations in a Spooler Worker process with a restricted token below SYSTEM integrity, and falls back to the inbox Microsoft IPP Class Driver.&quot; },
  { term: &quot;IPP class driver / Mopria certification&quot;, definition: &quot;The Microsoft-supplied inbox driver that uses the Internet Printing Protocol to communicate with printers implementing the Mopria-certified IPP everywhere subset. WPP-enforced clients use this driver instead of vendor-specific v3 drivers.&quot; },
  { term: &quot;v3 vs v4 driver model&quot;, definition: &quot;The two pre-WPP Windows printer driver packaging models. v3 (Windows 2000 era) loads driver render code into the spooler process by default. v4 (Windows 8 era) is XPS-based and more portable. WPP deprecates both in favor of the inbox IPP class driver.&quot; },
  { term: &quot;Universal Print&quot;, definition: &quot;Microsoft&apos;s cloud-hosted print service (GA March 2, 2021). Eliminates print servers like OneDrive eliminates file servers. The architectural exit it takes is breaking conjunct (a): no local pipe spoolss exposed on a Universal-Print-only endpoint.&quot; },
  { term: &quot;Redirection Guard&quot;, definition: &quot;A Windows process mitigation that refuses to follow filesystem junctions or symbolic links created by non-administrator users. Enabled on spoolsv.exe via Set-ProcessMitigation -Name spoolsv.exe -Enable RedirectionGuard. Mitigates the SpoolFool-class reparse-point primitive.&quot; }
]} questions={[
  { q: &quot;Which three Print Provider DLLs are loaded by the spooler router, and which one would handle a printer reached over IPP?&quot;, a: &quot;localspl.dll (Local), win32spl.dll (Remote), inetpp.dll (HTTP/IPP). inetpp.dll handles IPP.&quot; },
  { q: &quot;Why did Microsoft need four patch waves between June 2021 and August 2024 instead of one fix?&quot;, a: &quot;The first patch closed one RPC entry point (MS-RPRN&apos;s RpcAddPrinterDriverEx); the second closed the symmetric MS-PAR entry point (RpcAsyncAddPrinterDriver); the third flipped the RestrictDriverInstallationToAdministrators default from 0 to 1 and published the verbatim &apos;no combination of mitigations is equivalent&apos; concession; the fourth (SpoolFool) and fifth (CVE-2024-38198) exploited filesystem-side primitives that the RPC-side patches did not touch. The patches did not converge because they targeted entry points, not the architectural primitive.&quot; },
  { q: &quot;What is the three-conjunct architectural primitive that every PrintNightmare-class bug exploits, and how does each shipped Microsoft exit break exactly one conjunct?&quot;, a: &quot;(a) low-priv RPC entry, (b) caller-influenced DLL load, (c) SYSTEM context. Universal Print breaks (a); WPP module blocking breaks (b); WPP Spooler Worker and PrintIsolationHost.exe break (or weaken) (c).&quot; },
  { q: &quot;Why does Microsoft not sandbox spoolsv.exe in an AppContainer?&quot;, a: &quot;Because the third-party driver install base (v3/v4 drivers shipped by essentially every Windows-compatible printer manufactured since 1993) is packaged to load in LocalSystem context. Constraining the spooler&apos;s token would break the installed printer base. Microsoft&apos;s architectural exits (WPP module blocking, Spooler Worker, Universal Print) sidestep the constraint rather than violate it.&quot; },
  { q: &quot;What does WPP&apos;s module-blocking policy do, and what is the verbatim sentence Microsoft uses to describe it?&quot;, a: &quot;WPP refuses to load any third-party driver DLL into the spooler. Microsoft&apos;s verbatim phrase is &apos;only Microsoft Signed binaries required for IPP are loaded.&apos; The policy makes Point and Print &apos;never install third-party drivers&apos; on a WPP-enabled host.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>windows-security</category><category>print-spooler</category><category>printnightmare</category><category>spoolfool</category><category>windows-protected-print-mode</category><category>universal-print</category><category>vulnerability-research</category><category>windows-internals</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>Every UAC Prompt Is an ALPC Handshake: A Field Guide to Windows&apos; Most-Attacked Local IPC Fabric</title><link>https://paragmali.com/blog/every-uac-prompt-is-an-alpc-handshake-a-field-guide-to-windo/</link><guid isPermaLink="true">https://paragmali.com/blog/every-uac-prompt-is-an-alpc-handshake-a-field-guide-to-windo/</guid><description>ALPC and LRPC are the asynchronous local-IPC fabric under every Windows service. This is the story of the kernel object Microsoft does not document and the attack surface almost every Patch Tuesday still fixes.</description><pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate><content:encoded>
Every Windows service that exposes a local API does so through **LRPC**, the RPC runtime&apos;s local-only transport, and LRPC rides on top of **ALPC**, the kernel&apos;s asynchronous message-and-attribute IPC primitive. The kernel layer is settled engineering. The interface-callback layer in user-mode RPC application code is the load-bearing local elevation-of-privilege surface that almost every Patch Tuesday since 2018 has shipped fixes for. Microsoft does not publish a Win32 or WDK reference for the kernel-side ALPC API; the public knowledge of both layers comes from a handful of named researchers reverse-engineering it. And per-connection ALPC ports are unnamed, which is the asymmetry that makes the threat model coherent -- Section 4 walks why.
&lt;h2&gt;1. Every UAC Prompt Is an ALPC Handshake&lt;/h2&gt;
&lt;p&gt;Double-click an installer. The screen dims, a familiar dialog asks whether you want to allow this app to make changes, and a moment later either nothing happens or the installer keeps running. That moment of dim-and-prompt -- the &lt;a href=&quot;https://paragmali.com/blog/adminless-how-windows-finally-made-elevation-a-security-boun/&quot; rel=&quot;noopener&quot;&gt;User Account Control&lt;/a&gt; consent dialog -- is the most-seen artefact of one of the most-attacked primitives in the Windows kernel: a four-phase handshake on an asynchronous local-IPC port whose name does not appear in any Win32 or WDK reference Microsoft publishes.&lt;/p&gt;
&lt;p&gt;Trace the call from the user side. The Explorer shell invokes &lt;code&gt;ShellExecuteEx&lt;/code&gt; with the verb set to &lt;code&gt;runas&lt;/code&gt;. That call does not magically elevate the process; it sends a request &lt;em&gt;to another process&lt;/em&gt;, the &lt;strong&gt;Application Information service&lt;/strong&gt; (&lt;code&gt;appinfo&lt;/code&gt;) running as &lt;code&gt;svchost.exe -k netsvcs&lt;/code&gt; with SYSTEM authority [@msdocs-svchost] [@forshaw-rpc-2019]. The hand-off is an RPC call. The RPC runtime, asked for a local endpoint, selects the &lt;code&gt;ncalrpc&lt;/code&gt; protocol sequence -- &quot;Local procedure call&quot; in Microsoft&apos;s own protocol-sequence reference [@msdocs-protseq]. Underneath that string is the LRPC transport in &lt;code&gt;rpcrt4.dll&lt;/code&gt;, and underneath the LRPC transport is a kernel ALPC port that lives at the &lt;a href=&quot;https://paragmali.com/blog/the-object-manager-namespace/&quot; rel=&quot;noopener&quot;&gt;Object Manager name&lt;/a&gt; &lt;code&gt;\RPC Control\appinfo&lt;/code&gt;. The kernel resolves the name, the handshake completes, and a single syscall named &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt; [@ntdoc-ntalpc] carries the request message into the SYSTEM-context server and the reply back.&lt;/p&gt;
&lt;p&gt;That syscall is the load-bearing entry point for the entire local-IPC fabric. Microsoft Learn does not publish a reference page for it. The de facto reference is a community-maintained header dump at &lt;code&gt;ntdoc.m417z.com&lt;/code&gt; [@ntdoc-ntalpc] that lists all eight parameters of the function. The kernel object behind the call is the &lt;code&gt;_ALPC_PORT&lt;/code&gt;, and the per-connection structure layouts are documented only on Geoff Chappell&apos;s site [@chappell-alpc] [@chappell-alpcp] and inside the chapter named &lt;em&gt;Advanced local procedure call (ALPC)&lt;/em&gt; of &lt;em&gt;Windows Internals 7e Part 2&lt;/em&gt; [@wininternals-7e].&lt;/p&gt;

The kernel object and syscall family that replaced classic LPC in Windows Vista (November 2006). ALPC is an asynchronous, message-and-attribute IPC primitive built around the `_ALPC_PORT` object. The user-mode entry points are the undocumented `Nt*Alpc*` and `Alpc*` functions exported from `ntdll.dll`. Every local RPC call in modern Windows transits an ALPC port [@csandker-alpc].

The Microsoft RPC runtime&apos;s transport selected when an application binds to the `ncalrpc` protocol sequence [@msdocs-protseq]. LRPC layers the RPC interface-registration model -- IDL, NDR marshalling, security callbacks -- on top of ALPC ports. LRPC is implemented inside `rpcrt4.dll`; the kernel does not know it exists. The kernel sees only ALPC messages.
&lt;p&gt;The abbreviation collision is real and bites every newcomer. &lt;strong&gt;LPC&lt;/strong&gt; is the original Windows NT 3.1 kernel primitive. &lt;strong&gt;LRPC&lt;/strong&gt; is the RPC runtime&apos;s local transport, named in Windows NT 3.5 (1994), a full decade before ALPC existed [@custer-solomon-2e]. LRPC was a transport name when the underlying kernel object was still LPC. Vista renamed the kernel object to ALPC; nobody renamed the transport. The two abbreviations differ by one letter and refer to different layers.&lt;/p&gt;
&lt;p&gt;Two layers sit on top of one kernel object. The kernel layer is what &lt;code&gt;Nt*Alpc*&lt;/code&gt; syscalls touch. The user-mode layer is the RPC runtime&apos;s interface dispatch -- the IDL stubs, the NDR encoders, the per-interface security callback the application registers with &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; [@msdocs-rpcregisterif2]. The rest of this article pulls these two layers apart, walks the history that produced them, and explains why almost every Patch Tuesday since 2018 has shipped fixes inside the second one.&lt;/p&gt;

sequenceDiagram
    participant Client as Client (ShellExecuteEx peer)
    participant ConnPort as Connection port \RPC Control\appinfo
    participant CommPort as Per-connection communication ports (unnamed)
    participant Server as AppInfo service (SYSTEM)
    Client-&amp;gt;&amp;gt;ConnPort: NtAlpcConnectPort (CONNECT)
    ConnPort-&amp;gt;&amp;gt;Server: ALPC connect message queued
    Server-&amp;gt;&amp;gt;CommPort: NtAlpcAcceptConnectPort (ACCEPT, returns paired handles)
    Client-&amp;gt;&amp;gt;CommPort: NtAlpcSendWaitReceivePort (REQUEST)
    CommPort-&amp;gt;&amp;gt;Server: ALPC message with NDR-encoded args
    Server-&amp;gt;&amp;gt;CommPort: NtAlpcSendWaitReceivePort (REPLY)
    CommPort-&amp;gt;&amp;gt;Client: NDR-encoded reply delivered
    Client-&amp;gt;&amp;gt;CommPort: NtAlpcDisconnectPort (CLOSE)
&lt;p&gt;The diagram is the article in miniature. Three of the four labelled actors are kernel objects: a named connection port, an unnamed pair of communication ports, and the message queue between them. The fourth is application code running in two different processes. The bugs of the next thirteen years live in the application code. The diagram&apos;s correctness rests on a structural fact almost every secondary writeup gets wrong, and Section 4 spells it out in full.&lt;/p&gt;
&lt;p&gt;If this primitive is everywhere, why does nobody talk about it? Because nobody had to, for thirteen years.&lt;/p&gt;
&lt;h2&gt;2. Origins -- Cutler&apos;s NT and the Birth of LPC (1989-1993)&lt;/h2&gt;
&lt;p&gt;Dave Cutler talked about it, in October 1988, to a room of people he was trying to recruit out of Digital Equipment Corporation [@zachary-showstopper]. The pitch was a from-scratch portable operating system at Microsoft. The architectural commitment that mattered for our story was a microkernel-style design: the Windows personality, the OS/2 personality, the POSIX personality would all run as user-mode subsystems, each in its own process, talking to clients through a fast in-machine remote procedure call. The kernel would not implement the Win32 API directly. The kernel would implement an IPC primitive shaped like a procedure call and cheap enough to use for every Win32 API a process made.&lt;/p&gt;
&lt;p&gt;That decision created a design problem the team had to solve before any of the subsystems could be written. Microkernel-style separation of subsystems means that the Win32 client of &lt;code&gt;CreateWindow&lt;/code&gt; is in one process and the Win32 server that draws the window is in another. Every API call crosses a process boundary. The IPC primitive that carries the crossing has to look like a function call, return like a function call, and cost no more than tens of microseconds. The Cutler team -- Lou Perazzoli, Mark Lucovsky, Steven Wood, Darryl Havens, and the larger NT design group [@zachary-showstopper] -- shipped that primitive as &lt;strong&gt;Local Procedure Call&lt;/strong&gt;, or LPC, with the first release of Windows NT in July 1993. Helen Custer documented the design that same year in &lt;em&gt;Inside Windows NT&lt;/em&gt; [@custer-print], the canonical first-edition print primary.&lt;/p&gt;

The original Windows NT kernel IPC primitive, introduced with NT 3.1 in July 1993 as a synchronous inter-process communication facility [@csandker-alpc]. LPC was synchronous-call-shaped, used three port objects per connection (one named connection port plus two unnamed communication ports), and was the transport for every Win32 API call into the Client/Server Runtime Subsystem (CSRSS) until Windows Vista. The kernel removed classic LPC entirely by Windows 7; legacy `NtCreatePort` callers were silently redirected onto the ALPC implementation [@csandker-alpc].
&lt;p&gt;The classic LPC mechanism worked like this. A server process calls &lt;code&gt;NtCreatePort&lt;/code&gt; to create a &lt;em&gt;connection port&lt;/em&gt; under an Object Manager name (for example, &lt;code&gt;\Windows\ApiPort&lt;/code&gt; for CSRSS). The server then waits on the connection port. A client process opens the connection port by name and calls &lt;code&gt;NtConnectPort&lt;/code&gt; to request a session. The kernel creates two new, unnamed &lt;em&gt;communication ports&lt;/em&gt; -- one the client holds, one the server holds -- and ties them to the connection through the kernel&apos;s port-routing tables. From that point on, the client and server send messages through their respective communication-port handles; neither party has to look up the other in the Object Manager namespace. The three-port model is the architectural ancestor of every ALPC handshake the rest of this article will walk.&lt;/p&gt;

flowchart LR
    A[Client process] -- &quot;NtConnectPort by name&quot; --&amp;gt; B[Connection port \Windows\ApiPort -- NAMED]
    B -- &quot;NtAcceptConnectPort&quot; --&amp;gt; C[Server process]
    C -- &quot;issues a pair of handles&quot; --&amp;gt; D[Client comm port -- UNNAMED]
    C -- &quot;issues a pair of handles&quot; --&amp;gt; E[Server comm port -- UNNAMED]
    A -- &quot;NtRequestWaitReplyPort&quot; --&amp;gt; D
    D -- &quot;kernel routes the message&quot; --&amp;gt; E
    E -- &quot;delivered to&quot; --&amp;gt; C
&lt;p&gt;The two design pinch-points that Vista would later have to fix are visible already in the 1993 mechanism. First, the call surface was synchronous: &lt;code&gt;NtRequestWaitReplyPort&lt;/code&gt; sent a message and blocked the caller until the reply came back, which forced the higher-level RPC runtime to wrap its own asynchronous machinery around the syscall and doubled the syscall cost for every async RPC. Second, the message payload had a small fixed inline budget -- on the order of 256 bytes [@csandker-alpc] -- with anything larger requiring an explicit &lt;code&gt;NtMapViewOfSection&lt;/code&gt; dance to set up a shared section the server would then peek into. The split between &quot;short message in the syscall&quot; and &quot;long payload in a shared section&quot; was awkward, racy, and a perennial source of off-by-one bugs in the server stubs.&lt;/p&gt;
&lt;p&gt;The third pinch-point was security, and it is the one Cesar Cerrudo will name in 2006. LPC&apos;s access check happened once, at &lt;code&gt;NtConnectPort&lt;/code&gt;, against the connection port&apos;s discretionary access control list (DACL). After the handshake, the kernel had no further opinion about who could send what to whom over the established channel. The server trusted every message it received because the kernel had already vouched that the client cleared the DACL at connect time. In 1993 that trust model was fine. The only callers of CSRSS were Win32 client processes the team controlled. POSIX clients talked to the POSIX subsystem; OS/2 clients talked to the OS/2 subsystem; the trust boundaries were the subsystem boundaries and nobody crossed them on purpose.&lt;/p&gt;

The microkernel idea -- pull as much out of the kernel as possible, run it as user-mode servers -- was a late-1980s academic enthusiasm, energised by Carnegie Mellon&apos;s Mach. Cutler brought it to NT after building VMS and the never-shipped Mica research kernel at Digital. The catch was performance. Every API call that used to be a function call inside the kernel now had to be a context switch, a message copy, and a reply, twice. If that round trip cost a millisecond, Windows would feel like a 1980s timesharing system. LPC&apos;s job was to make it cost microseconds, and the team&apos;s success there is one reason NT could ship at all. The structural cost -- a synchronous primitive whose security check ran once and then trusted the channel -- was not the 1993 team&apos;s problem, because they controlled both ends of every conversation.
&lt;p&gt;The 1993 design assumed the only callers of CSRSS were Win32 client processes the team controlled. That assumption held for thirteen years.&lt;/p&gt;
&lt;h2&gt;3. The First Reckoning -- LPC&apos;s Failure Modes and Cerrudo&apos;s WLSI 2006&lt;/h2&gt;
&lt;p&gt;In March 2006, at Black Hat Europe in Amsterdam, Cesar Cerrudo gave a talk titled &lt;em&gt;WLSI -- Windows Local Shellcode Injection&lt;/em&gt;. Twelve weeks later, Microsoft shipped the Vista ALPC redesign. The temporal compression is intentional, but it is not the whole story: the Vista redesign had been underway inside the kernel team for years before Cerrudo&apos;s talk. What the talk did was give the public security community a name and a shape for the structural class of bug the redesign was about to address.&lt;/p&gt;
&lt;p&gt;Cerrudo&apos;s paper, archived at Exploit-DB under the title &lt;em&gt;WLSI Windows Local Shellcode Injection&lt;/em&gt; and dated March 14, 2006 [@cerrudo-exploitdb], with the speaker deck mirrored on Black Hat&apos;s own server [@cerrudo-bh-pdf], walked an end-to-end attack on an LPC server inside CSRSS. The exact server is less important than the attack&apos;s three-clause shape, which Cerrudo articulated and which would recur, over the next two decades, in every later ALPC and LRPC privilege-escalation primitive.&lt;/p&gt;

flowchart LR
    A[Port is reachable -- the connection port DACL admits the attacker] --&amp;gt; D[Local elevation-of-privilege primitive]
    B[Server trusts the message -- no per-message identity check or per-procedure authorization] --&amp;gt; D
    C[Channel survives the access check -- LPC checks the DACL once at NtConnectPort, then forgets] --&amp;gt; D
&lt;p&gt;Clause one: &lt;em&gt;the port is reachable&lt;/em&gt;. The LPC connection port has a DACL; the attacker happens to be inside it. For CSRSS&apos;s &lt;code&gt;\Windows\ApiPort&lt;/code&gt;, that means &quot;any Win32 process on the desktop&quot;, which is exactly what NT was supposed to permit. Clause two: &lt;em&gt;the DACL is permissive&lt;/em&gt;. Every authenticated user is in scope of the LPC servers that brokered the user-mode Win32 API surface, by design. Clause three: &lt;em&gt;the server trusts the message&lt;/em&gt;. The LPC kernel object exposes a &lt;code&gt;PORT_MESSAGE&lt;/code&gt; header with two fields the receiver can use for bookkeeping -- a process ID and a thread ID. The fields are not authenticated. The receiving server, in the WLSI demonstration, read attacker-controlled offsets and lengths out of the message body and walked into the server&apos;s own address space.&lt;/p&gt;
&lt;p&gt;The three clauses together produce a local elevation primitive. None of the clauses, taken individually, is a kernel bug. None, taken individually, is even an application bug. The bug -- in the WLSI exemplar -- is that the CSRSS server trusted a length field that came from a process the server itself had no reason to trust. The OS did exactly what its security model promised. The application did exactly what the IPC primitive made easy.&lt;/p&gt;

A Windows access control list attached to a securable object (a file, a registry key, a kernel object such as an LPC or ALPC port) that names the security principals allowed or denied each access right. For an LPC connection port, the DACL governs whether a calling process is allowed to open the port at all. Once the port is opened, the DACL is no longer consulted for messages flowing across the established connection -- which is exactly the once-and-done check at the centre of Cerrudo&apos;s structural class.

The 1993 trust model held until 2006 because the team controlled both ends of every conversation. Cerrudo named the class of bug that emerged when that assumption stopped holding.
&lt;p&gt;That structural class is the load-bearing reason the Vista redesign was about to be a redesign and not a patch. The three LPC failure modes the kernel team had identified -- the ones that motivated re-architecting the primitive rather than fixing the WLSI server -- compose a near-perfect mirror of Cerrudo&apos;s three clauses. They are: (1) the synchronous-only design forced the RPC runtime to layer its own asynchronous wrapper around &lt;code&gt;NtRequestWaitReplyPort&lt;/code&gt;, doubling the per-call syscall cost for async RPC; (2) the 256-byte inline plus shared-section dance was awkward and prone to race conditions in the server stub; (3) the port-DACL-only security model checked access once at connect and then trusted the channel, with no kernel primitive for per-message caller identity. A redesign was the only way to attack all three at once without breaking every NT 4-era server in the field.&lt;/p&gt;
&lt;p&gt;One LPC failure mode that did not make Cerrudo&apos;s slide and that Microsoft has never publicly discussed in detail was the reply-port confusion class. In classic LPC, a server&apos;s reply traveled back over the client&apos;s communication port handle, and a misbehaving server could be tricked into replying to the wrong client when multiple connections were interleaved. Microsoft addressed this quietly in the Vista era; the only public references are footnotes in &lt;em&gt;Windows Internals&lt;/em&gt; editions and the occasional aside in csandker [@csandker-alpc]. The public security community did not catch the bug class at the time.&lt;/p&gt;
&lt;p&gt;In November 2006 -- eight months after WLSI -- Windows Vista shipped. The new kernel called the replacement primitive &lt;strong&gt;Advanced LPC&lt;/strong&gt;. The redesign closed half of Cerrudo&apos;s structural class -- the &lt;em&gt;permissive port DACL&lt;/em&gt; half, by giving servers fine-grained tools to control who reaches their connection ports and by introducing a per-message security attribute the server could query for caller identity. It left the other half completely intact, because the other half is not a kernel property. The other half lives in the user-mode RPC runtime and in the application code that registers RPC interfaces on top of ALPC ports. That intact half is what the next thirteen years of public security research is about.&lt;/p&gt;
&lt;p&gt;The naive read of Cerrudo&apos;s paper is &quot;Microsoft will fix the bug.&quot; The structural read is harder: Cerrudo did not find a bug. He named a class of bug whose root cause is a property of the trust model. The Vista redesign closed the half of the class the kernel could close. It could not close the rest, because the rest is application code, and the kernel cannot inspect application code.&lt;/p&gt;
&lt;h2&gt;4. The Breakthrough -- ALPC, the Vista Redesign, and the Message-Attribute System&lt;/h2&gt;
&lt;p&gt;The Vista kernel team&apos;s answer to Cerrudo was not a patch. It was a complete replacement of the kernel object.&lt;/p&gt;
&lt;p&gt;ALPC re-cast the LPC port as an &lt;strong&gt;asynchronous, message-and-attribute-based&lt;/strong&gt; primitive. The classic LPC quartet -- &lt;code&gt;NtRequestPort&lt;/code&gt;, &lt;code&gt;NtReplyPort&lt;/code&gt;, &lt;code&gt;NtRequestWaitReplyPort&lt;/code&gt;, &lt;code&gt;NtReplyWaitReplyPort&lt;/code&gt; -- collapsed into a single syscall, &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt; [@ntdoc-ntalpc], with eight parameters whose combinations express every variant the older quartet supported. The kernel object behind the syscall is the &lt;code&gt;_ALPC_PORT&lt;/code&gt;. The structure layout is documented only in the chapter named &lt;em&gt;Advanced local procedure call (ALPC)&lt;/em&gt; of &lt;em&gt;Windows Internals 7e Part 2&lt;/em&gt; [@wininternals-7e], in the reverse-engineered header dumps on Geoff Chappell&apos;s site [@chappell-alpc] [@chappell-alpcp], and in the community-maintained &lt;code&gt;phnt&lt;/code&gt; headers that the Process Hacker project ships. None of those is a Microsoft Learn page.&lt;/p&gt;

The kernel object at the centre of Vista-and-later local IPC. Named connection ports are referenced by Object Manager name (typically under `\RPC Control`, `\BaseNamedObjects`, or per-session AppContainer subtrees). The per-connection communication ports created by `NtAlpcAcceptConnectPort` are unnamed and exist only as handles in the connecting and accepting processes. The structure layout is undocumented by Microsoft; the canonical reverse-engineered reference is Geoff Chappell&apos;s site [@chappell-alpc].
&lt;p&gt;The user-mode syscall surface, enumerated as exhaustively as anyone outside Microsoft can: &lt;code&gt;NtAlpcCreatePort&lt;/code&gt;, &lt;code&gt;NtAlpcConnectPort&lt;/code&gt;, &lt;code&gt;NtAlpcAcceptConnectPort&lt;/code&gt;, &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt;, &lt;code&gt;NtAlpcDisconnectPort&lt;/code&gt;, &lt;code&gt;NtAlpcCancelMessage&lt;/code&gt;, &lt;code&gt;NtAlpcCreatePortSection&lt;/code&gt;, &lt;code&gt;NtAlpcCreateResourceReserve&lt;/code&gt;, plus the &lt;code&gt;PORT_ATTRIBUTES&lt;/code&gt; and message-attribute structures that decorate each call. Microsoft Learn does not list any of them under a Win32 or WDK developer-facing reference. NtDoc [@ntdoc-ntalpc] is the de facto syscall reference, and the &lt;em&gt;Windows Internals 7e Part 2&lt;/em&gt; chapter is the de facto architectural reference.&lt;/p&gt;

Microsoft has documented the user-mode RPC runtime exhaustively on Learn -- the IDL syntax, the marshalling rules, the binding-handle API, the interface-registration flags. The `Nt*Alpc*` and `Alpc*` kernel surface is the deliberate exception. Microsoft&apos;s framing is that ALPC is an *internal* implementation detail of the RPC runtime, not a stable developer-facing API. Application authors are supposed to write RPC code, not ALPC code. The framing is defensible -- the ALPC ABI does change between Windows versions -- but it leaves the entire defender community reverse-engineering the surface from public symbols, the *Windows Internals* book series, NtDoc, Geoff Chappell, and the open-source `phnt` headers. The Vista-and-later structural correctness story this article tells is one that Microsoft has never written down for outside readers.
&lt;p&gt;The structural break with classic LPC is the &lt;strong&gt;message-attribute&lt;/strong&gt; system. Every ALPC message can carry four optional attributes, each of which targets one of the awkward LPC patterns the old kernel forced server authors to roll by hand.&lt;/p&gt;

An optional decoration on an ALPC message that lets the sender or receiver request a kernel service in band with the message itself. The four attribute types are **Context**, **Handle**, **Security**, and **View**. Each one targets a workflow that classic LPC required application code to perform out of band; in ALPC the kernel does the work atomically with the message exchange.
&lt;p&gt;&lt;strong&gt;The Context attribute&lt;/strong&gt; carries a per-message per-client cookie the server uses to associate the message with a logical operation. In classic LPC, a server tracking a multi-step protocol had to maintain its own client-to-state map indexed by client process ID, with all the race conditions that map invited; the Context attribute moves that bookkeeping into the kernel and makes it correct by construction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Handle attribute&lt;/strong&gt; is first-class handle passing inside the message itself. In classic LPC, transferring a kernel handle from sender to receiver required the sender to call &lt;code&gt;DuplicateHandle&lt;/code&gt; with the receiver&apos;s process handle, hope the receiver hadn&apos;t exited, and then send the resulting handle value in the message body. The Handle attribute lets the kernel do the duplication atomically with delivery; the receiver finds the duplicated handle already in its own handle table when the message lands.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Security attribute&lt;/strong&gt; is the per-message identity primitive whose absence Cerrudo had named in 2006. The sender can opt to attach its caller token to a message; the receiver can opt to query the token (process ID, thread ID, integrity level, AppContainer SID) when it dispatches the message. The classic LPC pattern -- &quot;trust the channel because the kernel checked the DACL at connect&quot; -- gets replaced by &quot;ask the kernel who is actually sending this message right now.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The View attribute&lt;/strong&gt; is the shared-section dance, rewritten. In classic LPC, payloads larger than the inline budget required the sender to call &lt;code&gt;NtCreateSection&lt;/code&gt;, both parties to call &lt;code&gt;NtMapViewOfSection&lt;/code&gt;, and the receiver to peek into the shared mapping. The View attribute hands the receiver a section view automatically as a side effect of message delivery; no out-of-band coordination is required.&lt;/p&gt;

flowchart TD
    A[Context attribute] --&amp;gt; A1[Replaces: server-side client-to-state map indexed by PID]
    B[Handle attribute] --&amp;gt; B1[Replaces: out-of-band DuplicateHandle dance]
    C[Security attribute] --&amp;gt; C1[Replaces: trust the channel because DACL was checked at connect]
    D[View attribute] --&amp;gt; D1[Replaces: NtCreateSection plus NtMapViewOfSection dance for large payloads]
&lt;p&gt;The handshake topology survives from classic LPC and tightens. The server creates a named connection port with &lt;code&gt;NtAlpcCreatePort&lt;/code&gt;. The client opens the connection port by name with &lt;code&gt;NtAlpcConnectPort&lt;/code&gt; and sends an initial connect message; the kernel queues the connect on the server&apos;s port. The server calls &lt;code&gt;NtAlpcAcceptConnectPort&lt;/code&gt;, and the kernel returns a &lt;em&gt;pair&lt;/em&gt; of communication-port handles -- one to the client, one to the server -- that are bound to that single connection. From that point on, the kernel routes messages through the paired handles, and every send or receive is a single call to &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt;. Asynchronous is the default; synchronous semantics are a flag combination. The per-port message queue, the blocked-receiver wake, and the cross-port routing all run inside the kernel dispatcher.&lt;/p&gt;

flowchart LR
    A[Client process] -- &quot;NtAlpcConnectPort by name&quot; --&amp;gt; B[Connection port -- NAMED in \RPC Control]
    B -- &quot;kernel queues the connect&quot; --&amp;gt; C[Server process]
    C -- &quot;NtAlpcAcceptConnectPort&quot; --&amp;gt; D[Paired comm ports -- UNNAMED]
    A -- &quot;NtAlpcSendWaitReceivePort&quot; --&amp;gt; D
    D -- &quot;kernel routing&quot; --&amp;gt; C
&lt;p&gt;Here is the structural correction the input premise to this article got wrong, and that almost every secondary writeup gets wrong. &lt;strong&gt;Only the named connection port has an Object Manager name.&lt;/strong&gt; The per-connection communication ports created by &lt;code&gt;NtAlpcAcceptConnectPort&lt;/code&gt; are unnamed. They have no path under &lt;code&gt;\RPC Control&lt;/code&gt; or &lt;code&gt;\BaseNamedObjects&lt;/code&gt; or anywhere else. They exist only as handles in the address spaces of the two processes that completed the handshake. No third party can open them, because no third party has a name with which to ask the Object Manager for them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; ALPC&apos;s structural correctness rests on a single move: the per-connection communication ports are unnamed. Only the parties that completed the handshake can address the channel. The kernel does not let anyone else find it. This is the half of Cerrudo&apos;s structural class the Vista redesign actually closed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A statement like &quot;every ALPC port has an Object Manager name&quot; is wrong, and it propagates a wrong threat model. Named ports are the entry points an attacker can knock on. Unnamed communication ports are the established channels the attacker cannot reach without first being admitted through the connection port&apos;s DACL. Defenders who get this wrong start hunting for the unnamed children in the Object Manager namespace and find nothing, then conclude the tooling is broken. The tooling is fine. The ports are not there.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Microsoft&apos;s documentation choice has consequences for tooling. The Wireshark dissector for MSRPC handles the on-the-wire NDR encoding well, but it has no view into the kernel ALPC layer because the kernel does not emit a packet capture. To see ALPC at the kernel level the tooling has to subscribe to the &lt;code&gt;Microsoft-Windows-Kernel-ALPC&lt;/code&gt; ETW provider [@msdocs-etwsys], and even that provider is gated behind &lt;code&gt;EVENT_TRACE_SYSTEM_LOGGER_MODE&lt;/code&gt;, which a non-SYSTEM caller cannot enable. The structural opacity of the kernel layer is partly an artefact of the deliberate &quot;no public WDK developer-facing reference&quot; position.&lt;/p&gt;
&lt;p&gt;Backward compatibility was preserved by silent rewiring rather than by parallel kernel objects. The classic LPC syscall names continue to link in any pre-Vista binary, but from Windows 7 onward the kernel routes those calls into the ALPC implementation underneath [@csandker-alpc]. Classic LPC, as an independent kernel object, no longer exists. The 1993 syscall surface is alive only as a thin compatibility shim. The 2006 kernel object is what every modern Windows service actually uses.&lt;/p&gt;
&lt;p&gt;The Vista redesign closed the &lt;em&gt;permissive port DACL&lt;/em&gt; half of the structural problem. It left the &lt;em&gt;interface callback returns RPC_S_OK when it should return RPC_S_ACCESS_DENIED&lt;/em&gt; half completely intact.The Vista kernel team&apos;s collective attribution stops short of naming individual ALPC architects. &lt;em&gt;Windows Internals 7e Part 2&lt;/em&gt; [@wininternals-7e] credits the work institutionally rather than to a single engineer, and no public Microsoft artefact identifies a single ALPC architect by name; secondary attributions in conference talks and blog posts trace back to footnotes rather than to primary record. That intact half is the rest of this article.&lt;/p&gt;
&lt;h2&gt;5. The Universalisation -- ALPC as the Local IPC Fabric (2009-2013)&lt;/h2&gt;
&lt;p&gt;By 2013, ALPC ran the local-IPC traffic of every Windows service that mattered. The kernel team had removed classic LPC. The Vista replacement had not been &lt;em&gt;replaced&lt;/em&gt;; it had been &lt;em&gt;adopted&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The transition was technically backwards-compatible. Pre-Vista binaries that called &lt;code&gt;NtCreatePort&lt;/code&gt; and &lt;code&gt;NtRequestWaitReplyPort&lt;/code&gt; continued to link and run; the kernel preserved the syscall names and silently rerouted the calls into the ALPC implementation underneath [@csandker-alpc]. The compatibility was not lossless -- the old single-message-per-call semantics map onto the ALPC asynchronous primitive only at the cost of an extra wait -- but it was good enough that no Microsoft-shipped service ever needed a port from classic LPC. Every service author upgrading to Vista or later was implicitly upgraded to ALPC.&lt;/p&gt;
&lt;p&gt;By Windows 8.1 the roll-call of services riding LRPC on ALPC was effectively the roll-call of services that ship with Windows. The Client/Server Runtime Subsystem (CSRSS) had been ALPC-only since Vista. The Local Security Authority Subsystem Service (LSASS) -- which brokers logon, token issuance, and Kerberos ticket caching -- exposes its API surface over LRPC. The Service Control Manager (SCM, &lt;code&gt;services.exe&lt;/code&gt;) accepts service-control commands over an LRPC interface. The DCOM activation service (&lt;code&gt;rpcss&lt;/code&gt;) marshals every local COM activation request through an LRPC pipeline. Windows Error Reporting, the audio service (&lt;code&gt;audiosrv&lt;/code&gt;), Task Scheduler (&lt;code&gt;schedsvc&lt;/code&gt;/&lt;code&gt;schrpc&lt;/code&gt;), the Application Information service (&lt;code&gt;appinfo&lt;/code&gt;) that brokers UAC, the Encrypting File System extension (&lt;code&gt;efslsaext&lt;/code&gt;, the EFSRPC server documented in the [MS-EFSR] specification [@ms-efsr]), the print spooler (&lt;code&gt;spoolsv&lt;/code&gt;), and the Background Intelligent Transfer Service (BITS) all expose at least one LRPC interface for client communication [@csandker-rpc].&lt;/p&gt;

flowchart TD
    K[Kernel ALPC layer -- _ALPC_PORT objects, NtAlpcSendWaitReceivePort dispatcher]
    K --&amp;gt; CSRSS[CSRSS -- Win32 subsystem]
    K --&amp;gt; LSASS[LSASS -- logon and token issuance]
    K --&amp;gt; SCM[Service Control Manager]
    K --&amp;gt; RPCSS[RPCSS -- DCOM activator and epmapper]
    K --&amp;gt; APPINFO[AppInfo -- UAC consent broker]
    K --&amp;gt; SPOOL[Print Spooler]
    K --&amp;gt; SCHRPC[Task Scheduler -- schrpc and schedsvc]
    K --&amp;gt; BITS[BITS -- background transfers]
    K --&amp;gt; AUDIO[Audio service -- audiosrv]
    K --&amp;gt; EFS[EFS -- efslsaext]
&lt;p&gt;That fan-out is the article&apos;s load-bearing diagram for understanding why ALPC is the most-attacked local IPC fabric in modern Windows. Every named service in that diagram is reachable over an LRPC interface. Every LRPC interface registers a per-interface security callback through &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; [@msdocs-rpcregisterif2] or &lt;code&gt;RpcServerRegisterIf3&lt;/code&gt; [@msdocs-rpcregisterif3]. Every callback is application code that the kernel cannot inspect. A single permissive interface in a single one of those services is a structural primitive that works against the transport every service uses. Trail of Bits, announcing their RPC Investigator tool in January 2023, captured the surface area in one line: MSRPC is &quot;involved on some level in nearly every activity that you can take on a Windows system, from logging in to your laptop to opening a file&quot; [@tob-rpcinv-blog].&lt;/p&gt;

MSRPC is involved on some level in nearly every activity that you can take on a Windows system, from logging in to your laptop to opening a file. -- Trail of Bits, *RPC Investigator* announcement, January 2023 [@tob-rpcinv-blog]
&lt;p&gt;To see the fabric in operation, walk one call. An unprivileged user invokes &lt;code&gt;StartServiceW&lt;/code&gt; from the SCM client library inside &lt;code&gt;sechost.dll&lt;/code&gt;. The library binds to the SCM&apos;s local RPC endpoint -- the &lt;code&gt;\RPC Control\ntsvcs&lt;/code&gt; ALPC port that the Service Control Manager registers at boot. The MIDL-generated client stub packs the service name and arguments into NDR and hands them to &lt;code&gt;NdrClientCall3&lt;/code&gt;. &lt;code&gt;rpcrt4.dll&lt;/code&gt; crosses into the kernel through &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt;. The kernel routes the ALPC message to the SCM&apos;s blocked worker thread inside &lt;code&gt;services.exe&lt;/code&gt;. The worker, running as SYSTEM, unpacks the NDR body with &lt;code&gt;NdrStubCall3&lt;/code&gt; and prepares to dispatch the server-side procedure. Before the procedure runs, the RPC runtime invokes the interface security callback, which checks whether the caller&apos;s token holds &lt;code&gt;SC_MANAGER_CONNECT&lt;/code&gt; and the target service&apos;s DACL grants &lt;code&gt;SERVICE_START&lt;/code&gt;. If the callback returns &lt;code&gt;RPC_S_OK&lt;/code&gt;, the SCM starts the service. The reply -- an NDR-encoded error code -- rides another &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt; back to the client. One user call, five layers crossed, and the kernel never knew it was running an RPC.&lt;/p&gt;
&lt;p&gt;One consequence of the silent kernel rewiring is that pre-Vista NT 4-era code samples appear to work on Windows 11. A textbook example from a 1996 driver-development book that calls &lt;code&gt;NtCreatePort&lt;/code&gt; will link, load, and exchange messages just fine; the messages are travelling over the 2006 ALPC kernel object behind a 1993 syscall name. This is unusual generosity from a kernel team that breaks driver ABIs every few releases, and it is one of the reasons Microsoft has preserved the option not to publish a &lt;code&gt;Nt*Alpc*&lt;/code&gt; developer-facing reference: as long as everyone is supposed to use the RPC runtime, the kernel object can keep evolving.&lt;/p&gt;
&lt;p&gt;Once the transport was universal, enumeration became valuable. If only LSASS used ALPC, listing LSASS&apos;s interfaces by hand was fine. Once every service did, automation was the only tractable methodology. The answer to who built that automation is the next section.&lt;/p&gt;
&lt;h2&gt;6. The Eureka Year -- Public Tooling and the Interface-Callback Class (2017-2019)&lt;/h2&gt;
&lt;p&gt;In an eighteen-month span between October 2017 and December 2019, four researchers turned ALPC from internal NT plumbing into the most-attacked local-IPC surface in modern Windows. The exemplars were structurally identical: an LRPC server registered an RPC interface with a callback that either was NULL or returned &lt;code&gt;RPC_S_OK&lt;/code&gt; for a caller that should have received &lt;code&gt;RPC_S_ACCESS_DENIED&lt;/code&gt;. The kernel ALPC layer behaved correctly in every one of them. The application code did not.&lt;/p&gt;

gantt
    title Public ALPC and LRPC research, October 2017 to December 2019
    dateFormat YYYY-MM
    section Tooling and disclosure
    PacSec -- A view into ALPC-RPC plus CVE-2017-11783       :2017-10, 1M
    SandboxEscaper -- CVE-2018-8440 0-day on GitHub          :2018-08, 1M
    Forshaw -- PPL and COM injection through LRPC            :2018-10, 1M
    Ormandy -- CVE-2019-1162 MSCTF disclosure                :2019-08, 1M
    Forshaw -- Calling local RPC servers from .NET           :2019-12, 1M
&lt;p&gt;The first publication is &lt;strong&gt;Clement Rouault and Thomas Imbert&apos;s &quot;A view into ALPC-RPC&quot;&lt;/strong&gt;, presented at PacSec in November 2017 [@hakril-pacsec] [@slideshare-pacsec] and at Hack.lu the same season [@youtube-hacklu]. The talk is the first end-to-end mechanical walk of the LRPC-over-ALPC stack to appear at a public security conference, and the talk&apos;s deliverable was a working NDR-aware fuzzer named &lt;strong&gt;RPCForge&lt;/strong&gt; [@rpcforge]. RPCForge surfaced &lt;strong&gt;CVE-2017-11783&lt;/strong&gt; [@nvd-cve-2017-11783], the first publicly-acknowledged ALPC elevation-of-privilege issue surfaced by an outside-Microsoft fuzzer. The NVD entry phrases the bug class as &quot;the way it handles calls to Advanced Local Procedure Call (ALPC)&quot; -- the canonical &quot;ALPC EoP&quot; classification that NVD reuses for every later instance.&lt;/p&gt;
&lt;p&gt;The second is &lt;strong&gt;James Forshaw&apos;s &lt;code&gt;NtObjectManager&lt;/code&gt; tooling&lt;/strong&gt;, distributed through the &lt;code&gt;sandbox-attacksurface-analysis-tools&lt;/code&gt; repository at Google Project Zero [@forshaw-saatools]. The tooling is a PowerShell module backed by a .NET library originally called &lt;code&gt;NtApiDotNet&lt;/code&gt; and renamed to &lt;code&gt;NtCoreLib&lt;/code&gt; in 2024. Forshaw introduced the design intent in a December 17, 2019 Project Zero post titled &lt;em&gt;Calling Local Windows RPC Servers from .NET&lt;/em&gt; [@forshaw-rpc-2019], opening with what amounts to a personal manifesto: &lt;em&gt;&quot;As much as I enjoy finding security vulnerabilities in Windows, in many ways I prefer the challenge of writing the tools to make it easier for me and others to do the hunting.&quot;&lt;/em&gt; The post named a gap in his own methodology -- &lt;em&gt;&quot;one of my big blind spots was anything which directly interacted with a Local RPC server&quot;&lt;/em&gt; -- and introduced &lt;code&gt;Get-RpcServer&lt;/code&gt;, &lt;code&gt;Get-NtAlpcServer&lt;/code&gt;, and &lt;code&gt;New-RpcClient&lt;/code&gt; as the cmdlets that closed it.&lt;/p&gt;

As much as I enjoy finding security vulnerabilities in Windows, in many ways I prefer the challenge of writing the tools to make it easier for me and others to do the hunting. -- James Forshaw, *Calling Local Windows RPC Servers from .NET*, Project Zero, December 17, 2019 [@forshaw-rpc-2019]
&lt;p&gt;The conceptual workflow Forshaw&apos;s tooling enables is short enough to fit on one screen. Enumerate every DLL on the system that contains RPC interface metadata. Parse the metadata to recover the IDL-equivalent description of each interface -- the UUID, the version, the procedures, the parameter types. Filter to the ones bound to a local-only protocol sequence. The result is an inventory of &quot;every local RPC procedure callable on this Windows install.&quot; Diff the inventory across a Patch Tuesday and the changes -- new procedures, retired procedures, changed security descriptors -- become a research backlog.&lt;/p&gt;
&lt;p&gt;{`
// PowerShell equivalent (run inside an elevated session with NtObjectManager installed):
//   Install-Module NtObjectManager
//   Get-RpcServer -DbgHelpPath &apos;C:\\Program Files\\Debugging Tools for Windows\\dbghelp.dll&apos; |
//     Where-Object { $&lt;em&gt;.Endpoints.ProtocolSequence -eq &apos;ncalrpc&apos; } |
//     Select-Object Name, InterfaceId, @{N=&apos;ProcCount&apos;;E={$&lt;/em&gt;.Procedures.Count}}&lt;/p&gt;
&lt;p&gt;// The runnable below mirrors the same logic in plain JS so the in-browser engine can execute it.
const interfaces = [
  { name: &apos;AppInfo&apos;,        interfaceId: &apos;201ef99a-7fa0-444c-9399-19ba84f12a1a&apos;, protocolSequence: &apos;ncalrpc&apos;, procedures: 12 },
  { name: &apos;schrpc&apos;,         interfaceId: &apos;86d35949-83c9-4044-b424-db363231fd0c&apos;, protocolSequence: &apos;ncalrpc&apos;, procedures: 27 },
  { name: &apos;spoolss&apos;,        interfaceId: &apos;12345678-1234-abcd-ef00-0123456789ab&apos;, protocolSequence: &apos;ncacn_np&apos;, procedures: 96 },
  { name: &apos;lsarpc-local&apos;,   interfaceId: &apos;12345778-1234-abcd-ef00-0123456789ab&apos;, protocolSequence: &apos;ncalrpc&apos;, procedures: 81 },
  { name: &apos;epmapper&apos;,       interfaceId: &apos;e1af8308-5d1f-11c9-91a4-08002b14a0fa&apos;, protocolSequence: &apos;ncalrpc&apos;, procedures: 5  },
];&lt;/p&gt;
&lt;p&gt;const local = interfaces
  .filter(i =&amp;gt; i.protocolSequence === &apos;ncalrpc&apos;)
  .map(i =&amp;gt; ({ name: i.name, interfaceId: i.interfaceId, procCount: i.procedures }));&lt;/p&gt;
&lt;p&gt;console.log(&apos;Local RPC interfaces (ncalrpc only):&apos;);
local.forEach(i =&amp;gt; console.log(`  ${i.name.padEnd(16)} ${i.interfaceId}  procs=${i.procCount}`));
console.log(`Total: ${local.length}`);
`}&lt;/p&gt;
&lt;p&gt;The third publication is &lt;strong&gt;SandboxEscaper&apos;s CVE-2018-8440&lt;/strong&gt; [@nvd-cve-2018-8440], dropped as a 0-day on GitHub on August 27, 2018, and triaged by CERT/CC as VU#906424 on August 28 with the note that the vulnerability was &quot;being exploited in the wild&quot; [@cert-vu906424]. The 0patch team published a micropatch within days and walked the bug specifics [@0patch-micropatch]. The structural shape of the bug is canonical and is worth tracing carefully.&lt;/p&gt;

sequenceDiagram
    participant Att as Unprivileged attacker process
    participant Sch as Task Scheduler ALPC port \RPC Control\atsvc
    participant Srv as schedsvc.dll worker thread (SYSTEM)
    participant FS as Target file -- C:\WINDOWS\System32\example.dll
    Att-&amp;gt;&amp;gt;Sch: NtAlpcConnectPort plus LRPC SchRpcSetSecurity request
    Sch-&amp;gt;&amp;gt;Srv: dispatch -- IfCallbackFn is NULL, no security callback runs
    Srv-&amp;gt;&amp;gt;FS: SetSecurityInfo as SYSTEM, grant Everyone:F to attacker-chosen path
    Srv-&amp;gt;&amp;gt;Att: RPC_S_OK
    Att-&amp;gt;&amp;gt;FS: overwrite the now-writable file
    Note over Att,FS: next call into the modified binary executes attacker code as SYSTEM
&lt;p&gt;The Task Scheduler service exposes an LRPC interface containing a procedure named &lt;code&gt;SchRpcSetSecurity&lt;/code&gt;, registered through &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; with &lt;code&gt;IfCallbackFn&lt;/code&gt; set to NULL. NULL has a specific meaning, documented verbatim on Microsoft Learn: &lt;em&gt;&quot;IfCallbackFn: Security-callback function, or NULL for no callback&quot;&lt;/em&gt; [@msdocs-rpcregisterif2]. No callback means the RPC runtime dispatches the call without asking the application whether the caller should be allowed.&lt;/p&gt;
&lt;p&gt;Once dispatched, &lt;code&gt;SchRpcSetSecurity&lt;/code&gt; running in the SYSTEM-context Task Scheduler worker thread set a permissive DACL on a file the attacker specified. The attacker chose a file the attacker did not have write access to. The SYSTEM-context service made it writable. The attacker then wrote attacker-controlled bytes into the file, triggered execution, and inherited SYSTEM.&lt;/p&gt;
&lt;p&gt;The 0patch micropatch writeup named the structural pattern as &quot;the Task Scheduler fails to impersonate the requesting client&quot; [@0patch-micropatch] -- which is to say, the service did the operation in its own privileged identity instead of the caller&apos;s. CERT/CC framed the same bug in transport terms: a vulnerability &quot;in the handling of ALPC&quot; that lets an authenticated user overwrite an arbitrary file [@cert-vu906424].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A NULL &lt;code&gt;IfCallbackFn&lt;/code&gt; is the canonical elevation-of-privilege-by-default bug shape. Microsoft Learn documents it as a legal value [@msdocs-rpcregisterif2], and the runtime accepts it without warning. Every notable LRPC EoP since 2017 either left the callback NULL or registered a callback whose body said the wrong thing. Defenders auditing in-house LRPC services should treat any &lt;code&gt;RpcServerRegisterIf2(..., NULL)&lt;/code&gt; in production code as a finding.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The fourth is &lt;strong&gt;Tavis Ormandy&apos;s CVE-2019-1162&lt;/strong&gt; [@nvd-cve-2019-1162], disclosed in the August 13, 2019 Project Zero post &lt;em&gt;Down the Rabbit-Hole...&lt;/em&gt; [@ormandy-ctf-2019]. The bug class Ormandy named is the structural exemplar of &quot;shared system ALPC ports that ignore caller integrity.&quot; The Microsoft Text Services Framework (MSCTF) shipped a global ALPC port -- present since Windows XP in 2001 -- that any process on the desktop could open regardless of integrity level. The CTF subsystem trusted clients to identify themselves correctly in the messages they sent; the protocol had no integrity-level check or AppContainer enforcement. A low-integrity browser process could send messages that impersonated a high-integrity privileged process, and the CTF service would honour them. The fix narrowed the specific instance and left the general class of &quot;shared ALPC ports without caller-integrity enforcement&quot; open.&lt;/p&gt;
&lt;p&gt;A partially-overlapping fifth example -- the same interface-callback class expressed through DCOM activation rather than direct LRPC -- is &lt;strong&gt;Forshaw&apos;s October 18, 2018 Project Zero post&lt;/strong&gt; &lt;em&gt;Injecting Code into Windows Protected Processes using COM&lt;/em&gt; [@forshaw-com-ppl-2018]. The post documented a class of &lt;a href=&quot;https://paragmali.com/blog/protected-process-light-when-the-administrator-isnt-enough/&quot; rel=&quot;noopener&quot;&gt;Protected Process Light&lt;/a&gt; (PPL) bypass in which a DCOM activator marshalled an impersonated client token into a privileged COM server, and the server&apos;s interface callback trusted the marshalled identity too early in the dispatch flow. The kernel ALPC layer is doing exactly what the spec says; the bug is in the user-mode interface code that interprets the message.&lt;/p&gt;

Before `NtObjectManager`, a researcher looking at an LRPC service had to disassemble the service&apos;s DLL by hand, locate the calls to `RpcServerRegisterIf2`, read out the interface UUID and procedure-table pointer, parse the MIDL-generated stub manually, and assemble enough information to send a single well-formed call. After `NtObjectManager`, the same workflow was a one-line PowerShell pipeline. The methodology change cascaded into the Patch-Tuesday cycle. Differential analysis on the RPC interface inventory across a single Patch Tuesday became a research workflow that a small team could run in a single afternoon. Forshaw&apos;s December 2019 post named it explicitly: he wrote the tools because the tools were the bottleneck.

The application-supplied function whose pointer is passed as the `IfCallbackFn` argument to `RpcServerRegisterIf2` [@msdocs-rpcregisterif2] or `RpcServerRegisterIf3` [@msdocs-rpcregisterif3]. The RPC runtime invokes the callback after the port-level access check passes and before the call is dispatched to the IDL-named procedure. The callback inspects the binding handle, the calling user&apos;s token, the integrity level, and any other attribute the application chooses to consult. The callback returns `RPC_S_OK` to permit the call or any other status code to reject it. A NULL callback pointer is documented as a legal value and means &quot;permit every call that reaches the runtime.&quot;

The wire format that LRPC payloads marshal through. NDR is the original 32-bit Network Data Representation transfer syntax used by DCE/RPC; NDR64 is the 64-bit extension Microsoft introduced for 64-bit Windows [@msdocs-ndr64]. Local LRPC and remote MSRPC use the same transfer syntax; the only difference is that local calls travel inside an ALPC `PORT_MESSAGE` body rather than over a TCP or named-pipe transport.
&lt;p&gt;By the end of 2019, the inventory was visible, the bug class had been named, and four worked exemplars had been published. The mechanism underneath -- what an interface-registration callback actually is, why the OS cannot enforce its correctness -- is what the next section unpacks.&lt;/p&gt;
&lt;p&gt;The deeper realisation is that none of these are kernel bugs. The kernel ALPC layer behaved correctly in every one; the bugs live in the user-mode interface-callback layer that Section 7 walks next.&lt;/p&gt;
&lt;h2&gt;7. The LRPC Overlay -- Interface Registration and the Asymmetry the OS Cannot Fix&lt;/h2&gt;
&lt;p&gt;Look at the signature of &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt;. The seventh parameter is named &lt;code&gt;IfCallbackFn&lt;/code&gt;. Microsoft&apos;s own reference page documents that NULL is a legal value, and that NULL means &quot;no callback&quot; [@msdocs-rpcregisterif2]. That parameter is the asymmetry the rest of this section is about.&lt;/p&gt;
&lt;p&gt;A canonical server-side LRPC startup sequence looks like this. The service compiles an IDL file with MIDL; MIDL emits an &lt;code&gt;RPC_SERVER_INTERFACE&lt;/code&gt; structure that pins down the interface&apos;s UUID, version, and procedure table. The service calls &lt;code&gt;RpcServerUseProtseqEp&lt;/code&gt; with the protocol sequence &lt;code&gt;&quot;ncalrpc&quot;&lt;/code&gt;, an endpoint name, and a security descriptor; that call asks the kernel, by way of the RPC runtime, to create an ALPC connection port at the requested name under &lt;code&gt;\RPC Control&lt;/code&gt;. The service calls &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; or, since Windows 8, &lt;code&gt;RpcServerRegisterIf3&lt;/code&gt; [@msdocs-rpcregisterif3]. The newer call additionally accepts a per-interface security descriptor that the runtime enforces before consulting the callback. Both calls store the IDL spec, the interface-registration flags, and the per-interface security callback. Finally the service calls &lt;code&gt;RpcServerListen&lt;/code&gt;, and worker threads in the RPC runtime block inside &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Per call, the dispatch sequence is: accept the inbound ALPC connection, read the NDR-encoded request from the message body, invoke the registered security callback (if any), dispatch to the MIDL-generated server stub, and marshal the reply back.&lt;/p&gt;

sequenceDiagram
    participant Client as Client stub (rpcrt4.dll, user mode)
    participant Kernel as Kernel ALPC dispatcher
    participant Worker as Server worker thread (rpcrt4.dll, user mode)
    participant Cb as Interface security callback (application code)
    participant Stub as MIDL-generated server stub (application code)
    Client-&amp;gt;&amp;gt;Kernel: NtAlpcSendWaitReceivePort (REQUEST with NDR body)
    Kernel-&amp;gt;&amp;gt;Worker: deliver message to blocked worker
    Worker-&amp;gt;&amp;gt;Cb: invoke IfCallbackFn (if registered)
    Cb-&amp;gt;&amp;gt;Worker: return RPC_S_OK or RPC_S_ACCESS_DENIED
    Worker-&amp;gt;&amp;gt;Stub: dispatch to MIDL procedure (if callback returned OK)
    Stub-&amp;gt;&amp;gt;Worker: result returned through NDR encoder
    Worker-&amp;gt;&amp;gt;Kernel: NtAlpcSendWaitReceivePort (REPLY)
    Kernel-&amp;gt;&amp;gt;Client: deliver reply
&lt;p&gt;The kernel&apos;s job ends at &quot;deliver the message to a worker thread.&quot; Everything after that is application code. The RPC runtime is a DLL that the service loads into its own address space, and the runtime&apos;s notion of authorization is whatever the callback returns. If the callback returns &lt;code&gt;RPC_S_OK&lt;/code&gt;, the call proceeds. If the callback is NULL, the call proceeds without ever asking the application. The kernel has no notion of &quot;this call requires &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt;&quot; or &quot;this call requires the caller to be in the local Administrators group&quot;, because those notions are policy choices the application makes, not properties of the IPC primitive.&lt;/p&gt;

The RPC service-discovery primitive at the well-known ALPC port `\RPC Control\epmapper`. An LRPC client that knows the interface UUID it wants to call -- but not which endpoint name a particular service is listening on -- calls into the endpoint mapper, hands over the UUID, and gets back the endpoint name. The mapper is itself an LRPC service; it bootstraps the rest. `rpcss` (the DCOM activator service) hosts the endpoint mapper on every Windows install.

The Microsoft dialect of OSF DCE IDL used to declare RPC interfaces. An `.idl` file pins down the interface UUID, version, methods, and parameter types; the MIDL compiler produces three artifacts: a header for both client and server, a client-side stub that marshals call arguments into NDR, and a server-side stub that unmarshals NDR back into call arguments and dispatches to the application&apos;s implementation.
&lt;p&gt;The interface-registration flag inventory tells the same story from a different angle. Microsoft Learn enumerates the flags on a single reference page [@msdocs-ifflags]; the four that matter for this section are quoted verbatim from that page.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;What Microsoft says it does&lt;/th&gt;
&lt;th&gt;What it closes&lt;/th&gt;
&lt;th&gt;What it leaves open&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&quot;the RPC runtime invokes the registered security callback for all calls, regardless of identity, protocol sequence, or authentication level of the client&quot;&lt;/td&gt;
&lt;td&gt;Forces the callback to run even for unauthenticated calls&lt;/td&gt;
&lt;td&gt;The correctness of the callback&apos;s return value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RPC_IF_ALLOW_SECURE_ONLY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rejects callers that did not authenticate at the runtime&apos;s minimum authentication level&lt;/td&gt;
&lt;td&gt;Unauthenticated callers&lt;/td&gt;
&lt;td&gt;Authenticated-but-unauthorized callers; Microsoft notes verbatim that &quot;Using the RPC_IF_ALLOW_SECURE_ONLY flag does not imply or guarantee a high level of privilege on the part of the calling user&quot; [@msdocs-ifflags]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RPC_IF_SEC_NO_CACHE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&quot;Disables security callback caching, forcing a security callback for each RPC call on a given interface&quot;&lt;/td&gt;
&lt;td&gt;Stale cached approval after a token-state change&lt;/td&gt;
&lt;td&gt;The correctness of the callback&apos;s body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RPC_IF_ALLOW_LOCAL_ONLY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rejects remote callers at the runtime layer&lt;/td&gt;
&lt;td&gt;Cross-machine reachability&lt;/td&gt;
&lt;td&gt;Local elevation primitives&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The table is the argument. Every flag closes a specific known-bad pattern. No flag changes the fact that the per-interface authorization decision is application code. The runtime can be configured to &lt;em&gt;force the callback to run&lt;/em&gt;. It cannot be configured to &lt;em&gt;make the callback return the right answer&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Port-level security is kernel infrastructure. Interface-level security is application code. The kernel can enforce the first; it cannot enforce the second. Everything in the rest of this article follows from that asymmetry.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Microsoft Learn&apos;s verbatim note on &lt;code&gt;IfCallbackFn&lt;/code&gt; reads: &lt;em&gt;&quot;Security-callback function, or NULL for no callback. Each registered interface can have a different callback function.&quot;&lt;/em&gt; [@msdocs-rpcregisterif2] A NULL callback means &quot;anyone who can open the connection port can call any procedure on this interface.&quot; Many in-house services interpret the parameter as if NULL meant &quot;default deny.&quot; It does not. NULL is a default &lt;em&gt;allow&lt;/em&gt;, gated only by the port DACL. The CVE-2018-8440 SchRpcSetSecurity disclosure [@cert-vu906424] [@0patch-micropatch] is the canonical example of what that interpretation costs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;RpcServerRegisterIf3&lt;/code&gt;, introduced in Windows 8 [@msdocs-rpcregisterif3], partially mitigates the structural concern by adding a per-interface security descriptor argument the runtime checks before the callback runs. Microsoft Learn documents the order: &lt;em&gt;&quot;If both SecurityDescriptor and IfCallbackFn are specified, the security descriptor in SecurityDescriptor will be checked first and the callback in IfCallbackFn will be called after the access check against the security descriptor passes.&quot;&lt;/em&gt; The &lt;code&gt;If3&lt;/code&gt; API also bakes in an &lt;a href=&quot;https://paragmali.com/blog/appcontainer-and-lowbox-tokens-windowss-capability-sandbox/&quot; rel=&quot;noopener&quot;&gt;AppContainer&lt;/a&gt; default-deny: in the absence of an explicit security descriptor, the runtime refuses calls from AppContainer processes. These are real defences. They do not change the underlying property that the per-call authorization decision -- the one that says &quot;this caller is allowed to invoke this procedure with these arguments&quot; -- is delegated to an application function the kernel cannot inspect.&lt;/p&gt;
&lt;p&gt;The kernel-vs-application boundary inside &lt;code&gt;rpcrt4.dll&lt;/code&gt; is unusual and easy to miss. The same DLL contains both the user-mode side of the kernel ALPC syscall surface (the thin wrappers around &lt;code&gt;NtAlpcSendWaitReceivePort&lt;/code&gt; that the runtime threads call) and the interface dispatch loop that ends in the application callback. Both halves run inside the service process; both halves are user-mode code from the kernel&apos;s point of view. The kernel does not know which RPC interface a given ALPC message is going to dispatch to. It just hands the message to a worker thread and forgets.&lt;/p&gt;
&lt;p&gt;The endpoint-mapper bootstrap path is the other piece of the LRPC overlay worth naming. A client that knows the interface UUID it wants to talk to -- say, the AppInfo interface UUID for UAC -- but does not know which endpoint name &lt;code&gt;appinfo&lt;/code&gt; happens to be listening on, opens the well-known ALPC port &lt;code&gt;\RPC Control\epmapper&lt;/code&gt;, sends a query containing the UUID, and gets back the endpoint name. The endpoint mapper is itself an LRPC service running inside &lt;code&gt;rpcss&lt;/code&gt;. It bootstraps the rest of the local-IPC fabric.&lt;/p&gt;
&lt;p&gt;NDR and NDR64 are the wire format. &lt;code&gt;NdrClientCall3&lt;/code&gt; on the client side packs the call arguments into the NDR representation Microsoft documents on Learn [@msdocs-ndr64]; the bytes ride inside an ALPC &lt;code&gt;PORT_MESSAGE&lt;/code&gt; body to the server; &lt;code&gt;NdrStubCall3&lt;/code&gt; on the server side unpacks them. The same NDR format that travels over a TCP socket for cross-machine MSRPC travels through an ALPC port for local LRPC. The transport is the only thing that differs.&lt;/p&gt;

The intuitive question -- &quot;if the callback is the problem, why doesn&apos;t the kernel just check it?&quot; -- bumps into two impossibility results. First, the callback is a function pointer into application code. The kernel cannot symbolically execute the function to determine whether its return value is correct; that is a halting-problem-shaped task in the general case. Second, even if the kernel could execute the function, the kernel does not know what &quot;correct&quot; means for an arbitrary application&apos;s authorization policy. &quot;Correct&quot; is the application&apos;s specification of who should be allowed to call what, and the application is the only party that has that specification. Closing the gap requires either a new ABI in which the application declares its authorization policy in a language the OS can validate, or a runtime sandbox that confines what the callback can do. Neither has been proposed as a stable Microsoft direction in any public artefact.
&lt;p&gt;The structural punchline is that the RPC runtime is application code -- the callback runs in user mode in the server&apos;s address space, the runtime trusts whatever the callback returns, and the OS cannot validate the callback&apos;s body. The CVE-2019-1162 MSCTF disclosure [@ormandy-ctf-2019] and the local-COM-over-LRPC PPL-bypass class [@forshaw-com-ppl-2018] are &lt;em&gt;both&lt;/em&gt; structural instances of this asymmetry; no kernel change could have prevented them.&lt;/p&gt;
&lt;p&gt;That asymmetry is the engine. Almost every CVE on the Patch-Tuesday treadmill since 2018 -- the Task Scheduler ACL bug, the CTF subsystem disclosure, the PPL-COM bypasses, the Potato-family activations -- is structurally the same shape. Some are LRPC bugs. Some are not. The next section explains which is which.&lt;/p&gt;
&lt;h2&gt;8. Competing Approaches -- Named Pipes, COM, Filter Ports, and the Potato Disambiguation&lt;/h2&gt;
&lt;p&gt;Roughly half the time a defender reads &quot;Potato&quot; in a CVE writeup, the underlying primitive is not ALPC. The other half of the time, it is. Knowing which is which is the single most-cited reason defenders mis-classify privilege-escalation attacks. The disambiguation matters because the mitigations differ: an LRPC-on-ALPC Potato is closed (or worsened) by RPC interface-flag changes; a named-pipe Potato is closed (or worsened) by &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; policy.&lt;/p&gt;
&lt;p&gt;Before the Potato classification, four local-IPC primitives sit alongside LRPC-on-ALPC and deserve a brief tour.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Named pipes&lt;/strong&gt; [@msdocs-protseq] [@msdocs-impnp] [@csandker-np] are the first-class alternative that works both locally &lt;em&gt;and&lt;/em&gt; across machines over SMB. The Windows RPC runtime supports a &lt;code&gt;ncacn_np&lt;/code&gt; (Network Computing Architecture, Connection-oriented, Named Pipe) protocol sequence that lets an RPC interface be reached either through &lt;code&gt;\\.\pipe\name&lt;/code&gt; locally or through an SMB tree-connect remotely. The load-bearing security primitive for the named-pipe-Potato class is &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; [@msdocs-impnp], a Win32 API that lets the server end of a named pipe impersonate the client process; the API requires the caller to hold &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt;. The privilege is granted by default to LocalSystem, LocalService, NetworkService, and to processes that hold the privilege in their token through policy. The named-pipe-Potato attack pattern is &quot;a service running with &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; is tricked into connecting to a named pipe the attacker controls, and the attacker calls &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; to inherit the service&apos;s token.&quot;&lt;/p&gt;

The Windows user-right that permits a thread to impersonate another security principal -- specifically by calling APIs such as `ImpersonateNamedPipeClient` [@msdocs-impnp] or `ImpersonateLoggedOnUser`. The privilege is granted by default to `LocalSystem`, `NetworkService`, `LocalService`, and processes started by the Service Control Manager. As Clement Labro summarised the practical implication: *&quot;if you have SeAssignPrimaryToken or SeImpersonate privilege, you are SYSTEM&quot;* [@itm4n-printspoofer], because every interactive way to use either privilege ends in a SYSTEM token under the right circumstances. The named-pipe-Potato family exploits exactly this fact.

The DCOM lookup primitive that translates an object exporter identifier (OXID) to a string binding (a protocol sequence plus an endpoint) where the corresponding COM server is listening. By default the OXID resolver runs in `rpcss` on TCP port 135. RoguePotato [@roguepotato-blog] [@roguepotato-repo] -- the post-Windows-10-1809 evolution of the Potato family -- redirects an outbound OXID-resolver query to an attacker-controlled host, which lets the attacker substitute an arbitrary endpoint and, through that, an arbitrary impersonation token.
&lt;p&gt;&lt;strong&gt;Shared sections plus events&lt;/strong&gt; is the lowest-level local-IPC pattern. Two processes call &lt;code&gt;NtCreateSection&lt;/code&gt; to back the same shared memory, then synchronise with kernel events or semaphores. There is no framing, no caller-identity primitive, and no message boundary. The pattern is used in performance-sensitive contexts such as browser sandboxes and DirectX swapchain handoff; it is not a competitor with LRPC-on-ALPC for general request-reply use cases.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;COM local activation&lt;/strong&gt; [@forshaw-com-ppl-2018] [@roguepotato-blog] is not a competitor. It is a higher-level overlay. The DCOM activation service (&lt;code&gt;rpcss&lt;/code&gt;) takes a CoCreateInstance-style activation request and, for local activations, marshals into LRPC under the hood. This is why DCOM-activation attacks are &lt;em&gt;also&lt;/em&gt; LRPC attacks: the trigger transport is DCOM, but the impersonation primitive ends up being the LRPC &lt;code&gt;RpcImpersonateClient&lt;/code&gt; machinery that runs inside the activated server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Filter Communication Ports&lt;/strong&gt; [@msdocs-minifilter-replacement] [@msdocs-fltsendmessage] are the minifilter-specific IPC channel for talking between a kernel-mode file-system filter driver and a user-mode service. A minifilter calls &lt;code&gt;FltCreateCommunicationPort&lt;/code&gt; to set up the server side; a user-mode application calls &lt;code&gt;FilterConnectCommunicationPort&lt;/code&gt; to attach to it; the kernel-side &lt;code&gt;FltSendMessage&lt;/code&gt; and the user-side &lt;code&gt;FilterReplyMessage&lt;/code&gt; carry payloads in either direction. Filter Communication Ports are a separate primitive from ALPC and live in their own namespace; the only reason to mention them in this section is that defenders sometimes conflate &quot;any named local IPC endpoint&quot; with ALPC, and they should not.&lt;/p&gt;
&lt;p&gt;Now the Potato disambiguation. The &lt;a href=&quot;https://paragmali.com/blog/windows-access-control-25-years-of-attacks/&quot; rel=&quot;noopener&quot;&gt;Potato family&lt;/a&gt; is the loudest local-EoP cluster of the last decade, and the family contains two structurally different sub-families that share the surname for historical reasons.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;DCOM-activation Potato&lt;/th&gt;
&lt;th&gt;Named-pipe Potato&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Triggering protocol&lt;/td&gt;
&lt;td&gt;DCOM &lt;code&gt;CoGetInstanceFromIStorage&lt;/code&gt; activation against &lt;code&gt;127.0.0.1&lt;/code&gt; plus the local OXID resolver&lt;/td&gt;
&lt;td&gt;Service connects out to a named pipe controlled by the attacker (often via UNC or by tricking a print or EFS hook)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impersonation primitive&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RpcImpersonateClient&lt;/code&gt; invoked by the activated COM server during the LRPC dispatch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; invoked by the attacker on the receiving end of the pipe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required attacker privilege&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; or &lt;code&gt;SeAssignPrimaryTokenPrivilege&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; plus the ability to direct the service to connect to the attacker&apos;s pipe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canonical exemplars&lt;/td&gt;
&lt;td&gt;RoguePotato (May 2020) [@roguepotato-blog] [@roguepotato-repo], JuicyPotato, RottenPotato&lt;/td&gt;
&lt;td&gt;PrintSpoofer (2020) [@itm4n-printspoofer], EfsPotato, PetitPotam&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-KB5004442 status&lt;/td&gt;
&lt;td&gt;OXID redirection to remote hosts blocked by &lt;code&gt;RPC_C_AUTHN_LEVEL_PKT_INTEGRITY&lt;/code&gt; enforcement, March 2023 [@mssupport-kb5004442]&lt;/td&gt;
&lt;td&gt;Unchanged at the OS level; mitigation is &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; hygiene&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Underlying IPC fabric&lt;/td&gt;
&lt;td&gt;LRPC on ALPC&lt;/td&gt;
&lt;td&gt;Named pipes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The HITB Amsterdam 2021 talk &lt;em&gt;The Rise of Potatoes: Privilege Escalation in Windows Services&lt;/em&gt; by Andrea Pierini and Antonio Cocomazzi [@hitb-potatoes] is the canonical end-to-end family classification. Pierini and Cocomazzi are also the disclosers of RoguePotato [@roguepotato-blog] -- the variant that broke the post-Windows-10-1809 mitigation by redirecting the OXID resolver to an attacker-controlled host on a port other than 135. The disclosure was May 11, 2020, building on their December 6, 2019 &quot;RogueWinRM&quot; precursor work [@roguewinrm-blog] in which they obtained a SYSTEM identification token but not yet a usable impersonation token.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Does the writeup say &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; or &lt;code&gt;RpcImpersonateClient&lt;/code&gt;? The first is a named-pipe primitive. The second is an LRPC-on-ALPC primitive. The trigger transport may be shared (DCOM activation, RPRN, EFSR), but the impersonation primitive is what tells you which IPC surface the attack actually exercises -- and which mitigation closes it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The KB5004442 DCOM hardening rollout [@mssupport-kb5004442], which addresses CVE-2021-26414, completed phase 3 on March 14, 2023. Phase 3 enabled the hardening with no override path: DCOM activations are subject to &lt;code&gt;RPC_C_AUTHN_LEVEL_PKT_INTEGRITY&lt;/code&gt; as a mandatory minimum, and the previously available registry overrides were removed. The OS-default configuration since March 2023 closes the JuicyPotato variant that depended on outbound DCOM to TCP/135 with downgraded authentication. RoguePotato and its descendants survived the rollout because they did not depend on the downgrade -- they depend on the OXID redirect itself, which the hardening did not block at the OS-default configuration.&lt;/p&gt;

Two adjacent kernel-IPC primitives deserve a footnote. The Windows Notification Facility (WNF) is a kernel-mode publish-subscribe channel for one-way state notifications [@tob-wnf]; processes register interest in named &quot;state names&quot; and the kernel delivers updates. Event Tracing for Windows (ETW) is the kernel&apos;s one-way event-streaming substrate [@tob-etw]; providers emit structured events, controllers configure sessions, and consumers read the events back. Yarden Shafir&apos;s Trail of Bits posts on both are the canonical practitioner references for the architectural-cousin framing. Neither WNF nor ETW competes with LRPC for the request-reply use case, because neither is request-reply. They are family of ALPC -- kernel-mediated message buses -- but they solve different problems.
&lt;p&gt;The comparison matrix gives us the surface area of competing primitives. The next section asks: given this surface area, what can the OS structurally not guarantee?&lt;/p&gt;
&lt;h2&gt;9. The Limits -- Three Things ALPC and LRPC Structurally Cannot Enforce&lt;/h2&gt;
&lt;p&gt;The Vista redesign closed half the structural problem of LPC. It left three other things permanently open, and no future ALPC version can close them without a new ABI. Each of the three is a property of the trust model, not a bug in any specific server. Each has a CVE-history footprint that confirms the structural framing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The interface-callback gate cannot be enforced by the OS.&lt;/strong&gt; The &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; contract [@msdocs-rpcregisterif2] accepts a function pointer into the application&apos;s address space; the runtime trusts whatever the callback returns. The OS-side enforcement available without an ABI change is at most &quot;invoke the callback&quot; (which &lt;code&gt;RPC_IF_SEC_NO_CACHE&lt;/code&gt; [@msdocs-ifflags] already enforces on every call). The OS cannot read the callback&apos;s source, cannot infer its policy, and cannot decide whether the callback&apos;s verdict matches what the application&apos;s specification says it should be. Every interface-callback EoP -- CVE-2019-1162 MSCTF [@ormandy-ctf-2019], the PPL-COM class [@forshaw-com-ppl-2018], CVE-2018-8440 [@nvd-cve-2018-8440] -- is a structural instance of this bound. Closing it requires either inventing a declarative authorization ABI the OS can validate, or sandboxing callback execution. Neither has been proposed as a stable Microsoft direction in any public artefact through 2026.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;There is no transitive caller identity.&lt;/strong&gt; ALPC&apos;s Security message attribute captures the caller&apos;s token at handshake or on demand; it does not carry a chain of trust across multiple hops. A proxy server in the middle of a call chain has to impersonate explicitly or marshal identity in band, and the receiving party at the far end has no kernel primitive that tells it &quot;the message came from caller A, was forwarded by proxy B, and the original token is still attached.&quot; Confused-deputy attacks in the LRPC fabric are not bugs; they are an inherent property of the trust model. The DCOM-activation Potato class [@roguepotato-blog] [@roguepotato-repo] exploits exactly this property: the DCOM activator passes a token into a privileged COM server, and the server cannot reliably tell whether the token chain on the way in matches what the activator&apos;s specification said it should be.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The kernel routing path is in the trusted computing base.&lt;/strong&gt; The ALPC dispatcher runs in Ring 0. Any bug in &lt;code&gt;_ALPC_PORT&lt;/code&gt; object lifecycle, in &lt;code&gt;_ALPC_HANDLE_DATA&lt;/code&gt; reference counting, in message-attribute marshalling, or in any of the dozens of structures Geoff Chappell&apos;s site [@chappell-alpc] [@chappell-alpcp] documents but Microsoft does not, is a direct kernel-elevation primitive. The CVE history demonstrates the assumption is wishful: CVE-2018-8440 [@nvd-cve-2018-8440] has a kernel reference-counting flavour in addition to the well-known interface-callback flavour, and several of the Patch-Tuesday ALPC EoP advisories of 2020-2024 carry NVD descriptions that say &quot;improperly handles calls to Advanced Local Procedure Call (ALPC)&quot; with no further detail because the underlying bug is a kernel bookkeeping issue Microsoft does not enumerate. The kernel routing path is settled engineering by any reasonable standard, but settled engineering is not zero-bug engineering. A new ALPC CVE in any given Patch Tuesday is consistent with the structural model.&lt;/p&gt;

flowchart TD
    A[The interface-callback gate -- the OS cannot validate the callback body] --&amp;gt; D[Patch-Tuesday treadmill -- interface callback CVEs, integrity-level CVEs, kernel ALPC CVEs]
    B[No transitive caller identity -- ALPC has no chain-of-trust primitive across hops] --&amp;gt; D
    C[The kernel routing path is in the TCB -- any _ALPC_PORT or attribute bug is a direct kernel EoP] --&amp;gt; D
&lt;p&gt;There is a fourth observation that is not an impossibility result but is worth stating in the same breath: &lt;strong&gt;the practical upper bound on local authentication strength&lt;/strong&gt;. &lt;code&gt;RPC_C_AUTHN_LEVEL_PKT_INTEGRITY&lt;/code&gt; is the practical ceiling for local LRPC; the &lt;code&gt;ncalrpc&lt;/code&gt; transport supports only &lt;code&gt;RPC_C_AUTHN_WINNT&lt;/code&gt; authentication [@msdocs-protseq], and the strongest integrity check the runtime offers under that authentication service is packet integrity. The KB5004442 DCOM rollout [@mssupport-kb5004442] raised the &lt;em&gt;minimum&lt;/em&gt; for DCOM activations to &lt;code&gt;PKT_INTEGRITY&lt;/code&gt; in March 2023; it did not change the &lt;em&gt;ceiling&lt;/em&gt;. The gap between upper and lower bounds is substantial and structural: raising mandatory authentication closes the unauthenticated vector and leaves the authenticated-but-unauthorized vector -- the interface-callback class -- wide open.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The OS can require that the callback runs. It cannot require that the callback returns the right answer. The Patch-Tuesday treadmill is the consequence.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; CVE-2017-11783, CVE-2018-8440, and CVE-2019-1162 were the canonical exemplars of the interface-callback class. They were not unlucky outliers from an otherwise sound engineering effort. They are instances of a class the design of &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; cannot exclude. Almost every subsequent year of Patch Tuesdays has shipped further instances of the same class, and 2026&apos;s count is on track to be no smaller than 2018&apos;s.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Closing the interface-callback gap would look like one of two architectural shifts. Either Microsoft would introduce a declarative authorization language for RPC interfaces -- a manifest the application ships alongside the IDL that the runtime can parse and the OS can validate -- and then forbid the imperative callback. Or the runtime would execute the callback inside a sandbox that constrains what the callback can do (no arbitrary memory reads of the service&apos;s address space, no ability to issue privileged syscalls, no ability to side-channel through global state). Neither is on a publicly-named Microsoft roadmap; the closest public artefact is Forshaw&apos;s ongoing tooling work on parsing the interface inventory [@forshaw-saatools] [@forshaw-rpc-2019] [@forshaw-poc2023], which equips defenders to audit the callbacks they have rather than to replace the model.&lt;/p&gt;
&lt;p&gt;The limits are honest. They are also not the whole story. Research has not stopped trying to close the gap, and the next section names what is still active.&lt;/p&gt;
&lt;p&gt;The Patch-Tuesday treadmill is the &lt;em&gt;expected&lt;/em&gt; steady state, not a transitional embarrassment. Closing the class requires reworking the contract -- a different ABI, or a sandboxed execution model -- and no public Microsoft roadmap commits to either.&lt;/p&gt;
&lt;h2&gt;10. Open Problems and a Practical Field Guide (2024-2026)&lt;/h2&gt;
&lt;p&gt;The 2024-2026 conference cycle is still arguing about how to make the interface-callback class scalable to defend. This section enumerates the open problems and then closes with the practical workflow a defender or an in-house RPC author can run today. The practical recipe is in part an answer to the open problems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 1: public RPC fuzzing at Microsoft-internal scale.&lt;/strong&gt; The public ceiling is RPCForge [@rpcforge] for NDR-aware fuzzing, Forshaw&apos;s &lt;code&gt;NtObjectManager&lt;/code&gt; for interface inventory and client generation [@forshaw-saatools] [@forshaw-rpc-2019], and the November 2023 PoC talk &lt;em&gt;Building More Windows RPC Tooling for Security Research&lt;/em&gt; [@forshaw-poc2023] for the latest research-tooling continuation. Microsoft&apos;s internal pipeline is not public; whether a coverage-guided NDR64 fuzzer can become a small-team repeatable Patch-Tuesday tool is open.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 2: auditing the interface-registration model for structural permissiveness.&lt;/strong&gt; A defender using &lt;code&gt;Get-RpcServer&lt;/code&gt; can enumerate every LRPC interface on a Windows install and dump each interface&apos;s procedures and security descriptor. The defender cannot tell, without per-interface manual review, whether a registered callback is correct. Heuristic detection of NULL &lt;code&gt;IfCallbackFn&lt;/code&gt; is mechanical; detection of &lt;em&gt;semantically&lt;/em&gt; permissive callbacks -- callbacks whose body trusts a field the caller controls -- is open and probably AI-shaped.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 3: &lt;code&gt;RPC_IF_SEC_NO_CACHE&lt;/code&gt; adoption and cost.&lt;/strong&gt; No public catalogue of which Microsoft services use the flag exists. No per-call cost benchmark is published. Defender heuristics that recommend the flag for high-risk interfaces cannot quantify the performance trade-off they are recommending.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 4: the local-COM-over-LRPC bypass class.&lt;/strong&gt; Forshaw&apos;s 2018 PPL-COM post [@forshaw-com-ppl-2018] articulated a class of attack against Protected Process Light that continues to surface in CVE reports. The structural class is unaddressed at the OS level.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 5: ALPC as covert channel.&lt;/strong&gt; The CVE-2019-1162 MSCTF fix [@ormandy-ctf-2019] narrowed the MSCTF subsystem&apos;s exposure. The general class of &quot;shared system ALPC ports that ignore caller integrity&quot; is structural; identifying others requires the kind of systematic audit Open Problem 2 names.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 6: defender SOC integration of the &lt;code&gt;Microsoft-Windows-Kernel-ALPC&lt;/code&gt; &lt;a href=&quot;https://paragmali.com/blog/etw-how-windows-2000s-performance-hack-became-the-edr-substr/&quot; rel=&quot;noopener&quot;&gt;ETW provider&lt;/a&gt;&lt;/strong&gt; [@msdocs-etwsys]. The provider is high-volume; production SOC pipelines rarely subscribe to it because the event rate overwhelms commodity collection. Per-call ALPC visibility today is concentrated inside &lt;a href=&quot;https://paragmali.com/blog/from-cmdexe-to-a-kusto-row-in-90-seconds-how-sysmon-and-defe/&quot; rel=&quot;noopener&quot;&gt;EDR vendors&lt;/a&gt; that gate it behind antimalware-PPL processes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open problem 7: AppContainer-aware RPC capability checking.&lt;/strong&gt; &lt;code&gt;RpcServerRegisterIf3&lt;/code&gt; [@msdocs-rpcregisterif3] introduces an AppContainer default-deny, but there is no standard pattern for in-house service authors who want to express &quot;this procedure requires capability X.&quot; Service authors roll their own; some get it right.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Author / Org&lt;/th&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NtObjectManager&lt;/code&gt; / &lt;code&gt;NtCoreLib&lt;/code&gt; (formerly &lt;code&gt;NtApiDotNet&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;LRPC interface enumeration, decompilation, and client generation from PowerShell or .NET&lt;/td&gt;
&lt;td&gt;James Forshaw, Project Zero&lt;/td&gt;
&lt;td&gt;[@forshaw-saatools] [@forshaw-rpc-2019]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RpcView&lt;/td&gt;
&lt;td&gt;Qt5/C++ GUI for browsing RPC servers and decompiled interface metadata across Windows versions&lt;/td&gt;
&lt;td&gt;silverf0x&lt;/td&gt;
&lt;td&gt;[@rpcview-repo]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RPC Investigator&lt;/td&gt;
&lt;td&gt;.NET Forms UI built on &lt;code&gt;NtApiDotNet&lt;/code&gt; for enumeration, client workbench, and an &quot;RPC Sniffer&quot; ETW-backed live view&lt;/td&gt;
&lt;td&gt;Trail of Bits, January 2023&lt;/td&gt;
&lt;td&gt;[@tob-rpcinv-blog] [@rpcinv-repo]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RPCMon&lt;/td&gt;
&lt;td&gt;ETW-based GUI for scanning RPC communication, built like Sysinternals Procmon, depending on Forshaw&apos;s library&lt;/td&gt;
&lt;td&gt;CyberArk Labs&lt;/td&gt;
&lt;td&gt;[@rpcmon-repo]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RPCForge&lt;/td&gt;
&lt;td&gt;NDR-aware local Python fuzzer for ALPC-exposed RPC interfaces&lt;/td&gt;
&lt;td&gt;Clement Rouault and Thomas Imbert, Sogeti ESEC&lt;/td&gt;
&lt;td&gt;[@rpcforge]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forshaw NDR64 / RPC research pipeline (2023)&lt;/td&gt;
&lt;td&gt;Continued research tooling and conference materials&lt;/td&gt;
&lt;td&gt;James Forshaw&lt;/td&gt;
&lt;td&gt;[@forshaw-poc2023]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;The practical field guide.&lt;/strong&gt; Eight numbered actions for the defender or in-house RPC service author. Each cites a verified source the reader can re-read in full.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; 1. &lt;strong&gt;Enumerate registered LRPC interfaces&lt;/strong&gt; with &lt;code&gt;Install-Module NtObjectManager; Get-RpcServer ... | Where-Object { $_.Endpoints.ProtocolSequence -eq &apos;ncalrpc&apos; }&lt;/code&gt; [@forshaw-saatools] [@forshaw-rpc-2019]. Snapshot before and after Patch Tuesday and diff on (UUID, procedure list, security descriptor). 2. &lt;strong&gt;Enumerate live ALPC server ports&lt;/strong&gt; with &lt;code&gt;Get-NtAlpcServer&lt;/code&gt;. The cmdlet returns the named connection ports; the unnamed per-connection ports are not enumerable by design (see Section 4) [@forshaw-saatools]. 3. &lt;strong&gt;Reach a local RPC server from PowerShell&lt;/strong&gt; with Forshaw&apos;s &lt;code&gt;New-RpcClient&lt;/code&gt; cmdlet, which generates a &lt;code&gt;[NtCoreLib.Win32.Rpc.Client]&lt;/code&gt;-derived class from the parsed server metadata [@forshaw-rpc-2019]. This is the primitive that lets a Patch-Tuesday differential become an actual interaction. 4. &lt;strong&gt;Audit your own RPC service&lt;/strong&gt; for the canonical mistake: any &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt; or &lt;code&gt;RpcServerRegisterIf3&lt;/code&gt; call with a NULL &lt;code&gt;IfCallbackFn&lt;/code&gt; argument is &quot;anyone who can open the port can call any procedure on the interface&quot; [@msdocs-rpcregisterif2] [@msdocs-rpcregisterif3]. Treat NULL callbacks as a finding, not a default. 5. &lt;strong&gt;Harden an exposed LRPC interface&lt;/strong&gt; with the flag combination &lt;code&gt;RPC_IF_ALLOW_SECURE_ONLY | RPC_IF_SEC_NO_CACHE&lt;/code&gt; plus an explicit callback that validates &lt;code&gt;I_RpcBindingInqLocalClientPID&lt;/code&gt; and the caller&apos;s token integrity level [@msdocs-ifflags]. The Microsoft Learn note that &quot;Using the RPC_IF_ALLOW_SECURE_ONLY flag does not imply or guarantee a high level of privilege on the part of the calling user&quot; [@msdocs-ifflags] makes the explicit callback non-optional. 6. &lt;strong&gt;For DCOM-activated services&lt;/strong&gt;, accept the KB5004442 default (&lt;code&gt;RPC_C_AUTHN_LEVEL_PKT_INTEGRITY&lt;/code&gt; minimum) and do not invoke registry overrides. The override path was removed in the March 14, 2023 phase 3 rollout [@mssupport-kb5004442]. 7. &lt;strong&gt;For runtime visibility&lt;/strong&gt;, enable the Microsoft-Windows-RPC ETW provider via RPCMon [@rpcmon-repo] or RPC Investigator&apos;s RPC Sniffer [@tob-rpcinv-blog] [@rpcinv-repo]; correlate per-process per-procedure call rates against the service inventory from step 1. 8. &lt;strong&gt;For per-message kernel-level visibility&lt;/strong&gt;, enable the Microsoft-Windows-Kernel-ALPC system provider from an &lt;code&gt;EVENT_TRACE_SYSTEM_LOGGER_MODE&lt;/code&gt; session [@msdocs-etwsys]. Budget for the documented high-volume warning; consider an EDR vendor that runs the provider already if you do not want to host the collection yourself.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;{`
// Real shell pipeline that produces the inputs:
//   Get-RpcServer | Export-Clixml -Path C:\\Snaps\\rpc-pre-patch.xml
//   
//   Get-RpcServer | Export-Clixml -Path C:\\Snaps\\rpc-post-patch.xml
//   Compare-Object (Import-Clixml C:\\Snaps\\rpc-pre-patch.xml) ...
// The diff logic below is what Compare-Object is doing under the hood, in plain JS.&lt;/p&gt;
&lt;p&gt;const pre = new Map([
  [&apos;201ef99a-7fa0-444c-9399-19ba84f12a1a&apos;, [&apos;Activate&apos;,&apos;Cancel&apos;,&apos;Continue&apos;,&apos;GetElevationType&apos;]],
  [&apos;86d35949-83c9-4044-b424-db363231fd0c&apos;, [&apos;SchRpcRegisterTask&apos;,&apos;SchRpcRetrieveTask&apos;,&apos;SchRpcSetSecurity&apos;]],
  [&apos;e1af8308-5d1f-11c9-91a4-08002b14a0fa&apos;, [&apos;ept_lookup&apos;,&apos;ept_map&apos;,&apos;ept_insert&apos;]],
]);&lt;/p&gt;
&lt;p&gt;const post = new Map([
  [&apos;201ef99a-7fa0-444c-9399-19ba84f12a1a&apos;, [&apos;Activate&apos;,&apos;Cancel&apos;,&apos;Continue&apos;,&apos;GetElevationType&apos;,&apos;RequestElevation2&apos;]],
  [&apos;86d35949-83c9-4044-b424-db363231fd0c&apos;, [&apos;SchRpcRegisterTask&apos;,&apos;SchRpcRetrieveTask&apos;,&apos;SchRpcSetSecurityV2&apos;]],
  [&apos;e1af8308-5d1f-11c9-91a4-08002b14a0fa&apos;, [&apos;ept_lookup&apos;,&apos;ept_map&apos;,&apos;ept_insert&apos;]],
]);&lt;/p&gt;
&lt;p&gt;const interfaces = new Set([...pre.keys(), ...post.keys()]);
for (const uuid of interfaces) {
  const a = new Set(pre.get(uuid) || []);
  const b = new Set(post.get(uuid) || []);
  const added   = [...b].filter(p =&amp;gt; !a.has(p));
  const removed = [...a].filter(p =&amp;gt; !b.has(p));
  if (added.length || removed.length) {
    console.log(`Interface ${uuid}`);
    if (added.length)   console.log(&apos;  + added:   &apos; + added.join(&apos;, &apos;));
    if (removed.length) console.log(&apos;  - removed: &apos; + removed.join(&apos;, &apos;));
  }
}
`}&lt;/p&gt;
&lt;p&gt;RPCMon ships a hard-coded RPC interface dictionary named &lt;code&gt;RPC_UUID_Map_Windows10_1909_18363.1977.rpcdb.json&lt;/code&gt; [@rpcmon-repo] -- a snapshot of Windows 10 1909 build 18363.1977 -- as the baseline against which it labels traced interfaces. The choice to bake in a build-specific baseline is evidence of how often the inventory needs refreshing: a defender running RPCMon on Windows 11 23H2 in 2026 is looking up call sites against a six-year-old dictionary. The accompanying tooling Forshaw built makes the regeneration mechanical in principle; the burden of &lt;em&gt;running&lt;/em&gt; the regeneration is what stays on the defender.&lt;/p&gt;

Install Forshaw&apos;s module and dump every local-only RPC interface on the current Windows install, one row per interface, sorted by procedure count:&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;Install-Module NtObjectManager -Scope CurrentUser
Get-RpcServer -DbgHelpPath &quot;$env:ProgramFiles\Debugging Tools for Windows\dbghelp.dll&quot; |
  Where-Object { $_.Endpoints.ProtocolSequence -eq &apos;ncalrpc&apos; } |
  Sort-Object { $_.Procedures.Count } -Descending |
  Select-Object Name, InterfaceId, @{N=&apos;Procs&apos;;E={$_.Procedures.Count}} |
  Format-Table -AutoSize
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expect dozens of named interfaces on a clean Windows 11 install. Save the output, install Patch Tuesday, run it again, and &lt;code&gt;Compare-Object&lt;/code&gt; the two snapshots. That diff is the canonical research workflow that the December 2019 Project Zero post [@forshaw-rpc-2019] introduced.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;

The single most effective change an in-house LRPC author can make tomorrow morning is to move from `RpcServerRegisterIf2` with `IfCallbackFn = NULL` to `RpcServerRegisterIf3` with both an explicit per-interface security descriptor and a callback that explicitly validates caller identity. The migration is mechanical -- the function signatures are upward-compatible -- and the runtime check the `If3` API adds gives the application a per-call enforcement gate that does not depend on the application&apos;s callback being correct. Pair it with `RPC_IF_SEC_NO_CACHE` if the callback inspects token state that can change during a session (group membership, integrity level, AppContainer SID).
&lt;p&gt;The practical recipe answers the everyday question: what do I do tomorrow morning? The misconceptions section answers a harder question: what should I stop believing?&lt;/p&gt;
&lt;h2&gt;11. FAQ -- Six Misconceptions, Removed&lt;/h2&gt;
&lt;p&gt;Half the operational confusion about ALPC and LRPC comes from premises that sound plausible and are wrong. This section names six of them. Each answer starts with the wrong answer, explicitly, before correcting it.&lt;/p&gt;

Wrong answer: yes. Right answer: every service that exposes an LRPC interface is. Services that expose only `ncacn_np` (named-pipe RPC) or `ncacn_ip_tcp` (TCP RPC) are not reachable over ALPC, even when the caller is on the same machine [@msdocs-protseq]. The print spooler, for example, exposes its primary interface over named pipes and is the trigger for several of the named-pipe-Potato attacks; AppInfo, Task Scheduler, and the endpoint mapper expose theirs over LRPC and are reachable through the kernel ALPC fabric. The right mental model is &quot;every Windows service that wants to be reachable locally with first-class kernel-mediated transport uses LRPC on ALPC&quot;, not &quot;every service uses ALPC.&quot;

Wrong answer: yes. Right answer: the DCOM-activation Potatoes (RoguePotato [@roguepotato-blog] [@roguepotato-repo], JuicyPotato, RottenPotato) exercise LRPC-on-ALPC because local DCOM activation rides that fabric; the impersonation primitive is `RpcImpersonateClient` inside the activated COM server. The named-pipe Potatoes (EfsPotato, PrintSpoofer [@itm4n-printspoofer], PetitPotam) use `ImpersonateNamedPipeClient` [@msdocs-impnp] as the impersonation primitive and exercise the named-pipe fabric. The trigger transport can be shared (DCOM, RPRN, EFSR), but the impersonation primitive is what tells you which IPC surface the attack actually exercises. See Section 8 for the 30-second classifier and the HITB 2021 Pierini and Cocomazzi talk [@hitb-potatoes] for the canonical end-to-end family classification.

Wrong answer: yes. Several secondary writeups (and the original input premise for this article) say so. Right answer: named connection ports have Object Manager names, typically under `\RPC Control` or per-session AppContainer subtrees. The per-connection communication ports created by `NtAlpcAcceptConnectPort` are unnamed and exist only as handles. This is the structural correction Section 4 walks in full and the load-bearing invariant the Vista redesign rests on: only the parties that completed the handshake can address the per-connection channel. The kernel does not let anyone else find it because there is no name to find.

Wrong answer: yes, it is in the SDK. Right answer: partially. Microsoft *does not* publish a Win32 or WDK API reference for the `Nt*Alpc*` and `Alpc*` surface; the de facto syscall reference is NtDoc [@ntdoc-ntalpc], and the de facto structure reference is Geoff Chappell&apos;s site [@chappell-alpc] [@chappell-alpcp]. Microsoft *does* document ALPC architecturally in *Windows Internals 7th Edition Part 2* [@wininternals-7e], Chapter 8 section &quot;Advanced local procedure call (ALPC)&quot;; through the `Microsoft-Windows-Kernel-ALPC` ETW provider [@msdocs-etwsys]; and indirectly through the user-mode RPC runtime documentation. The documentation gap is a deliberate choice -- Microsoft&apos;s position is that application authors should use the RPC runtime, not the kernel ALPC API -- and the gap is the reason the public knowledge of ALPC comes from a handful of named researchers reverse-engineering it.

Wrong answer: yes, the abbreviations collide so they must be related. Right answer: LPC was the original Windows NT 3.1-through-Server-2003 kernel IPC primitive, replaced by ALPC in Vista (November 2006) and removed from the kernel by Windows 7 [@csandker-alpc]. LRPC is the Microsoft RPC runtime&apos;s *transport* selected when `ncalrpc` is the protocol sequence [@msdocs-protseq]; it has always lived inside `rpcrt4.dll`, and it rides on top of kernel ALPC ports. The two entities are at different layers (kernel object vs user-mode transport) and were named a decade apart -- LRPC in 1994, ALPC in 2006. The abbreviation collision is real; the entities are not the same thing.

Wrong answer: on the Trail of Bits blog. Right answer: it does not exist under that title. The input premise for this article (and several AI-generated summaries circulating in 2024-2025) referenced a *Trail of Bits &quot;ALPC Internals&quot; series* by Shafir. The Trail of Bits author page for Yarden Shafir [@tob-shafir-author] lists her actual posts; the kernel-IPC posts are *Introducing Windows Notification Facility&apos;s WNF Code Integrity* (May 2023) [@tob-wnf] and *ETW Internals for Security Research and Forensics* (November 2023) [@tob-etw]. Her dedicated ALPC material lives in her conference training surface, indexed via the Winsider Seminars author page [@winsider-yarden]. The cousin posts (WNF and ETW) are the right Trail of Bits citations for the architectural-cousin framing.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Three sources are worth the rest of an afternoon. Christian Sandker&apos;s three-part &lt;em&gt;Offensive Windows IPC&lt;/em&gt; series [@csandker-alpc] [@csandker-rpc] [@csandker-np] is the highest-signal practitioner walkthrough of LPC, ALPC, LRPC, and named pipes available for free on the open web. &lt;em&gt;Windows Internals 7th Edition Part 2&lt;/em&gt; Chapter 8 section &lt;em&gt;Advanced local procedure call (ALPC)&lt;/em&gt; [@wininternals-7e] is the Microsoft-blessed architectural reference; cite by ISBN 978-0-13-546238-6. James Forshaw&apos;s December 17, 2019 Project Zero post &lt;em&gt;Calling Local Windows RPC Servers from .NET&lt;/em&gt; [@forshaw-rpc-2019] is the canonical introduction to the &lt;code&gt;NtObjectManager&lt;/code&gt; tooling and the methodology change it unlocked. For the sister-article context in this series: the Object Manager Namespace post explains the &lt;code&gt;\RPC Control&lt;/code&gt; parent that every named ALPC connection port lives under, and the upcoming Potato sister post walks the DCOM-activation and named-pipe sub-families through to a working PoC.&lt;/p&gt;
&lt;/blockquote&gt;

The kernel did its job at the port-DACL layer. The application disclaimed responsibility at the interface-callback layer. Almost every Patch-Tuesday LRPC fix since 2018 is some recombination of those two halves, and the half the kernel cannot fix is the half that keeps shipping.
&lt;p&gt;The named-researcher canon for ALPC -- Forshaw, Shafir, csandker, Cerrudo, Cocomazzi, Pierini, Rouault, Imbert, Ormandy, Chappell -- is what this article is an attempt to read in one place.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;alpc-and-lrpc-the-local-ipc-fabric-under-every-windows-service&quot; keyTerms={[
  { term: &quot;ALPC&quot;, definition: &quot;Advanced Local Procedure Call. The Vista-and-later kernel asynchronous message-and-attribute IPC primitive; replaces classic LPC. Microsoft does not publish a developer-facing reference for the kernel surface.&quot; },
  { term: &quot;LRPC&quot;, definition: &quot;The Microsoft RPC runtime&apos;s local-only transport, selected when the protocol sequence is &lt;code&gt;ncalrpc&lt;/code&gt;. Implemented in &lt;code&gt;rpcrt4.dll&lt;/code&gt;; rides on top of ALPC ports.&quot; },
  { term: &quot;LPC&quot;, definition: &quot;Local Procedure Call. The original NT 3.1 kernel IPC primitive, synchronous, three-port; replaced by ALPC in Vista and removed from the kernel by Windows 7.&quot; },
  { term: &quot;Connection port (ALPC)&quot;, definition: &quot;The named ALPC port a server creates so clients can find it. Lives in the Object Manager namespace, typically under &lt;code&gt;\\RPC Control&lt;/code&gt;.&quot; },
  { term: &quot;Communication port (ALPC)&quot;, definition: &quot;The unnamed per-connection ALPC port created by &lt;code&gt;NtAlpcAcceptConnectPort&lt;/code&gt;. Exists only as handles in the connecting and accepting processes; not reachable by name.&quot; },
  { term: &quot;Message attribute&quot;, definition: &quot;An optional in-message kernel service: Context, Handle, Security, or View. Each retires an awkward LPC pattern by moving the work into a single ALPC transaction.&quot; },
  { term: &quot;Interface security callback&quot;, definition: &quot;The application-supplied &lt;code&gt;IfCallbackFn&lt;/code&gt; passed to &lt;code&gt;RpcServerRegisterIf2&lt;/code&gt;/&lt;code&gt;RpcServerRegisterIf3&lt;/code&gt;. The kernel cannot inspect or constrain it. NULL is a legal value and means &apos;no callback&apos;.&quot; },
  { term: &quot;Endpoint mapper&quot;, definition: &quot;The well-known LRPC service at &lt;code&gt;\\RPC Control\\epmapper&lt;/code&gt; that translates an interface UUID into the endpoint name a service is listening on. Hosted by &lt;code&gt;rpcss&lt;/code&gt;.&quot; },
  { term: &quot;NDR / NDR64&quot;, definition: &quot;The (Network) Data Representation transfer syntax that MIDL-generated stubs use to marshal RPC arguments. Local LRPC and remote MSRPC use the same wire format.&quot; },
  { term: &quot;SeImpersonatePrivilege&quot;, definition: &quot;Windows user-right that permits a thread to impersonate another security principal via APIs such as &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt;. The privilege the named-pipe-Potato family abuses.&quot; }
]} questions={[
  { q: &quot;Why does the per-connection ALPC communication port have no Object Manager name?&quot;, a: &quot;So that no third party can address the channel. Only the parties that completed the handshake hold the paired handles; the kernel does not expose the unnamed port through any namespace operation. This is the half of Cerrudo&apos;s 2006 structural class the Vista redesign closed.&quot; },
  { q: &quot;Why can the OS not enforce the correctness of an interface security callback?&quot;, a: &quot;The callback is a function pointer into application code. The kernel cannot symbolically execute the function to determine whether its return value is correct, and even if it could, the kernel does not know what &apos;correct&apos; means for an arbitrary application&apos;s authorization policy. Closing the gap requires either a declarative authorization ABI or a sandbox; Microsoft has not publicly committed to either.&quot; },
  { q: &quot;What distinguishes a DCOM-activation Potato from a named-pipe Potato?&quot;, a: &quot;The impersonation primitive. DCOM-activation Potatoes (RoguePotato, JuicyPotato, RottenPotato) use &lt;code&gt;RpcImpersonateClient&lt;/code&gt; inside an LRPC-on-ALPC dispatch path. Named-pipe Potatoes (PrintSpoofer, EfsPotato, PetitPotam) use &lt;code&gt;ImpersonateNamedPipeClient&lt;/code&gt; on a named pipe. The trigger transport (DCOM, RPRN, EFSR) can be shared; the impersonation primitive is what determines which IPC surface the attack exercises.&quot; },
  { q: &quot;What changed in March 2023 for DCOM-activated services?&quot;, a: &quot;KB5004442 phase 3 enabled the DCOM hardening with no override path. &lt;code&gt;RPC_C_AUTHN_LEVEL_PKT_INTEGRITY&lt;/code&gt; is now a mandatory minimum for DCOM activations, and the previously available registry override is removed. The change closed the JuicyPotato variant at the OS-default configuration.&quot; },
  { q: &quot;Where can a defender see ALPC traffic at the per-message level?&quot;, a: &quot;From the &lt;code&gt;Microsoft-Windows-Kernel-ALPC&lt;/code&gt; system ETW provider, enabled in an &lt;code&gt;EVENT_TRACE_SYSTEM_LOGGER_MODE&lt;/code&gt; session. The provider is high-volume; production SOC pipelines rarely subscribe directly and instead rely on EDR vendors that gate the provider behind antimalware-PPL processes.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>windows-internals</category><category>alpc</category><category>lrpc</category><category>ipc</category><category>privilege-escalation</category><category>rpc</category><category>reverse-engineering</category><category>security</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>eBPF vs ETW: Two Generations of Kernel Observability</title><link>https://paragmali.com/blog/ebpf-vs-etw-two-generations-of-kernel-observability/</link><guid isPermaLink="true">https://paragmali.com/blog/ebpf-vs-etw-two-generations-of-kernel-observability/</guid><description>Why Windows ETW emits events and Linux eBPF computes them -- and what eBPF-for-Windows reveals about the convergence of two operating systems.</description><pubDate>Sat, 16 May 2026 00:00:00 GMT</pubDate><content:encoded>
**ETW (Windows 2000) is event emission only.** Per-CPU lock-free ring buffers, manifest-defined providers, kernel-mediated dispatch. Sessions filter by provider, keyword, and level; every enabled event is fully serialized and crosses the kernel/user boundary.&lt;p&gt;&lt;strong&gt;eBPF (Linux 2014) inverts the model.&lt;/strong&gt; The consumer ships verified bytecode into the kernel; programs filter and aggregate at the hook site before any data crosses the boundary. JIT-compiled, with hooks across kprobe, uprobe, tracepoint, XDP, TC, and LSM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The verifier is the trust boundary -- and the catch.&lt;/strong&gt; Rice&apos;s theorem says no in-kernel verifier can be simultaneously sound, complete, and decidable. Linux&apos;s verifier trades soundness in the corner cases (CVE-2023-2163 and three predecessors); PREVAIL (the verifier used by eBPF-for-Windows) trades completeness more heavily for stronger formal grounding.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;eBPF-for-Windows is the first cross-OS-portable kernel-observability primitive.&lt;/strong&gt; PREVAIL verifies in user mode, &lt;code&gt;bpf2c&lt;/code&gt; transliterates verified bytecode to C, MSVC compiles to a signed &lt;code&gt;.sys&lt;/code&gt; driver. Networking-subset hooks only as of 2026; full kprobe-equivalent coverage is the work in progress.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;1. The SOC Analyst Sees the Same Thing Twice&lt;/h2&gt;
&lt;p&gt;A Security Operations Center analyst opens two &lt;code&gt;Sysmon/Operational&lt;/code&gt; event channels side by side. One channel is streaming from a Red Hat Enterprise Linux host; the other is streaming from a Windows Server 2022 domain controller. The XML configuration is the same. The Event IDs are the same. A &lt;code&gt;ProcessCreate&lt;/code&gt; record from either host carries the same &lt;code&gt;Image&lt;/code&gt;, &lt;code&gt;CommandLine&lt;/code&gt;, &lt;code&gt;ParentImage&lt;/code&gt;, &lt;code&gt;IntegrityLevel&lt;/code&gt;, and &lt;code&gt;Hashes&lt;/code&gt; fields. Detection rules written against one channel match the other. To the analyst, the two operating systems are interchangeable.&lt;/p&gt;
&lt;p&gt;Underneath, they are not even close.&lt;/p&gt;
&lt;p&gt;On the Windows side, every event was emitted by a kernel provider -- &lt;code&gt;Microsoft-Windows-Sysmon&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt; -- before the Sysmon user-mode service ever ran its XML filter. The kernel produced a fully formatted event, dropped it into a per-CPU ring buffer, and let user space pick it up. Every enabled event made the kernel-to-user trip in full. The filter inside Sysmon&apos;s user-mode service is what kept the on-disk log small. The wire between the kernel and the consumer carried the full firehose.&lt;/p&gt;
&lt;p&gt;On the Linux side, no kernel module owned by Microsoft is running. The same Sysmon binary is attached to roughly twenty Linux kernel probes through the &lt;code&gt;SysinternalsEBPF&lt;/code&gt; library [@github-com-microsoft-sysmonforlinux]. Each probe is an eBPF program: bytecode that was compiled by clang, verified by the kernel before load, JIT-compiled to native instructions, and attached to a hook inside the kernel [@ebpf-io-is-ebpf]. When &lt;code&gt;execve&lt;/code&gt; fires, the verified program runs on the producing CPU, reads its arguments out of the kernel context, decides whether the call matches the XML configuration&apos;s predicates, and -- only then -- writes a record into a ring buffer. The events that arrive in user space were already filtered inside the kernel. The wire carries only what the configuration cares about.&lt;/p&gt;
&lt;p&gt;The output channels match because Sysmon for Linux is engineered to look exactly like Sysmon for Windows [@github-com-microsoft-sysmonforlinux]. The substrate underneath is engineered for two different decades. ETW is from 2000. eBPF is from 2014. The fourteen-year gap shows up not in features but in &lt;em&gt;how the kernel does its job&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; ETW emits. eBPF computes. That gap is the entire generation difference. Everything else in this article is a consequence of it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This article is about why those two designs exist, why the second one is strictly more powerful, why &quot;strictly more powerful&quot; cost the Linux kernel a new class of CVE, and what Microsoft&apos;s &lt;code&gt;microsoft/ebpf-for-windows&lt;/code&gt; [@github-com-for-windows] project -- now in its sixth year of development -- reveals about which design wins at the point of convergence. By the end you will know both substrates well enough to choose between them, understand their failure modes, and see why &quot;two generations&quot; is not marketing language but a literal description of the engineering arc.&lt;/p&gt;
&lt;h2&gt;2. A Tale of Two Lineages&lt;/h2&gt;
&lt;p&gt;In 1992, Van Jacobson and Steven McCanne at Lawrence Berkeley Laboratory wrote a small virtual machine for packet filtering [@tcpdump-org-bpf-usenix93pdf]. In 2000, a separate Microsoft team shipped a kernel event bus inside Windows 2000. Neither group knew the other existed. Each was solving a different version of the same problem: &lt;em&gt;how do you watch the kernel from user space without owning the kernel?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The two answers ran in parallel for twenty-two years before they collided.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1992 -- The BSD Packet Filter.&lt;/strong&gt; McCanne and Jacobson published &quot;The BSD Packet Filter: A New Architecture for User-level Packet Capture&quot; at USENIX Winter 1993, describing work that landed in 4.3BSD-Reno earlier in 1992. The motivation was painfully concrete: &lt;code&gt;tcpdump&lt;/code&gt; was copying every packet through the kernel-user boundary, then discarding the ones the user did not want. BPF moved that filter into the kernel. A tiny two-register, 32-bit virtual machine evaluated a user-supplied predicate against each packet before any copy; only matching packets crossed into user space. The architectural insight that would survive thirty years is one sentence: &lt;em&gt;filter where the data is produced, not where it is consumed.&lt;/em&gt;&lt;/p&gt;

A safe, sandboxed virtual machine inside the Linux kernel that runs user-supplied programs at attached hook points. Programs are written in restricted C, compiled to a 64-bit RISC-style bytecode, statically verified before load, and JIT-compiled to native code. The &quot;extended&quot; version, introduced in Linux 3.18 (December 2014) [@kernel-org-bpf-indexhtml], generalized BPF from a packet-filter language into a general kernel-extensibility mechanism.
&lt;p&gt;&lt;strong&gt;2000 -- Event Tracing for Windows.&lt;/strong&gt; Microsoft shipped ETW with Windows 2000. The reference portal [@learn-microsoft-com-tracing-portal] describes the design Microsoft had been refining since the late 1990s: a kernel-mediated event bus with three roles -- providers, sessions, and consumers -- and per-CPU lock-free ring buffers. ETW&apos;s architectural insight was the inverse of BPF&apos;s: &lt;em&gt;event identity and causal order are first-class. A kernel-mediated dispatch makes them cheap.&lt;/em&gt; A &lt;code&gt;tcpdump&lt;/code&gt; filter wants to throw events away. A security telemetry system wants to keep them, attribute them, and order them.&lt;/p&gt;

A kernel-mediated tracing facility shipped in Windows 2000. Providers (kernel or user-mode components) emit structured events to per-CPU ring buffers; sessions own the buffers and select which providers to enable at which level; consumers receive the event stream either in real time or by reading the on-disk `.etl` log. ETW is documented at `learn.microsoft.com/.../etw/event-tracing-portal` [@learn-microsoft-com-tracing-portal].
&lt;p&gt;&lt;strong&gt;2003-2005 -- DTrace.&lt;/strong&gt; Bryan Cantrill, Mike Shapiro, and Adam Leventhal at Sun Microsystems started work in 2003 on what would become the first production-grade dynamic tracing system. DTrace shipped publicly in Solaris 10 in January 2005 [@en-wikipedia-org-wiki-dtrace] and quickly ported to FreeBSD and macOS. Its central idea -- safe in-kernel scripts attached to probes, with a single language for tracing the entire system -- is the spiritual ancestor of every modern kernel observability tool, including eBPF.Wikipedia gives DTrace&apos;s initial public release as January 2005, with Sun&apos;s internal development starting around 2003. The &quot;DTrace 2003&quot; claim that appears in some retrospectives conflates project inception with public release; we use the 2005 ship date here and note 2003 only as a development start. Linux could not adopt it directly: DTrace is licensed under the CDDL, which is GPLv2-incompatible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2005 -- SystemTap.&lt;/strong&gt; Red Hat attempted to fill the Linux DTrace gap with SystemTap [@sourceware-org-systemtap]. The architectural compromise that doomed it: SystemTap scripts compile to a &lt;em&gt;kernel module&lt;/em&gt;, loaded at runtime. Allowing user-supplied kernel modules to be loaded on demand is a privileged operation by definition, so production SystemTap deployments restricted use to local root. That made the observability case study moot: if you already have root, you can use any debugging tool. SystemTap survives as a niche tracing system; it did not become the Linux answer to DTrace.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1992-2014 -- classic BPF stagnates.&lt;/strong&gt; The original BPF VM kept finding new jobs. Linux Socket Filtering [@kernel-org-networking-filtertxt] ported the BSD filter into the Linux kernel in 1997. seccomp-bpf in 2012 gave it a second job: filtering system calls for sandboxing. But the language remained a 32-bit two-register packet-filter VM. It could not be extended to general kernel observability without rewriting the instruction set architecture from the ground up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2014 -- eBPF.&lt;/strong&gt; Alexei Starovoitov&apos;s &quot;extended BPF&quot; patch series landed in Linux 3.18 in December 2014 [@kernel-org-bpf-indexhtml], described in LWN&apos;s contemporaneous article on Starovoitov&apos;s eBPF patch set [@lwn-net-articles-603983]. The rewrite was thorough: 64-bit instruction set, eleven registers, maps for in-kernel state, helper calls into kernel APIs, a JIT compiler, and -- the part that mattered most -- a kernel verifier that statically proves safety before any program runs. The verifier is what turned the packet filter into a general kernel extension mechanism. Without it, every BPF program would have to be trusted; with it, untrusted user code can execute in kernel mode.&lt;/p&gt;
&lt;p&gt;By the time eBPF shipped, Windows had ETW everywhere. Linux had &lt;code&gt;auditd&lt;/code&gt;&apos;s pull-based audit log and a handful of &lt;code&gt;perf&lt;/code&gt; events. Then Starovoitov rewrote BPF, and the architectural balance shifted overnight. The next decade of Linux observability was built on the new instruction set. The next decade of Windows observability stayed on ETW. The two designs ran in parallel until 2021, when Microsoft announced that eBPF would also run on Windows.&lt;/p&gt;

flowchart LR
    A[BPF -- 1992 -- LBL]
    B[ETW -- 2000 -- Windows 2000]
    C[DTrace -- 2005 -- Solaris 10]
    D[SystemTap -- 2005 -- Red Hat]
    E[seccomp-bpf -- 2012 -- Linux 3.5]
    F[eBPF -- 2014 -- Linux 3.18]
    G[BPF Trampoline -- 2019 -- Linux 5.5]
    H[BPF Ringbuf -- 2020 -- Linux 5.8]
    I[eBPF for Windows -- 2021 -- Microsoft]
    J[RFC 9669 BPF ISA -- 2024 -- IETF]
    A --&amp;gt; B --&amp;gt; C --&amp;gt; D --&amp;gt; E --&amp;gt; F --&amp;gt; G --&amp;gt; H --&amp;gt; I --&amp;gt; J
&lt;p&gt;The diagram lays the substrate stories side by side. Each arrow is an architectural decision that constrained what came after. The next two sections walk each design end to end -- ETW first, because it is older and emission-only and easier to internalize.&lt;/p&gt;
&lt;h2&gt;3. ETW: Pure Event Emission&lt;/h2&gt;
&lt;p&gt;A natural question that turns out to be the wrong one: &lt;em&gt;why didn&apos;t Microsoft just keep extending performance counters?&lt;/em&gt; By the late 1990s, Windows already had a mature counter facility -- &lt;code&gt;perfmon&lt;/code&gt;, the Windows Performance Counters portal [@learn-microsoft-com-counters-portal]. It exposed CPU percentage, page-fault rate, queue lengths, and hundreds of other scalar metrics. If you wanted to know how loaded your system was, perfmon told you.&lt;/p&gt;
&lt;p&gt;It also told you almost nothing useful for security telemetry.&lt;/p&gt;

Three structural failures of the counter model show up the moment you try to use it as the substrate for an EDR.&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Sampling-rate floor.&lt;/strong&gt; A counter can only be observed at the rate the consumer queries. On a busy host -- sshd children, container init forks, a CI runner -- process-creation rates routinely exceed any sane query rate. The counter aggregates the events it cannot expose into a single integer that hides the structure of what happened.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No identity.&lt;/strong&gt; &quot;Three hundred process creations in the last second&quot; is a counter. &quot;User &lt;code&gt;bob&lt;/code&gt; ran &lt;code&gt;/tmp/.x&lt;/code&gt; with parent &lt;code&gt;/usr/sbin/cron&lt;/code&gt; at 14:33:07.221Z&quot; is an event. The security model requires identity; the counter model erases it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No causal order.&lt;/strong&gt; Two counters sampled in sequence are not causally ordered with respect to the system events they describe. ETW&apos;s per-CPU buffers with QPC timestamps preserve causal order across CPUs to within the timer&apos;s accuracy.&lt;/li&gt;&lt;/ol&gt;

&lt;p&gt;The fix was not a faster perfmon. The fix was an entirely different shape of telemetry. ETW was that shape: push-based, per-event, kernel-attributed, with stable schemas declared up front. The contrast between perfmon (a sampling counter) and ETW (an event bus) is not parametric. The two systems answer different questions. Security needs the event-bus answer.&lt;/p&gt;
&lt;h3&gt;Provider, session, consumer&lt;/h3&gt;
&lt;p&gt;ETW&apos;s data plane has three roles, every one of them a kernel-mediated object.&lt;/p&gt;
&lt;p&gt;A &lt;em&gt;provider&lt;/em&gt; is a kernel or user-mode component that calls &lt;code&gt;EventWrite&lt;/code&gt; or &lt;code&gt;EtwWrite&lt;/code&gt; to emit a structured event. Providers identify themselves by GUID. They declare the schema of their events ahead of time: classic providers via MOF, the Vista-and-later manifest format [@learn-microsoft-com-event-tracing] called &lt;code&gt;WEVT&lt;/code&gt;, or TraceLogging [@learn-microsoft-com-logging-portal] for self-describing events. The schema is part of the contract: a consumer that knows the provider&apos;s manifest knows the field layout of every event the provider will ever emit.&lt;/p&gt;
&lt;p&gt;A &lt;em&gt;session&lt;/em&gt; is a kernel object created by &lt;code&gt;StartTrace&lt;/code&gt;. It owns a set of per-CPU buffers and a list of enabled providers, with per-provider level and keyword masks. Sessions can write events to disk (&lt;code&gt;.etl&lt;/code&gt; files) or be consumed in real time.The &lt;code&gt;.etl&lt;/code&gt; file extension stands for &quot;Event Trace Log.&quot; It is the on-disk format read by Windows Performance Analyzer and by &lt;code&gt;tracerpt.exe&lt;/code&gt; for post-hoc analysis.&lt;/p&gt;
&lt;p&gt;A &lt;em&gt;consumer&lt;/em&gt; is a user-mode process that calls &lt;code&gt;OpenTrace&lt;/code&gt; and &lt;code&gt;ProcessTrace&lt;/code&gt; and receives event callbacks. EDR agents like Sysmon, Defender, and the third-party agents that ship with Microsoft Defender for Endpoint [@learn-microsoft-com-defender-endpoint] are real-time consumers.&lt;/p&gt;

ETW&apos;s three-role architecture. *Providers* emit events into per-CPU ring buffers. *Sessions* are kernel objects that own buffers and select which providers to enable. *Consumers* are user-mode processes that read the buffers in real time or open the on-disk `.etl` file. The taxonomy is defined in the ETW provider documentation [@learn-microsoft-com-event-tracing].
&lt;h3&gt;The per-CPU ring buffer&lt;/h3&gt;
&lt;p&gt;The algorithmic core of ETW is a per-CPU lock-free ring buffer. When a provider on CPU 3 calls &lt;code&gt;EventWrite&lt;/code&gt;, the kernel formats the event according to the provider&apos;s manifest, stamps it with a QPC timestamp, and &lt;code&gt;memcpy&lt;/code&gt;s the result into the per-CPU buffer for CPU 3. A kernel writer thread drains the buffer asynchronously into the session&apos;s destination -- either an &lt;code&gt;.etl&lt;/code&gt; file on disk or a consumer&apos;s callback queue. The producer-side cost is constant: a function call plus a buffered &lt;code&gt;memcpy&lt;/code&gt;, all on the local CPU, with no cross-CPU synchronization.&lt;/p&gt;

The Windows monotonic timestamp source used for ETW event timestamps. QPC is backed by hardware timers (TSC on modern x86, generic counter on ARM64) and provides a high-resolution counter that does not go backward.
&lt;p&gt;QPC guarantees monotonic timestamps per CPU.QPC is monotonic per CPU on modern hardware, but cross-CPU ordering still relies on the kernel writer thread&apos;s serialization when events from different CPUs are merged into a single output stream. Per-event timestamps from different CPUs can be ordered after the fact, but the merge happens in the writer, not in the producer.&lt;/p&gt;

flowchart LR
    P1[Provider on CPU 0]
    P2[Provider on CPU 1]
    P3[Provider on CPU 2]
    B0[Per-CPU buffer 0]
    B1[Per-CPU buffer 1]
    B2[Per-CPU buffer 2]
    W[Kernel writer thread]
    S[Session]
    F[.etl file]
    C[Real-time consumer]
    P1 -- EventWrite --&amp;gt; B0
    P2 -- EventWrite --&amp;gt; B1
    P3 -- EventWrite --&amp;gt; B2
    B0 --&amp;gt; W
    B1 --&amp;gt; W
    B2 --&amp;gt; W
    W --&amp;gt; S
    S --&amp;gt; F
    S --&amp;gt; C
&lt;h3&gt;The cost story&lt;/h3&gt;
&lt;p&gt;Microsoft&apos;s reference portal [@learn-microsoft-com-tracing-portal] describes ETW as &quot;high-volume, low-overhead.&quot; That qualitative claim has been the consensus practitioner finding for two decades. The most useful practical writeup is Bruce Dawson&apos;s &lt;em&gt;ETW Central&lt;/em&gt; index [@randomascii-wordpress-com-etw-central], which links to more than forty blog posts on real ETW deployments and measurements. The honest summary, anchored to Dawson&apos;s practical experience plus the architectural reason (per-CPU lock-free buffers and a &lt;code&gt;memcpy&lt;/code&gt; per event), is that typical telemetry configurations sit in the low single-digit-percent CPU range, and pathological &quot;log everything&quot; configurations can reach measurable user-visible slowdowns -- on the order of 5-10% in the worst cases. These are practitioner estimates, not benchmarked figures; the BenchmarkDotNet documentation [@benchmarkdotnet-org-configs-diagnosershtml] for the &lt;code&gt;EtwProfiler&lt;/code&gt; diagnoser explicitly acknowledges the cost: &lt;em&gt;&quot;In order to not affect main results we perform a separate run if any diagnoser is used.&quot;&lt;/em&gt; The overhead is small but it is not zero.&lt;/p&gt;
&lt;p&gt;The cost has a structural cause. ETW has no in-kernel filter. The producer pays the full event-formatting cost on every emission, and the only filter is the session&apos;s level and keyword mask. If you enable a provider, every event that provider emits flows through the buffer. Filtering happens at the consumer, in user mode, after the event has crossed the boundary.&lt;/p&gt;
&lt;h3&gt;The Threat-Intelligence provider&lt;/h3&gt;
&lt;p&gt;ETW providers are not equal. The most architecturally important one for security is &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt;, a kernel-only provider that emits signals only the kernel can see: image loads, remote-thread creations, &lt;code&gt;VirtualProtect&lt;/code&gt; changes that flip memory from data to executable. Only a process running under Protected Process Light with the AntiMalware signer [@learn-microsoft-com-downloads-sysmon] can subscribe. That is why Defender, CrowdStrike Falcon, SentinelOne, and Carbon Black [@github-com-providers-docs] all run as PPL-Antimalware: it is the entry ticket to the kernel-only telemetry that distinguishes serious EDR from script-level monitoring.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; ETW&apos;s biggest weakness is that providers run inside the very process they are observing. A process can patch its own copy of &lt;code&gt;ntdll!EtwEventWrite&lt;/code&gt; with a &lt;code&gt;ret&lt;/code&gt; instruction and silence its own emissions before they reach the kernel buffer. EDR vendors monitor for this integrity violation out of band, treating the patch itself as a high-confidence detection signal. The very existence of the tell is an admission that ETW&apos;s original design assumed an honest user-mode producer -- a reasonable assumption in 2000, increasingly untenable in 2025.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sysmon 6.20 [@learn-microsoft-com-downloads-sysmon], released in 2018, was the version that tied ETW into the modern EDR stack as a turnkey configuration.The 2018 Sysmon 6.20 release added the configuration schema that the cybersecurity community converged on. By 2026, the same XML configuration -- including the &lt;code&gt;ProcessCreate&lt;/code&gt;, &lt;code&gt;NetworkConnect&lt;/code&gt;, &lt;code&gt;ImageLoad&lt;/code&gt;, and &lt;code&gt;FileCreate&lt;/code&gt; event IDs -- works on both Sysmon for Windows and Sysmon for Linux. Sysmon, Microsoft&apos;s own free reference consumer authored by Mark Russinovich and Thomas Garnier [@learn-microsoft-com-downloads-sysmon], demonstrated that an XML configuration plus an ETW consumer plus protected-process status was enough to build a useful EDR. Sysmon is not Defender; it is the open shape that the commercial EDR vendors built proprietary versions of.&lt;/p&gt;
&lt;h3&gt;Closing on ETW&lt;/h3&gt;
&lt;p&gt;ETW emits. Every enabled event crosses the kernel-user boundary, fully formatted, with no in-kernel filtering language whatsoever. The session&apos;s level and keyword mask is a coarse on/off switch, not a programmable filter. Aggregation, sampling, and stack-trace folding happen in user mode, after the event is already across the boundary.&lt;/p&gt;
&lt;p&gt;Now you can read the question that drove Starovoitov&apos;s 2014 rewrite: &lt;em&gt;what if you could filter in the kernel itself? What if you could compute -- not just emit?&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;4. eBPF: Programmable In-Kernel Computation&lt;/h2&gt;
&lt;p&gt;The architectural inversion is one sentence. ETW is the producer telling the consumer what happened. eBPF is the consumer telling the producer what to compute. The producer is the kernel; the consumer is a user-mode process that has compiled, verified, and attached a small program that will run inside the kernel at a chosen hook. The roles are inverted, the data flow is inverted, and the trust model is inverted.&lt;/p&gt;
&lt;h3&gt;The lifecycle&lt;/h3&gt;
&lt;p&gt;A canonical eBPF program goes through six stages before it does any useful work. The flow below is the same on every Linux kernel since 3.18, with refinements added over the years for BTF (BPF Type Format), CO-RE (Compile Once, Run Everywhere), and link primitives:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. clang -target bpf -O2 -c prog.c -o prog.o            # ELF with BTF
2. fd = bpf(BPF_PROG_LOAD, &amp;amp;attr)                       # kernel verifier runs
3. for each map referenced:
       map_fd = bpf(BPF_MAP_CREATE, &amp;amp;attr)
4. link = bpf(BPF_LINK_CREATE, kprobe|tracepoint|xdp|lsm|cgroup, fd)
5. at hook fire: JIT-compiled native code runs on the
   producing CPU, reads context, calls bpf_* helpers,
   writes to map or ringbuf
6. user space mmaps the ringbuf and consumes records
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The lifecycle is documented in the canonical kernel BPF documentation index [@kernel-org-bpf-indexhtml]. It is worth lingering on stage 2. Between the user-space &lt;code&gt;bpf()&lt;/code&gt; syscall and the moment the kernel hands back a file descriptor for the loaded program, a static analyzer runs. That analyzer is the most consequential piece of code in this entire article. We treat it on its own in section 5.&lt;/p&gt;

flowchart TD
    A[&quot;Restricted C source -- (prog.c)&quot;]
    B[&quot;clang -target bpf -- BPF ELF + BTF&quot;]
    C[bpf BPF_PROG_LOAD]
    D[Kernel verifier]
    E[JIT compiler]
    F[Kernel hook]
    G[bpf BPF_MAP_CREATE]
    H[&quot;BPF maps -- (arrays, hashes, ringbuf)&quot;]
    I[&quot;bpf BPF_LINK_CREATE -- (kprobe/xdp/lsm/...)&quot;]
    J[Hook fires]
    K[User space mmap ringbuf]
    A --&amp;gt; B --&amp;gt; C --&amp;gt; D
    D --&amp;gt;|reject| Z[E_INVAL to userspace]
    D --&amp;gt;|accept| E --&amp;gt; F
    C --&amp;gt; G --&amp;gt; H
    F --&amp;gt; I --&amp;gt; J
    J --&amp;gt; H
    H --&amp;gt; K
&lt;h3&gt;Hooks: where programs attach&lt;/h3&gt;
&lt;p&gt;The thing that distinguishes eBPF from a packet filter is its hook surface. A &lt;em&gt;hook&lt;/em&gt; is a place inside the kernel where a verified program can be attached, fired at the moment something happens. Linux has a lot of hooks.&lt;/p&gt;

An attachment point in kernel code where a verified eBPF program runs. Different hook types receive different context arguments: a kprobe receives the function&apos;s CPU registers; an XDP program receives a packet buffer; an LSM hook receives the security operation&apos;s parameters. The hook type also determines what helpers and map types the verifier allows.
&lt;p&gt;The hook taxonomy, drawn from the kernel BPF docs [@kernel-org-bpf-indexhtml] and Cilium&apos;s BPF architecture reference [@docs-cilium-io-bpf-architecture], is broad:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kprobe&lt;/code&gt; and &lt;code&gt;kretprobe&lt;/code&gt; -- entry and return of any non-inlined kernel function.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fentry&lt;/code&gt; and &lt;code&gt;fexit&lt;/code&gt; -- BPF trampoline replacement for kprobes, with no &lt;code&gt;int3&lt;/code&gt; trap-frame cost.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uprobe&lt;/code&gt; -- any user-space symbol in any process.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tracepoint&lt;/code&gt; -- stable kernel tracepoints with version-locked schemas.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;perf_event&lt;/code&gt; -- sampling-profile hooks tied to perf events.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XDP&lt;/code&gt; -- driver tail-call, before allocation of an &lt;code&gt;sk_buff&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TC&lt;/code&gt; -- Linux traffic-control qdisc hooks.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LSM&lt;/code&gt; -- Linux Security Module hooks (mandatory-access-control points), available since Linux 5.7.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cgroup&lt;/code&gt;, &lt;code&gt;sched&lt;/code&gt;, &lt;code&gt;sock_ops&lt;/code&gt; -- policy and socket-state hooks.&lt;/li&gt;
&lt;/ul&gt;

flowchart TD
    K[&quot;eBPF -- Programs&quot;]
    T[&quot;Tracing -- (kprobe, fentry, -- uprobe, tracepoint)&quot;]
    N[&quot;Networking -- (XDP, TC, sock_ops, -- sk_lookup)&quot;]
    S[&quot;Security -- (LSM, seccomp, -- landlock)&quot;]
    P[&quot;Policy &amp;amp; scheduling -- (cgroup, sched, -- perf_event)&quot;]
    K --&amp;gt; T
    K --&amp;gt; N
    K --&amp;gt; S
    K --&amp;gt; P
&lt;p&gt;That hook surface is what makes eBPF the universal Linux instrumentation substrate. Once a developer learns the load-verify-attach lifecycle, the same toolchain instruments a TCP retransmit, a &lt;code&gt;do_sys_open&lt;/code&gt; call, an LSM &lt;code&gt;file_open&lt;/code&gt; check, and an XDP fast-path drop -- all in the same language with the same verifier and the same JIT.&lt;/p&gt;
&lt;h3&gt;Maps: in-kernel state&lt;/h3&gt;
&lt;p&gt;The second piece of architecture eBPF adds over classic BPF is the &lt;em&gt;map&lt;/em&gt; -- a kernel-managed key-value store accessible from inside a verified program and from user space. Maps are how eBPF programs hold state between invocations and how they communicate with user space.&lt;/p&gt;

A kernel-managed data structure that an eBPF program can read and write from inside the kernel, and a user-space process can read and write through the `bpf()` syscall. Common map types include hash, array, LRU hash, per-CPU hash, ring buffer, and program array (used for tail calls). Each map has a maximum capacity declared at creation and a verifier-checked size for keys and values.
&lt;p&gt;The kernel hash-map documentation [@docs-kernel-org-bpf-maphashhtml] distinguishes shared and per-CPU variants. The decision between them is one of the consequential design choices in writing real eBPF code.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Map type&lt;/th&gt;
&lt;th&gt;Cross-CPU semantics&lt;/th&gt;
&lt;th&gt;Update cost&lt;/th&gt;
&lt;th&gt;Memory cost&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BPF_MAP_TYPE_HASH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One value per key, shared across CPUs&lt;/td&gt;
&lt;td&gt;Atomic &lt;code&gt;__sync_fetch_and_add&lt;/code&gt; or &lt;code&gt;BPF_F_LOCK&lt;/code&gt; spinlock&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max_entries * (key_size + value_size)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;State that must be globally consistent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BPF_MAP_TYPE_PERCPU_HASH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Separate value slot per CPU&lt;/td&gt;
&lt;td&gt;Non-atomic read-modify-write&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max_entries * value_size * num_cpus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counters and histograms where rate matters and snapshot consistency does not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BPF_MAP_TYPE_RINGBUF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Single MPSC ring with global FIFO order&lt;/td&gt;
&lt;td&gt;Reservation-spinlock on producer&lt;/td&gt;
&lt;td&gt;Fixed buffer&lt;/td&gt;
&lt;td&gt;Event streams whose user-space order must match cross-CPU producer order&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The per-CPU variant exists because cache-coherence cost on a contended hash slot dominates the time spent updating it; per-CPU maps remove that contention entirely at the price of cross-CPU consistency. A per-CPU counter on a 96-vCPU host occupies &lt;code&gt;96 * value_size&lt;/code&gt; bytes per key, but updates are local loads and stores. A shared counter on the same host is &lt;code&gt;value_size&lt;/code&gt; bytes per key, but every increment is an atomic.&lt;/p&gt;

A multi-producer single-consumer kernel-to-user transport added in Linux 5.8 and documented at `docs.kernel.org/bpf/ringbuf.html` [@docs-kernel-org-bpf-ringbufhtml]. Unlike the legacy `perf_event_array` (one ring per CPU), the BPF ringbuf is a single ring shared across all CPUs, with cross-CPU producer ordering preserved in the user-visible record stream.
&lt;p&gt;The ringbuf documentation [@docs-kernel-org-bpf-ringbufhtml] is explicit about why the design exists: &lt;em&gt;&quot;more efficient memory use by sharing ring buffer across CPUs; preserving ordering of events that happen sequentially in time, even across multiple CPUs (e.g., fork/exec/exit events for a task).&quot;&lt;/em&gt; A security telemetry consumer that needs to see &lt;code&gt;fork&lt;/code&gt; on CPU 0 before &lt;code&gt;kill&lt;/code&gt; on CPU 1 cannot use a per-CPU ring; it needs a single MPSC ring. The trade-off is real: the producer pays a brief spinlock for slot reservation, where a per-CPU ring would pay nothing. For event streams the trade is worth it; for histograms it is not.&lt;/p&gt;
&lt;h3&gt;The aggregation pattern&lt;/h3&gt;
&lt;p&gt;The reason eBPF is strictly more powerful than ETW is captured in one bpftrace one-liner. The DSL &lt;code&gt;bpftrace&lt;/code&gt; [@github-com-iovisor-bpftrace] -- inspired explicitly by DTrace -- compiles a single-line query into a verified eBPF program:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bpftrace&quot;&gt;kprobe:vfs_read { @[comm] = hist(arg2); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This program attaches to the &lt;code&gt;vfs_read&lt;/code&gt; kernel function. For every call, it indexes a per-CPU map by the calling process&apos;s name (&lt;code&gt;comm&lt;/code&gt;), buckets the &lt;code&gt;arg2&lt;/code&gt; value (the read length) into a power-of-two histogram, and increments the bucket. Nothing crosses the kernel-user boundary while &lt;code&gt;vfs_read&lt;/code&gt; is firing -- not at 10K calls per second, not at 10M. When the user hits Ctrl-C, bpftrace iterates the per-CPU maps from user space, merges the buckets across CPUs, and prints a histogram.&lt;/p&gt;
&lt;p&gt;ETW cannot do this. To produce the same histogram with ETW, a consumer would have to subscribe to every &lt;code&gt;vfs_read&lt;/code&gt;-equivalent kernel event, receive each one in user mode, compute its bucket, and update an in-process histogram. The kernel-user wire would carry the full firehose. eBPF carries only the final histogram.&lt;/p&gt;
&lt;p&gt;{`
// The bpftrace one-liner:
//   kprobe:vfs_read { @[comm] = hist(arg2); }
// lowers (conceptually) to this kernel-side and user-side flow.&lt;/p&gt;
&lt;p&gt;// --- inside the kernel, at every vfs_read call ---
function on_vfs_read(ctx) {
  const comm = bpf_get_current_comm();
  const len  = ctx.regs.rsi;                  // arg2: read length
  const bucket = log2(len);                   // 0..63&lt;/p&gt;
&lt;p&gt;  // per-CPU hash keyed by (comm, bucket); no cross-CPU atomics.
  const key = { comm, bucket };
  const slot = percpu_map.lookup_or_init(key, 0);
  *slot += 1;
}&lt;/p&gt;
&lt;p&gt;// --- in user space, on Ctrl-C ---
function print_histogram() {
  const merged = {};
  for (const cpu of all_cpus) {
    for (const [key, count] of percpu_map.iter(cpu)) {
      merged[key] = (merged[key] || 0) + count;
    }
  }
  render_power_of_two_histogram(merged);
}
`}&lt;/p&gt;
&lt;p&gt;The kernel-side per-event cost is a few instructions plus a non-atomic increment. The user-space cost is paid once, at print time. The wire between kernel and user carries one batch read of the entire per-CPU map. ETW&apos;s equivalent would carry every single &lt;code&gt;vfs_read&lt;/code&gt; event in full.&lt;/p&gt;
&lt;h3&gt;The instruction-count and complexity limits&lt;/h3&gt;
&lt;p&gt;Two distinct limits constrain what the verifier will accept. The constants are easy to confuse, and earlier drafts of this article confused them. The correct distinction comes straight from the kernel headers.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BPF_MAXINSNS&lt;/code&gt; is defined as 4096 in &lt;code&gt;include/uapi/linux/bpf_common.h&lt;/code&gt;. This is the maximum number of bytecode instructions per program for unprivileged callers. A program longer than 4096 instructions is rejected at load time regardless of what the verifier finds.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BPF_COMPLEXITY_LIMIT_INSNS&lt;/code&gt; is defined as 1,000,000 in &lt;code&gt;kernel/bpf/verifier.c&lt;/code&gt;. This is the maximum number of &lt;em&gt;explored states&lt;/em&gt; the verifier will visit during its symbolic execution. It applies to privileged callers with &lt;code&gt;CAP_BPF&lt;/code&gt;, who are allowed to load larger programs but still bound the cost of verifying them.The two limits answer different questions. &lt;code&gt;BPF_MAXINSNS = 4096&lt;/code&gt; bounds the &lt;em&gt;size&lt;/em&gt; of an unprivileged program. &lt;code&gt;BPF_COMPLEXITY_LIMIT_INSNS = 1,000,000&lt;/code&gt; bounds the &lt;em&gt;cost&lt;/em&gt; of verification for privileged programs. Conflating them is a common error: production EDRs run with &lt;code&gt;CAP_BPF&lt;/code&gt; plus &lt;code&gt;CAP_PERFMON&lt;/code&gt; or root and load programs much longer than 4096 instructions, but the verifier&apos;s exploration is still bounded.&lt;/p&gt;
&lt;p&gt;Linux 5.16 (March 2022) [@kernel-org-bpf-indexhtml] made &lt;code&gt;kernel.unprivileged_bpf_disabled=1&lt;/code&gt; the default.The change followed a series of verifier soundness CVEs, including CVE-2020-8835 and CVE-2021-3490, that were exploitable from unprivileged user space. Production EDRs run with &lt;code&gt;CAP_BPF&lt;/code&gt; plus &lt;code&gt;CAP_PERFMON&lt;/code&gt; or full root; the unprivileged path is reserved for sandboxed workloads where the kernel team has weighed the risk.&lt;/p&gt;
&lt;h3&gt;The JIT and the trampoline&lt;/h3&gt;
&lt;p&gt;Brendan Gregg&apos;s &lt;em&gt;BPF Performance Tools&lt;/em&gt; [@brendangregg-com-tools-bookhtml], published by Addison-Wesley in 2019 (ISBN-13 9780136554820 [@pearson-com-p200000007897-9780136554820]), reports a 10x to 12x speedup of the JIT over the interpreter on x86-64. The number is qualitative -- the workload, the kernel version, and the program shape all matter -- but the order of magnitude is consistent across kernel docs and measurements. The JIT is what makes eBPF practically usable inside hot kernel paths.&lt;/p&gt;
&lt;p&gt;A second performance refinement landed in 2019 with the BPF trampoline patch series. Starovoitov&apos;s v1 cover letter [@lore-kernel-org-1-astkernelorg] introduced &lt;code&gt;fentry&lt;/code&gt; and &lt;code&gt;fexit&lt;/code&gt; -- BPF program attach points that use a tiny JIT-emitted dispatcher to call the attached programs directly, rather than relying on kprobe&apos;s &lt;code&gt;int3&lt;/code&gt; trap mechanism. The framing is worth quoting:&lt;/p&gt;

Unlike k[ret]probe there is practically zero overhead to call a set of BPF programs before or after kernel function. -- Alexei Starovoitov, BPF trampoline cover letter [@lore-kernel-org-1-astkernelorg]
&lt;p&gt;The v3 patch in the same series [@lore-kernel-org-4-astkernelorg] explains the structural reason: &lt;em&gt;&quot;To avoid the high cost of retpoline the attached BPF programs are called directly.&quot;&lt;/em&gt; kprobe goes through an indirect-jump dispatch, which on Spectre-mitigated kernels pays a retpoline penalty per call. The BPF trampoline replaces the indirect jump with a direct call patched in at attach time, eliminating that penalty entirely. The qualitative result is &quot;practically zero overhead&quot; relative to the function call itself. The exact numbers vary; the architectural reason does not.&lt;/p&gt;
&lt;h3&gt;Tail calls&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;bpf_tail_call(ctx, &amp;amp;prog_array, index)&lt;/code&gt; is a helper that, when the &lt;code&gt;prog_array&lt;/code&gt; slot at &lt;code&gt;index&lt;/code&gt; contains a loaded program, replaces the current program&apos;s execution context with the target program&apos;s. The architecture is documented in the Cilium BPF architecture reference [@docs-cilium-io-bpf-architecture], which describes the 33-call nesting ceiling: &lt;em&gt;&quot;This, too, comes with an upper nesting limit of 33 calls, and is usually used to decouple parts of the program logic, for example, into stages.&quot;&lt;/em&gt; The 33-call cap bounds the worst-case execution time of a chain that the verifier cannot symbolically follow (the destination is a runtime-resolved map slot, not a static call target). We will return to the security implications of tail calls in section 7.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; eBPF inverts the observability model. ETW asks the kernel &quot;what happened?&quot; eBPF asks the kernel &quot;compute this and tell me the answer.&quot; The asymmetry is the reason a histogram of &lt;code&gt;vfs_read&lt;/code&gt; lengths costs nothing on the wire under eBPF, and costs a fully formatted event per call under ETW.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;eBPF is strictly more powerful than ETW: programmable filter, programmable aggregation, hooks everywhere. But that power has a cost that does not exist in ETW at all. The verifier.&lt;/p&gt;
&lt;h2&gt;5. The Verifier: Where Mathematics Meets the Kernel&lt;/h2&gt;
&lt;p&gt;May 2023. NIST publishes CVE-2023-2163 [@nvd-nist-gov-2023-2163]. The advisory describes the eBPF verifier in every Linux kernel since 5.4 quietly accepting programs it should have rejected: &lt;em&gt;&quot;Incorrect verifier pruning in BPF in Linux Kernel &amp;gt;=5.4 leads to unsafe code paths being incorrectly marked as safe, resulting in arbitrary read/write in kernel memory, lateral privilege escalation, and container escape.&quot;&lt;/em&gt; The fix was a small correction to a state-pruning heuristic. The lesson is bigger than the patch: &lt;em&gt;no in-kernel verifier for a Turing-complete instruction set can be simultaneously sound, complete, and decidable.&lt;/em&gt; That is not a bug. It is a theorem.&lt;/p&gt;
&lt;h3&gt;Rice&apos;s theorem in the kernel&lt;/h3&gt;
&lt;p&gt;Alan Turing proved in 1936 that the halting problem is undecidable: no algorithm can decide, for every possible program, whether that program halts on every input. Henry Gordon Rice extended the result in 1953: any &lt;em&gt;non-trivial semantic property&lt;/em&gt; of a program -- including memory safety, type safety, and bounded resource use -- is undecidable for the general case. The verifier has to decide a non-trivial semantic property: &lt;em&gt;does this eBPF program access kernel memory only through valid pointers, with valid offsets, and terminate?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It cannot. Not in general. The verifier has to give up at least one of three properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Soundness&lt;/em&gt; -- never accept an unsafe program.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Completeness&lt;/em&gt; -- never reject a safe program.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Scalability&lt;/em&gt; -- run in polynomial time on real programs.&lt;/li&gt;
&lt;/ul&gt;

The halting problem is about a single property: termination. Rice&apos;s theorem generalizes the result to all non-trivial extensional properties -- any property that depends on what a program computes rather than how it is written. Memory safety on a Turing-complete instruction set is a non-trivial extensional property: there exist programs that are safe and programs that are unsafe. Rice&apos;s theorem says no decision procedure can correctly classify every program. Any real verifier must therefore be an *approximation* -- either it sometimes rejects safe programs (loss of completeness), sometimes accepts unsafe ones (loss of soundness), or runs out of resources on hard inputs (loss of scalability).
&lt;p&gt;Jia and colleagues at HotOS 2023 [@sigops-org-papers-jiapdf] formalized this trilemma for in-kernel verifiers. The paper&apos;s title is the thesis: &lt;em&gt;&quot;Kernel Extension Verification Is Untenable.&quot;&lt;/em&gt; The authors argue that any verifier for a kernel extension language with the expressiveness of eBPF must trade off at least one of the three properties, and that real verifiers ship by trading all three approximately.&lt;/p&gt;

Kernel Extension Verification Is Untenable. -- Jia et al., HotOS 2023, `sigops.org/s/conferences/hotos/2023/papers/jia.pdf` [@sigops-org-papers-jiapdf]

flowchart TD
    A[Soundness -- never accept -- unsafe programs]
    B[Completeness -- never reject -- safe programs]
    C[Scalability -- polynomial time -- on real programs]
    A --- B
    B --- C
    C --- A
    X[&quot;No verifier can have -- all three on a -- Turing-complete ISA&quot;]
    A -.-&amp;gt; X
    B -.-&amp;gt; X
    C -.-&amp;gt; X
&lt;p&gt;The Linux verifier ships with all three approximately. PREVAIL, the verifier used by eBPF-for-Windows, ships with stronger soundness and weaker completeness. The two designs occupy different points on the triangle, and the difference shows up in production.&lt;/p&gt;
&lt;h3&gt;The Linux verifier&lt;/h3&gt;
&lt;p&gt;The kernel verifier documentation [@docs-kernel-org-bpf-verifierhtml] describes the algorithm:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The safety of the eBPF program is determined in two steps. First step does DAG check to disallow loops and other CFG validation. ... Second step starts from the first insn and descends all possible paths. It simulates execution of every insn and observes the state change of registers and stack.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The state the verifier tracks is a register-state lattice. Each register holds a type from a finite set: &lt;code&gt;PTR_TO_CTX&lt;/code&gt; (a pointer to the program&apos;s context argument), &lt;code&gt;PTR_TO_MAP_VALUE&lt;/code&gt; (a pointer into a map entry), &lt;code&gt;PTR_TO_MAP_VALUE_OR_NULL&lt;/code&gt; (the return type of &lt;code&gt;bpf_map_lookup_elem&lt;/code&gt;, which can be null), &lt;code&gt;SCALAR_VALUE&lt;/code&gt; (an integer with min/max range), and so on. Each register also has a min/max range that tightens at every operation.&lt;/p&gt;

The kernel-side static analyzer that proves termination and memory safety of every eBPF program before load. The Linux verifier is documented at `docs.kernel.org/bpf/verifier.html` [@docs-kernel-org-bpf-verifierhtml]. It uses a register-state lattice plus min/max range tracking and explores all reachable program paths with state pruning to keep the cost manageable.
&lt;p&gt;Consider the canonical pattern: look up a map value, check for null, dereference. Every eBPF tracing program does some version of this.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct value *v = bpf_map_lookup_elem(&amp;amp;map, &amp;amp;key);   // r0 := PTR_TO_MAP_VALUE_OR_NULL
if (!v) return 0;                                    // branch on r0 == 0
return v-&amp;gt;field;                                     // deref r0 + offset(field)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The verifier traces both branches. On the taken branch (&lt;code&gt;r0 == 0&lt;/code&gt;), the type stays nullable, and the program returns. On the not-taken branch, the verifier refines the type from &lt;code&gt;PTR_TO_MAP_VALUE_OR_NULL&lt;/code&gt; to &lt;code&gt;PTR_TO_MAP_VALUE&lt;/code&gt; -- the null qualifier is gone, the dereference is bounds-checked against the map&apos;s value size, and the program is accepted.&lt;/p&gt;
&lt;p&gt;This refinement is exactly the thing that broke in CVE-2023-2163. The bug was not in the dereference logic; it was in the &lt;em&gt;state pruning&lt;/em&gt; that keeps the verifier&apos;s exploration tractable. Once the verifier has visited a program point with a given abstract state, it prunes subsequent visits from different predecessors with &quot;the same&quot; state. CVE-2023-2163 was a case where the pruner&apos;s notion of &quot;the same state&quot; was &lt;em&gt;narrower&lt;/em&gt; than the predecessor&apos;s true state. The verifier accepted a program in which a register&apos;s true type at a join point did not match the type the verifier had pruned against. The program ran with hidden type confusion. Kernel arbitrary read/write followed.&lt;/p&gt;
&lt;h3&gt;PREVAIL, the abstract-interpretation verifier&lt;/h3&gt;
&lt;p&gt;PREVAIL [@github-com-ebpf-verifier], published by Gershuni and colleagues at PLDI 2019 [@vbpf-github-io-prevail-paperpdf], takes a structurally different approach. Where Linux&apos;s verifier is a heuristic abstract interpreter with a discrete type lattice, PREVAIL uses &lt;em&gt;numerical abstract interpretation&lt;/em&gt; over the &lt;em&gt;zone domain&lt;/em&gt; plus intervals.&lt;/p&gt;

A general framework for static analysis, introduced by Patrick and Radhia Cousot in 1977. The analyzer computes over an *abstract domain* -- intervals, zones, polyhedra, octagons -- rather than concrete program states. A safe abstract operation must over-approximate every possible concrete behavior. The soundness of the analysis reduces to the soundness of the abstract domain operations, which can be proved once and reused.
&lt;p&gt;In the zone domain, the abstract state can express &lt;em&gt;relational&lt;/em&gt; constraints between registers and memory base addresses -- not just &quot;register &lt;code&gt;r0&lt;/code&gt; is in &lt;code&gt;[base, base + size)&lt;/code&gt;&quot; but &quot;&lt;code&gt;r0 - map_base&lt;/code&gt; is in &lt;code&gt;[0, value_size)&lt;/code&gt;.&quot; That extra expressiveness is what lets PREVAIL prove pointer-arithmetic safety more directly than the Linux verifier&apos;s case enumeration. Walking the same null-check program:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Program point&lt;/th&gt;
&lt;th&gt;Linux verifier (register lattice)&lt;/th&gt;
&lt;th&gt;PREVAIL (zone domain)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;After &lt;code&gt;bpf_map_lookup_elem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PTR_TO_MAP_VALUE_OR_NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;r0 in {0} U [base, base+sz)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Taken branch (r0 == 0)&lt;/td&gt;
&lt;td&gt;refined to NULL&lt;/td&gt;
&lt;td&gt;r0 = 0 (equality)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not-taken branch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PTR_TO_MAP_VALUE&lt;/code&gt; (qualifier dropped)&lt;/td&gt;
&lt;td&gt;r0 - base in [0, sz)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At deref &lt;code&gt;v-&amp;gt;field&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bounds-checked deref&lt;/td&gt;
&lt;td&gt;r0 - base in [off, off+access)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Both verifiers accept the program. The difference is in the proof strategy. Linux&apos;s verifier reasons case-by-case over a finite lattice; PREVAIL reasons numerically over an abstract domain whose soundness is proved once and reused. The PREVAIL paper (Gershuni et al., PLDI 2019) [@vbpf-github-io-prevail-paperpdf] showed that the zone-domain approach is sound and runs in polynomial time per fixed abstract domain.&lt;/p&gt;

flowchart LR
    A[&quot;r0 := bpf_map_lookup_elem&quot;]
    B{&quot;r0 == 0?&quot;}
    C[&quot;return 0&quot;]
    D[&quot;return r0-&amp;gt;field&quot;]
    A --&amp;gt; B
    B -- yes --&amp;gt; C
    B -- no --&amp;gt; D
    A -. &quot;Linux: PTR_TO_MAP_VALUE_OR_NULL -- PREVAIL: r0 in {0} U [base, base+sz)&quot; .-&amp;gt; A
    C -. &quot;Linux: NULL -- PREVAIL: r0 = 0&quot; .-&amp;gt; C
    D -. &quot;Linux: PTR_TO_MAP_VALUE -- PREVAIL: r0 - base in [0, sz)&quot; .-&amp;gt; D
&lt;p&gt;The trade-off is concrete. PREVAIL accepts a broader class of programs the Linux verifier rejects (some bounded loops, some longer programs), and rejects others the Linux verifier accepts (Linux&apos;s heuristic pruning is more aggressive than zone-domain reasoning in some patterns). The contrast is a &lt;em&gt;trade&lt;/em&gt;, not a strict ordering. Each verifier is sound with respect to its own abstract domain. The Linux verifier&apos;s CVE history is what happens when the domain itself is implemented heuristically rather than from a once-and-for-all soundness proof. The work of Paul Chaignon [@pchaigno-github-io-ebpf-verifierhtml] walks through the architectural differences in more detail.&lt;/p&gt;
&lt;h3&gt;Four CVEs, one pattern&lt;/h3&gt;
&lt;p&gt;The Linux verifier has shipped four widely-disclosed soundness bugs, each one a case where the verifier accepted a program it should have rejected.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CVE&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Subsystem at fault&lt;/th&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;CVE-2020-8835 [@nvd-nist-gov-2020-8835]&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;32-bit register bounds tracking&lt;/td&gt;
&lt;td&gt;Out-of-bounds read/write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2021-3490 [@nvd-nist-gov-2021-3490]&lt;/td&gt;
&lt;td&gt;2021&lt;/td&gt;
&lt;td&gt;ALU32 bitwise-op bounds tracking&lt;/td&gt;
&lt;td&gt;Out-of-bounds R/W, arbitrary RCE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2022-23222 [@nvd-nist-gov-2022-23222]&lt;/td&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*_OR_NULL&lt;/code&gt; type-state tracking&lt;/td&gt;
&lt;td&gt;Local privilege escalation via type confusion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2023-2163 [@nvd-nist-gov-2023-2163]&lt;/td&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;Branch-pruning logic&lt;/td&gt;
&lt;td&gt;Arbitrary kernel R/W&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The CVE-2020-8835 NVD entry describes a flaw where the verifier &lt;em&gt;&quot;did not properly restrict the register bounds for 32-bit operations, leading to out-of-bounds reads and writes in kernel memory.&quot;&lt;/em&gt; CVE-2021-3490, also reported on the NVD, identifies the same class of bug in the bitwise-operation paths. The CVE-2022-23222 record is tracked across the SUSE bug [@bugzilla-suse-com-showbugcgi], Debian DSA-5050 [@debian-org-dsa-5050], and the openwall oss-security disclosure thread [@openwall-com-13-1].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; All four CVEs are the same shape: the verifier&apos;s abstract state at some program point was &lt;em&gt;narrower&lt;/em&gt; than the program&apos;s true reachable state, so the verifier proved a property that did not hold. Each fix tightened the abstract operation that introduced the narrowing -- range-tracking for the 2020 and 2021 bugs, type-state for 2022, branch pruning for 2023. None of the fixes were &quot;fix the runtime&quot;; they were all &quot;fix the static analysis.&quot; That is exactly the shape Rice&apos;s theorem predicts: a heuristic abstract interpreter that occasionally drops information at a join point.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The verifier is a research-grade static analyzer running as kernel code. When it gets the abstract domain wrong, the safety guarantee is a CVE. ETW does not have this failure mode because ETW does not run user-supplied code in the kernel.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;ETW has driver signing as its safety mechanism. eBPF has the verifier. Microsoft&apos;s eBPF-for-Windows project asked an interesting question: &lt;em&gt;what if you want both?&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;6. eBPF for Windows: The Convergence&lt;/h2&gt;
&lt;p&gt;On May 10, 2021, Dave Thaler of Microsoft published a blog post announcing a new project. The opening line is the kind of announcement that sounds modest and is not:&lt;/p&gt;

&quot;Today we are excited to announce a new Microsoft open source project to make eBPF work on Windows 10 and Windows Server 2016 and later.&quot; -- Dave Thaler, &quot;Making eBPF work on Windows&quot; [@cloudblogs-microsoft-com-on-windows], Microsoft Open Source Blog, May 2021
&lt;p&gt;The promise was a near-source-compatible eBPF surface on NT, so that programs and toolchains written for Linux eBPF -- libbpf, bpftool, BCC, clang &lt;code&gt;-target bpf&lt;/code&gt; -- would work on Windows with minimal change. The architectural surprise, visible only once you read the design docs, is that the Linux design does not port directly. The Windows trust model is different. The Windows code-integrity story is different. The choices Microsoft made reveal which parts of eBPF &lt;em&gt;are&lt;/em&gt; genuinely portable and which parts are deeply Linux-shaped.&lt;/p&gt;
&lt;h3&gt;Three execution modes&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;microsoft/ebpf-for-windows&lt;/code&gt; README [@github-com-for-windows] decomposes the runtime into three modes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;Native eBPF program (preferred, HVCI-compatible).&lt;/em&gt; PREVAIL verifies the bytecode in user mode. On success, the &lt;code&gt;bpf2c&lt;/code&gt; [@github-com-bpf2ctests-expected] tool transliterates each verified BPF instruction to equivalent C, MSVC compiles the C, and the result is a signed &lt;code&gt;.sys&lt;/code&gt; kernel driver. The signed driver is what gets loaded into the kernel.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;JIT compiler.&lt;/em&gt; A user-mode service (&lt;code&gt;eBPFSvc.exe&lt;/code&gt;) calls the uBPF [@github-com-iovisor-ubpf] JIT to produce x64 or ARM64 native code, loaded into the kernel-mode execution context. Disabled on HVCI hosts because dynamic code generation cannot be SiPolicy-signed.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Interpreter.&lt;/em&gt; uBPF&apos;s interpreter, debug-only.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The native mode is the architecturally interesting one. It treats eBPF bytecode as a &lt;em&gt;source language&lt;/em&gt; for a signed-driver compile, not as a target for a kernel-mode JIT. The choice is forced by Windows&apos; kernel-mode security model.&lt;/p&gt;

A Windows feature that uses the hypervisor to enforce that only signed code runs in kernel mode. With HVCI on, the kernel will refuse to execute any page that does not match a Code Integrity policy signature. Dynamic code generation -- the kind a JIT does -- is impossible on an HVCI host unless the JIT itself is privileged to bless the pages it produces.
&lt;h3&gt;bpf2c: the literal transliterator&lt;/h3&gt;
&lt;p&gt;The thing that makes the native pipeline work is &lt;code&gt;bpf2c&lt;/code&gt;. It takes verified eBPF bytecode and emits portable C that any modern compiler can build into a kernel driver. The transliteration is one bytecode instruction per C statement. A concrete excerpt from &lt;code&gt;droppacket_raw.c&lt;/code&gt; [@raw-githubusercontent-com-expected-droppacketrawc], the expected output for the XDP-class &lt;code&gt;droppacket.c&lt;/code&gt; [@github-com-sample-droppacketc] sample, shows the shape:&lt;/p&gt;
&lt;p&gt;{`
// Excerpt from microsoft/ebpf-for-windows
//   tests/bpf2c_tests/expected/droppacket_raw.c
// One verified BPF instruction maps to one C statement.&lt;/p&gt;
&lt;p&gt;#pragma code_seg(push, &quot;xdp&quot;)
static uint64_t
DropPacket(void* context, const program_runtime_context_t* runtime_context)
{
  uint64_t stack[(UBPF_STACK_SIZE + 7) / 8];
  register uint64_t r0 = 0;
  register uint64_t r1 = 0;
  // ... r2 .. r6, r10 declarations ...&lt;/p&gt;
&lt;p&gt;  // EBPF_OP_MOV64_REG pc=0 dst=r6 src=r1 offset=0 imm=0
  r6 = r1;
  // EBPF_OP_MOV64_IMM pc=1 dst=r1 src=r0 offset=0 imm=0
  r1 = IMMEDIATE(0);
  // EBPF_OP_STXDW pc=2 dst=r10 src=r1 offset=-8 imm=0
  WRITE_ONCE_64(r10, (uint64_t)r1, OFFSET(-8));&lt;/p&gt;
&lt;p&gt;  // ... one C statement per verified BPF instruction ...&lt;/p&gt;
&lt;p&gt;  r0 = runtime_context-&amp;gt;helper_data[0].address(r1, r2, r3, r4, r5, context);
}
`}&lt;/p&gt;

The eBPF-for-Windows transliterator from verified BPF bytecode to portable C suitable for MSVC compilation. The output is a signed-driver source file, one C statement per BPF instruction, that can be compiled and signed through the same pipeline as any other kernel driver. The golden test corpus lives at `microsoft/ebpf-for-windows/tests/bpf2c_tests/expected` [@github-com-bpf2ctests-expected].
&lt;p&gt;Four things stand out in the excerpt. &lt;em&gt;One BPF instruction maps to one C statement&lt;/em&gt;; the &lt;code&gt;// EBPF_OP_*&lt;/code&gt; comments name the opcode, and the line below it is the equivalent C. The eBPF VM&apos;s eleven registers become eleven C &lt;code&gt;uint64_t&lt;/code&gt; locals; MSVC&apos;s optimizer assigns them to native registers in the final &lt;code&gt;.sys&lt;/code&gt;. The &lt;code&gt;#pragma code_seg(push, &quot;xdp&quot;)&lt;/code&gt; directive names the program section the same way &lt;code&gt;SEC(&quot;xdp&quot;)&lt;/code&gt; does on Linux. And helper calls dispatch through a runtime table -- &lt;code&gt;runtime_context-&amp;gt;helper_data[0].address(...)&lt;/code&gt; -- so the signed driver remains portable across helper-ABI changes.&lt;/p&gt;
&lt;p&gt;The result is a kernel module that is a signed driver in every Windows sense of the term: HVCI checks pass, Kernel Mode Code Integrity (KMCI) [@learn-microsoft-com-downloads-sysmon] is satisfied, the Authenticode chain validates. eBPF-for-Windows native mode does not invent a new in-kernel trust boundary. It composes with the one Windows already has.&lt;/p&gt;

flowchart LR
    A[&quot;Restricted C source&quot;]
    B[&quot;clang -target bpf&quot;]
    C[&quot;BPF bytecode&quot;]
    D[&quot;PREVAIL verifier -- (user mode)&quot;]
    E[&quot;bpf2c -- transliterator&quot;]
    F[&quot;Portable C&quot;]
    G[&quot;MSVC compile&quot;]
    H[&quot;Signed .sys driver&quot;]
    I[&quot;Windows kernel -- (HVCI / KMCI)&quot;]
    A --&amp;gt; B --&amp;gt; C --&amp;gt; D --&amp;gt; E --&amp;gt; F --&amp;gt; G --&amp;gt; H --&amp;gt; I
&lt;h3&gt;The verifier moved&lt;/h3&gt;
&lt;p&gt;The most consequential architectural choice in eBPF-for-Windows is not visible in the binary. PREVAIL does not run inside the kernel. It runs inside the user-mode &lt;code&gt;eBPFSvc.exe&lt;/code&gt; service, which orchestrates verification and the subsequent compile-and-sign pipeline. The kernel never sees an unverified BPF program. By the time anything enters the kernel, it is either a signed driver (native mode) or a JIT-produced buffer that has already passed verification in user space (JIT mode, on non-HVCI hosts).&lt;/p&gt;
&lt;p&gt;This is a deliberate divergence from Linux. Linux runs its verifier inside the kernel because the kernel is the only place that can prevent unprivileged user space from loading unsafe programs. Windows can move the verifier out of the kernel because the kernel-mode trust boundary -- &lt;em&gt;the thing that can run&lt;/em&gt; -- is already protected by code signing. The verifier becomes a &lt;em&gt;correctness&lt;/em&gt; check rather than a &lt;em&gt;safety&lt;/em&gt; check at the kernel boundary; safety at the boundary is enforced by HVCI.&lt;/p&gt;
&lt;h3&gt;Hook coverage as of 2026&lt;/h3&gt;
&lt;p&gt;The hook surface on Windows is narrower than Linux&apos;s. As of 2026, eBPF-for-Windows exposes XDP-class network hooks, BIND, SOCK_OPS, SOCK_ADDR, and process-creation and process-exit hooks via Windows Filtering Platform callouts plus a process hook surface. There is no full kprobe surface. There are no LSM-equivalent hooks. The project README [@github-com-for-windows] labels itself &quot;work-in-progress.&quot; The networking-subset claim in this article is not marketing softening; it is the actual hook list.&lt;/p&gt;

The naive model of cross-OS eBPF says: same bytecode runtime, runs on both kernels. The actual model is more subtle and more interesting.&lt;p&gt;The bytecode is portable because both verifiers accept the same instruction encoding, now standardized at IETF as RFC 9669 [@rfc-editor-org-rfc-rfc9669html]. The verifier is portable because PREVAIL is an abstract interpreter that does not depend on Linux-specific kernel data structures. The &lt;em&gt;runtime&lt;/em&gt; is not portable: Linux runs verified bytecode through its in-kernel JIT; Windows transliterates verified bytecode to C and compiles it into a signed driver.&lt;/p&gt;
&lt;p&gt;So the cross-platform abstraction is the verifier, not the runtime. PREVAIL is the contract; each OS lifts verified bytecode into its own trust model. Linux trusts the verifier&apos;s output enough to JIT it in kernel mode; Windows distrusts in-kernel dynamic code by policy and lifts the verified bytecode out through a signed-driver compile. The portability boundary moved from &quot;same VM&quot; to &quot;same static analysis,&quot; and that is the architectural insight that makes the project work.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The runtime is not the cross-platform abstraction. The verifier is. PREVAIL is the contract; each OS lifts verified bytecode into its own trust model -- in-kernel JIT on Linux, signed-driver compile on Windows. eBPF-for-Windows is not &quot;same kernel hook, different OS&quot;; it is &quot;same bytecode contract, different OS-specific lifting.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Cross-OS eBPF works for the networking subset today. The general kernel observability case -- arbitrary kprobes, full LSM hooks, deep process introspection -- is still Linux-only because the &lt;em&gt;hooks themselves&lt;/em&gt; are Linux-internal. eBPF-for-Windows is a real convergence, but it is a &lt;em&gt;subset&lt;/em&gt; convergence. Section 7 zooms out and compares the two designs across the full set of dimensions practitioners actually use to choose.&lt;/p&gt;
&lt;h2&gt;7. Head-to-Head: Performance and Trust Models&lt;/h2&gt;
&lt;p&gt;Two designs. One emits, one computes. Practitioners need to know what each one costs, where each one&apos;s edges cut, and what attack classes each design enables. The right form for that comparison is a table.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;ETW&lt;/th&gt;
&lt;th&gt;Linux eBPF&lt;/th&gt;
&lt;th&gt;eBPF for Windows&lt;/th&gt;
&lt;th&gt;DTrace&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;In-kernel filter language&lt;/td&gt;
&lt;td&gt;None (level + keyword mask only)&lt;/td&gt;
&lt;td&gt;Verified bytecode&lt;/td&gt;
&lt;td&gt;Verified bytecode&lt;/td&gt;
&lt;td&gt;D scripting language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-kernel aggregation&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Maps (per-CPU and shared)&lt;/td&gt;
&lt;td&gt;Maps&lt;/td&gt;
&lt;td&gt;Aggregations primitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Producer per-event cost&lt;/td&gt;
&lt;td&gt;Constant: format + memcpy to per-CPU buffer&lt;/td&gt;
&lt;td&gt;JIT-compiled native code at hook&lt;/td&gt;
&lt;td&gt;JIT or signed-driver call at hook&lt;/td&gt;
&lt;td&gt;Probe handler call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verifier&lt;/td&gt;
&lt;td&gt;Driver signing only&lt;/td&gt;
&lt;td&gt;Linux in-kernel heuristic verifier&lt;/td&gt;
&lt;td&gt;PREVAIL in user mode + KMCI&lt;/td&gt;
&lt;td&gt;None (D is interpreted, safe-by-construction)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verifier soundness incidents&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;4 widely-disclosed CVEs (2020-2023)&lt;/td&gt;
&lt;td&gt;None disclosed&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hook coverage&lt;/td&gt;
&lt;td&gt;Universal across Windows API surface&lt;/td&gt;
&lt;td&gt;Universal: kprobe, uprobe, tracepoint, XDP, TC, LSM, sched&lt;/td&gt;
&lt;td&gt;XDP, BIND, SOCK_OPS, SOCK_ADDR, process&lt;/td&gt;
&lt;td&gt;Solaris/BSD/macOS provider set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-platform&lt;/td&gt;
&lt;td&gt;Windows only&lt;/td&gt;
&lt;td&gt;Linux only&lt;/td&gt;
&lt;td&gt;Source-compatible with Linux subset&lt;/td&gt;
&lt;td&gt;Solaris, FreeBSD, macOS (legacy)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;Per-CPU ring buffer, .etl files&lt;/td&gt;
&lt;td&gt;Ringbuf, perf_event_array, maps&lt;/td&gt;
&lt;td&gt;Ringbuf, maps&lt;/td&gt;
&lt;td&gt;Per-CPU buffers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust model&lt;/td&gt;
&lt;td&gt;Manifest registration + driver signing&lt;/td&gt;
&lt;td&gt;Verifier + CAP_BPF + CAP_PERFMON&lt;/td&gt;
&lt;td&gt;Verifier + HVCI + driver signing&lt;/td&gt;
&lt;td&gt;Privilege check + safe-by-construction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adoption pattern&lt;/td&gt;
&lt;td&gt;Defender, Sysmon, CrowdStrike, SentinelOne, Carbon Black&lt;/td&gt;
&lt;td&gt;Cilium, Falco, Tetragon, Tracee, Pixie, Sysmon for Linux&lt;/td&gt;
&lt;td&gt;Pre-production; Azure test deployments&lt;/td&gt;
&lt;td&gt;Solaris/macOS legacy + bpftrace via inspiration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best suited for&lt;/td&gt;
&lt;td&gt;Forensic capture across the entire Windows API surface&lt;/td&gt;
&lt;td&gt;Hot-path filtering and aggregation with arbitrary kernel hooks&lt;/td&gt;
&lt;td&gt;Cross-platform networking observability&lt;/td&gt;
&lt;td&gt;Interactive debugging on Solaris-lineage systems&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;The asymptotic argument&lt;/h3&gt;
&lt;p&gt;Two designs can be compared asymptotically. ETW carries N events of average size S; the kernel-to-user wire cost is Omega(NS) -- the unavoidable lower bound for streaming N events. eBPF can reduce that to O(M) where M is the aggregation size, for workloads that aggregate before the events cross the boundary. The bpftrace histogram from section 4 is the concrete example: &lt;code&gt;vfs_read&lt;/code&gt; can fire ten million times per second while the user-side bandwidth is zero, because the per-CPU histogram never crosses the boundary until print time.&lt;/p&gt;
&lt;p&gt;The asymmetry is the entire reason eBPF makes sense for high-frequency telemetry. It is also the reason every cloud-native observability tool from 2018 onward is on eBPF. When the producer rate exceeds the user-space consumption rate, you do not have a choice: you either drop events or aggregate them in-kernel. ETW can drop. Only eBPF can aggregate.&lt;/p&gt;
&lt;h3&gt;The tail-call attack class&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;bpf_tail_call(ctx, &amp;amp;prog_array, index)&lt;/code&gt; is powerful and its power has structural consequences. From the BPF trampoline v3 cover letter [@lore-kernel-org-1-astkernelorg-2], the kernel team is explicit that the trampoline was designed in part as a &lt;em&gt;replacement&lt;/em&gt; for tail-call-based chaining: &lt;em&gt;&quot;In many cases it can be used as a replacement for bpf_tail_call-based program chaining.&quot;&lt;/em&gt; The motivation is structural -- there are three attack classes implicit in the tail-call mechanism, and the trampoline avoids them.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Branch-target injection on the tail-call dispatcher.&lt;/em&gt; Pre-mitigation kernels exposed an indirect branch from kernel mode -- the dispatcher selecting its target from a user-controllable &lt;code&gt;prog_array&lt;/code&gt; index. That is exactly the shape of a Spectre-v2 gadget. Mitigation: retpolined dispatcher and the BPF trampoline replacement that avoids the indirect branch entirely.The qualitative reason fentry beats kprobe is not a benchmark; it is the avoidance of a retpoline. The v3 patch cover letter spells this out: &lt;em&gt;&quot;To avoid the high cost of retpoline the attached BPF programs are called directly.&quot;&lt;/em&gt; Real numbers vary by microarchitecture, retpoline implementation, and the rest of the kernel-build configuration, but the structural reason is the same on every machine.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Recursion-bound bypass.&lt;/em&gt; The 33-call cap protects the verifier&apos;s termination proof for a single program from being bypassed by chaining, but it is a per-execution counter. A sequence of attached programs at different attach points can still produce arbitrary aggregate work. The mitigation lives in per-event scheduling, not in the verifier.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Speculative type confusion.&lt;/em&gt; The verifier proves a single program&apos;s register-type invariants. The target of a tail call is selected at runtime from a map, so speculative execution can execute a different program under the calling program&apos;s type-state. Mitigation: indirect-call hardening shared with the rest of the kernel.&lt;/p&gt;

flowchart LR
    A[&quot;Calling BPF program&quot;]
    B[&quot;bpf_tail_call(ctx, &amp;amp;arr, idx)&quot;]
    C[&quot;JIT dispatcher -- (indirect jump)&quot;]
    D{&quot;Map slot at idx&quot;}
    E[&quot;Target BPF program&quot;]
    F[&quot;Speculative path -- (wrong target)&quot;]
    G[&quot;Retpoline / BPF trampoline -- (direct call)&quot;]
    A --&amp;gt; B --&amp;gt; C --&amp;gt; D
    D -- correct --&amp;gt; E
    D -. speculative .-&amp;gt; F
    G -. mitigation .-&amp;gt; C
&lt;h3&gt;The ETW user-mode bypass&lt;/h3&gt;
&lt;p&gt;ETW has its own structural attack class, mentioned in section 3 and worth restating in the trust-model context. A process that wants to silence its own ETW emissions can patch &lt;code&gt;ntdll!EtwEventWrite&lt;/code&gt; to a &lt;code&gt;ret&lt;/code&gt; instruction in its own address space. The kernel buffer never sees the event. EDR vendors monitor for this integrity violation out of band, and use the patch itself as a high-confidence detection signal.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; ETW&apos;s emission path runs in the calling process&apos;s own address space. A process that wants to hide its activity can patch the &lt;code&gt;ntdll!EtwEventWrite&lt;/code&gt; thunk to &lt;code&gt;ret&lt;/code&gt;, silencing emissions before they reach the kernel buffer. EDR vendors monitor for this integrity violation out of band, and treat the patch as a detection in its own right. The deeper question is whether any user-mode emission primitive can be tamper-resistant under hostile user-mode code. The current answer is &quot;no&quot;: the mitigation has been to move the trust boundary into the kernel, via PPL, the kernel-only Threat-Intelligence provider, and (on Linux) LSM hooks that observe &lt;code&gt;mprotect&lt;/code&gt; and image-load operations directly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Trust models, side by side&lt;/h3&gt;
&lt;p&gt;ETW trusts manifest registration plus Code Integrity for kernel drivers. The kernel only emits events; the only adversary-controllable surface is the user-mode provider, and the integrity-violation tell catches the obvious attack.&lt;/p&gt;
&lt;p&gt;Linux eBPF trusts the verifier plus &lt;code&gt;CAP_BPF&lt;/code&gt; and &lt;code&gt;CAP_PERFMON&lt;/code&gt;. The verifier is the kernel-mode safety boundary; capabilities gate who can load programs at all. Both have been the source of soundness CVEs and exploitation paths. Defense in depth: unprivileged eBPF off by default since 5.16, hardening of the indirect-call dispatcher, ongoing verifier work.&lt;/p&gt;
&lt;p&gt;eBPF for Windows trusts PREVAIL plus HVCI driver signing. The verifier runs in user mode; the kernel only ever sees a signed driver or a JIT-emitted buffer that has already passed the verifier. The composition is &lt;em&gt;strictly more conservative&lt;/em&gt; than Linux eBPF, because it stacks the verifier on top of the signing model rather than replacing it. Microsoft is using the Windows kernel-mode trust mechanism &lt;em&gt;and&lt;/em&gt; adding the eBPF verifier to it, not choosing between them.&lt;/p&gt;
&lt;p&gt;The next layer up from the kernel substrate is the consumer layer -- the agents and SIEM pipelines practitioners actually ship. That production stack is what determines which substrate practitioners reach for first.&lt;/p&gt;
&lt;h2&gt;8. Production Adoption: The Agent Layer&lt;/h2&gt;
&lt;p&gt;The substrate matters because the consumer stack does. On Linux, eBPF is the foundation of every serious cloud-native security and observability project. On Windows, ETW is the same. The portable subset is small but real, and it is growing.&lt;/p&gt;
&lt;h3&gt;The Linux side&lt;/h3&gt;
&lt;p&gt;Cilium [@cilium-io] is the dominant eBPF-based networking project, CNCF-graduated [@falco-org-docs] and shipping Kubernetes cluster networking, NetworkPolicy enforcement, and a service mesh implementation. Falco [@falco-org], originally created by Sysdig and now CNCF-graduated, provides eBPF-based runtime threat detection driven by a rules engine. Tetragon [@tetragon-io-docs-overview], a Cilium subproject, attaches eBPF programs to kprobes and LSM hooks for in-kernel enforcement -- not just observation but the ability to block. Tracee [@github-com-aquasecurity-tracee] from Aqua Security is an eBPF runtime security tool. Pixie [@docs-px-dev], originally Pixie Labs and now under New Relic, uses eBPF for auto-instrumentation of services running in Kubernetes.&lt;/p&gt;
&lt;p&gt;Sysmon for Linux [@github-com-microsoft-sysmonforlinux] is the most architecturally interesting member of the list. Microsoft, the company that built ETW and Sysmon, ported Sysmon to Linux by replacing the ETW back end with eBPF kprobes via the &lt;code&gt;SysinternalsEBPF&lt;/code&gt; library. The XML configuration schema and Event IDs are preserved, so SOC analysts see the same channel from either OS. It is the production demonstration that ETW and eBPF can be made surface-equivalent to a consumer.&lt;/p&gt;
&lt;h3&gt;The Windows side&lt;/h3&gt;
&lt;p&gt;Sysmon [@learn-microsoft-com-downloads-sysmon] is the canonical ETW consumer reference design, authored by Mark Russinovich and Thomas Garnier and free from Microsoft. Microsoft Defender for Endpoint [@learn-microsoft-com-defender-endpoint] is the commercial Microsoft EDR product, ETW-driven and cloud-connected. CrowdStrike Falcon, SentinelOne, and Carbon Black are the major third-party EDRs, all built on ETW. krabsetw [@github-com-microsoft-krabsetw] is Microsoft&apos;s C++ ETW consumer library; the &lt;code&gt;Microsoft.Diagnostics.Tracing.TraceEvent&lt;/code&gt; package is the .NET equivalent.&lt;/p&gt;
&lt;h3&gt;The toolchain layer&lt;/h3&gt;
&lt;p&gt;The eBPF world comes with a toolchain that does not have a direct ETW counterpart. &lt;code&gt;libbpf&lt;/code&gt; [@github-com-libbpf-libbpf] is the canonical C library for loading and managing eBPF programs. &lt;code&gt;bpftool&lt;/code&gt; [@github-com-libbpf-bpftool] is the inspection utility. &lt;code&gt;BCC&lt;/code&gt; [@github-com-iovisor-bcc] is the older Python-binding toolkit. &lt;code&gt;bpftrace&lt;/code&gt; [@github-com-iovisor-bpftrace] is the DSL inspired by DTrace. &lt;code&gt;cilium/ebpf&lt;/code&gt; [@github-com-cilium-ebpf] is the Go library; &lt;code&gt;aya&lt;/code&gt; [@github-com-rs-aya] and &lt;code&gt;libbpf-rs&lt;/code&gt; [@github-com-libbpf-rs] are the Rust libraries. The toolchain coverage tells you something about the substrate: a Go developer can write an eBPF program and have it loaded by their existing service binary, because the load-verify-attach lifecycle has a Go binding.&lt;/p&gt;
&lt;p&gt;ETW has its own toolchain -- &lt;code&gt;tracerpt.exe&lt;/code&gt;, Windows Performance Analyzer, BenchmarkDotNet, krabsetw -- but the toolchain is shaped around &lt;em&gt;consuming&lt;/em&gt; events, not around emitting programs into the kernel. The asymmetry of the toolchains mirrors the asymmetry of the substrates.&lt;/p&gt;
&lt;h3&gt;The decision guide&lt;/h3&gt;

**Windows EDR or building on Microsoft Defender for Endpoint.** Use ETW plus Sysmon plus the `Microsoft-Windows-Threat-Intelligence` provider. eBPF for Windows is not yet a substitute for Defender-grade kernel telemetry; the hook surface is too narrow.&lt;p&gt;&lt;strong&gt;Linux runtime-security or cluster networking.&lt;/strong&gt; Use eBPF. Pick &lt;code&gt;libbpf&lt;/code&gt; or &lt;code&gt;cilium/ebpf&lt;/code&gt; for the language binding. Attach LSM hooks for enforcement; fentry for observability. The verifier will fight you; that is expected.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cross-platform networking observability with one source surface.&lt;/strong&gt; Use eBPF for Windows and Linux eBPF together, restricted to the XDP, SOCK_ADDR, SOCK_OPS, and BIND hooks. The Linux source compiles unchanged on Windows for this subset.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Forensic capture across the full Windows API surface.&lt;/strong&gt; Use ETW into &lt;code&gt;.etl&lt;/code&gt; files, analyzed in Windows Performance Analyzer. Nothing else covers that breadth on Windows.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The Sysmon-for-Linux case study is the cleanest practical justification for the abstract-surface convergence. If your SIEM consumes Sysmon XML and matches on Event ID and field, you can run a fleet of Windows hosts on ETW and Linux hosts on eBPF and the SIEM will not know the difference. The substrate is invisible at the consumer&apos;s contract; what matters is that the contract is preserved across the back-end change. This is the production realization of the engineering pattern -- different mechanisms, identical schemas -- that the rest of the article has been describing in architectural terms.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The consumer stack has converged at the surface layer: XML configs, Event IDs, EDR vendor APIs. The substrate has not, and the open problems in the next section are what stands in the way.&lt;/p&gt;
&lt;h2&gt;9. Open Problems and the Frontier&lt;/h2&gt;
&lt;p&gt;What can we not do yet? Four open problems will shape the next five years of kernel observability.&lt;/p&gt;
&lt;h3&gt;9.1 Verifier-driven false rejection&lt;/h3&gt;
&lt;p&gt;Programs that PREVAIL and a human can both prove safe still get rejected by the Linux verifier, which returns the cryptic &lt;em&gt;&quot;verifier complexity limit reached&quot;&lt;/em&gt; error. EDR vendors end up fighting the verifier rather than writing the program they want. The workarounds are real and ugly: &lt;code&gt;__attribute__((noinline))&lt;/code&gt; annotations to force the compiler to emit function boundaries the verifier can prune around, explicit bound assertions that re-derive properties the compiler already knows, &lt;code&gt;bpf_loop()&lt;/code&gt; to externalize loops the verifier cannot trace. The HotOS 2023 thesis is exactly that this is not a bug -- it is a property of any heuristic verifier under the soundness-completeness-scalability triangle. The completeness leg is the one the Linux verifier gives up first, every time.&lt;/p&gt;
&lt;p&gt;The frontier here is twofold. On one side, the verifier is becoming more capable: bounded loops, &lt;code&gt;bpf_for_each_map_elem&lt;/code&gt;, kfuncs, and the trampoline-based attach mechanisms have all expanded what the verifier can prove. On the other side, PREVAIL&apos;s polynomial-time abstract-interpretation approach represents an alternative architectural lineage. Neither approach removes the underlying undecidability. Both make the rejection threshold higher.&lt;/p&gt;
&lt;h3&gt;9.2 Cross-OS eBPF ABI&lt;/h3&gt;
&lt;p&gt;The eBPF Foundation&apos;s RFC 9669 [@rfc-editor-org-rfc-rfc9669html], published as an IETF Independent Submission in October 2024, standardized the &lt;em&gt;instruction set architecture&lt;/em&gt; for BPF programs. The RFC describes the 64-bit ISA, the encoding of instructions, the memory model, and the verifier&apos;s basic obligations. It is the cleanest cross-OS contract eBPF has ever had.&lt;/p&gt;
&lt;p&gt;What the RFC does &lt;em&gt;not&lt;/em&gt; standardize: helpers, map types, and hook semantics. Those remain Linux-defined-in-practice. The eBPF-for-Windows helper set is a subset, with extensions for Windows-specific concepts. The FreeBSD and illumos ports have their own subsets. A single observability agent that runs everywhere needs more than a standardized ISA; it needs a standardized helper API and a standardized hook taxonomy. Today, EDR vendors writing cross-OS agents ship two distinct programs that share a build system and not much else.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; RFC 9669 is the ISA standard. It defines what BPF bytecode looks like and what the verifier must check. It does not define which helpers a program can call, what the map types are, or what hooks the program can attach to. Those are the parts that vary between Linux, Windows, and the BSDs. Standardizing them is more of a committee problem than a research problem -- a meaningful subset is achievable; a full superset probably is not.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;9.3 ETW evasion at the trust boundary&lt;/h3&gt;
&lt;p&gt;The user-mode &lt;code&gt;EtwEventWrite&lt;/code&gt; patching attack class is roughly 2020-vintage but has not gone away. The kernel-emitted &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; provider is the current best mitigation: kernel signals cannot be patched from user mode, so an attacker who silences user-mode emissions still trips kernel-only signals on &lt;code&gt;mprotect&lt;/code&gt;, image load, and remote thread creation.&lt;/p&gt;
&lt;p&gt;The deeper structural question is whether any user-mode primitive can ever be tamper-resistant under hostile user-mode code. The short answer is no, which is why the answer keeps moving the trust boundary into the kernel -- through PPL, through LSM, through signed drivers. On Linux, the same pattern shows up: hostile-user-mode-resistant telemetry must run inside the kernel, which is why the LSM hooks are the part of the eBPF hook surface that matters most for EDR.&lt;/p&gt;
&lt;h3&gt;9.4 Hot-path overhead at scale&lt;/h3&gt;
&lt;p&gt;Production environments routinely run Falco, Cilium, and a vendor EDR on the same kernel, each attaching probes to the same hook. The marginal cost of an eBPF kprobe on a five-million-events-per-second syscall is not zero, and the cost compounds non-linearly when three different agents attach to the same hook with three different programs.&lt;/p&gt;
&lt;p&gt;The current partial mitigations are real. &lt;code&gt;fentry&lt;/code&gt;/&lt;code&gt;fexit&lt;/code&gt; plus the BPF trampoline removed the per-attach trap-frame cost. &lt;code&gt;kprobe.multi&lt;/code&gt;, added in Linux 5.18, lets a single program attach to multiple functions with one trampoline. BPF-link iteration lets one agent observe what another has attached. But none of these compose perfectly: three different vendors with three different agents end up with three different trampolines on the same function. The structural fix is &lt;em&gt;trampoline sharing&lt;/em&gt;, and the implementation is attach-type-specific.The multi-agent attach problem is the eBPF version of a familiar systems issue: when N independent consumers each install their own instrumentation at the same point, the cost is N times the cost of one. Linux has solved this once for kprobes (with &lt;code&gt;kprobe.multi&lt;/code&gt;) and is solving it again for the BPF trampoline. Whether the same pattern can be made cheap for fentry attaches across LSM hooks is an open implementation question.&lt;/p&gt;
&lt;p&gt;The frontier of kernel observability is not &quot;build a new substrate.&quot; It is &quot;make the existing substrates compose under multi-tenant production load.&quot;&lt;/p&gt;
&lt;h2&gt;10. Two Generations&lt;/h2&gt;
&lt;p&gt;Return to the SOC analyst from section 1. The Sysmon Operational channel looks the same on both hosts. Now you know why -- and also why the similarity is a deliberate engineering choice rather than a coincidence.&lt;/p&gt;
&lt;p&gt;ETW is mature, has full Windows coverage, is emission-only. It is a &lt;em&gt;catalog&lt;/em&gt; of events. Every Windows subsystem registers a provider, every provider declares a manifest, every event has a stable schema. A consumer that knows the manifest knows what to expect. The trust boundary is the kernel-mode driver signing model. The cost is that aggregation, sampling, and filtering all happen in user space, after the event has crossed the boundary.&lt;/p&gt;
&lt;p&gt;eBPF is programmable, has filter and aggregation in-kernel, has a verifier. It is a &lt;em&gt;language&lt;/em&gt; for asking questions of the kernel, not a catalog of pre-defined answers. The trust boundary is the verifier, which is a research-grade static analyzer running as kernel code. Linux&apos;s verifier shipped four widely-disclosed soundness bugs in four years. PREVAIL trades that soundness leg for a more conservative completeness story. The trade-offs are not finished.&lt;/p&gt;
&lt;p&gt;eBPF-for-Windows is the convergence experiment. The native mode -- PREVAIL plus &lt;code&gt;bpf2c&lt;/code&gt; plus MSVC plus a signed &lt;code&gt;.sys&lt;/code&gt; driver -- is the first cross-OS-portable kernel-observability primitive. As of 2026 it covers a networking subset of hooks, not the full Linux surface. That gap is not architectural; it is a list of hooks Microsoft has not yet exposed. The pattern is generalizable: cross-OS observability lives in the verifier, not in the runtime, and each OS lifts verified bytecode into its own trust model.&lt;/p&gt;
&lt;p&gt;The generation gap is literal. ETW (2000) is an event bus. eBPF (2014) is a programmable kernel substrate. Both will still ship in 2035. Both will still be the right answer for some workloads. The interesting work for the next decade is in the convergence layer -- helper-API standardization, hook-point taxonomy alignment, verifier completeness -- and in the multi-tenant production engineering that makes ten different agents on one kernel cheaper than ten times one agent.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Kernel observability has matured from event emission to programmable kernel computation. That generation gap is why eBPF-for-Windows -- a small, work-in-progress project -- is one of the more architecturally significant operating-system-telemetry events of the last decade. The portable abstraction is not the runtime. It is the static analyzer.&lt;/p&gt;
&lt;/blockquote&gt;

No. As of 2026, eBPF for Windows [@github-com-for-windows] covers a networking-heavy subset of hooks -- XDP, BIND, SOCK_OPS, SOCK_ADDR, and process creation and exit -- and is not yet a substitute for Defender-grade kernel telemetry. ETW remains the canonical Windows observability substrate. The convergence between the two is real for the networking subset, and is the work-in-progress for the rest of the surface.

Because it is a heuristic abstract interpreter on a Turing-complete ISA, and Rice&apos;s theorem says no such verifier can be simultaneously sound, complete, and decidable. Real verifiers ship with all three approximately, and the soundness leg fails first when state pruning loses information at a join point. CVE-2023-2163 [@nvd-nist-gov-2023-2163], CVE-2022-23222 [@nvd-nist-gov-2022-23222], CVE-2021-3490 [@nvd-nist-gov-2021-3490], and CVE-2020-8835 [@nvd-nist-gov-2020-8835] are all instances of that pattern.

For the networking subset (XDP, SOCK_ADDR, SOCK_OPS, BIND), yes -- eBPF for Windows [@github-com-for-windows] is source-compatible with Linux eBPF for those hooks. For arbitrary kprobes or LSM hooks, no -- those hooks are Linux-internal and eBPF for Windows does not expose equivalents. Cross-platform agents typically ship two binaries that share a build system.

Since Linux 5.16 (March 2022) [@kernel-org-bpf-indexhtml], `kernel.unprivileged_bpf_disabled=1` is the kernel default. Production EDRs run with `CAP_BPF` plus `CAP_PERFMON` or root. Leaving unprivileged eBPF enabled was the entry point for several verifier CVEs, so the conservative default is correct.

A kprobe is a runtime breakpoint mechanism: the kernel patches a trap instruction at the target address, and the trap handler invokes the attached eBPF program. fentry uses the BPF trampoline [@lore-kernel-org-1-astkernelorg] -- a small JIT-emitted dispatcher that calls attached BPF programs with a direct call, avoiding the retpoline penalty an indirect dispatch would pay on Spectre-mitigated kernels. Starovoitov&apos;s framing: *&quot;practically zero overhead&quot;* for fentry, relative to the kprobe trap-frame cost.

No. ETW sessions filter by provider, keyword, and level. That is it. Any per-event computation -- counting, sampling, stack-trace folding, downsampling -- runs in user mode on the consumer side, after the event has crossed the kernel-user boundary. The lack of an in-kernel filter language is the structural reason eBPF can do things ETW cannot, like aggregate ten million `vfs_read` calls per second into a histogram without saturating the wire.

Sysmon for Linux [@github-com-microsoft-sysmonforlinux] replaces the ETW back end with eBPF kprobes via Microsoft&apos;s `SysinternalsEBPF` library. The XML configuration schema, Event IDs, and Operational channel output are preserved, so a SIEM consumer sees identical telemetry from either OS. It is the production demonstration that ETW and eBPF can be made surface-equivalent to a consumer.
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;ebpf-vs-etw-two-generations-of-kernel-observability&quot; keyTerms={[
  { term: &quot;ETW&quot;, definition: &quot;Event Tracing for Windows. The Windows 2000-onward kernel-mediated event bus, with providers, sessions, consumers, and per-CPU ring buffers.&quot; },
  { term: &quot;eBPF&quot;, definition: &quot;Extended Berkeley Packet Filter. A safe, sandboxed kernel virtual machine introduced in Linux 3.18 (2014) that runs verified user-supplied bytecode at attached hook points.&quot; },
  { term: &quot;Verifier&quot;, definition: &quot;The kernel-side static analyzer that proves termination and memory safety of every eBPF program before load. The Linux verifier uses a heuristic register-state lattice; PREVAIL uses zone-domain abstract interpretation.&quot; },
  { term: &quot;BPF Map&quot;, definition: &quot;A kernel-managed key-value store accessible from inside an eBPF program and from user space. Types include hash, array, per-CPU hash, and ring buffer.&quot; },
  { term: &quot;Ringbuf&quot;, definition: &quot;The BPF ring buffer map type (Linux 5.8). A multi-producer single-consumer transport that preserves cross-CPU event ordering.&quot; },
  { term: &quot;HVCI&quot;, definition: &quot;Hypervisor-enforced Code Integrity. The Windows feature that uses the hypervisor to enforce kernel-mode code signing. Blocks dynamic kernel-mode code generation by default.&quot; },
  { term: &quot;PREVAIL&quot;, definition: &quot;The user-mode eBPF verifier used by eBPF for Windows. Based on numerical abstract interpretation over the zone domain plus intervals, with formal grounding in Gershuni et al. PLDI 2019.&quot; },
  { term: &quot;bpf2c&quot;, definition: &quot;The eBPF-for-Windows transliterator that emits portable C from verified BPF bytecode, one C statement per BPF instruction. The C is compiled by MSVC into a signed .sys driver.&quot; }
]} questions={[
  { q: &quot;Why did performance counters fail for security telemetry?&quot;, a: &quot;Three structural reasons: sampling-rate floor (counters aggregate at the consumer&apos;s query rate, hiding individual events), no event identity (a count tells you N happened, not which user did what), and no causal order (two counters sampled in sequence are not causally ordered with respect to the events they describe).&quot; },
  { q: &quot;What three properties does the soundness-completeness-scalability triangle say a verifier can&apos;t have all of?&quot;, a: &quot;Soundness (never accept an unsafe program), completeness (never reject a safe program), and scalability (run in polynomial time on real programs). Rice&apos;s theorem implies no decision procedure for a non-trivial semantic property on a Turing-complete ISA can have all three. Real verifiers must trade off.&quot; },
  { q: &quot;How does eBPF for Windows lift verified bytecode into the Windows kernel?&quot;, a: &quot;In native mode, PREVAIL verifies the bytecode in user space. On success, the bpf2c tool transliterates each verified BPF instruction to one C statement, MSVC compiles the C to a signed .sys kernel driver, and the kernel loads the driver through the standard Authenticode / HVCI / KMCI signing pipeline.&quot; },
  { q: &quot;Name two structural attack-class implications of bpf_tail_call.&quot;, a: &quot;Branch-target injection on the tail-call dispatcher (an indirect jump from kernel mode selecting its target from a user-controllable map slot is a Spectre-v2 gadget) and speculative type confusion (the verifier proves a single program&apos;s register types, but a tail call&apos;s target is a runtime-resolved map slot, so speculative execution can run a different program under the wrong type-state).&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>ebpf</category><category>etw</category><category>kernel-observability</category><category>edr</category><category>verifier</category><category>windows-internals</category><category>linux-kernel</category><category>tracing</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>ETW: How Windows 2000&apos;s Performance Hack Became the EDR Substrate</title><link>https://paragmali.com/blog/etw-how-windows-2000s-performance-hack-became-the-edr-substr/</link><guid isPermaLink="true">https://paragmali.com/blog/etw-how-windows-2000s-performance-hack-became-the-edr-substr/</guid><description>Event Tracing for Windows is the kernel-buffered observability bus every modern Windows EDR consumes. This is the architecture, the attacks, and the one provider that survives them.</description><pubDate>Mon, 11 May 2026 00:00:00 GMT</pubDate><content:encoded>
Event Tracing for Windows is the high-rate, kernel-buffered observability bus that every modern Windows EDR consumes. A 2007-era architectural decision -- letting eight sessions read the same provider concurrently -- is what makes multi-vendor coexistence possible on a single host. Microsoft&apos;s `Microsoft-Windows-Threat-Intelligence` provider, gated behind Protected Process Light and an ELAM-signed Antimalware certificate since the Windows 10 RS-era, fires from the kernel side of memory-modifying syscalls and survives the user-mode `EtwEventWrite` patch class that defined red-team tradecraft from 2020 to 2022. The remaining attack surface -- BYOVD-driven kernel tampering -- is structurally narrowed by the Vulnerable Driver Blocklist enabled by default since Windows 11 22H2, with the residual sub-microsecond-payload gap remaining as ETW&apos;s irreducible &quot;observation, not enforcement&quot; limit.
&lt;h2&gt;1. Why didn&apos;t the patch silence Defender?&lt;/h2&gt;
&lt;p&gt;A red-team operator drops onto a 2026 Defender [@paragmali-com-war-it]-protected box and runs the move that worked five years ago. They locate &lt;code&gt;ntdll!EtwEventWrite&lt;/code&gt; in the calling process, write the byte &lt;code&gt;0xC3&lt;/code&gt; over the function prologue, and the calling process now silently fails to emit user-mode ETW events. The .NET CLR provider goes dark. &lt;code&gt;Invoke-Mimikatz&lt;/code&gt; loads from &lt;code&gt;execute-assembly&lt;/code&gt; without lighting up &lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt;. Defender catches the credential dump [@paragmali-com-and-the] anyway, four seconds later, and the operator is on a SOC analyst&apos;s screen before the shellcode finishes running.&lt;/p&gt;
&lt;p&gt;The patch worked. The .NET tracing provider in that process is mute. Attach a debugger and disassemble the function prologue: the first byte is now &lt;code&gt;0xC3&lt;/code&gt;, the near-return opcode [@felixcloutier-ret] [@felixcloutier-ret], and any caller falls straight back to its return address before producing a single event. The technique is the one Adam Chester documented in March 2020 [@xpn-hiding-dotnet] [@xpn-hiding-dotnet], and to a generation of red teamers it has functioned as a near-universal ETW evasion ever since.&lt;/p&gt;
&lt;p&gt;So why did Defender still fire?&lt;/p&gt;
&lt;p&gt;Because Defender does not consume &lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt; to detect a credential dump. It consumes &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; [@fluxsec-eti] [@fluxsec-eti] -- a provider whose GUID is &lt;code&gt;{f4e1897c-bb5d-5668-f1d8-040f4d8dd344}&lt;/code&gt;, whose events fire from inside the kernel side of memory-modifying syscalls, and whose producer the user-mode patcher cannot reach. The patch operated on a &lt;code&gt;ntdll&lt;/code&gt; trampoline. The signal Defender used was emitted from a different layer entirely.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Modern Windows EDR is layered on ETW, and the layers fail under different attacks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That single asymmetry -- one provider goes dark to a one-byte patch, another fires from a place the patcher cannot touch -- is the spine of this article. Around it sits a 26-year story of one Microsoft team accidentally building the substrate of every modern Windows endpoint security product.&lt;/p&gt;

A high-rate, kernel-buffered tracing facility built into Windows since 2000. Components called *providers* emit events tagged with a GUID; *controllers* configure trace sessions; *consumers* subscribe to live event streams or read recorded `.etl` files. ETW was designed for low-overhead developer diagnostics; it was retrofitted into the security-telemetry substrate that all modern Windows EDR products consume.

A class of endpoint security product that ingests behavioural telemetry (process creation, image load, memory allocation, network connection, registry change), correlates it against detection logic, and produces alerts and response actions. On Windows, the dominant EDRs (Microsoft Defender for Endpoint, CrowdStrike Falcon, SentinelOne, Elastic Defend, Wazuh, Sysmon-plus-SIEM) all build on ETW or on the same kernel callbacks ETW exposes to the user-mode tier.
&lt;p&gt;To understand why a one-byte patch silences one provider but not another, we have to go back to a Windows 2000 design decision about per-CPU ring buffers.&lt;/p&gt;
&lt;h2&gt;2. ETW in Windows 2000: the performance problem that started it all&lt;/h2&gt;
&lt;p&gt;Imagine a 1999 network-driver author. A customer&apos;s NT4 production server is corrupting packets under load and the only available instrumentation is &lt;code&gt;DbgPrint&lt;/code&gt;. Each call serialises through a kernel debug port, costs measurable percentage points of CPU on a busy box, and ships data to whoever happens to have the kernel debugger attached. The customer says no. The bug reproduces only at production traffic levels. You cannot ship enough printf-debugging through a debug port to find it.&lt;/p&gt;
&lt;p&gt;That is the engineering pain Insung Park and Ricky Buch&apos;s team was solving when ETW shipped with Windows 2000. Their design moves -- recorded years later in the definitive April 2007 MSDN Magazine article on the Vista upgrade [@ms-park-buch-2007] [@ms-park-buch-2007] -- still define the architecture two and a half decades later.&lt;/p&gt;
&lt;p&gt;The first move was per-CPU ring buffers. A producer on CPU 7 writes to CPU 7&apos;s buffer with no lock contention against producers on other CPUs. Hot-path tracing on a 64-core machine does not serialise. The kernel allocates at least two buffers per logical processor [@ms-event-trace-props] [@ms-event-trace-props] so a producer can keep writing while a writer thread drains the previous buffer.&lt;/p&gt;
&lt;p&gt;The second move was an asynchronous writer thread. The producer never blocks on disk I/O. It writes to its CPU&apos;s buffer and returns. A separate kernel thread drains buffers to file or hands them to a real-time consumer. ETW pushes the latency tax onto the consumer and the storage path, never onto the producer&apos;s hot loop.&lt;/p&gt;
&lt;p&gt;The third move was dynamic enable and disable. Park and Buch describe the resulting capability in one sentence:&lt;/p&gt;

ETW gives you the ability to enable and disable logging dynamically, making it easy to perform detailed tracing in production environments without requiring reboots or application restarts. -- Park &amp;amp; Buch, *MSDN Magazine*, April 2007 [@ms-park-buch-2007]
&lt;p&gt;That sentence is the entire reason ETW could later become the EDR substrate. A producer compiles its trace points into shipping code at low cost; a controller flips them on at runtime when somebody actually wants the data. Without that property, you cannot build a security product that ships universal kernel tracing on a billion endpoints.&lt;/p&gt;
&lt;p&gt;The fourth move was the trichotomy of providers, controllers, and consumers [@ms-etw-wdk] [@ms-etw-wdk]. Microsoft did not write ETW as an internal-only facility. From the start, third parties could write providers (driver authors instrumenting their own code), controllers (performance tools starting and stopping sessions), and consumers (analyzers reading event streams). The architecture is open by design.&lt;/p&gt;

A component that emits ETW events, identified by a GUID. A provider is registered with the system at runtime via the `EventRegister` API (or its predecessor `RegisterTraceGuids` for classic providers) and emits events via `EventWrite` (or `TraceEvent`). Providers ship inside Windows itself, inside Microsoft applications, and inside any third-party binary that wants to expose tracing.

A component that creates, configures, enables, and stops trace sessions. Controllers select which providers a session subscribes to and at which level and keyword bitmask. The Windows Performance Recorder, `logman`, `xperf`, and every EDR&apos;s session-management code are controllers.

A component that reads events from a session in real time or from an `.etl` file on disk. Consumers register a callback that the system invokes once per delivered event. The Windows Performance Analyzer, the krabsetw library, SilkETW, and every EDR&apos;s sensor process are consumers.

flowchart LR
    Ctl[Controller&lt;br /&gt;StartTrace + EnableTrace] --&amp;gt; Sess[Trace Session&lt;br /&gt;per-session buffer pool]
    P1[Provider on CPU 0] --&amp;gt; CPU0[CPU 0 buffer]
    P2[Provider on CPU 1] --&amp;gt; CPU1[CPU 1 buffer]
    P3[Provider on CPU N] --&amp;gt; CPUN[CPU N buffer]
    CPU0 --&amp;gt; WT[Writer thread&lt;br /&gt;asynchronous drain]
    CPU1 --&amp;gt; WT
    CPUN --&amp;gt; WT
    Sess -.governs.-&amp;gt; CPU0
    Sess -.governs.-&amp;gt; CPU1
    Sess -.governs.-&amp;gt; CPUN
    WT --&amp;gt; File[(.etl file)]
    WT --&amp;gt; RT[Real-time consumer&lt;br /&gt;OpenTrace + ProcessTrace]
&lt;p&gt;The original Windows 2000 implementation supported 32 trace sessions running simultaneously [@ms-etw-sessions] [@ms-etw-sessions], a number Microsoft later raised to 64 globally. ETW was framed as a developer-diagnostics facility -- the Windows Driver Kit primary still describes it that way [@ms-etw-wdk] [@ms-etw-wdk] -- and the security-telemetry use case did not exist for almost a decade.&lt;/p&gt;
&lt;p&gt;But the design choices that made ETW good for low-overhead production diagnostics turn out to be exactly the design choices a security telemetry bus needs. Per-CPU buffers solve the multi-core throughput problem. Asynchronous writes solve the producer-latency problem. Dynamic enable solves the always-shipping-but-mostly-off problem. The trichotomy solves the third-party-extensibility problem. Twenty-five years later, every modern Windows EDR consumes telemetry through the same four primitives.Windows 2000&apos;s 32-session global cap [@ms-etw-sessions] is preserved verbatim on the modern Microsoft Learn page: &quot;Windows 2000: Supports only 32 event tracing sessions.&quot; The cap doubled to 64 in later releases and has stayed there ever since.&lt;/p&gt;
&lt;p&gt;The 2000-era design carried one limit, however, that turned out to matter for security: only one trace session could enable a classic provider at a time. The next ten years would be defined by the consequences.&lt;/p&gt;
&lt;h2&gt;3. The MOF era: one session, one steal, one decade of coexistence pain&lt;/h2&gt;
&lt;p&gt;In 2005, a third-party performance monitor that registered a classic provider could find itself silently disabled the moment Microsoft&apos;s &lt;code&gt;wprui.exe&lt;/code&gt; started its own session against the same provider GUID. The first session got no error. It just stopped receiving events. That second-consumer-steals-first behavior is the architectural fact of the entire 2000-2007 era.&lt;/p&gt;
&lt;p&gt;Microsoft Learn still documents the rule in one sentence:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &quot;Up to eight trace sessions can enable and receive events from the same manifest-based provider. However, only one trace session can enable a classic provider. If more than one trace session tries to enable a classic provider, the first session would stop receiving events when the second session enables the provider.&quot; -- Microsoft Learn, Configuring and Starting an Event Tracing Session [@ms-etw-config] [@ms-etw-config]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That single rule made multi-EDR coexistence on classic providers structurally impossible. If Defender&apos;s predecessor and a third-party HIPS both wanted real-time process events from the same classic provider, they had to fight for it. The loser got silence with no notification.&lt;/p&gt;
&lt;p&gt;The provider class involved was &lt;em&gt;MOF-based&lt;/em&gt;, named after the schema language that described its events.&lt;/p&gt;

The schema description language inherited from WBEM (Web-Based Enterprise Management). For ETW, MOF files describe each event a classic provider can emit -- field names, types, tasks, opcodes -- and are compiled into the WMI repository at install time using `mofcomp`. Consumers decode events by querying the WMI repository for the matching MOF schema.

A synonym for *MOF provider*. The original ETW provider class introduced in Windows 2000. Registered with `RegisterTraceGuids`, emits events via `TraceEvent`, decoded against a MOF schema in the WMI repository. Capped at one trace session per provider.
&lt;p&gt;The MOF model was workable for a single-consumer world. A performance-tuning team running an in-house tool could enable the provider, capture, and disable. As the substrate of a security stack with multiple agents on the same host, it could not work. The mid-2000s had not yet produced a &quot;multiple agents on the same host&quot; world, so the limit did not bite immediately. By 2007 it would.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Era&lt;/th&gt;
&lt;th&gt;Schema location&lt;/th&gt;
&lt;th&gt;Sessions/provider&lt;/th&gt;
&lt;th&gt;Adoption in 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;MOF / classic&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;WMI repository&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Niche; mostly NT Kernel Logger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WPP&lt;/td&gt;
&lt;td&gt;2002&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.pdb&lt;/code&gt; (TMF)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Pervasive inside Windows internals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest-based&lt;/td&gt;
&lt;td&gt;2007 (Vista)&lt;/td&gt;
&lt;td&gt;XML manifest&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Dominant for security telemetry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TraceLogging&lt;/td&gt;
&lt;td&gt;2015 (Win10)&lt;/td&gt;
&lt;td&gt;Inline (TLV)&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Rising for new app/service code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;A handful of classic providers survived the 2007 transition and are still significant. The most important is the NT Kernel Logger [@ms-etw-sessions] [@ms-etw-sessions], the special-purpose system session that captures high-throughput kernel events: file I/O, disk I/O, registry operations, network packets. On most consumer SKUs it remains the only path to those event streams at line rate. Sysmon and most kernel-level diagnostics tools use the NT Kernel Logger or its modern descendants.The NT Kernel Logger is a system reserved logger. There is exactly one of it on a host, and the kernel itself owns the buffers. Tools that want kernel disk, file, registry, or network events at high throughput typically subscribe through it rather than through manifest providers. This is why a host can have eight &lt;code&gt;Microsoft-Windows-Kernel-File&lt;/code&gt; consumers but cannot easily have two simultaneous full-fidelity disk I/O traces.&lt;/p&gt;
&lt;p&gt;By 2007 Microsoft knew the one-session limit had to go. The fix shipped with Windows Vista in January 2007, and it was the central architectural decision of the entire ETW-as-EDR-substrate story.&lt;/p&gt;
&lt;h2&gt;4. Vista&apos;s eight sessions: the architectural decision that made the modern EDR endpoint possible&lt;/h2&gt;
&lt;p&gt;Park and Buch open their April 2007 MSDN Magazine article with the line that frames every later development:&lt;/p&gt;

On Windows Vista, ETW has gone through a major upgrade, and one of the most significant changes is the introduction of the unified event provider model and APIs. -- Park &amp;amp; Buch, *MSDN Magazine*, April 2007 [@ms-park-buch-2007]
&lt;p&gt;The new model raised the per-provider session cap from one to eight. That single number is why Defender, CrowdStrike Falcon, SentinelOne, Sysmon, and a researcher&apos;s SilkETW tap can all read &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt; [@fireeye-silketw-launch] [@fireeye-silketw-launch] from the same host today without one of them stealing events from the others.&lt;/p&gt;
&lt;p&gt;The Vista model also unified two things that had been separate. ETW providers wrote to per-CPU ring buffers; the Win32 Event Log was a different facility with its own writer, its own format, and its own consumers. Park and Buch describe the unification verbatim:&lt;/p&gt;

The new unified APIs combine logging traces and writing to the Event Viewer into one consistent, easy-to-use mechanism for event providers. -- Park &amp;amp; Buch, *MSDN Magazine*, April 2007 [@ms-park-buch-2007]
&lt;p&gt;After Vista, a single &lt;code&gt;EventWrite&lt;/code&gt; call from a manifest-based provider lands both in the per-CPU ring buffer for ETW consumers &lt;em&gt;and&lt;/em&gt; in the &lt;code&gt;evtx&lt;/code&gt; channel for &lt;code&gt;wevtutil&lt;/code&gt; and Group Policy audit consumers, depending on how the manifest&apos;s channel mappings are configured. The &quot;Event Viewer&quot; the user sees is now a consumer of ETW.&lt;/p&gt;

The Vista-era ETW provider class. The provider author writes an XML manifest enumerating events, fields, tasks, opcodes, levels, keywords, and channels. The `mc.exe` message compiler turns the manifest into a binary resource embedded in the provider binary; `wevtutil im` registers the manifest with the system at install time. At runtime the provider calls `EventRegister` once per provider GUID and `EventWrite` per event. Capped at eight trace sessions per provider.

A logical destination for an event, declared in a manifest. The four standard channels are *Admin* (operational events for administrators), *Operational* (verbose events for operators), *Analytical* (high-volume events for diagnostics), and *Debug* (developer-only events). When the provider&apos;s `EventWrite` fires, the kernel demultiplexes by channel: events with channels enabled in the `evtx` configuration land in the corresponding channel log, while subscribed real-time consumers receive them through their session.
&lt;p&gt;The deployment pipeline for a manifest-based provider is heavier than for a classic provider. The author writes a manifest, compiles it, embeds the resource, and runs &lt;code&gt;wevtutil im&lt;/code&gt; at install time. Microsoft Learn calls out the distinction between provider registration and manifest installation [@ms-eventregister] [@ms-eventregister] explicitly, and notes that each process can register up to 1,024 providers [@ms-eventregister] [@ms-eventregister]. In practice few processes come close.&lt;/p&gt;

flowchart TD
    A[Author writes manifest.xml] --&amp;gt; B[mc.exe compiles to binary resource]
    B --&amp;gt; C[Resource embedded in provider .dll/.exe]
    C --&amp;gt; D[Installer runs wevtutil im manifest.xml]
    D --&amp;gt; E[System-wide manifest registry]
    F[Provider process at runtime] --&amp;gt; G[EventRegister GUID]
    G --&amp;gt; H[EventWrite per event]
    H --&amp;gt; I[Per-CPU ring buffer&lt;br /&gt;for ETW sessions]
    H --&amp;gt; J[Channel demux&lt;br /&gt;Admin / Operational / Analytical / Debug]
    J --&amp;gt; K[(.evtx log files)]
    I --&amp;gt; L[Real-time consumers]
    E -.decode metadata.-&amp;gt; L
    E -.decode metadata.-&amp;gt; K
&lt;p&gt;The cap rules now read like this: eight trace sessions can enable a manifest-based provider concurrently [@ms-about-etw] [@ms-about-etw]; up to 64 sessions can run on the system at once [@ms-etw-sessions] [@ms-etw-sessions]; &lt;code&gt;EnableTraceEx2&lt;/code&gt; returns &lt;code&gt;ERROR_NO_SYSTEM_RESOURCES&lt;/code&gt; when the per-provider cap binds [@ms-enabletraceex2] [@ms-enabletraceex2]. The 8-session number was chosen for ergonomics, not for security planning, but it is the load-bearing number in modern Windows endpoint security.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The eight-session cap on manifest-based providers is the single architectural decision that made multi-EDR coexistence on the same Windows host possible. Without it, the second EDR to subscribe to &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt; would silently steal events from the first.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A 2007-era driver author shipping the inaugural &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt; provider, GUID &lt;code&gt;{22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716}&lt;/code&gt;, authored a manifest declaring &lt;code&gt;ProcessStart&lt;/code&gt; (event ID 1), &lt;code&gt;ProcessStop&lt;/code&gt; (event ID 2), &lt;code&gt;ImageLoad&lt;/code&gt; (event ID 5), and so on. Defender&apos;s &lt;code&gt;MsMpEng.exe&lt;/code&gt; could subscribe; the future CrowdStrike Falcon could subscribe; the future Sysmon could subscribe; the future SilkETW researchers could subscribe. None starves another. The Vista unification is the architectural enabler of the modern multi-EDR Windows endpoint.&lt;/p&gt;
&lt;p&gt;With multi-consumer concurrency solved, the next problems were authoring overhead and producer integrity. Two parallel paths branched off the Vista manifest model: TraceLogging for the first, the EtwTi PPL/ELAM gate for the second.&lt;/p&gt;
&lt;h2&gt;5. Two more provider classes: WPP for the kernel tree, TraceLogging for the app tier&lt;/h2&gt;
&lt;p&gt;Vista&apos;s manifest-based providers solved coexistence and decoding, but they were heavy to deploy. Microsoft shipped two more provider classes -- one older than Vista and one younger -- that traded manifest deployment for two different kinds of simplicity.&lt;/p&gt;
&lt;h3&gt;WPP: the C-preprocessor approach&lt;/h3&gt;
&lt;p&gt;WPP -- Windows software trace PreProcessor -- predates Vista. Community references and the Park &amp;amp; Buch description of ETW being &quot;abstracted into the Windows preprocessor (WPP) software tracing technology&quot; [@ms-park-buch-2007] place its first WDK ship in the Windows XP era; no Microsoft primary pins a specific build. It became the standard tracing facility inside the Windows kernel tree itself for years. The WDK page [@ms-wpp] [@ms-wpp] frames its purpose:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;WPP software tracing supplements and enhances WMI event tracing by adding ways to simplify tracing the operation of the trace provider. It is an efficient mechanism for the trace provider to log real-time binary messages.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A WPP provider is authored in C with macros that look like printf calls. The C preprocessor expands &lt;code&gt;DoTraceMessage(FlagId, &quot;Frobnicating widget %d&quot;, widgetId)&lt;/code&gt; into an &lt;code&gt;EventWrite&lt;/code&gt; call against an auto-generated provider GUID. Format strings are extracted at build time into a &lt;em&gt;Trace Message Format&lt;/em&gt; file embedded in the binary&apos;s &lt;code&gt;.pdb&lt;/code&gt;. The producer cost is the smallest of any ETW provider class: emitting an event is a function call plus a few stores into a buffer. There is no manifest to deploy, no XML to author.&lt;/p&gt;
&lt;p&gt;The corresponding decode cost is the highest. A WPP event arrives at the consumer as a binary payload referencing a TMF identifier. To turn that into a human-readable message the consumer needs the producer&apos;s &lt;code&gt;.pdb&lt;/code&gt; file. If you do not have the symbols for the binary that emitted the event, you do not know what the event means.&lt;/p&gt;
&lt;p&gt;That decode cost is why WPP did not become the EDR substrate. Sealighter&apos;s README puts the operational consequence verbatim:&lt;/p&gt;

A C-preprocessor-based ETW authoring path inherited from the XP-era WDK. Format strings are extracted to a TMF resource that lives in the producer&apos;s `.pdb`. Producer cost is minimal; decode cost requires the producer&apos;s symbol files. WPP providers inherit the classic one-session-per-provider cap and are pervasively used inside Windows itself for in-tree dev-time tracing.
&lt;blockquote&gt;
&lt;p&gt;&quot;WPP traces compounds the issues, providing almost no easy-to-find data about provider and their events.&quot; -- Sealighter README [@gh-sealighter] [@gh-sealighter]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;WPP providers also inherit the classic one-session-per-provider cap [@ms-about-etw] [@ms-about-etw], which would have made them unworkable for multi-EDR consumption even if the decode problem were solved. So WPP became the kernel-tree internal tracing facility -- ubiquitous inside Microsoft&apos;s source tree, irrelevant outside it.&lt;/p&gt;
&lt;h3&gt;TraceLogging: schema in the payload&lt;/h3&gt;
&lt;p&gt;Eight years after Vista, in Windows 10 (2015), Microsoft shipped a parallel path that solved a different problem. TraceLogging [@ms-tracelogging-about] [@ms-tracelogging-about] keeps the eight-session cap of manifest providers but eliminates the manifest deployment burden:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;TraceLogging is a system for logging events that can be decoded without a manifest.&quot; -- Microsoft Learn, About TraceLogging [@ms-tracelogging-about] [@ms-tracelogging-about]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A TraceLogging event carries its own schema inline. The event payload is a sequence of typed-length-value triples: a one-byte type tag, a length, and the data. A consumer that has never seen the provider before can still decode the event because the names and types of every field are &lt;em&gt;in the event&lt;/em&gt;. The provider author needs no XML manifest, no &lt;code&gt;mc.exe&lt;/code&gt;, no &lt;code&gt;wevtutil im&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The trade-off is per-event size. Inline schema strings cost bytes per event. For a high-volume provider emitting millions of events per minute, the per-event size matters and a manifest-based provider is correct. For a new component author who wants tracing without an install-time deployment dance, TraceLogging is the right answer.&lt;/p&gt;

A self-describing ETW provider class shipped in Windows 10. Schema is inline in each event payload as type-length-value triples; consumers decode without a manifest. Available from C/C++ via `TraceLoggingProvider.h`, from .NET via `EventSource` with `EtwSelfDescribingEventFormat`, and from WinRT via `LoggingChannel`. Inherits the eight-session cap from the manifest-based class.
&lt;p&gt;TraceLogging is also the unified path across runtimes. The same self-describing payload format is emitted from native C/C++, from .NET (when an &lt;code&gt;EventSource&lt;/code&gt; opts into &lt;code&gt;EtwSelfDescribingEventFormat&lt;/code&gt;), and from kernel-mode drivers [@ms-tracelogging-portal] [@ms-tracelogging-portal]. A consumer using TDH (the Trace Data Helper API) decodes them without distinguishing between the runtime that emitted them.&lt;/p&gt;
&lt;h3&gt;Four classes, four trade-offs&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;First Shipped&lt;/th&gt;
&lt;th&gt;Schema Location&lt;/th&gt;
&lt;th&gt;Sessions/Provider&lt;/th&gt;
&lt;th&gt;Decode without symbols/manifest?&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;MOF / classic&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;WMI repository (&lt;code&gt;mofcomp&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Needs MOF&lt;/td&gt;
&lt;td&gt;Legacy components; NT Kernel Logger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WPP&lt;/td&gt;
&lt;td&gt;~2002&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.pdb&lt;/code&gt; (TMF)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;No -- needs producer PDB&lt;/td&gt;
&lt;td&gt;In-tree Windows kernel dev-time tracing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest-based&lt;/td&gt;
&lt;td&gt;2007 (Vista)&lt;/td&gt;
&lt;td&gt;XML manifest, system-installed&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Needs installed manifest&lt;/td&gt;
&lt;td&gt;Shipping security telemetry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TraceLogging&lt;/td&gt;
&lt;td&gt;2015 (Win10)&lt;/td&gt;
&lt;td&gt;Inline TLV in payload&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;New apps and services; cross-runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Sources for the table: [@ms-about-etw, @ms-etw-config, @ms-tracelogging-about, @ms-wpp].&lt;/p&gt;

For new shipping Windows components with a known event vocabulary and high volume, choose manifest-based: smallest per-event size, evtx integration, eight-consumer concurrency. For new cross-runtime open-source providers where deployment friction matters, choose TraceLogging: same eight-consumer concurrency, no XML to author, decodable everywhere. For in-source-tree dev-time tracing inside a binary you already have symbols for, WPP is fine. For new security-relevant providers, never choose classic: the one-session cap is structurally incompatible with multi-EDR coexistence.
&lt;p&gt;Four provider classes, four trade-offs. But every one of them shares a structural weakness: the producer fires from inside the calling process, and any code in that process can patch the runtime entry-point and silence the provider for itself. That is the weakness Adam Chester made famous in 2020, and the one EtwTi was built to defeat.&lt;/p&gt;
&lt;h2&gt;6. Sessions, buffers, and the autologger registry: where the telemetry actually lives&lt;/h2&gt;
&lt;p&gt;Open &lt;code&gt;regedit&lt;/code&gt; on a Windows host and navigate to &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger&lt;/code&gt;. You are looking at the persistence surface of every trace session that survives a reboot on this machine -- and the persistence surface every modern EDR uses to install itself.&lt;/p&gt;
&lt;p&gt;A session is the unit ETW actually exposes to controllers. It owns a per-session pool of buffers, a writer thread, a destination (file or real-time consumer), and a list of providers it has subscribed to. The lifecycle is short. A controller fills out an &lt;code&gt;EVENT_TRACE_PROPERTIES&lt;/code&gt; structure [@ms-event-trace-props] [@ms-event-trace-props] with a session name, buffer size, logging mode, and destination, then calls &lt;code&gt;StartTrace&lt;/code&gt;. The kernel allocates the buffers -- at least two per logical processor [@ms-event-trace-props] [@ms-event-trace-props] -- and returns a session handle. The controller then calls &lt;code&gt;EnableTraceEx2&lt;/code&gt; [@ms-enabletraceex2] [@ms-enabletraceex2] for each provider it wants to subscribe to, passing &lt;code&gt;EVENT_CONTROL_CODE_ENABLE_PROVIDER&lt;/code&gt; along with the provider GUID, level, and keyword bitmask.&lt;/p&gt;
&lt;p&gt;If the provider&apos;s per-class session cap is already saturated, &lt;code&gt;EnableTraceEx2&lt;/code&gt; returns &lt;code&gt;ERROR_NO_SYSTEM_RESOURCES&lt;/code&gt;. If the caller lacks the privilege to enable that provider, it returns &lt;code&gt;ERROR_ACCESS_DENIED&lt;/code&gt;. We will see both error codes again later, on different paths.The default buffer size sweet spot is small. The Microsoft Learn primary states it explicitly: &quot;Trace sessions with large buffers (256KB or larger) should be used only for diagnostic investigations or testing, not for production tracing.&quot; [@ms-event-trace-props] Production session buffer sizes typically sit in the 32-64KB range.&lt;/p&gt;
&lt;p&gt;There are three logging modes. &lt;em&gt;File mode&lt;/em&gt; writes events to a sequential &lt;code&gt;.etl&lt;/code&gt; file on disk; the writer thread drains buffers to disk and the file grows. &lt;em&gt;Circular mode&lt;/em&gt; writes to a fixed-size file in a circular buffer; old events are overwritten when the file fills. &lt;em&gt;Real-time mode&lt;/em&gt; delivers events to a real-time consumer process via a kernel callback. Defender, EDR sensors, and Sysmon all use real-time mode for their hot paths; they may also write to file as a forensic backup.&lt;/p&gt;

A process that calls `OpenTrace` with `LogFileMode = EVENT_TRACE_REAL_TIME_MODE` and receives events live via a registered callback rather than from an `.etl` file on disk. Real-time consumers must keep up with producer rate or events are lost.
&lt;p&gt;The autologger registry path is what makes a session survive a reboot. A subkey under &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\&amp;lt;SessionName&amp;gt;&lt;/code&gt; defines a session that the kernel starts at boot, before most user-mode services are running. Each subkey&apos;s values configure the session: &lt;code&gt;BufferSize&lt;/code&gt;, &lt;code&gt;MaximumBuffers&lt;/code&gt;, &lt;code&gt;LogFileMode&lt;/code&gt;, &lt;code&gt;FileName&lt;/code&gt;, plus a nested &lt;code&gt;&amp;lt;SessionName&amp;gt;\&amp;lt;ProviderGuid&amp;gt;&lt;/code&gt; subkey for each provider to enable.&lt;/p&gt;

A registry-persisted boot-time ETW session. The kernel reads `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\` at boot, creates the session, enables the configured providers, and begins capture before user-mode services start. Defender&apos;s Sense agent, CrowdStrike&apos;s Falcon sensor, and Sysmon&apos;s driver all install autologgers here.
&lt;p&gt;Defender&apos;s &lt;code&gt;DiagTrack&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-Diagnosis-PCW&lt;/code&gt;, the SQM kernel logger, the EventLog-Application channel autologger -- all live here (observable via &lt;code&gt;logman query -ets&lt;/code&gt; on a stock Windows install). Third-party EDRs add their own. The Palantir CIRT taxonomy [@palantir-tampering-wayback] (about which more in section 11) frames this registry surface as the persistent-tampering target: an attacker who can write to this subtree can disable an EDR&apos;s boot-time tracing without ever interacting with the running EDR process. The events of interest never get captured because the session never starts.&lt;/p&gt;
&lt;p&gt;There is a related concept worth naming: the &lt;em&gt;Global Logger&lt;/em&gt;. This is a special autologger session whose configuration lives in &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\GlobalLogger&lt;/code&gt;. It is the boot-time tracing path that comes online before any user-mode service, including before Sense and the EDR sensor. It exists to capture early-boot kernel events that no later session can record.&lt;/p&gt;

flowchart TD
    R[HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\] --&amp;gt; S1[DiagTrack-Listener]
    R --&amp;gt; S2[Defender-Listener]
    R --&amp;gt; S3[ThirdPartyEDR-Sensor]
    R --&amp;gt; SG[GlobalLogger]
    S2 --&amp;gt; S2P[Provider GUIDs subkeys]
    S2 --&amp;gt; S2C[BufferSize / MaximumBuffers / LogFileMode]
    S2 --&amp;gt; S2F[FileName=.etl path]
    S2P --&amp;gt; KS[Kernel reads at boot]
    S2C --&amp;gt; KS
    S2F --&amp;gt; KS
    KS --&amp;gt; Started[Session started before user-mode services]
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;logman query -ets&lt;/code&gt; enumerates every live trace session on the host. Cross-reference against the subkeys in &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\&lt;/code&gt; to find sessions configured to start at boot. Any unauthorised entry -- a session you do not recognise, an autologger pointed at a destination outside your EDR&apos;s data path, a provider GUID you cannot account for -- belongs in your incident response queue. We return to this in section 14.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;ERROR_NO_SYSTEM_RESOURCES&lt;/code&gt; from &lt;code&gt;EnableTraceEx2&lt;/code&gt; is the runtime symptom of the eight-session cap binding [@ms-enabletraceex2]. SOC engineers debugging multi-EDR coexistence problems should look for it in their sensor&apos;s diagnostic output. Eight subscribers per manifest provider is enough for the typical Defender + third-party EDR + Sysmon + research tap arrangement, but a host running multiple research-mode tracers can saturate it.&lt;/p&gt;
&lt;p&gt;Persistence solved: a session the OS starts at every boot. But who reads it? That requires a consumer process, and consumers are where the architecture forks along the security spectrum.&lt;/p&gt;
&lt;h2&gt;7. Consumer architecture: from &lt;code&gt;OpenTrace&lt;/code&gt; to KrabsETW to a 30-line process watcher&lt;/h2&gt;
&lt;p&gt;The consumer side of ETW is mechanically simple -- three calls to open a trace, register a callback, and process events -- but the choice of library tells you almost everything about what kind of EDR you are building.&lt;/p&gt;
&lt;p&gt;The native pattern is three Win32 calls. &lt;code&gt;EnableTraceEx2&lt;/code&gt; subscribes the session to a provider GUID with a level and keyword bitmask. &lt;code&gt;OpenTrace&lt;/code&gt; returns a handle on the session for consumption. &lt;code&gt;ProcessTrace&lt;/code&gt; blocks the calling thread, drains events from the kernel&apos;s per-CPU buffers, and dispatches each one to a registered callback. Each event arrives as an &lt;code&gt;EVENT_RECORD&lt;/code&gt; containing a header (provider GUID, event ID, level, keyword, opcode, timestamp, process ID, thread ID) and a payload that the consumer decodes.&lt;/p&gt;
&lt;p&gt;For manifest providers the consumer decodes via TDH (the Trace Data Helper API) against the system-installed manifest. For TraceLogging providers the consumer decodes from the inline TLV payload. For classic and WPP providers the consumer needs the MOF schema or the producer&apos;s PDB respectively.&lt;/p&gt;

The Win32 decoder API that turns a raw `EVENT_RECORD` payload into typed fields, using the registered manifest as the schema source. `TdhGetEventInformation` returns a `TRACE_EVENT_INFO` structure with the field names, types, and offsets; `TdhFormatProperty` extracts each field. TDH is what makes manifest events self-describing at the consumer end, even though the schema lives out of band.

sequenceDiagram
    participant C as Consumer process
    participant K as Kernel ETW subsystem
    participant P as Provider process
    C-&amp;gt;&amp;gt;K: StartTrace(session)
    C-&amp;gt;&amp;gt;K: EnableTraceEx2(session, providerGuid, level, keyword)
    K--&amp;gt;&amp;gt;P: Provider notified to begin emitting
    C-&amp;gt;&amp;gt;K: OpenTrace(session)
    K--&amp;gt;&amp;gt;C: TraceHandle
    C-&amp;gt;&amp;gt;K: ProcessTrace(handle) [blocking]
    P-&amp;gt;&amp;gt;K: EventWrite(payload)
    K--&amp;gt;&amp;gt;C: callback(EVENT_RECORD)
    P-&amp;gt;&amp;gt;K: EventWrite(payload)
    K--&amp;gt;&amp;gt;C: callback(EVENT_RECORD)
    Note over C,K: ProcessTrace returns only when session ends
&lt;p&gt;In production almost no one writes the raw three-call pattern. The library universe settled into a small set of widely-used wrappers, and the choice of wrapper maps almost one-to-one onto the kind of EDR the engineering team is building.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;krabsetw&lt;/strong&gt; [@gh-krabsetw] [@gh-krabsetw] is a Microsoft-authored C++ library that simplifies session and provider management. Its README explicitly notes the production caller: a C++/CLI wrapper called &lt;code&gt;Microsoft.O365.Security.Native.ETW&lt;/code&gt;, &quot;used in production by the Office 365 Security team. It&apos;s affectionately referred to as Lobsters.&quot; If you are building an in-house EDR or a security analytics pipeline in C++ on Windows, krabsetw is the default choice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Microsoft.Diagnostics.Tracing.TraceEvent&lt;/strong&gt; [@nuget-traceprocessing] [@nuget-traceprocessing] is the general-purpose .NET ETW library, distributed as a NuGet package and used heavily inside the .NET diagnostics community. Microsoft&apos;s separate &lt;code&gt;Microsoft.Windows.EventTracing.Processing.All&lt;/code&gt; package is the .NET TraceProcessing API [@ms-etw-portal] [@ms-etw-portal] that the Windows engineering team uses internally to analyze ETW data from the Windows engineering system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SilkETW&lt;/strong&gt; [@gh-silketw] [@gh-silketw], originally released by Ruben Boonen at FireEye in March 2019 [@fireeye-silketw-launch] [@fireeye-silketw-launch] (now maintained by Mandiant), wraps &lt;code&gt;Microsoft.Diagnostics.Tracing.TraceEvent&lt;/code&gt; to expose ETW telemetry to detection-engineering and threat-hunting workflows. SilkETW is the canonical &quot;blue team research&quot; consumer: the tool you reach for when you want to see what events a provider actually emits without writing C++.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sealighter&lt;/strong&gt; [@gh-sealighter] [@gh-sealighter], by &lt;code&gt;pathtofile&lt;/code&gt;, is a krabsetw-wrapping C++ tool that makes multi-provider subscription and filtering tractable from a JSON config. The README states: &quot;Sealighter leverages the feature-rich Krabs ETW Library to enable detailed filtering and triage of ETW and WPP Providers and Events.&quot; Sealighter is the canonical &quot;red/blue team triage&quot; consumer: more flexible than SilkETW, less code to write than raw krabsetw.&lt;/p&gt;
&lt;p&gt;The pitfalls are universal across all four libraries. The krabsetw README spells two of them out:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The call to &apos;start&apos; on the trace object is blocking so thread management may be necessary.&quot; -- [@gh-krabsetw]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Throwing exceptions in the event handler callback ... will cause the trace to stop processing events.&quot; -- [@gh-krabsetw]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Both have caused real production outages. An EDR that throws an unhandled exception in its event callback dies silently as an ETW consumer, and the next event the provider emits goes nowhere.The &quot;throwing in the callback stops the trace&quot; pitfall is the gotcha that bites every team writing their first ETW consumer. The kernel does not catch the exception; the trace simply ends. A production-quality consumer wraps every callback in try/catch (or its language equivalent) and routes failures through a side channel, not through the trace itself.&lt;/p&gt;
&lt;p&gt;To make the structure concrete, here is what a 30-line &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt; real-time consumer looks like, written in TypeScript pseudocode that mirrors the structure a Sealighter or krabsetw user would write:&lt;/p&gt;
&lt;p&gt;{`
// Pseudocode: the structure of a krabsetw / Sealighter consumer
// for the Microsoft-Windows-Kernel-Process provider.&lt;/p&gt;
&lt;p&gt;const KERNEL_PROCESS_GUID = &quot;{22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716}&quot;;&lt;/p&gt;
&lt;p&gt;const session = new UserTraceSession(&quot;MyEdrSensor&quot;);&lt;/p&gt;
&lt;p&gt;const provider = new Provider(KERNEL_PROCESS_GUID);
provider.level = TraceLevel.Information;
provider.anyKeyword = 0xFFFFFFFFFFFFFFFFn;&lt;/p&gt;
&lt;p&gt;provider.onEvent = (event) =&amp;gt; {
  try {
    switch (event.id) {
      case 1: // ProcessStart
        const pid = event.fields.ProcessID;
        const imageName = event.fields.ImageName;
        const cmdLine = event.fields.CommandLine;
        console.log(`Process start pid=${pid} image=${imageName}`);
        break;
      case 2: // ProcessStop
        console.log(`Process stop pid=${event.fields.ProcessID}`);
        break;
      case 5: // ImageLoad
        console.log(`Image load ${event.fields.ImageName} into pid=${event.fields.ProcessID}`);
        break;
    }
  } catch (e) {
    // never let an exception escape the callback
    sideChannelLog(e);
  }
};&lt;/p&gt;
&lt;p&gt;session.enable(provider);
session.start();  // blocks until session.stop() is called
`}&lt;/p&gt;
&lt;p&gt;That code, in production form, is a working EDR sensor&apos;s process watcher. Every commercial Windows EDR has something with the same structure inside it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; krabsetw wraps the C++ surface and is the default for production in-house EDRs. TraceEvent wraps .NET and is the default for diagnostics tooling. SilkETW exposes ETW to detection engineers without C++. Sealighter wraps krabsetw with a config file for triage. Pick the library that matches the team that will own the consumer, not the one that looks most powerful.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is what Sysmon, Wazuh, and Elastic Defend look like under the hood -- a SYSTEM-privileged user-mode service consuming public providers. But there is one provider this code cannot subscribe to. Try it and &lt;code&gt;EnableTraceEx2&lt;/code&gt; returns &lt;code&gt;ERROR_ACCESS_DENIED&lt;/code&gt;. The next two sections are about the GUID that requires a passport.&lt;/p&gt;
&lt;h2&gt;8. The security provider catalogue: what EDRs actually read&lt;/h2&gt;
&lt;p&gt;There are roughly 1,300 manifest-based providers shipped on a 2026 Windows 11 24H2 install -- the community-maintained jdu2600 inventory [@gh-jdu2600] [@gh-jdu2600] tracks the count across builds, and the repnz manifest archive [@gh-repnz] [@gh-repnz] holds byte-stable copies of the manifests for cross-version diffing. Eight of those providers carry almost all the security telemetry the EDR vendors read. This is the catalogue.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-Security-Auditing&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{54849625-5478-4994-A5BA-3E3B0328C30D}&lt;/code&gt;. The audit-policy-driven Security event log producer. Event ID 4624 (logon), 4625 (failed logon), 4634 (logoff), 4688 (process create with command line) [@learn-microsoft-com-event-4688] [@ms-event-4624], 4689 (process exit), and the broader subcategory audit policy events. This is the closure for the legacy Security event log: when an administrator turns on &quot;audit logon events&quot; in the local security policy, this is the provider that emits the events. EDRs that consume it are reading the same stream the Event Viewer&apos;s Security log shows.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716}&lt;/code&gt;. The canonical real-time process telemetry source for non-PPL EDR. Event ID 1 fires on &lt;code&gt;ProcessStart&lt;/code&gt; with PID, parent PID, image name, command line, and SID; event ID 2 on &lt;code&gt;ProcessStop&lt;/code&gt;; event ID 3 on thread create; event ID 4 on thread exit; event ID 5 on &lt;code&gt;ImageLoad&lt;/code&gt; with the loaded module name and base address. SilkETW&apos;s launch post enumerates the event record format inline [@fireeye-silketw-launch] [@fireeye-silketw-launch]. This provider is widely cited in EDR community documentation as available since Windows 7, though no Microsoft primary pins the exact build.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-Kernel-File&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-Kernel-Network&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-Kernel-Registry&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The per-subsystem siblings of &lt;code&gt;Kernel-Process&lt;/code&gt;. &lt;code&gt;Kernel-File&lt;/code&gt; surfaces file open / close / read / write / delete operations with the file path and the operating PID. &lt;code&gt;Kernel-Network&lt;/code&gt; surfaces TCP and UDP send / receive with the local and remote endpoints. &lt;code&gt;Kernel-Registry&lt;/code&gt; surfaces registry create / open / set value / delete with the key path and value name. All three use the manifest-based class and inherit the eight-session cap. EDRs that want full-fidelity per-syscall telemetry without writing kernel callbacks subscribe to these three.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Antimalware-Scan-Interface&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{2A576B87-09A7-520E-C21A-4942F0271D67}&lt;/code&gt;, documented in the Microsoft Learn AMSI portal [@ms-amsi-portal] [@ms-amsi-portal] and surveyed in the Palantir CIRT taxonomy [@palantir-tampering-wayback] [@palantir-tampering-wayback]. This is the ETW provider that surfaces AMSI scan results: a script block submitted by PowerShell, JScript, VBA, an Office macro engine, or any other AMSI client comes through here &lt;em&gt;after deobfuscation&lt;/em&gt;. Whatever string the script engine is about to execute, the registered antimalware engine sees in plaintext, and the result of the scan is published via this provider for any listener.&lt;/p&gt;

A COM interface exposed by Windows since 2015 that script engines and runtime hosts can call into to submit content for malware scanning. The Microsoft Learn AMSI portal lists PowerShell, JScript and VBScript via Windows Script Host, Office VBA macros, and User Account Control as in-box integrators [@ms-amsi-portal]; the .NET CLR&apos;s assembly load path joined the list with .NET Framework 4.8, as documented in Adam Chester&apos;s CLR walk-through [@xpn-hiding-dotnet]. The scanned content is the post-deobfuscation form -- the actual code about to execute, not the obfuscated wrapper. Scan results surface via the `Microsoft-Antimalware-Scan-Interface` ETW provider.
&lt;p&gt;The AMSI Operational event log channel typically appears empty by default. The Palantir taxonomy [@palantir-tampering-wayback] [@palantir-tampering-wayback] notes the keyword bitmask configured for the channel does not surface scan-result events. The events fire on the ETW bus and can be consumed in real time, but they do not land in the user-visible evtx log unless the consumer reconfigures the keyword mask.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-PowerShell&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{a0c1853b-5c40-4b15-8766-3cf1c58f985a}&lt;/code&gt;. Event ID 4104 is the script-block-logging event that records each PowerShell script block before execution; event ID 4103 records pipeline execution detail; event ID 4100 records errors. The Microsoft Learn &lt;code&gt;about_Logging_Windows&lt;/code&gt; reference (Windows PowerShell 5.1) [@ms-powershell-logging] [@ms-powershell-logging] documents EID 4104 verbatim (&quot;&lt;code&gt;EventId 4104 / 0x1008&lt;/code&gt; ... &lt;code&gt;Channel Operational&lt;/code&gt; ... &lt;code&gt;Task CommandStart&lt;/code&gt;&quot;) and the script-block-logging configuration. PowerShell Core 7+ uses a separate ETW provider (&lt;code&gt;PowerShellCore&lt;/code&gt;, GUID &lt;code&gt;{f90714a8-5509-434a-bf6d-b1624c8a19a2}&lt;/code&gt;). Combined with AMSI the two providers give an EDR the executed PowerShell content twice: once at AMSI submission, once at script-block logging. Detection engineers use both as cross-checks.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{e13c0d23-ccbc-4e12-931b-d9cc2eee27e4}&lt;/code&gt;, verbatim in Adam Chester&apos;s PoC source [@xpn-hiding-dotnet] [@xpn-hiding-dotnet]. The .NET CLR provider. Surfaces assembly load events, JIT compilation, AppDomain creation, exception throws. Critical for detecting Cobalt Strike&apos;s &lt;code&gt;execute-assembly&lt;/code&gt; style of in-memory .NET payload loading. This is the provider that goes dark in the section 1 hook scene after the operator&apos;s &lt;code&gt;EtwEventWrite&lt;/code&gt; patch.This is the provider Adam Chester targeted in the canonical March 17, 2020 ETW patching post [@xpn-hiding-dotnet]. The Cobalt Strike &lt;code&gt;execute-assembly&lt;/code&gt; workflow produces a loud signal here -- &quot;assembly X loaded into PID Y from in-memory source Z&quot; -- so silencing it locally was a valuable evasion. The story comes back in section 11.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-Sysmon&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{5770385F-C22A-43E0-BF4C-06F5698FFBD9}&lt;/code&gt;, surfaced by &lt;code&gt;wevtutil gp Microsoft-Windows-Sysmon&lt;/code&gt; and inventoried in [@gh-jdu2600]; the Microsoft Learn Sysmon page by Russinovich and Garnier [@ms-sysmon] [@ms-sysmon] documents authorship, the protected-process status, and the &lt;code&gt;Microsoft-Windows-Sysmon/Operational&lt;/code&gt; channel. This is the &lt;em&gt;publishing&lt;/em&gt; side of Sysmon. Sysmon&apos;s kernel driver &lt;code&gt;SysmonDrv.sys&lt;/code&gt; collects events through &lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt; and friends; the user-mode service then republishes via this ETW provider so any consumer (a SIEM forwarder, a SOC dashboard, a custom analytic) can subscribe without writing its own kernel driver. Events also land in the &lt;code&gt;Microsoft-Windows-Sysmon/Operational&lt;/code&gt; evtx channel.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; (EtwTi)&lt;/h3&gt;
&lt;p&gt;GUID &lt;code&gt;{f4e1897c-bb5d-5668-f1d8-040f4d8dd344}&lt;/code&gt;, verbatim in the fluxsec.red walkthrough [@fluxsec-eti] [@fluxsec-eti]. The only ETW source in the catalogue that fires from inside the kernel for memory-modifying syscalls. Ten task IDs, all prefixed &lt;code&gt;KERNEL_THREATINT_TASK_&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ALLOCVM&lt;/code&gt; (&lt;code&gt;NtAllocateVirtualMemory&lt;/code&gt; -- local and cross-process)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROTECTVM&lt;/code&gt; (&lt;code&gt;NtProtectVirtualMemory&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MAPVIEW&lt;/code&gt; (section mapping; cross-process and self)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;QUEUEUSERAPC&lt;/code&gt; (&lt;code&gt;NtQueueApcThread&lt;/code&gt; cross-process)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SETTHREADCONTEXT&lt;/code&gt; (&lt;code&gt;NtSetContextThread&lt;/code&gt; cross-process)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;READVM&lt;/code&gt; (&lt;code&gt;NtReadVirtualMemory&lt;/code&gt; -- local and cross-process)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WRITEVM&lt;/code&gt; (&lt;code&gt;NtWriteVirtualMemory&lt;/code&gt; -- local and cross-process)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SUSPENDRESUME_THREAD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SUSPENDRESUME_PROCESS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DRIVER_DEVICE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each task pairs with a 64-bit keyword bitmask that distinguishes &lt;code&gt;LOCAL&lt;/code&gt; vs &lt;code&gt;REMOTE&lt;/code&gt; (cross-process) and &lt;code&gt;KERNEL_CALLER&lt;/code&gt; vs not. The Elastic Security Labs walkthrough [@elastic-doubling-down] [@elastic-doubling-down] lists the named Win32/Nt syscalls that surface here:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The most notable addition to this visibility is the Microsoft-Windows-Threat-Intelligence Event Tracing for Windows (ETW) provider ... VirtualAlloc, VirtualProtect, MapViewOfFile, VirtualAllocEx, VirtualProtectEx, MapViewOfFile2, QueueUserAPC, SetThreadContext, WriteProcessMemory, ReadProcessMemory(lsass)&quot; -- Elastic Security Labs [@elastic-doubling-down] [@elastic-doubling-down]&lt;/p&gt;
&lt;/blockquote&gt;

The kernel-emitted ETW provider for memory-modifying syscalls. GUID `{f4e1897c-bb5d-5668-f1d8-040f4d8dd344}`. Events are emitted from the kernel side of the syscall path (not from a user-mode trampoline), which makes the provider unreachable from a user-mode patcher in the calling process. Consumption is gated behind Protected Process Light at the Antimalware signer level, paired with an Early Launch Antimalware driver. The provider first shipped in the Windows 10 RS-era; the precise build is not stated verbatim in any Microsoft primary located, with community references converging on no later than 1709.
&lt;p&gt;The first-ship-build is hedged: the provider GUID and task inventory are well-documented in third-party reverse-engineering primaries, but no Microsoft primary located in the source verification stage pins the exact build. The community reference range is Windows 10 1607 (RS1) through 1709 (RS3). The dispositive practical evidence is Yarden Shafir&apos;s 2023 Trail of Bits walkthrough [@trailofbits-shafir] [@trailofbits-shafir], which shows live-debugger output of &lt;code&gt;CSFalconService.exe&lt;/code&gt; (CrowdStrike) holding &lt;code&gt;EtwConsumer&lt;/code&gt; handles to multiple logger IDs simultaneously. By 2023 third-party EDRs were demonstrably consuming EtwTi at scale.&lt;/p&gt;
&lt;h3&gt;The catalogue as a single screen&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider name&lt;/th&gt;
&lt;th&gt;GUID&lt;/th&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Primary source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Security-Auditing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{54849625-5478-4994-A5BA-3E3B0328C30D}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Audit-policy events (4624/4625/4688/...)&lt;/td&gt;
&lt;td&gt;None (Local Security Policy)&lt;/td&gt;
&lt;td&gt;[@ms-event-4624]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Kernel-Process&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Process / thread / image-load events&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@fireeye-silketw-launch], [@gh-jdu2600]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Kernel-File&lt;/td&gt;
&lt;td&gt;(manifest archive)&lt;/td&gt;
&lt;td&gt;File I/O syscalls&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@gh-jdu2600], [@gh-repnz]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Kernel-Network&lt;/td&gt;
&lt;td&gt;(manifest archive)&lt;/td&gt;
&lt;td&gt;TCP/UDP send/receive&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@gh-jdu2600], [@gh-repnz]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Kernel-Registry&lt;/td&gt;
&lt;td&gt;(manifest archive)&lt;/td&gt;
&lt;td&gt;Registry create/open/set/delete&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@gh-jdu2600], [@gh-repnz]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Antimalware-Scan-Interface&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{2A576B87-09A7-520E-C21A-4942F0271D67}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Post-deobfuscation script content&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@ms-amsi-portal], [@palantir-tampering-wayback]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-PowerShell&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{a0c1853b-5c40-4b15-8766-3cf1c58f985a}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Script-block logging (4104), pipeline&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@gh-jdu2600]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-DotNETRuntime&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{e13c0d23-ccbc-4e12-931b-d9cc2eee27e4}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CLR assembly load, JIT, exceptions&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@xpn-hiding-dotnet]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Sysmon&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{5770385F-C22A-43E0-BF4C-06F5698FFBD9}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sysmon driver re-publication&lt;/td&gt;
&lt;td&gt;None (admin)&lt;/td&gt;
&lt;td&gt;[@gh-jdu2600], [@ms-sysmon]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft-Windows-Threat-Intelligence&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{f4e1897c-bb5d-5668-f1d8-040f4d8dd344}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Memory-modifying syscalls (kernel-emitted)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;PPL + ELAM (Antimalware signer level)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;[@fluxsec-eti], [@elastic-doubling-down]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

This is the *security* catalogue. The full Windows manifest-based provider list is roughly 1,300 entries on a current Windows 11 build; performance-tuning, diagnostic, and developer-facing providers fill out the rest. The jdu2600 inventory [@gh-jdu2600] [@gh-jdu2600] tracks the full list across Win10 versions; the repnz archive [@gh-repnz] [@gh-repnz] preserves byte-stable manifest copies for cross-version diffing.
&lt;p&gt;Nine of the ten rows in that table are accessible to any SYSTEM-privileged user-mode service. The tenth -- EtwTi -- requires a passport. The next section is about who issues the passport.&lt;/p&gt;
&lt;h2&gt;9. The PPL / ELAM gate: why EtwTi is not for everyone&lt;/h2&gt;
&lt;p&gt;To consume the one ETW provider that fires from the kernel for memory-modifying syscalls, your service must be (a) a Protected Process Light [@paragmali-com-app-ide], (b) signed at the Antimalware signer level with EKU &lt;code&gt;1.3.6.1.4.1.311.61.4.1&lt;/code&gt;, and (c) loaded from disk by an Early Launch Antimalware [@paragmali-com-to-userini] driver registered at boot. Two of those three were not possible for third parties until the Windows 10 RS-era.&lt;/p&gt;
&lt;p&gt;fluxsec.red [@fluxsec-eti] [@fluxsec-eti] gives the prerequisite list verbatim:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;In order to start receiving ETW:TI signals, we need: 1. A service running as Protected Process Light, 2. An Early Launch Antimalware driver and certificate, 3. A logging mechanism.&quot; -- [@fluxsec-eti]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Each prerequisite has a story.&lt;/p&gt;
&lt;h3&gt;Protected Process Light at the Antimalware signer level&lt;/h3&gt;
&lt;p&gt;Windows 8.1 introduced the &lt;em&gt;protected service&lt;/em&gt; concept specifically for antimalware engines. The motivation was simple: a malicious process running as administrator should not be able to inject code into the antimalware service or attach a debugger to it. The Microsoft Learn primary [@ms-protect-am] [@ms-protect-am] sets out the model:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Windows 8.1 introduced a new concept of protected services to protect anti-malware services... In addition to the existing ELAM driver certification requirements, the driver must have an embedded resource section containing the information of the certificates used to sign the user mode service binaries.&quot; -- [@ms-protect-am]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;PPL is a process-protection level. A given process has a level on the PPL lattice; another process can open it for write or debug only if the requesting process&apos;s level is greater than or equal to the target&apos;s. Antimalware-PPL is a &lt;em&gt;signer level&lt;/em&gt; on that lattice. The kernel admits a process to Antimalware-PPL when its image is signed with a certificate whose EKU includes &lt;code&gt;1.3.6.1.4.1.311.61.4.1&lt;/code&gt; (Windows Antimalware) &lt;em&gt;and&lt;/em&gt; whose certificate is enrolled in an ELAM driver&apos;s allow-list at boot.&lt;/p&gt;

A Windows process-protection model. Each process has a PPL level; another process may open it for write or debug only if the requestor is at an equal or higher level. Originally introduced for DRM, the lattice was extended in Windows 8.1 to host the Antimalware signer level for protecting antimalware services from administrative-rights attackers.

A specific signer level on the PPL lattice. Reserved in Windows 8.1 for Microsoft Defender; opened to third-party EDR vendors via ELAM onboarding in the Windows 10 RS-era. Consumption of the `Microsoft-Windows-Threat-Intelligence` ETW provider is gated at the Antimalware signer level: an `EnableTraceEx2` call from a non-Antimalware-PPL caller against the EtwTi GUID returns `ERROR_ACCESS_DENIED` (the `EnableTraceEx2` [@ms-enabletraceex2] [@ms-enabletraceex2] page documents the error code for callers that lack the documented administrative groups; the per-provider PPL-signer-level check that triggers it for the EtwTi GUID specifically is described in the [@fluxsec-eti] prerequisite list).
&lt;h3&gt;Early Launch Antimalware&lt;/h3&gt;
&lt;p&gt;ELAM is a driver class that loads before any other non-Microsoft boot driver. The Microsoft Learn primary [@ms-elam] [@ms-elam] describes it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Because an ELAM service runs as a PPL (Protected Process Light), you need to debug using a kernel debugger... AM drivers are initialized first and allowed to control the initialization of subsequent boot drivers, potentially not initializing unknown boot drivers.&quot; -- [@ms-elam]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The boot sequence runs like this. Winload loads the ELAM driver as part of the early-boot path. The ELAM driver registers a callback via &lt;code&gt;IoRegisterBootDriverCallback&lt;/code&gt; and gets to inspect each subsequent boot driver, returning a verdict (initialize / do not initialize / unknown) based on the certificate inventory it carries in its embedded resource section. The kernel honours that verdict. After boot drivers settle, the SCM launches the paired user-mode antimalware service with the &lt;code&gt;LaunchProtected = SERVICE_LAUNCH_PROTECTED_ANTIMALWARE_LIGHT&lt;/code&gt; flag, and the kernel admits that service to Antimalware-PPL because its signing certificate matches an entry in the ELAM driver&apos;s allow-list.&lt;/p&gt;

A driver class that loads before any non-Microsoft boot driver. The ELAM driver registers a boot-driver callback to inspect subsequent drivers and an embedded-resource certificate inventory of permitted user-mode antimalware service signatures. Together with PPL, ELAM gates which user-mode antimalware services can pass the Antimalware-PPL admission check.
&lt;h3&gt;The 1709 onboarding&lt;/h3&gt;
&lt;p&gt;Microsoft Defender&apos;s &lt;code&gt;MsMpEng.exe&lt;/code&gt; ran at the Antimalware signer level by default starting around the Windows 10 1709 timeframe (October 17, 2017), and the same release is widely cited in EDR-vendor documentation as the moment the Antimalware-PPL onboarding was extended to third-party EDR vendors. The Microsoft primary that pins the 1709 third-party onboarding date is not in the public ETW documentation; we treat the date as widely-cited rather than verified.&lt;/p&gt;
&lt;p&gt;The dispositive practical evidence is the Trail of Bits 2023 walkthrough by Yarden Shafir [@trailofbits-shafir] [@trailofbits-shafir]. Shafir&apos;s WinDbg JS scripts walk the live &lt;code&gt;_ETW_REALTIME_CONSUMER&lt;/code&gt; data structures of a running Windows host and print:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Process CSFalconService.exe with ID 0x1e54 has handle 0x760 to Logger ID 3&quot; -- [@trailofbits-shafir]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is CrowdStrike&apos;s user-mode service, holding a real-time consumer handle to an EtwTi logger session. By 2023 the third-party Antimalware-PPL story is operationally complete.&lt;/p&gt;

sequenceDiagram
    participant BL as Winload (boot)
    participant EL as ELAM Driver
    participant SCM as Service Control Manager
    participant SVC as EDR Service
    participant K as Kernel ETW
    BL-&amp;gt;&amp;gt;EL: Load ELAM driver (early boot)
    EL-&amp;gt;&amp;gt;EL: Register IoRegisterBootDriverCallback then read embedded cert inventory
    Note over EL: ELAM gates subsequent boot drivers
    SCM-&amp;gt;&amp;gt;SVC: Start EDR service with PROTECTED_ANTIMALWARE_LIGHT flag
    K-&amp;gt;&amp;gt;SVC: Verify signature against ELAM allow-list
    K--&amp;gt;&amp;gt;SVC: Admit to Antimalware-PPL
    SVC-&amp;gt;&amp;gt;K: EnableTraceEx2(session, EtwTi GUID, ...)
    K-&amp;gt;&amp;gt;K: Check caller signer level ge Antimalware
    K--&amp;gt;&amp;gt;SVC: SUCCESS
    Note over SVC,K: Non-PPL caller would receive ERROR_ACCESS_DENIED here
&lt;h3&gt;Why this gate matters for the section 1 hook&lt;/h3&gt;
&lt;p&gt;The asymmetry that defines the entire generation is one sentence in the fluxsec.red walkthrough [@fluxsec-eti] [@fluxsec-eti]:&lt;/p&gt;

We cannot patch out the Threat Intelligence provider as this is emitted from within the kernel itself. To do so, you&apos;d require kernelmode execution and then to patch out those signals so no ETW signals are emitted. -- [@fluxsec-eti]
&lt;p&gt;That is the answer to the puzzle the section 1 hook posed. The Adam Chester 2020 patch operates on a user-mode trampoline in the calling process. &lt;code&gt;ntdll!EtwEventWrite&lt;/code&gt; is a stub that calls down through &lt;code&gt;NtTraceEvent&lt;/code&gt; into the kernel; rewriting its first byte to &lt;code&gt;0xC3&lt;/code&gt; short-circuits the user-mode entry path and the calling process emits no events through that stub. But EtwTi does not fire from the user-mode entry path. EtwTi fires from inside the kernel implementation of &lt;code&gt;NtAllocateVirtualMemory&lt;/code&gt; and friends, after the syscall has crossed the boundary, on a path the user-mode patcher cannot reach without first achieving kernel execution.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; EtwTi is the only ETW provider in the catalogue whose producer fires from the kernel side of the syscall path -- and that is exactly why a user-mode patch in the calling process cannot silence it. The PPL+ELAM gate that controls &lt;em&gt;consumer&lt;/em&gt; admission is paired with a &lt;em&gt;producer&lt;/em&gt; location that no in-process attacker can reach.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The 2017 PPL+ELAM gate was a deliberate structural defense against the patch class that was only fully publicised three years later. By the time Chester wrote his March 2020 post, the load-bearing security signal was already structurally out of reach of his technique.&lt;/p&gt;

The combination of PPL and ELAM is not an arbitrary defense-in-depth stack. PPL gates *consumer identity* at signer level: only a binary signed with the Antimalware EKU and enrolled in an ELAM allow-list can subscribe. ELAM gates *load order*: the gate is set during early boot, before any code an attacker could load gets a chance to interfere. The signer-level check is hard because forging the signature requires breaking Microsoft&apos;s PKI; the load-order check is hard because subverting it requires compromising the boot path, which Secure Boot and the Vulnerable Driver Blocklist exist to defend.
&lt;p&gt;That is the gate. Now we walk the consumers that pass through it.&lt;/p&gt;
&lt;h2&gt;10. Six vendors, three spectra: a map of the EDR consumer architecture&lt;/h2&gt;
&lt;p&gt;Defender, CrowdStrike, SentinelOne, Sysmon, Wazuh, Elastic Defend. They look interchangeable on a vendor comparison sheet. They are not, and the differences are entirely about which substrates each one consumes.&lt;/p&gt;
&lt;p&gt;There are three axes that distinguish them.&lt;/p&gt;
&lt;h3&gt;Axis 1: kernel callbacks vs ETW&lt;/h3&gt;
&lt;p&gt;Some EDRs consume process-creation events through ETW (subscribing to &lt;code&gt;Microsoft-Windows-Kernel-Process&lt;/code&gt; from a SYSTEM-privileged user-mode service). Others register kernel callbacks directly through &lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt; [@ms-pssetprocnotify] [@ms-pssetprocnotify] and &lt;code&gt;PsSetCreateThreadNotifyRoutine&lt;/code&gt; [@ms-pssetthreadnotify] [@ms-pssetthreadnotify] from a kernel driver they ship.&lt;/p&gt;
&lt;p&gt;The trade-off is sharp. Kernel callbacks are synchronous: the kernel calls into the driver before the operation completes, the driver runs at PASSIVE_LEVEL in the originating thread context with normal kernel APCs disabled, and the driver can deny the operation by writing a non-success status to &lt;code&gt;CreationStatus&lt;/code&gt;. ETW is asynchronous: the event is emitted from the producer&apos;s hot path, drained from a per-CPU buffer by the writer thread, and delivered to the consumer&apos;s callback at some later point. ETW cannot deny anything; it can only observe.&lt;/p&gt;

The `PsSetCreate*NotifyRoutine` family of kernel APIs. A driver calls `PsSetCreateProcessNotifyRoutineEx` (process create/exit), `PsSetCreateThreadNotifyRoutine` (thread create/exit), or `PsSetLoadImageNotifyRoutine` (image load) at boot to register a callback. The kernel invokes the callback synchronously, in the originating thread context at PASSIVE_LEVEL with normal kernel APCs disabled. The `Ex` variant of the process callback receives a `CreationStatus` field the driver can write to deny the operation.
&lt;p&gt;CrowdStrike, SentinelOne, Sysmon, and Elastic Defend ship kernel drivers and use callbacks for the latency-critical hot path. Defender uses both -- callbacks from &lt;code&gt;WdFilter.sys&lt;/code&gt; and ETW consumption from &lt;code&gt;MsMpEng.exe&lt;/code&gt; -- because as the in-box engine it has the institutional position to do so. Wazuh ships no kernel driver; it consumes ETW exclusively via SilkETW-class wrappers, which makes it less invasive but unable to deny.&lt;/p&gt;
&lt;h3&gt;Axis 2: PPL adoption&lt;/h3&gt;
&lt;p&gt;Defender (&lt;code&gt;MsMpEng.exe&lt;/code&gt; and &lt;code&gt;MsMpEngCP.exe&lt;/code&gt;) runs at Antimalware-PPL by default. CrowdStrike&apos;s &lt;code&gt;CSFalconService.exe&lt;/code&gt; runs at Antimalware-PPL, demonstrably [@trailofbits-shafir] [@trailofbits-shafir]. SentinelOne&apos;s &lt;code&gt;SentinelAgent.exe&lt;/code&gt; is widely reported to run at Antimalware-PPL via vendor documentation, although it does not appear in the Trail of Bits sample debugger output. Sysmon runs as a &lt;em&gt;protected process&lt;/em&gt; but not at the Antimalware signer level [@ms-sysmon] [@ms-sysmon] -- the Microsoft Learn page states &quot;The service runs as a protected process, thus disallowing a wide range of user mode interactions&quot; without naming Antimalware specifically.&lt;/p&gt;
&lt;p&gt;Wazuh and Elastic Defend&apos;s user-mode services run as standard SYSTEM-privileged services without PPL.&lt;/p&gt;
&lt;h3&gt;Axis 3: EtwTi consumption&lt;/h3&gt;
&lt;p&gt;This axis is determined by axis 2. Defender consumes EtwTi by design -- it is the in-box reason EtwTi exists. CrowdStrike and SentinelOne consume EtwTi (the Trail of Bits debugger output is the practical demonstration). Sysmon does not consume EtwTi: it is not Antimalware-PPL, so its &lt;code&gt;EnableTraceEx2&lt;/code&gt; calls against the EtwTi GUID would receive &lt;code&gt;ERROR_ACCESS_DENIED&lt;/code&gt;. Sysmon relies on its own &lt;code&gt;SysmonDrv.sys&lt;/code&gt; callbacks for the in-memory threat surface that EtwTi covers for the others. Wazuh and Elastic Defend do not consume EtwTi for the same reason; Elastic Defend ships its own kernel driver to compensate [@elastic-doubling-down] [@elastic-doubling-down], using Microsoft-blessed kernel-callback paths for memory events.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Process surface&lt;/th&gt;
&lt;th&gt;PPL level&lt;/th&gt;
&lt;th&gt;EtwTi?&lt;/th&gt;
&lt;th&gt;Primary source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Microsoft Defender&lt;/td&gt;
&lt;td&gt;Driver callbacks (&lt;code&gt;WdFilter.sys&lt;/code&gt;) + ETW (&lt;code&gt;MsMpEng.exe&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Antimalware-PPL&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;[@ms-protect-am]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CrowdStrike Falcon&lt;/td&gt;
&lt;td&gt;Driver callbacks + ETW&lt;/td&gt;
&lt;td&gt;Antimalware-PPL&lt;/td&gt;
&lt;td&gt;Yes ([@trailofbits-shafir] live evidence)&lt;/td&gt;
&lt;td&gt;[@trailofbits-shafir]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SentinelOne&lt;/td&gt;
&lt;td&gt;Driver callbacks + ETW&lt;/td&gt;
&lt;td&gt;Antimalware-PPL&lt;/td&gt;
&lt;td&gt;Widely reported&lt;/td&gt;
&lt;td&gt;-- (vendor docs; SentinelAgent.exe not in [@trailofbits-shafir] sample)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sysmon&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SysmonDrv.sys&lt;/code&gt; callbacks; publishes via own ETW provider&lt;/td&gt;
&lt;td&gt;Protected (not Antimalware)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;[@ms-sysmon]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wazuh&lt;/td&gt;
&lt;td&gt;ETW only (SilkETW-class)&lt;/td&gt;
&lt;td&gt;Standard SYSTEM&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elastic Defend&lt;/td&gt;
&lt;td&gt;Own kernel driver + ETW&lt;/td&gt;
&lt;td&gt;Standard SYSTEM&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;[@elastic-doubling-down]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Sysmon is worth singling out as the canonical &lt;em&gt;callback-then-publish&lt;/em&gt; reference architecture. Its kernel driver registers &lt;code&gt;PsSetCreate*NotifyRoutine&lt;/code&gt; callbacks; its user-mode service consumes the events the driver delivers; and the service then publishes them via its own &lt;code&gt;Microsoft-Windows-Sysmon&lt;/code&gt; ETW provider for any downstream consumer (a SIEM forwarder, a SOC dashboard, a custom analytic) to read. The result is that Sysmon&apos;s events are universally consumable -- which is why Wazuh and Splunk both ship Sysmon configurations as their default kernel-event source.&lt;/p&gt;

Sysmon&apos;s design choice is the reference architecture for the callback-then-publish pattern, even though Sysmon is not itself an Antimalware-PPL EDR. By publishing through its own ETW provider rather than writing to a private channel, Sysmon makes its events consumable by any downstream pipeline. Wazuh and the Splunk Universal Forwarder can both ingest Sysmon events without any custom integration work. This is why Sysmon, despite being free, is the de facto kernel-event source for the open-source SIEM world.

flowchart LR
    K[Kernel callbacks&lt;br /&gt;synchronous, can deny] --- L1[Sysmon driver]
    K --- L2[CrowdStrike driver]
    K --- L3[SentinelOne driver]
    K --- L4[Elastic driver]
    K --- L5[Defender WdFilter.sys]
    M[ETW providers&lt;br /&gt;asynchronous, observe-only&lt;br /&gt;up to 8 consumers per provider] --- M1[Defender MsMpEng]
    M --- M2[CrowdStrike service]
    M --- M3[SentinelOne service]
    M --- M4[Sysmon service]
    M --- M5[Wazuh ETW reader]
    M --- M6[Elastic Defend service]
    K -.latency-vs-coupling axis.-&amp;gt; M
&lt;p&gt;The CrowdStrike July 2024 channel-file outage was a kernel-driver brittleness story, not an ETW story. The Falcon kernel driver&apos;s content-update parser dereferenced an out-of-bounds pointer when processing a channel file whose Rapid Response Content template had 21 input fields while the sensor&apos;s Content Interpreter expected only 20, triggering an out-of-bounds array read, BSOD-ing roughly 8.5 million Windows hosts [@ms-crowdstrike-2024][@crowdstrike-rca-2024]. That story belongs to the App Identity in Windows article [@paragmali-com-app-ide] in this series; it is mentioned here only to mark that the cost of the synchronous-kernel-driver path is a higher blast radius when the driver itself is buggy.&lt;/p&gt;
&lt;p&gt;A note on Defender&apos;s cloud schema. The events that surface in Microsoft Defender for Endpoint&apos;s hunting tables -- &lt;code&gt;DeviceProcessEvents&lt;/code&gt;, &lt;code&gt;DeviceFileEvents&lt;/code&gt;, &lt;code&gt;DeviceNetworkEvents&lt;/code&gt;, &lt;code&gt;DeviceImageLoadEvents&lt;/code&gt;, &lt;code&gt;DeviceRegistryEvents&lt;/code&gt; -- are the cloud-side abstraction over the kernel and ETW telemetry the Defender sensor collects locally. The full schema mapping from ETW provider to cloud column is out of scope here, but the substrate is the same.&lt;/p&gt;
&lt;p&gt;Six vendors, three axes, one substrate. Now we walk the attack tradition that the substrate has to survive.&lt;/p&gt;
&lt;h2&gt;11. The attack tradition: five generations of trying to blind ETW&lt;/h2&gt;
&lt;p&gt;Every generation of ETW has been attacked. Some attacks broke a single provider; some broke every user-mode provider on a host; one would, if it worked at scale, break Defender. The defense story is on the same five-generation timeline.&lt;/p&gt;
&lt;h3&gt;Gen 1 (2014-2018): autologger registry tampering&lt;/h3&gt;
&lt;p&gt;The dispositive taxonomy is Matt Graeber and Lee Christensen&apos;s December 24, 2018 Palantir CIRT post [@palantir-tampering-wayback] [@palantir-tampering-wayback], preserved in the Wayback Machine because the direct Medium URL has since returned HTTP 403 to non-browser fetchers. The opening framing is verbatim:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Event Tracing for Windows (ETW) is the mechanism Windows uses to trace and log system events. Attackers often clear event logs to cover their tracks. Though the act of clearing an event log itself generates an event, attackers who know ETW well may take advantage of tampering opportunities to cease the flow of logging temporarily or even permanently, without generating any event log entries in the process.&quot; -- [@palantir-tampering-wayback]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Graeber and Christensen split the technique into two classes. &lt;em&gt;Persistent tampering&lt;/em&gt; writes to the autologger registry path described in section 6, disabling a session before it ever starts at next boot; the events of interest are never captured because the session is never running. &lt;em&gt;Ephemeral tampering&lt;/em&gt; targets a live session: stopping the session via &lt;code&gt;ControlTrace&lt;/code&gt;, removing a provider from a session via &lt;code&gt;EnableTraceEx2(EVENT_CONTROL_CODE_DISABLE_PROVIDER, ...)&lt;/code&gt;, or directly clearing the session&apos;s buffers.&lt;/p&gt;
&lt;p&gt;The defense is direct: monitor the autologger registry surface. Sysmon Event ID 13 [@ms-sysmon] surfaces registry value-set events in &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\&lt;/code&gt;; a SOC playbook that alerts on any unexpected write to that subtree catches the persistent class of attack reliably. Matt Graeber&apos;s authorship is cross-confirmed by the palantir/exploitguard repository [@gh-palantir-exploitguard] [@gh-palantir-exploitguard], which credits him as the lead researcher on the ETW work.&lt;/p&gt;
&lt;h3&gt;Gen 2 (2020): user-mode &lt;code&gt;EtwEventWrite&lt;/code&gt; 0xC3 RET patch&lt;/h3&gt;
&lt;p&gt;The technique that made ETW patching a household tradecraft term is Adam Chester&apos;s &quot;Hiding your .NET - ETW&quot;, March 17, 2020 [@xpn-hiding-dotnet] [@xpn-hiding-dotnet]. The mechanic is one byte:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Locate &lt;code&gt;ntdll!EtwEventWrite&lt;/code&gt; (or in modern variants &lt;code&gt;ntdll!NtTraceEvent&lt;/code&gt;) in the calling process&apos;s memory.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;VirtualProtect&lt;/code&gt; to make the page writable.&lt;/li&gt;
&lt;li&gt;Write the byte &lt;code&gt;0xC3&lt;/code&gt; over the function&apos;s first byte.&lt;/li&gt;
&lt;li&gt;Restore the page protection.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;0xC3&lt;/code&gt; is the near-return opcode [@felixcloutier-ret] [@felixcloutier-ret]: &quot;C3 RET ZO Valid Valid Near return to calling procedure.&quot; Any caller into the function falls straight back to its return address before producing a single event. The calling process now silently fails to emit any user-mode ETW events for any provider that funnels through the patched stub -- including &lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The technique has been re-implemented in every language that can call &lt;code&gt;VirtualProtect&lt;/code&gt;. The fluxsec.red Rust port [@fluxsec-etw-patching] [@fluxsec-etw-patching] explains the modern variant verbatim:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;When a ETW Provider sends a notification, it will eventually reach into ntdll.dll for the function NtTraceEvent... we can simply patch the function address to return straight from byte 0. The opcode for a ret is C3, so we can swap out the opcode 4C with C3 to immediately return out of the stub.&quot; -- [@fluxsec-etw-patching]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here is the structure of the patch in TypeScript pseudocode -- not actually runnable Win32, but mirroring exactly what a Windows binary would do:&lt;/p&gt;
&lt;p&gt;{`
// Pseudocode: silence user-mode ETW for the calling process.
// This silences only the calling process and only user-mode providers
// that funnel through the patched stub.&lt;/p&gt;
&lt;p&gt;// 1. Resolve the address of ntdll!EtwEventWrite in this process.
const ntdll = getModuleHandle(&quot;ntdll.dll&quot;);
const fn = getProcAddress(ntdll, &quot;EtwEventWrite&quot;);&lt;/p&gt;
&lt;p&gt;// 2. Make the function&apos;s first page writable.
const PAGE_EXECUTE_READWRITE = 0x40;
let oldProtect = 0;
virtualProtect(fn, 1, PAGE_EXECUTE_READWRITE, /* out */ ref(oldProtect));&lt;/p&gt;
&lt;p&gt;// 3. Write 0xC3 (RET) over the first byte. Caller now returns immediately.
writeByte(fn, 0xC3);&lt;/p&gt;
&lt;p&gt;// 4. Restore original page protection.
virtualProtect(fn, 1, oldProtect, /* out */ ref(oldProtect));&lt;/p&gt;
&lt;p&gt;// Limits:
// - Silences only this process.
// - Silences only providers whose emit path funnels through this stub.
// - Cannot silence kernel-emitted providers like Microsoft-Windows-Threat-Intelligence.
`}&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The patch operates on the calling process&apos;s user-mode trampoline. Other processes on the host are unaffected; their ETW emissions continue normally. Kernel-emitted providers like &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; are unaffected even in the patched process; they fire from the kernel side of the syscall path, after control has crossed the user/kernel boundary, on a code path the user-mode patcher cannot reach without first achieving kernel execution.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Gen 3 (2021-2023): kernel-mode primitives&lt;/h3&gt;
&lt;p&gt;If a user-mode patch cannot reach EtwTi, can a kernel-mode patch? Yes -- but the attacker first needs kernel execution. The most common path is BYOVD [@paragmali-com-in-windows]: load a signed but vulnerable driver and use its primitive to read or write kernel memory. Once you can write kernel memory you can target ETW&apos;s internal data structures directly.&lt;/p&gt;
&lt;p&gt;Binarly&apos;s Black Hat Europe 2021 talk [@binarly-edr] [@binarly-edr] documents the surface verbatim:&lt;/p&gt;

Many ways to disable ETW logging are publicly available from passing a TRUE boolean parameter into a `nt!EtwpStopTrace` function to finding an ETW specific structure and dynamically modifying it or patching `ntdll!ETWEventWrite` or `advapi32!EventWrite` to return immediately thus stopping the user-mode loggers. -- [@binarly-edr]
&lt;p&gt;The kernel-side primitives Binarly enumerates target the &lt;code&gt;_ETW_GUID_ENTRY&lt;/code&gt; structure for a provider, the &lt;code&gt;EtwpRegistration&lt;/code&gt; linked list of registered providers, and the &lt;code&gt;EtwpEventTracingProhibited&lt;/code&gt; flag the kernel checks before emitting events. Yarden Shafir&apos;s 2023 Trail of Bits walkthrough [@trailofbits-shafir] [@trailofbits-shafir] provides the contemporary kernel-side data structure walk through &lt;code&gt;_ETW_REALTIME_CONSUMER&lt;/code&gt; and &lt;code&gt;_ETW_SILODRIVERSTATE&lt;/code&gt;, and notes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Most recently, the Lazarus Group bypassed EDR detection by disabling ETW providers&quot; -- [@trailofbits-shafir]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The architectural-level treatment is well-documented; the specific kernel offsets that change between Windows builds are a moving target. We treat the technique class as well-established and the per-build offset details as out of scope.&lt;/p&gt;
&lt;h3&gt;Defense Gen 1 (2017): Antimalware-PPL + ELAM gate on EtwTi&lt;/h3&gt;
&lt;p&gt;Section 9 covered this in detail. The point to record here, in the attack-tradition timeline, is that the Antimalware-PPL gate predates the Adam Chester 2020 user-mode patch by three years. Microsoft did not respond to Chester&apos;s post; they had already put the load-bearing security signal structurally out of reach of any user-mode patch in the calling process. The user-mode patch class is generic against &lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt; and the rest of the user-mode catalogue; it is structurally impotent against &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Defense Gen 2 (2022): Vulnerable Driver Blocklist on by default&lt;/h3&gt;
&lt;p&gt;The kernel-mode primitive class needs a kernel write. Without a vulnerability in the EDR&apos;s kernel driver, the realistic path is BYOVD: load a third-party signed driver that exposes a memory-write primitive. The structural defense is Microsoft&apos;s Vulnerable Driver Blocklist [@ms-vdb] [@ms-vdb]:&lt;/p&gt;

Since the Windows 11 2022 update, the vulnerable driver blocklist is enabled by default for all devices, and can be turned on or off via the Windows Security app... the vulnerable driver blocklist is also enforced when either memory integrity, also known as hypervisor-protected code integrity (HVCI), Smart App Control, or S mode is active... The blocklist is updated quarterly. In addition, blocklist updates are delivered through the monthly Windows updates as part of the standard servicing process. -- [@ms-vdb]
&lt;p&gt;The blocklist enumerates known-vulnerable signed drivers by hash; the kernel refuses to load anything on the list. On a Windows 11 22H2-or-later host with the default settings, the BYOVD primitive against most known-vulnerable drivers is closed. With HVCI on, the closure is enforced even against attackers who would otherwise try to load drivers via legacy paths. The empirical bound is the LOLDrivers project&apos;s catalogue of known-vulnerable drivers; the blocklist tracks public discovery with a lag of approximately one quarter, which is the residual window an attacker can exploit before a freshly disclosed driver is added.&lt;/p&gt;

The attack pattern of loading a known-vulnerable but signed driver to obtain a kernel-mode primitive (memory read, memory write, or arbitrary code execution). Used in real-world EDR-blinding attacks, including by the Lazarus Group as cited in Trail of Bits&apos; 2023 ETW walk [@trailofbits-shafir].

The Microsoft-maintained blocklist of known-vulnerable signed drivers, by hash. Enabled by default on Windows 11 22H2 and later. Enforced more strictly when HVCI, Smart App Control, or S mode is active. Updated quarterly per the Microsoft Learn primary [@ms-vdb].
&lt;p&gt;The LOLDrivers project [@loldrivers] [@loldrivers] is the empirical anchor for the BYOVD lag story. It catalogues known-vulnerable signed drivers as a community resource; the Microsoft blocklist updates quarterly, but blocklist updates are also delivered through monthly Windows servicing, so a freshly-disclosed driver can live in an exploitation window of as short as ~1 month (via Patch Tuesday) or up to a full quarter before its hash is added.&lt;/p&gt;

flowchart LR
    subgraph Attacks
        A1[&quot;Gen 1 2014-2018: Autologger registry tampering -- Palantir CIRT taxonomy&quot;]
        A2[&quot;Gen 2 2020: EtwEventWrite 0xC3 RET -- Adam Chester&quot;]
        A3[&quot;Gen 3 2021-2023: Kernel _ETW_GUID_ENTRY -- EtwpRegistration EtwpStopTrace via BYOVD&quot;]
    end
    subgraph Defenses
        D1[&quot;Sysmon Event ID 13 -- monitor Autologger subtree&quot;]
        D2[&quot;Antimalware-PPL plus ELAM -- gate on EtwTi 2017&quot;]
        D3[&quot;Vulnerable Driver Blocklist -- default-on Win11 22H2 plus HVCI&quot;]
    end
    A1 --&amp;gt; D1
    A2 --&amp;gt; D2
    A3 --&amp;gt; D3
&lt;h3&gt;The 2026 picture&lt;/h3&gt;
&lt;p&gt;User-mode patching cannot reach the kernel-mode provider that EDR cares about. The BYOVD primitive that could reach it is structurally narrowed by default on supported hardware. The remaining gap is the long tail of newly-disclosed vulnerable drivers between disclosure and blocklist update, plus any custom kernel zero-day an attacker discovers in an EDR&apos;s own driver. Both are real, both are exploited in the wild, neither is the universally-applicable evasion the 2020-era user-mode patch class was.&lt;/p&gt;
&lt;p&gt;That is the operational story. But ETW has structural limits even when no attacker is patching anything.&lt;/p&gt;
&lt;h2&gt;12. Theoretical limits: what ETW cannot see, even with every defence engaged&lt;/h2&gt;
&lt;p&gt;Even on a perfectly-configured Windows 11 box -- HVCI [@paragmali-com-in-windows] on, Vulnerable Driver Blocklist on, Antimalware-PPL Defender consuming EtwTi, third-party EDR ELAM-onboarded -- there are events ETW does not emit. Some are observed too late. Some are not observed at all.&lt;/p&gt;
&lt;p&gt;There are three structural ceilings.&lt;/p&gt;
&lt;h3&gt;Pre-ETW kernel paths&lt;/h3&gt;
&lt;p&gt;The Global Logger session is one of the earliest things to come up at boot, but it is not the first. Some early-init driver paths run before any ETW session exists; they cannot be traced via ETW. Measured Boot is the discipline that records this prefix into TPM PCRs, with attestation handled by the platform integrity layer rather than by ETW. The implication for EDR is that any malicious code executing during early boot, before the Global Logger session is up, is invisible to ETW.&lt;/p&gt;
&lt;h3&gt;Incomplete EtwTi syscall coverage&lt;/h3&gt;
&lt;p&gt;The 10 &lt;code&gt;KERNEL_THREATINT_TASK_*&lt;/code&gt; task IDs are the public surface. The underlying syscall set the kernel actually instruments is not exhaustively documented. The fluxsec.red inventory [@fluxsec-eti] [@fluxsec-eti] is the public surface, not the private one. Some syscalls are clearly covered (&lt;code&gt;NtAllocateVirtualMemory&lt;/code&gt; for cross-process allocation surfaces as &lt;code&gt;KERNEL_THREATINT_TASK_ALLOCVM&lt;/code&gt;); some have partial coverage (&lt;code&gt;MAPVIEW_LOCAL&lt;/code&gt; and &lt;code&gt;MAPVIEW_REMOTE&lt;/code&gt; keywords cover some but not all of the section-mapping primitive set across &lt;code&gt;NtCreateSection&lt;/code&gt;, &lt;code&gt;NtMapViewOfSection&lt;/code&gt;, &lt;code&gt;NtMapViewOfSectionEx&lt;/code&gt;, image-section vs file-section variants); some are not enumerated at all in the public manifest. Process-hollowing primitives that combine &lt;code&gt;NtUnmapViewOfSection&lt;/code&gt; and &lt;code&gt;NtMapViewOfSection&lt;/code&gt; may be partially covered depending on which path the attacker takes.&lt;/p&gt;
&lt;h3&gt;The async-flush gap&lt;/h3&gt;
&lt;p&gt;ETW&apos;s per-CPU ring buffer is asynchronous. If a process allocates RWX memory, writes shellcode, executes it, and returns within one writer-thread flush interval, the event is &lt;em&gt;recorded&lt;/em&gt; but the attacker&apos;s payload has &lt;em&gt;already executed&lt;/em&gt;. The synchronous denial primitive on Windows belongs to kernel notify routines, not to ETW. The Microsoft Learn primary on About Event Tracing [@ms-about-etw] [@ms-about-etw] is explicit that events can be lost:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Events can be lost if any of the following conditions occur ... The total event size is greater than 64K ... The disk is too slow to keep up with the rate at which events are being generated. ... For real-time logging, the real-time consumer is not consuming events fast enough.&quot; -- [@ms-about-etw]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;No ETW-only EDR can prevent a syscall whose payload completes inside one writer flush. EDRs that ship a kernel driver and register synchronous callbacks (CrowdStrike, SentinelOne, Sysmon, Elastic Defend) can deny operations through the &lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt; [@ms-pssetprocnotify] [@ms-pssetprocnotify] &lt;code&gt;CreationStatus&lt;/code&gt; field; ETW-only EDRs cannot. ETW is observation, not enforcement.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; ETW is observation, not enforcement. The synchronous denial primitive on Windows belongs to kernel notify routines, not to ETW. Sub-microsecond payloads execute before the writer thread flushes; the layered defense stack of 2026 is an empirical bar, not a theoretical guarantee.&lt;/p&gt;
&lt;/blockquote&gt;

The VBS-backed code-integrity enforcement for kernel-mode code on Windows. With HVCI enabled, the hypervisor enforces that only signed kernel pages can execute. Closes the attack class that loads unsigned drivers; combined with the Vulnerable Driver Blocklist it closes most of the realistic BYOVD primitive surface as well.
&lt;p&gt;The &quot;events can be lost&quot; enumeration in [@ms-about-etw] is the dispositive Microsoft acknowledgement of ETW&apos;s lossy substrate. SOC playbooks should treat ETW telemetry as best-effort, not as a guaranteed audit trail. Forensic claims that depend on completeness need an independent corroborating source.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A detection-only EDR can alert on a malicious operation, but only after the operation has happened. By the time the SOC sees the alert, the syscall has completed, the shellcode has executed, the credentials have been stolen. This is why the kernel-callback path (with its ability to deny via &lt;code&gt;CreationStatus&lt;/code&gt;) coexists with ETW even though ETW is more flexible: a SOC playbook needs both the speed of denial and the breadth of observation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The 2026 layered stack -- Antimalware-PPL + EtwTi + HVCI + VBL -- raises the empirical bar enormously. It does not close the architectural gap. Sub-microsecond payloads still execute before the writer thread flushes. The BYOVD primitive on a non-HVCI box still defeats the kernel-callback layer. There are still problems the substrate cannot solve in principle.&lt;/p&gt;
&lt;p&gt;Those are the limits we can describe. The next section is about the limits we cannot yet measure.&lt;/p&gt;
&lt;h2&gt;13. Open problems: keyword drift, secure kernel ETW, and the BYOVD arms race&lt;/h2&gt;
&lt;p&gt;The 2026 state of the art has five active open problems. Each has a partial workaround; none has a complete solution.&lt;/p&gt;
&lt;h3&gt;1. EtwTi keyword inventory drift across builds&lt;/h3&gt;
&lt;p&gt;Microsoft has not published a complete, current &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; keyword inventory. The community-maintained references -- the jdu2600 cross-build inventory [@gh-jdu2600] [@gh-jdu2600] and the repnz manifest archive [@gh-repnz] [@gh-repnz] -- are partial coverage and lag Microsoft&apos;s quarterly servicing cadence. EDR vendors that hard-code keyword bitmasks against an old build can silently miss events on newer builds because the keyword definitions have shifted underneath them. Detection engineers writing rules against &lt;code&gt;KERNEL_THREATINT_TASK_*&lt;/code&gt; IDs that move between builds can get false negatives.&lt;/p&gt;

There are three plausible reasons, and Microsoft has not stated which (or which combination) is operative. *Operational secrecy*: a complete keyword inventory tells attackers exactly which syscall paths are observed and which are not, narrowing the search for evasion paths. *Documentation cost*: the inventory shifts every build, and maintaining a synchronised public reference is engineering work without an obvious internal champion. *Deliberate moving target*: keeping the public surface incomplete forces attackers to reverse-engineer per build, raising the cost of stable evasion. The community references partially defeat all three rationales; the absence remains.
&lt;h3&gt;2. Secure ETW (the &lt;code&gt;EtwSi*&lt;/code&gt; family)&lt;/h3&gt;
&lt;p&gt;Windows VBS Trustlets run in the Secure Kernel (VTL1), insulated from the normal-world kernel (VTL0) by the hypervisor. The Secure Kernel exposes its own ETW family for VTL1 components; this is enumerated in fragments in Alex Ionescu&apos;s BlackHat 2015 deck on the Secure Kernel and in subsequent BlueHatIL talks. There is no public consumer-facing primary on &lt;code&gt;EtwSi*&lt;/code&gt; in 2026. Cross-link: this article&apos;s companion piece on VBS Trustlets [@paragmali-vbs-trustlets] [@paragmali-vbs-trustlets] covers the producer side of the story.&lt;/p&gt;
&lt;h3&gt;3. Forensic soundness of ETW telemetry&lt;/h3&gt;
&lt;p&gt;ETW is lossy by design (per the [@ms-about-etw] enumeration). Whether ETW-derived telemetry is &lt;em&gt;forensically sound&lt;/em&gt; -- chain-of-custody complete, lossless under load, attestable as untampered between event emission and SIEM ingestion -- is an open question. Courts have not ruled. The current best partial result is to treat ETW as supporting evidence and require independent corroboration (file-system snapshots, network captures, OS state captures) for any claim that depends on completeness. Sysmon&apos;s Event ID 16 (Sysmon configuration changed) [@ms-sysmon] and the autologger registry write events on &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\&lt;/code&gt; are useful integrity signals: an attacker who silenced ETW typically leaves a footprint here.&lt;/p&gt;
&lt;h3&gt;4. The BYOVD arms race&lt;/h3&gt;
&lt;p&gt;The Vulnerable Driver Blocklist [@ms-vdb] [@ms-vdb] is hash-based and updated quarterly. The LOLDrivers project [@loldrivers] [@loldrivers] documents the public catalogue of known-vulnerable signed drivers. The gap between disclosure and blocklist update--as short as ~1 month via Patch Tuesday or up to a full quarter--is the residual exploitation window. The deeper structural issue is that the blocklist is hash-based; an attacker who finds a new vulnerability in a previously-trusted signed driver enjoys a fresh window every quarter. Closing this gap requires either a different trust model (allow-listing of known-good drivers, as Smart App Control does for executables) or behavioural detection of suspicious driver loads. Both are active areas of work.&lt;/p&gt;
&lt;h3&gt;5. Cross-process section-mapping coverage&lt;/h3&gt;
&lt;p&gt;EtwTi&apos;s &lt;code&gt;KERNEL_THREATINT_TASK_MAPVIEW&lt;/code&gt; covers some but not all section-mapping primitives. The public fluxsec.red [@fluxsec-eti] inventory lists &lt;code&gt;MAPVIEW_LOCAL&lt;/code&gt; and &lt;code&gt;MAPVIEW_REMOTE&lt;/code&gt; keywords, but the underlying syscall set (&lt;code&gt;NtMapViewOfSection&lt;/code&gt;, &lt;code&gt;NtMapViewOfSectionEx&lt;/code&gt;, &lt;code&gt;NtCreateSection&lt;/code&gt;, image-section vs file-section variants) is not exhaustively documented. Detection engineers who depend on full coverage of cross-process section mapping are working from an incomplete map.&lt;/p&gt;
&lt;h3&gt;What would a v2 ETW look like?&lt;/h3&gt;
&lt;p&gt;A theoretical ideal: synchronous kernel-emitted events on every security-relevant syscall, with the consumer running in VTL1 (Secure Kernel) so even a kernel-mode attacker in VTL0 cannot tamper with the consumer. The &lt;code&gt;EtwSi*&lt;/code&gt; family is the partial realisation. The full ideal is incompatible with x64 syscall performance: synchronous notification on every syscall would dominate the cost of the syscall itself. The pragmatic answer Microsoft has been building toward is &lt;em&gt;selective&lt;/em&gt; synchronous notification (the kernel notify routines for high-value control points) layered with &lt;em&gt;broad&lt;/em&gt; asynchronous observation (ETW for everything else), with the most security-critical of the broad observations promoted to PPL/ELAM-gated kernel-emitted producers (EtwTi). Two decades of layering, no single architectural endpoint.For the producer side of the Secure Kernel ETW story (&lt;code&gt;EtwSi*&lt;/code&gt;), see this article&apos;s companion piece on VBS Trustlets [@paragmali-vbs-trustlets] [@paragmali-vbs-trustlets] in the same series. The Trustlet-side architecture is a separate topic large enough to need its own walkthrough.&lt;/p&gt;
&lt;p&gt;Open problems are interesting but they are not actionable. The next section is about what an engineer can do on Monday morning.&lt;/p&gt;
&lt;h2&gt;14. Practical guide: five things to do Monday morning&lt;/h2&gt;
&lt;p&gt;You have read 12,000 words about ETW. Here are five concrete checks an engineer can run on a Windows host this morning.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;logman query providers&lt;/code&gt; enumerates every registered provider on the host. Cross-reference the output against the section 8 catalogue and flag any security-relevant provider your EDR is not consuming. Pay specific attention to &lt;code&gt;Microsoft-Antimalware-Scan-Interface&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-PowerShell&lt;/code&gt;, &lt;code&gt;Microsoft-Windows-DotNETRuntime&lt;/code&gt;, and &lt;code&gt;Microsoft-Windows-Sysmon&lt;/code&gt; if Sysmon is installed. Missing coverage of any of these on a host you are responsible for is a detection-coverage gap, not a configuration issue.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Run &lt;code&gt;wevtutil gp Microsoft-Windows-Threat-Intelligence&lt;/code&gt; to confirm the provider is registered and inspect its keyword definitions. Then check whether your EDR is actually a consumer: walk the live-debugger handle enumeration in Yarden Shafir&apos;s Trail of Bits post [@trailofbits-shafir] [@trailofbits-shafir] (the WinDbg JS scripts are linked from the post). If your EDR is supposed to be ELAM-onboarded but does not appear in the consumer enumeration for an EtwTi logger session, your installation may have lost the gate. This is the difference between a configured EDR and a functional EDR.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Enumerate &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\&lt;/code&gt; for unauthorised session entries. Per the Palantir CIRT taxonomy [@palantir-tampering-wayback] [@palantir-tampering-wayback], this is the persistent-tampering surface. A baseline audit should produce a known list of expected sessions (Defender, your EDR, Sysmon if installed, the standard Windows diagnostic listeners). Any subkey not on the baseline list is an investigation candidate. Sysmon Event ID 13 (registry value set) [@ms-sysmon] on this subtree is a high-signal alert in any SIEM.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Run &lt;code&gt;Get-CimInstance Win32_DeviceGuard | Select-Object SecurityServicesConfigured, SecurityServicesRunning, VirtualizationBasedSecurityStatus&lt;/code&gt; to expose whether HVCI and the Vulnerable Driver Blocklist are active. Per the Microsoft Learn primary [@ms-vdb] [@ms-vdb], the BYOVD ceiling is your kernel-tampering integrity guarantee. If VBS is &lt;code&gt;Off&lt;/code&gt; on a managed endpoint, your detection coverage is structurally weaker than it should be on supported hardware. Treat it as a hardening item, not a nice-to-have.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Write a hunting query for the pattern: &quot;process X registers as ETW consumer for &lt;code&gt;Microsoft-Windows-Threat-Intelligence&lt;/code&gt; and X is not on the EDR allow-list.&quot; The provider&apos;s PPL+ELAM gate makes this a high-signal alert: only a signed Antimalware-PPL service can pass the gate, so an unexpected process holding an &lt;code&gt;EtwConsumer&lt;/code&gt; handle to the TI logger ID is either a misconfigured tool, a legitimate research session you forgot about, or an attacker chain that has acquired Antimalware-PPL trust on your fleet. The first two are quick to triage; the third is an incident.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The structure of the check in pseudocode -- mirroring the WinDbg JS approach in [@trailofbits-shafir]:&lt;/p&gt;
&lt;p&gt;{`
// Pseudocode: inventory providers and identify EtwTi consumers.&lt;/p&gt;
&lt;p&gt;// 1. Enumerate registered providers and find Microsoft-Windows-Threat-Intelligence.
const providers = enumerateRegisteredProviders();
const tiProvider = providers.find(p =&amp;gt; p.guid === &quot;{f4e1897c-bb5d-5668-f1d8-040f4d8dd344}&quot;);
if (!tiProvider) {
  warn(&quot;EtwTi provider not registered on this host&quot;);
}&lt;/p&gt;
&lt;p&gt;// 2. Enumerate live trace sessions and find any that subscribe to TI.
const sessions = enumerateLoggerSessions();  // logman query -ets equivalent
const tiSessions = sessions.filter(s =&amp;gt;
  s.providers.some(p =&amp;gt; p.guid === tiProvider?.guid));&lt;/p&gt;
&lt;p&gt;// 3. Walk EtwConsumer handles for each TI session; identify the consuming processes.
const expectedConsumers = [&quot;MsMpEng.exe&quot;, &quot;CSFalconService.exe&quot;, &quot;SentinelAgent.exe&quot;];
for (const session of tiSessions) {
  const consumers = enumerateEtwConsumers(session.loggerId);  // Shafir WinDbg JS
  for (const consumer of consumers) {
    if (!expectedConsumers.includes(consumer.processName)) {
      alert(`Unexpected EtwTi consumer: ${consumer.processName} (PID ${consumer.pid})`);
    }
  }
}&lt;/p&gt;
&lt;p&gt;// 4. Audit autologger persistence entries against a known baseline.
const baseline = loadAutologgerBaseline();
const live = enumerateAutologgerSubkeys();  // HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger
for (const entry of live) {
  if (!baseline.includes(entry.name)) {
    alert(`Unexpected autologger entry: ${entry.name}`);
  }
}
`}&lt;/p&gt;
&lt;p&gt;With those five checks, the catalogue is no longer an abstraction. You have an inventory of what your host emits, an inventory of who consumes the most security-critical provider, an audit of the persistence surface that defines what gets emitted at all, a confirmation of the integrity layer that closes BYOVD, and a hunt for anyone who has somehow obtained the passport. Now we close with the questions every reader should expect to have.&lt;/p&gt;
&lt;h2&gt;15. Frequently asked questions&lt;/h2&gt;

Yes, for *publication*. Sysmon&apos;s kernel driver `SysmonDrv.sys` registers `PsSetCreateProcessNotifyRoutineEx` and the related thread- and image-load callbacks; the user-mode service then publishes the resulting events via its own `Microsoft-Windows-Sysmon` ETW provider GUID `{5770385F-C22A-43E0-BF4C-06F5698FFBD9}` [@ms-sysmon]. It does not consume the public catalogue providers via ETW for its kernel-event hot path; the kernel taps come straight from the callback API. This callback-then-publish architecture is why Sysmon&apos;s events are universally consumable by SIEM forwarders and downstream tools.

Because Defender consumes `Microsoft-Windows-Threat-Intelligence`, which fires from the kernel side of memory-modifying syscalls, not from the user-mode `ntdll!EtwEventWrite` trampoline. The fluxsec.red walkthrough states the asymmetry verbatim: &quot;we cannot patch out the Threat Intelligence provider as this is emitted from within the kernel itself&quot; [@fluxsec-eti]. The Adam Chester 2020 patch silences user-mode providers (like `Microsoft-Windows-DotNETRuntime`) for the patched process; it cannot silence kernel-emitted providers for any process. Defender&apos;s load-bearing security signal is structurally out of reach of the user-mode patch class.

No. The provider&apos;s security descriptor admits only Antimalware-PPL signers loaded by an ELAM driver. A non-PPL `EnableTraceEx2` call against the EtwTi GUID returns `ERROR_ACCESS_DENIED` (the Microsoft Learn primary on EnableTraceEx2 [@ms-enabletraceex2] [@ms-enabletraceex2] documents the error code for insufficient-privilege callers; the PPL-specific gate that triggers it for EtwTi is described in [@fluxsec-eti]). The gate exists because an attacker who could trivially become an EtwTi consumer would have direct visibility into the kernel&apos;s view of every memory-modifying syscall on the host -- exactly the inventory needed to evade everything else.

Schema location. Manifest-based providers ship an out-of-band XML manifest registered with `wevtutil im`; consumers decode events against the system-installed manifest using TDH. TraceLogging providers carry the schema *inline* in each event payload as type-length-value triples; consumers decode without any registered manifest. TraceLogging events are larger because the schema bytes ride in the payload; manifest events have a smaller per-event size at the cost of installation friction. Both inherit the eight-session cap [@ms-about-etw], [@ms-tracelogging-about].

Sixty-four globally per [@ms-etw-sessions], with Windows 2000 limited to 32. Per-provider, manifest-based and TraceLogging providers admit up to 8 simultaneous sessions; classic and WPP providers admit only 1 [@ms-about-etw], [@ms-etw-config]. The runtime symptom of the per-provider 8-session cap binding is `ERROR_NO_SYSTEM_RESOURCES` from `EnableTraceEx2` [@ms-enabletraceex2]; the runtime symptom of the global 64-session cap binding is the same error from `StartTrace`.

No. EventPipe is a managed-runtime cross-platform analogue to ETW that shipped in .NET Core 3.0 (September 2019) and remains available in every later release including .NET 5+. It runs on Linux and macOS as well as Windows. On Windows, the kernel-mode providers and the EtwTi security substrate have no EventPipe equivalent; EventPipe is a complement to ETW for managed workloads, not a replacement. The Windows EDR substrate remains ETW; managed-runtime tracing has acquired an additional cross-platform path that does not displace it.
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;etw-event-tracing-for-windows-and-the-edr-substrate&quot; keyTerms={[
  { term: &quot;ETW&quot;, definition: &quot;Event Tracing for Windows: kernel-buffered observability bus introduced in Windows 2000.&quot; },
  { term: &quot;Provider&quot;, definition: &quot;A component that emits ETW events tagged with a GUID.&quot; },
  { term: &quot;Controller&quot;, definition: &quot;A component that creates, configures, and stops trace sessions.&quot; },
  { term: &quot;Consumer&quot;, definition: &quot;A component that reads events from a session in real time or from an .etl file.&quot; },
  { term: &quot;Manifest-based provider&quot;, definition: &quot;Vista-era ETW provider class with XML manifest schema and 8-session cap.&quot; },
  { term: &quot;TraceLogging&quot;, definition: &quot;Self-describing ETW provider class with inline TLV schema, shipped in Windows 10.&quot; },
  { term: &quot;EtwTi&quot;, definition: &quot;Microsoft-Windows-Threat-Intelligence: the kernel-emitted memory-syscall provider; PPL+ELAM-gated.&quot; },
  { term: &quot;Antimalware-PPL&quot;, definition: &quot;Signer level on the PPL lattice for antimalware services; gates EtwTi consumption.&quot; },
  { term: &quot;ELAM&quot;, definition: &quot;Early Launch Antimalware: driver class that gates the certificate inventory for permitted Antimalware-PPL binaries.&quot; },
  { term: &quot;BYOVD&quot;, definition: &quot;Bring Your Own Vulnerable Driver: load a known-vulnerable signed driver to obtain kernel primitive.&quot; },
  { term: &quot;Vulnerable Driver Blocklist&quot;, definition: &quot;Microsoft-maintained hash blocklist; default-on in Windows 11 22H2.&quot; },
  { term: &quot;Autologger&quot;, definition: &quot;Registry-persisted boot-time ETW session under HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\.&quot; }
]} /&amp;gt;&lt;/p&gt;
&lt;p&gt;ETW is now twenty-six years old. It started as a performance facility for Windows 2000 driver authors who could not afford &lt;code&gt;DbgPrint&lt;/code&gt; on production servers, and it became the substrate of every major Windows endpoint security product through a decade of unintended consequences. The Vista team that raised the per-provider session cap from 1 to 8 was thinking about ergonomics. The Windows 8.1 team that introduced Antimalware-PPL was thinking about Defender&apos;s hardening, not about future third-party EDRs. The team that shipped EtwTi in the Windows 10 RS-era understood the security stakes precisely. By 2026 those three decisions, taken in three different Microsoft contexts a decade apart, are the architecture of detection on the Windows endpoint -- and the reason the operator in the section 1 hook scene loses the round even when the patch works exactly as it should.&lt;/p&gt;
</content:encoded><category>etw</category><category>windows-internals</category><category>edr</category><category>security</category><category>kernel</category><category>detection-engineering</category><category>threat-intelligence</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>The Object Manager Namespace: The Hierarchical Filesystem Underneath Every Windows Security Boundary</title><link>https://paragmali.com/blog/the-object-manager-namespace/</link><guid isPermaLink="true">https://paragmali.com/blog/the-object-manager-namespace/</guid><description>A bottom-up tour of the Windows Object Manager namespace, the 1993 Cutler-era kernel data structure that every Windows security boundary quietly assumes.</description><pubDate>Mon, 11 May 2026 00:00:00 GMT</pubDate><content:encoded>
**The Windows Object Manager namespace is the kernel-resident, filesystem-shaped tree that every Windows security boundary quietly assumes.** Every named kernel object -- processes, threads, sections, files, registry keys, tokens, mutants, semaphores, ALPC ports, devices, drivers, jobs, silos -- lives somewhere under `\`. Six generations of isolation primitives (Session 0 isolation, AppContainer lowbox, integrity levels, VBS trustlets, Server Silos, and the `ObRegisterCallbacks` EDR sensor surface) are all path rewrites, per-directory ACLs, or kernel callbacks layered on the same 1993 Cutler-era four-piece structure. This article builds the namespace bottom-up -- `OBJECT_HEADER`, `OBJECT_TYPE`, `ParseProcedure`, `OBJECT_DIRECTORY` -- walks the 2026 top-level directory atlas on Windows 11 25H2, surveys the exploit tradition (symbolic-link redirection, namespace squatting, bait-and-switch on `\??` and `\Device`, arbitrary directory creation), and closes on the EDR pivot in `ObRegisterCallbacks`.
&lt;h2&gt;1. The path that isn&apos;t a path&lt;/h2&gt;
&lt;p&gt;Open &lt;code&gt;WinObj.exe&lt;/code&gt; as administrator on any Windows 11 25H2 machine (&lt;a href=&quot;https://en.wikipedia.org/wiki/Windows_11_version_history&quot; rel=&quot;noopener&quot;&gt;Windows 11 version history&lt;/a&gt;). For about ten seconds the screen looks like a filesystem. The root is named &lt;code&gt;\&lt;/code&gt;. Below it sit folders called &lt;code&gt;\Device&lt;/code&gt;, &lt;code&gt;\BaseNamedObjects&lt;/code&gt;, &lt;code&gt;\Sessions&lt;/code&gt;, &lt;code&gt;\RPC Control&lt;/code&gt;, &lt;code&gt;\KnownDlls&lt;/code&gt;, and &lt;code&gt;\ObjectTypes&lt;/code&gt;. Double-click any of them and you see children. Right-click any node and you can read a security descriptor. This is essentially the same UI a 1996 SysAdmin would have recognised; the tool first shipped that year as part of Mark Russinovich and Bryce Cogswell&apos;s Winternals [@en-wikipedia-mark-russinovich], and the current build is a Microsoft-signed Sysinternals binary whose navigation surface has not been redesigned in three decades [@ms-winobj].&lt;/p&gt;
&lt;p&gt;Navigate to &lt;code&gt;\Sessions\1\AppContainerNamedObjects&lt;/code&gt; and the picture starts to fracture. Inside that directory you will find one subdirectory per running AppContainer-sandboxed app, each named after a long Security Identifier of the form &lt;code&gt;S-1-15-2-...&lt;/code&gt;. Pick the one belonging to the Microsoft Edge renderer process you are reading this article in. Every named mutant, event, section, semaphore, and ALPC port the renderer can ever name lives inside that one subdirectory. The renderer cannot escape it. Not because of a permission check that comes second, but because the kernel rewrites every name the renderer asks for, transparently, before path resolution begins. Microsoft&apos;s AppContainer Isolation documentation [@ms-appcontainer-isolation] calls this &quot;sandboxing the application kernel objects.&quot;&lt;/p&gt;
&lt;p&gt;This tree is not a filesystem. There is no disk persistence; nothing under &lt;code&gt;\&lt;/code&gt; survives a reboot. It is not the Windows registry either; the registry is a separate subsystem with its own hive format that hangs off the namespace only through a parse procedure on the &lt;code&gt;Key&lt;/code&gt; object type. What this tree is, instead, is the Object Manager namespace: the in-memory, kernel-resident, hierarchical name service that the Windows kernel uses to locate every nameable kernel object [@ms-managing-kernel-objects]. Its top-level directories are catalogued in the driver kit&apos;s Object Directories reference [@ms-object-directories].&lt;/p&gt;

The Windows Object Manager, internally called `Ob`, is a kernel-mode subsystem of the Windows Executive that manages the lifetime, naming, security, and accounting of every resource the kernel exposes to user mode as a named object. Wikipedia summarises it as a &quot;subsystem implemented as part of the Windows Executive which manages Windows resources... each [resource] reside[s] in a namespace for categorization&quot; [@en-wikipedia-object-manager].
&lt;p&gt;Here is the thesis the rest of this article spends nine thousand words unpacking. Every Windows security boundary you have read about -- Session 0 isolation, Mandatory Integrity Control, AppContainer, the Virtualization-Based Security trustlets, Server Silos and Windows containers, the EDR sensor surface that fires when something opens a handle to &lt;code&gt;lsass.exe&lt;/code&gt; -- is &lt;em&gt;physically realised&lt;/em&gt; in this tree. Each boundary is either a path rewrite at lookup time, a per-directory ACL, a token-keyed name substitution, or a kernel callback registered against an &lt;code&gt;OBJECT_TYPE&lt;/code&gt;. The boundaries you read about elsewhere are the &lt;em&gt;policies&lt;/em&gt;; this tree is the &lt;em&gt;mechanism&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The Object Manager has shipped without architectural change for thirty-three years. Whose decision was that? And why did a 1993 data structure survive untouched while the GUI, the driver model, the security subsystem, and the boot path around it were rewritten more than once?&lt;/p&gt;
&lt;h2&gt;2. Where the namespace came from&lt;/h2&gt;
&lt;p&gt;The decision belongs to Dave Cutler. In 1988 Microsoft hired Cutler away from Digital Equipment Corporation. The Wikipedia biography records the line of operating systems Cutler had developed at DEC: &quot;RSX-11M, VAXELN, VMS, and MICA&quot; [@en-wikipedia-dave-cutler]. Three of those shipped commercially; the fourth, MICA, was cancelled with the Prism RISC program. Cutler walked out, and Microsoft signed him with a charter from Bill Gates to build a portable next-generation kernel that could host the existing Windows API on top of a 32-bit, multi-architecture base [@en-wikipedia-architecture-of-windows-nt]. Cutler brought a small team of DEC veterans with him.&lt;/p&gt;
&lt;p&gt;The Object Manager is one of that team&apos;s earliest design decisions. The architectural bet was to &lt;em&gt;unify every named kernel object&lt;/em&gt; under one filesystem-shaped tree, with each type carrying a parse procedure so a single family of syscalls (&lt;code&gt;NtCreateFile&lt;/code&gt;, &lt;code&gt;NtOpenSection&lt;/code&gt;, &lt;code&gt;NtOpenProcess&lt;/code&gt;, and so on) could address files, registry keys, processes, ports, sections, drivers, devices, jobs, and synchronization primitives using the same path-walk algorithm. That was an unusual choice in 1989. VMS had a more typed, less unified resource broker. Mach treated kernel objects as capability-style port rights and never gave them a hierarchical name. Cutler&apos;s choice was, at heart, a Plan-9-style &quot;every named resource is a filesystem path&quot; idea, imported into a Windows shell.Plan 9 from Bell Labs (Pike, Thompson, et al.) was the academic articulation of the &quot;everything is a path&quot; property: every kernel-named resource, including processes and network connections, surfaced as a file under a 9P-served namespace. Plan 9 never reached commercial scale, but its design idea reached production through NT, and through Linux&apos;s /proc, /sys, and FUSE.&lt;/p&gt;
&lt;p&gt;Windows NT 3.1 shipped on July 27, 1993. It was &quot;Microsoft&apos;s first 32-bit operating system,&quot; supported on IA-32, DEC Alpha, and MIPS [@en-wikipedia-windows-nt-3-1]. The Object Manager was already one of its executive subsystems, sitting alongside the I/O Manager, the Memory Manager, the Process Manager, the Security Reference Monitor, and the Local Procedure Call subsystem [@en-wikipedia-architecture-of-windows-nt]. The four pieces this article will rebuild from scratch -- the &lt;code&gt;OBJECT_HEADER&lt;/code&gt; that prefixes every object in memory, the &lt;code&gt;OBJECT_TYPE&lt;/code&gt; singleton that owns each type&apos;s method table, the &lt;code&gt;ParseProcedure&lt;/code&gt; that delegates path resolution to the owning subsystem, and the &lt;code&gt;OBJECT_DIRECTORY&lt;/code&gt; hash table that maps names to objects -- were all in the NT 3.1 kernel. None of them has been rearchitected since.&lt;/p&gt;
&lt;p&gt;That same year, Microsoft Press published &lt;em&gt;Inside Windows NT&lt;/em&gt;, written by technical writer Helen Custer with a Foreword by Cutler himself. The book&apos;s Object Manager chapter is the canonical pre-2000 description of the namespace, cited on the Sysinternals WinObj page [@ms-winobj] as &quot;Helen Custer&apos;s &lt;em&gt;Inside Windows NT&lt;/em&gt; provides a good overview of the Object Manager namespace.&quot; Custer&apos;s book has been out of print for two decades, but the citation chain through Russinovich&apos;s tool is durable.&lt;/p&gt;
&lt;p&gt;Three years later, in 1996, Russinovich and Cogswell co-founded Winternals and released WinObj 1.0 [@en-wikipedia-mark-russinovich]. WinObj was the first publicly distributed tool to walk &lt;code&gt;\&lt;/code&gt; from user mode, using the native &lt;code&gt;NtOpenDirectoryObject&lt;/code&gt; and &lt;code&gt;NtQueryDirectoryObject&lt;/code&gt; syscalls that the Object Manager exposed through NTDLL [@ms-winobj]. The following year, Russinovich&apos;s October 1997 &lt;em&gt;Windows IT Pro&lt;/em&gt; column &quot;Inside the Object Manager&quot; gave the namespace its first treatment in the trade press. The original URL did not survive changes to TechTarget&apos;s web property portfolio in 2025 (TechTarget was acquired by Informa PLC in 2025), but the WinObj page still cites the column by name as &quot;Mark&apos;s October 1997 [WindowsITPro Magazine] column, &apos;Inside the Object Manager&apos;.&quot;The Russinovich 1997 column has no surviving direct URL because the URL did not survive changes to TechTarget&apos;s web property portfolio in 2025. The most accessible surviving citation is through the WinObj page itself. The same archive failure also explains why Helen Custer&apos;s 1993 biography returns HTTP 404 on Wikipedia in 2026; the book (ISBN 1-55615-481-X) survives in used-book channels only.&lt;/p&gt;
&lt;p&gt;The line of book-length internals references that began with Custer continued through &lt;em&gt;Inside Windows 2000&lt;/em&gt; (third edition) and the &lt;em&gt;Windows Internals&lt;/em&gt; series that succeeded it. The 7th edition Part 1 was published by Microsoft Press in May 2017, authored by Russinovich, Alex Ionescu, and David A. Solomon [@microsoftpressstore-wininternals7-part1]; its Chapter 8 is the current canonical reference for the Object Manager. James Forshaw&apos;s April 2024 &lt;em&gt;Windows Security Internals&lt;/em&gt; [@nostarch-windows-security-internals] is the contemporary companion that ties the namespace into the access-check pipeline.&lt;/p&gt;
&lt;p&gt;The 1993 design assumed a single global namespace. One process tree, one &lt;code&gt;\BaseNamedObjects&lt;/code&gt;, one &lt;code&gt;\Windows\WindowStations\WinSta0&lt;/code&gt;, one &lt;code&gt;\??&lt;/code&gt; view of DOS device letters. Everyone shared everything. Did that assumption survive the Internet?&lt;/p&gt;
&lt;h2&gt;3. The pre-Vista namespace and how it broke&lt;/h2&gt;
&lt;p&gt;It did not. By the late 1990s every interactive Windows user was sharing a name service with every running service. The single-global-namespace assumption produced three distinct exploit classes, each rediscovered repeatedly between 1996 and 2007, and each ultimately closed only by architectural change.&lt;/p&gt;
&lt;p&gt;The most public failure was the &lt;em&gt;shatter attack&lt;/em&gt;. In August 2002 a researcher named Chris Paget published a paper titled &quot;Exploiting design flaws in the Win32 API for privilege escalation.&quot; Wikipedia&apos;s article on the disclosure preserves the chronology: &quot;Shatter attacks became a topic of intense conversation in the security community in August 2002 after the publication of Chris Paget&apos;s paper&quot; [@en-wikipedia-shatter-attack]. The proof-of-concept was about thirty lines. As an unprivileged interactive user, Paget sent a &lt;code&gt;WM_TIMER&lt;/code&gt; window message to a service&apos;s hidden window in the same &lt;code&gt;\Windows\WindowStations\WinSta0&lt;/code&gt; (which all services and all interactive users shared in pre-Vista Windows), with a callback parameter pointing to attacker-placed shellcode. The shellcode ran as SYSTEM.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s initial response, preserved in the Wikipedia article, was that &quot;the flaw lies in the specific, highly privileged service&quot;: a per-service bug, patch the services. That stance did not survive the structural-class argument. The exploit was not a bug in one service. It was a &lt;em&gt;property of the namespace&lt;/em&gt;: as long as services and users shared a window station and a &lt;code&gt;\BaseNamedObjects&lt;/code&gt;, any service that ever called a Windows API processing a message from its message queue was reachable from any logged-in user.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A second class of pre-Vista failure was &lt;em&gt;named-object squatting&lt;/em&gt;. A low-privilege user pre-creates &lt;code&gt;\BaseNamedObjects\Some_Global_Event&lt;/code&gt; with a permissive DACL. A privileged service later calls &lt;code&gt;CreateEvent(&quot;Some_Global_Event&quot;)&lt;/code&gt; with default open-or-create semantics and ends up inheriting the squatter&apos;s object, security descriptor and all. This is not one service-author&apos;s bug; it is the consequence of every service-author trusting that names in a shared namespace would resolve to objects they themselves created. The pattern has been rediscovered approximately once a year for two decades. James Forshaw documents the contemporary named-pipe analog in his 2017 &quot;Named Pipe Secure Prefixes&quot; post [@tiraniddo-named-pipe-secure-prefixes], where the SMSS-created prefixes &lt;code&gt;\Device\NamedPipe\ProtectedPrefix\Administrators&lt;/code&gt;, &lt;code&gt;\Device\NamedPipe\ProtectedPrefix\LocalService&lt;/code&gt;, and &lt;code&gt;\Device\NamedPipe\ProtectedPrefix\NetworkService&lt;/code&gt; are TCB-privilege-gated -- only &lt;code&gt;smss.exe&lt;/code&gt; can create sibling protected prefixes, so a service that publishes its pipe below one of these prefixes inherits a DACL that low-privilege squatters cannot reach.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The third class was &lt;em&gt;symbolic-link redirection&lt;/em&gt;. The pre-Vista Object Manager exposed two kinds of user-creatable symbolic link: object-manager symbolic links inside &lt;code&gt;\??&lt;/code&gt; (the per-session DOS-devices view) and NTFS mount points on disk. The attack pattern was the same in both. A privileged process is asked to open a path the user controls part of. The user has pre-planted a symbolic link partway through the path that redirects the residual walk into a target the user could not otherwise write. The privileged process opens the redirected file and treats it as if it were the original.&lt;/p&gt;
&lt;p&gt;Forshaw&apos;s 2015 Project Zero post on the symbolic-link hardening generation is the canonical taxonomy: &quot;There are three types of symbolic links you can access from a low privileged user, Object Manager Symbolic Links, Registry Key Symbolic Links and NTFS Mount Points&quot; [@p0-symlink-mitigations]. His worked example for the Internet Explorer 11 EPM sandbox is CVE-2015-0055 [@nvd-cve-2015-0055], described in the post as &quot;an information disclosure issue in the IE EPM sandbox which abused symbolic links to bypass a security check.&quot;&lt;/p&gt;
&lt;p&gt;The aha moment from this section is the one Microsoft eventually conceded. The pre-Vista failure mode was not three independent bug families. It was &lt;em&gt;one&lt;/em&gt; structural problem -- a single global namespace shared by every principal -- with three faces. No amount of per-service patching could close it. The fix had to be architectural: the namespace itself had to be partitioned.The Interactive Services Detection Service (ISDS) was Vista&apos;s backward-compatibility hack for legacy services that drew GUIs into Session 0. ISDS displayed a &quot;An interactive service has requested attention&quot; prompt that let the user switch to Session 0 long enough to dismiss the dialog. It was deprecated in Windows 10 1803 and is the historical artifact of just how much pre-Vista code assumed services and users would share a window station.&lt;/p&gt;
&lt;p&gt;That fix took five years to ship. Windows Vista RTM was released on November 8, 2006 and General Availability arrived on January 30, 2007 [@en-wikipedia-windows-vista]. Vista did not ship one fix; it shipped three independent partition mechanisms in the same release window, because the structural failure had three faces and each face needed its own mechanism. The next section catalogues those mechanisms and the four additional generations of additive isolation that have built on them since.&lt;/p&gt;
&lt;h2&gt;4. Six generations of namespace isolation&lt;/h2&gt;
&lt;p&gt;The namespace itself has not been rearchitected since 1993. What has evolved, in six discrete generations between 1993 and 2026, is the set of &lt;em&gt;partition primitives&lt;/em&gt; layered on top: the mechanisms that let the kernel hide subtrees from particular callers, rewrite paths transparently for particular tokens, or invoke a registered watcher when a particular handle is created. Each generation closes a structural class. None has rendered its predecessor obsolete. On 2026 Windows 11 25H2 all six are simultaneously load-bearing.&lt;/p&gt;

flowchart LR
    G1[&quot;Gen 1&lt;br /&gt;NT 3.1, Jul 1993&lt;br /&gt;Single global namespace&quot;] --&amp;gt; G2
    G2[&quot;Gen 2&lt;br /&gt;Vista, Jan 2007 / SP1, Feb 2008&lt;br /&gt;Session 0 + MIC + ObRegisterCallbacks&quot;] --&amp;gt; G3
    G3[&quot;Gen 3&lt;br /&gt;Windows 8, Oct 2012&lt;br /&gt;AppContainer / Lowbox / per-package directory&quot;] --&amp;gt; G4
    G4[&quot;Gen 4&lt;br /&gt;Windows 10 RTM, Jul 2015&lt;br /&gt;VBS / IUM secure-kernel namespace&quot;] --&amp;gt; G5
    G5[&quot;Gen 5&lt;br /&gt;Windows Server 2016, Oct 2016&lt;br /&gt;Server Silos / silo-scoped views&quot;] --&amp;gt; G6
    G6[&quot;Gen 6&lt;br /&gt;MS15-090, Aug 2015 -&amp;gt;&lt;br /&gt;symbolic-link class hardening&quot;]
&lt;p&gt;Generation numbering is thematic (by isolation capability introduced) rather than strictly chronological. Gen 6 (MS15-090, August 11, 2015) predates Gen 5 (Windows Server 2016, October 12, 2016) by 14 months; the numbering reflects the logical layering of isolation mechanisms, not their calendar sequence.&lt;/p&gt;
&lt;h3&gt;4.1 Generation 2 -- Session 0 isolation, integrity levels, ObRegisterCallbacks&lt;/h3&gt;
&lt;p&gt;Vista shipped three mechanisms in one release window because the structural failure had three faces.&lt;/p&gt;
&lt;p&gt;The first was &lt;em&gt;Session 0 isolation&lt;/em&gt;. From Vista forward, services run in Session 0 alone; the first interactive logon starts at Session 1. Each session gets its own subtree at &lt;code&gt;\Sessions\&amp;lt;n&amp;gt;\BaseNamedObjects&lt;/code&gt;, &lt;code&gt;\Sessions\&amp;lt;n&amp;gt;\Windows\WindowStations&lt;/code&gt;, and &lt;code&gt;\Sessions\&amp;lt;n&amp;gt;\DosDevices&lt;/code&gt;. The Win32 &lt;code&gt;Local\&lt;/code&gt; prefix routes through &lt;code&gt;kernel32!BaseGetNamedObjectDirectory&lt;/code&gt; into the per-session BNO; &lt;code&gt;Global\&lt;/code&gt; routes into the shared &lt;code&gt;\BaseNamedObjects&lt;/code&gt; [@ms-termserv-kernel-object-namespaces]. The Wikipedia Shatter article preserves the architectural fix verbatim: &quot;Local user logins were moved from Session 0 to Session 1, thus separating the user&apos;s processes from system services that could be vulnerable&quot; [@en-wikipedia-shatter-attack]. After Vista an interactive user could no longer &lt;code&gt;SendMessage(WM_TIMER)&lt;/code&gt; into a service&apos;s hidden window because the user and the service no longer shared a window station.&lt;/p&gt;
&lt;p&gt;The second mechanism was &lt;em&gt;Mandatory Integrity Control&lt;/em&gt;. Vista introduced a new ACE type, &lt;code&gt;SYSTEM_MANDATORY_LABEL_ACE&lt;/code&gt;, attached to every object&apos;s security descriptor. Each token carries one of four integrity levels (Low S-1-16-4096, Medium S-1-16-8192, High S-1-16-12288, or System S-1-16-16384), and the Security Reference Monitor compares the requester&apos;s level against the object&apos;s level &lt;em&gt;after&lt;/em&gt; path resolution succeeds [@en-wikipedia-mandatory-integrity-control]. MIC is not a namespace partition. A Low-IL process and a Medium-IL process resolve the same &lt;code&gt;\BaseNamedObjects&lt;/code&gt; directory; only the open is denied at the leaf. The structural property MIC adds is that the leaf check is &lt;em&gt;unbypassable from user mode&lt;/em&gt;; the check fires regardless of which DACL the object carries.&lt;/p&gt;
&lt;p&gt;The third mechanism was &lt;code&gt;ObRegisterCallbacks&lt;/code&gt;. Microsoft&apos;s wdm.h documentation records the API&apos;s first ship date verbatim: &quot;Available starting with Windows Vista with Service Pack 1 (SP1) and Windows Server 2008&quot; [@ms-obregistercallbacks]. The API lets a KMCS-signed driver intercept handle creation and handle duplication on &lt;code&gt;PsProcessType&lt;/code&gt;, &lt;code&gt;PsThreadType&lt;/code&gt;, and the desktop object type. The registration carries an Altitude (a FltMgr-style collision key) and an array of &lt;code&gt;OB_OPERATION_REGISTRATION&lt;/code&gt; records [@ms-ob-callback-registration]. Pre-operation callbacks can strip access-mask bits before the handle is granted; post-operation callbacks fire for logging. The parallel API &lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt; [@ms-pssetcreateprocessnotifyroutineex] covers process creation. Together, these are the kernel-mode primitives every modern EDR product depends on; they ship inside the Object Manager itself and they are the reason an EDR knows when something opens a handle to &lt;code&gt;lsass.exe&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;4.2 Generation 3 -- AppContainer and the lowbox token&lt;/h3&gt;
&lt;p&gt;Windows 8 shipped on October 26, 2012 [@en-wikipedia-windows-8]. Modern / UWP apps downloaded from the Microsoft Store needed a sandbox finer-grained than per-session BNO. The Vista path rewriting in &lt;code&gt;kernel32!BaseGetNamedObjectDirectory&lt;/code&gt; happened in user mode, which made it the wrong layer for a sandbox: a hostile renderer could in principle bypass the user-mode rewrite. The new layer moved into the kernel.&lt;/p&gt;
&lt;p&gt;Each UWP / MSIX process runs under a special token type, the &lt;em&gt;AppContainer / LowBox token&lt;/em&gt; (referred to in kernel code as the &lt;em&gt;lowbox token&lt;/em&gt;), created by &lt;code&gt;NtCreateLowBoxToken&lt;/code&gt;. The token carries a &lt;code&gt;TOKEN_APPCONTAINER_INFORMATION&lt;/code&gt; block that names the process&apos;s package SID (&lt;code&gt;S-1-15-2-...&lt;/code&gt;) and an &lt;code&gt;AppContainerNumber&lt;/code&gt;. Inside &lt;code&gt;ObpLookupObjectName&lt;/code&gt;, &lt;em&gt;before&lt;/em&gt; the path is walked, the kernel checks whether the caller&apos;s token is a lowbox token; if it is, lookups of &lt;code&gt;\BaseNamedObjects\X&lt;/code&gt;, &lt;code&gt;\RPC Control\X&lt;/code&gt;, and other rewriteable paths get redirected into &lt;code&gt;\Sessions\&amp;lt;n&amp;gt;\AppContainerNamedObjects\&amp;lt;package-sid&amp;gt;\X&lt;/code&gt;. The user-mode caller never sees the rewrite. The package-SID directory is created by SYSTEM at process-creation time with a security descriptor that grants the package SID, and only the package SID, full access. Microsoft&apos;s wording is precise: AppContainer works by &quot;sandboxing the application kernel objects, the AppContainer environment prevents the application from influencing, or being influenced by, other application processes&quot; [@ms-appcontainer-isolation].&lt;/p&gt;

The AppInfo service, which is responsible for creating the new application, calls the undocumented API CreateAppContainerToken to do some internal housekeeping. Unfortunately this API creates object directories under the user&apos;s AppContainerNamedObjects object directory to support redirecting BaseNamedObjects and RPC endpoints by the OS. -- James Forshaw, Project Zero Issue 1550 [@p0-issue1550]
&lt;p&gt;The residual class the AppContainer model has not closed is the one Forshaw&apos;s August 30, 2018 Project Zero post [@p0-issue1550] documents: because the SYSTEM-side AppInfo service has to write into the user&apos;s AppContainerNamedObjects subtree to set up redirection, an unprivileged caller can race the directory creation and end up planting a symbolic link the SYSTEM service then follows. The class -- &quot;SYSTEM-privileged directory creation in user-controllable territory&quot; -- is the worked example of why &quot;the kernel rewrites the name&quot; is an isolation property only when the SYSTEM helpers also use the rewrite.&lt;/p&gt;
&lt;h3&gt;4.3 Generation 4 -- VBS trustlets and the IUM secure-kernel namespace&lt;/h3&gt;
&lt;p&gt;Windows 10 RTM shipped on July 29, 2015 [@en-wikipedia-windows-10-version-history]. The Virtualization-Based Security (VBS) feature set introduced a parallel object-manager-shaped namespace that lives in Virtual Trust Level 1 (VTL1) and is inaccessible to the VTL0 NT kernel. Inside VTL1 the Secure Kernel (&lt;code&gt;securekernel.exe&lt;/code&gt;) maintains its own root, its own type registry, and its own handle-table machinery. The VTL0 NT kernel can see &lt;em&gt;trustlet processes&lt;/em&gt; -- the per-trustlet user-mode containers running in Isolated User Mode (IUM) -- but it cannot reach into their secure-side state.&lt;/p&gt;
&lt;p&gt;Alex Ionescu&apos;s Black Hat USA 2015 talk Battle of SKM and IUM [@ionescu-bh2015-pdf] is the canonical inventory of the inbox Trustlet IDs at ship: Trustlet 0 is the Secure Kernel Process hosting Device Guard; Trustlet 1 is LSAISO.EXE for Credential Guard; Trustlet 2 is VMSP.EXE hosting the virtual TPM; Trustlet 3 is the vTPM provisioning trustlet. Each is identified by a Trustlet ID and reachable only through narrow Secure Kernel ALPC ports. The VBS Trustlets piece in this series unpacks the threat model.&lt;/p&gt;
&lt;h3&gt;4.4 Generation 5 -- Server Silos and the silo-scoped namespace&lt;/h3&gt;
&lt;p&gt;Windows Server 2016 shipped on October 12, 2016 [@en-wikipedia-windows-server-2016]. Microsoft needed a Linux-namespaces equivalent so that container runtimes -- Docker, containerd, and the Azure Kubernetes Service Windows-node pods that followed -- could host adjacent workloads on one kernel. The answer was &lt;em&gt;Server Silo&lt;/em&gt;: a new &lt;code&gt;OBJECT_TYPE&lt;/code&gt; registered alongside &lt;code&gt;Job&lt;/code&gt;, &lt;code&gt;Process&lt;/code&gt;, and &lt;code&gt;Thread&lt;/code&gt;, that carries its own &lt;code&gt;RootDirectory&lt;/code&gt;, &lt;code&gt;DosDevicesDirectory&lt;/code&gt;, and &lt;code&gt;ServerSiloGlobals&lt;/code&gt;. A process attached to a silo via &lt;code&gt;PsAttachSiloToCurrentThread&lt;/code&gt; sees the silo&apos;s namespace as its root; the silo&apos;s &lt;code&gt;\GLOBAL??\C:&lt;/code&gt; resolves to the silo&apos;s &lt;code&gt;\Device\HarddiskVolume*&lt;/code&gt;, which is a different &lt;code&gt;Device&lt;/code&gt; object from the host&apos;s. Job objects [@ms-job-objects] provide the cgroups-equivalent resource-accounting dimension; the Silo type builds on top.&lt;/p&gt;
&lt;p&gt;The canonical reverse-engineering reference is Daniel Prizmant&apos;s July 2020 Unit 42 writeup, which spells out the architecture: &quot;job objects are used in a similar way control groups (cgroups) are used in Linux, and... server silo objects were used as a replacement for namespaces support in the kernel&quot; [@unit42-rev-eng-windows-containers].&lt;/p&gt;
&lt;p&gt;The companion piece, Prizmant&apos;s June 2021 &lt;em&gt;Siloscape&lt;/em&gt; [@unit42-siloscape], is the first known malware family that escapes the silo boundary: Prizmant named the malware &quot;Siloscape (sounds like silo escape) because its primary goal is to escape the container, and in Windows this is implemented mainly by a server silo.&quot; James Forshaw&apos;s April 2021 Project Zero post &lt;em&gt;Who Contains the Containers?&lt;/em&gt; [@p0-who-contains-containers] is the four-LPE companion disclosure. Microsoft&apos;s standing position is that Server Silo is not a security boundary; the Hyper-V Container, which adds a Hyper-V VM around the container&apos;s silo, is the security-boundary product.&lt;/p&gt;
&lt;h3&gt;4.5 Generation 6 -- the symbolic-link hardening continuum&lt;/h3&gt;
&lt;p&gt;The cross-cutting hardening generation closes the symlink subclass that recurred in Generations 1, 3, and 5. MS15-090 shipped on August 11, 2015 [@ms-ms15-090] and &quot;corrects how Windows Object Manager handles object symbolic links created by a sandbox process, by preventing improper interaction with the registry by sandboxed applications, and by preventing improper interaction with the filesystem by sandboxed applications.&quot; The bulletin&apos;s canonical Object Manager CVE is CVE-2015-2428 [@nvd-cve-2015-2428], described verbatim as the case where the &quot;Object Manager in Microsoft Windows... does not properly constrain impersonation levels during interaction with object symbolic links that originated in a sandboxed process.&quot; Subsequent Windows 10 builds added &lt;code&gt;OBJ_DONT_REPARSE&lt;/code&gt;, an open-time flag that disables symbolic-link substitution for callers willing to opt in, and post-Siloscape patches in 2021 closed &lt;code&gt;NtSetInformationSymbolicLink&lt;/code&gt; retargeting from inside a silo.&lt;/p&gt;
&lt;p&gt;The scope document for this article originally attributed MS15-090 to CVE-2015-2528 and CVE-2015-1463. Independent NVD verification confirmed neither is correct: CVE-2015-2528 [@nvd-cve-2015-2528] is the MS15-102 Task Management EoP, and CVE-2015-1463 [@nvd-cve-2015-1463] is a ClamAV denial-of-service crash. The canonical MS15-090 OM-symlink CVE is CVE-2015-2428. Separately, CVE-2018-0824 [@nvd-cve-2018-0824] is a CWE-502 COM deserialization issue that joined the CISA KEV catalog on 2024-08-05, not a namespace-squatting CVE.&lt;/p&gt;
&lt;p&gt;The residual subclass MS15-090 did not close was the per-session &lt;code&gt;\??&lt;/code&gt; DosDevices remapping path under impersonation. A low-privileged process whose token is impersonated by a SYSTEM service can plant a &lt;code&gt;DefineDosDevice&lt;/code&gt; remapping that survives into the impersonation-time &lt;code&gt;\??&lt;/code&gt; view, and the SYSTEM-side activation-context resolver then opens the redirected path while running with elevated privileges. The canonical 2023 worked example is HackSys&apos;s &lt;em&gt;Activation Context Hell -- DosDevices Remapping Attack under Impersonation&lt;/em&gt; [@hacksys-activation-context-hell], which targets the CSRSS / SxS activation-context resolver and shipped as CVE-2023-35359 [@nvd-cve-2023-35359], with the closely-related CVE-2022-22047 [@nvd-cve-2022-22047] covering the underlying CSRSS surface. The mitigation has to live inside the impersonation-aware &lt;code&gt;\??&lt;/code&gt; resolver in the SYSTEM caller, not at the symlink-creation gate.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Every generation since Generation 1 has &lt;em&gt;layered&lt;/em&gt; a new isolation primitive on top of the prior generation. None has rendered its predecessor obsolete. On 2026 Windows 11 25H2 all six generations coexist simultaneously: a UWP / MSIX app inside a Server Silo on a VBS-enabled host is session-partitioned, lowbox-rewritten, silo-scoped, VTL0-confined, integrity-gated, and watched by every loaded EDR&apos;s &lt;code&gt;ObRegisterCallbacks&lt;/code&gt; filter. Each layer adds an independent enforcement point at &lt;code&gt;ObpLookupObjectName&lt;/code&gt; time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Six generations of isolation primitives is a tidy story, but it has glossed the most important question. What is the actual kernel data structure all six generations parameterize? What does the path-walk algorithm look like, what is the type registry, and where does the hash table live?&lt;/p&gt;
&lt;h2&gt;5. The four load-bearing primitives&lt;/h2&gt;
&lt;p&gt;If you remember one paragraph from this article, make it this one. The Object Manager namespace is built out of four kernel data structures: an &lt;code&gt;OBJECT_HEADER&lt;/code&gt; that prefixes every named object in memory, an &lt;code&gt;OBJECT_TYPE&lt;/code&gt; singleton that owns each type&apos;s method table, a &lt;code&gt;ParseProcedure&lt;/code&gt; that delegates path resolution to the owning subsystem when needed, and an &lt;code&gt;OBJECT_DIRECTORY&lt;/code&gt; hash table that maps names to objects. Every Windows security boundary you have read about is a parameter to one of these four pieces. The next eight subsections rebuild them one at a time.&lt;/p&gt;

flowchart TB
    OD[&quot;OBJECT_DIRECTORY&lt;br /&gt;(37-bucket hash table)&quot;] --&amp;gt;|&quot;hash(name) % 37&quot;| OH
    OH[&quot;OBJECT_HEADER&lt;br /&gt;(PointerCount, HandleCount,&lt;br /&gt;TypeIndex, InfoMask,&lt;br /&gt;SecurityDescriptor, Body offset)&quot;] --&amp;gt;|&quot;TypeIndex XOR&lt;br /&gt;ObHeaderCookie&quot;| OT
    OT[&quot;OBJECT_TYPE singleton&lt;br /&gt;(in nt!ObTypeIndexTable)&quot;] --&amp;gt;|&quot;TypeInfo&quot;| TI
    TI[&quot;TYPE_INFO method table&lt;br /&gt;(Dump, Open, Close, Delete,&lt;br /&gt;ParseProcedure,&lt;br /&gt;Security, QueryName, ...)&quot;]
    OH --&amp;gt;|&quot;Body[]&quot;| BODY[&quot;Type-specific body&lt;br /&gt;(EPROCESS, FILE_OBJECT,&lt;br /&gt;SECTION_OBJECT, ...)&quot;]
&lt;h3&gt;5.1 OBJECT_HEADER&lt;/h3&gt;
&lt;p&gt;Every named kernel object lives in non-paged pool. Immediately &lt;em&gt;before&lt;/em&gt; each object&apos;s typed body sits an &lt;code&gt;OBJECT_HEADER&lt;/code&gt;, a 0x30-byte (48-byte on x64) structure that the Object Manager owns. &lt;code&gt;PointerCount&lt;/code&gt; and &lt;code&gt;HandleCount&lt;/code&gt; are the two reference counts: the former tracks raw kernel-mode pointer references, the latter tracks user-mode handles. &lt;code&gt;TypeIndex&lt;/code&gt; is a single byte that indexes into the &lt;code&gt;nt!ObTypeIndexTable&lt;/code&gt; to find the object&apos;s type singleton; since Windows 10 1709, the byte is XOR-obfuscated against the per-boot &lt;code&gt;nt!ObHeaderCookie&lt;/code&gt; so that simple type confusion is non-trivial.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;InfoMask&lt;/code&gt; is a bitmap of optional sub-headers that may precede the main header: &lt;code&gt;OBJECT_HEADER_NAME_INFO&lt;/code&gt; for named objects, &lt;code&gt;OBJECT_HEADER_QUOTA_INFO&lt;/code&gt; for objects that charge a quota block, &lt;code&gt;OBJECT_HEADER_HANDLE_INFO&lt;/code&gt; for objects that need per-process handle accounting. &lt;code&gt;SecurityDescriptor&lt;/code&gt; is a tagged pointer to the object&apos;s DACL/SACL. &lt;code&gt;Body[]&lt;/code&gt; is the offset at which the type-specific payload begins; for a process object that payload is an &lt;code&gt;EPROCESS&lt;/code&gt;, for a file it is a &lt;code&gt;FILE_OBJECT&lt;/code&gt;, and so on. The canonical reference is Chapter 8 of &lt;em&gt;Windows Internals 7th Edition Part 1&lt;/em&gt; [@microsoftpressstore-wininternals7-part1].&lt;/p&gt;

The per-object header (`nt!_OBJECT_HEADER`) that precedes every named kernel object in non-paged pool. Carries reference counts (`PointerCount`, `HandleCount`), a `TypeIndex` byte that points into `nt!ObTypeIndexTable` (XOR-obfuscated against `nt!ObHeaderCookie` since Windows 10 1709), an `InfoMask` describing optional sub-headers, a `SecurityDescriptor` pointer, and the offset to the typed `Body[]`.
&lt;p&gt;The &lt;code&gt;TypeIndex&lt;/code&gt; XOR-with-cookie is one of the smallest kernel hardening changes Microsoft has shipped: a single byte that prevents a poisoned &lt;code&gt;OBJECT_HEADER&lt;/code&gt; from naming an arbitrary type after a heap-corruption primitive. The cookie is per-boot and lives in &lt;code&gt;nt!ObHeaderCookie&lt;/code&gt;. The hardening is documented in &lt;em&gt;Windows Internals 7th Edition&lt;/em&gt; Chapter 8 [@microsoftpressstore-wininternals7-part1] and in Geoff Chappell&apos;s reverse-engineering studies; Microsoft has not, as of 2026, published a Learn-hosted reference for the cookie itself.&lt;/p&gt;
&lt;h3&gt;5.2 OBJECT_TYPE&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;OBJECT_TYPE&lt;/code&gt; is the per-type singleton. There is exactly one &lt;code&gt;OBJECT_TYPE&lt;/code&gt; per registered kernel type, and they live in &lt;code&gt;\ObjectTypes&lt;/code&gt;. On Windows 11 25H2 the count sits at roughly seventy-five: &lt;code&gt;Type&lt;/code&gt;, &lt;code&gt;Directory&lt;/code&gt;, &lt;code&gt;SymbolicLink&lt;/code&gt;, &lt;code&gt;Token&lt;/code&gt;, &lt;code&gt;Job&lt;/code&gt;, &lt;code&gt;Process&lt;/code&gt;, &lt;code&gt;Thread&lt;/code&gt;, &lt;code&gt;Section&lt;/code&gt;, &lt;code&gt;Key&lt;/code&gt;, &lt;code&gt;File&lt;/code&gt;, &lt;code&gt;Event&lt;/code&gt;, &lt;code&gt;Mutant&lt;/code&gt;, &lt;code&gt;Semaphore&lt;/code&gt;, &lt;code&gt;Timer&lt;/code&gt;, &lt;code&gt;WindowStation&lt;/code&gt;, &lt;code&gt;Desktop&lt;/code&gt;, &lt;code&gt;Device&lt;/code&gt;, &lt;code&gt;Driver&lt;/code&gt;, &lt;code&gt;IoCompletion&lt;/code&gt;, &lt;code&gt;ALPC Port&lt;/code&gt;, &lt;code&gt;EtwRegistration&lt;/code&gt;, &lt;code&gt;Silo&lt;/code&gt;, and dozens more.&lt;/p&gt;

The per-type singleton (`nt!_OBJECT_TYPE`) that owns each kernel type&apos;s method table. The `TypeInfo` field carries eight procedure pointers and one offset field (WaitObjectFlagOffset): `DumpProcedure`, `OpenProcedure`, `CloseProcedure`, `DeleteProcedure`, `ParseProcedure` (the path-resolution callback), `SecurityProcedure`, `QueryNameProcedure`, `OkayToCloseProcedure`, and a `WaitObjectFlagOffset` offset for waitable types. Every `OBJECT_TYPE` instance is reachable through `\ObjectTypes`.
&lt;p&gt;The &lt;code&gt;TypeInfo&lt;/code&gt; field on each &lt;code&gt;OBJECT_TYPE&lt;/code&gt; carries eight procedure pointers and one offset field (WaitObjectFlagOffset). The most consequential is the &lt;code&gt;ParseProcedure&lt;/code&gt;. When &lt;code&gt;ObpLookupObjectName&lt;/code&gt; is walking a path component-by-component, and a step lands on an object whose &lt;code&gt;OBJECT_TYPE&lt;/code&gt; defines a &lt;code&gt;ParseProcedure&lt;/code&gt;, the OM hands the &lt;em&gt;residual&lt;/em&gt; path and the desired access to that procedure, which becomes the namespace authority below that point. That is how the registry&apos;s &lt;code&gt;Key&lt;/code&gt; type, the I/O Manager&apos;s &lt;code&gt;Device&lt;/code&gt; type, and the various WMI / Volume-Manager subsystems insert themselves into the namespace without the Object Manager having to know any of their internal structure [@en-wikipedia-object-manager].&lt;/p&gt;
&lt;h3&gt;5.3 The parse procedure&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ObpLookupObjectName&lt;/code&gt; walks &lt;code&gt;\Foo\Bar\Baz\...\Leaf&lt;/code&gt; left-to-right. At each component the walker does one of three things. The common case is a hash-table lookup in the current &lt;code&gt;OBJECT_DIRECTORY&lt;/code&gt;&apos;s 37 buckets to find the child object by name. The second case is &lt;code&gt;SymbolicLink&lt;/code&gt; substitution: if the child object&apos;s type is &lt;code&gt;SymbolicLink&lt;/code&gt;, the walker substitutes the link target and re-enters the walk at the substitution. The third and most consequential case is &lt;em&gt;parse-procedure handoff&lt;/em&gt;. If the child object&apos;s &lt;code&gt;OBJECT_TYPE&lt;/code&gt; has a non-null &lt;code&gt;ParseProcedure&lt;/code&gt;, the walker stops, hands the residual path string to that procedure, and lets it decide what to do.&lt;/p&gt;

The load-bearing method pointer on each `OBJECT_TYPE`&apos;s `TypeInfo` field. When `ObpLookupObjectName` encounters an object whose type defines a `ParseProcedure`, the residual path is handed to that procedure for resolution. The two canonical parse procedures are `IopParseDevice` (for the `Device` type, which delegates further resolution to the device&apos;s owning driver via `IRP_MJ_CREATE`) and `CmpParseKey` (for the `Key` type, which walks the registry hive).
&lt;p&gt;&lt;code&gt;IopParseDevice&lt;/code&gt; is the parse procedure for the &lt;code&gt;Device&lt;/code&gt; type. When the walker reaches &lt;code&gt;\Device\HarddiskVolume1&lt;/code&gt; and is asked to continue with &lt;code&gt;\Users\me\file.txt&lt;/code&gt;, the I/O Manager builds an &lt;code&gt;IRP_MJ_CREATE&lt;/code&gt; packet, dispatches it to the filesystem driver that owns the volume (NTFS, ReFS, ExFAT, FAT32, or one of several others), and lets that driver walk the rest of the path inside its own on-disk structures. The driver returns a &lt;code&gt;FILE_OBJECT&lt;/code&gt;, which the Object Manager packages into a handle.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CmpParseKey&lt;/code&gt; is the parse procedure for the &lt;code&gt;Key&lt;/code&gt; type. When the walker reaches &lt;code&gt;\REGISTRY&lt;/code&gt; and is asked to continue with &lt;code&gt;\MACHINE\Software\Microsoft\Windows&lt;/code&gt;, the Configuration Manager takes over and walks the in-memory hive structures.&lt;/p&gt;
&lt;p&gt;The structural consequence is profound. Every named file in Windows is, technically, a leaf in the Object Manager namespace. NTFS, ReFS, ExFAT, and the registry are not separate naming systems; they are parse-procedure callbacks that hand &lt;code&gt;FILE_OBJECT&lt;/code&gt; or &lt;code&gt;KEY&lt;/code&gt; bodies back to the OM.&lt;/p&gt;

sequenceDiagram
    participant User as User Process
    participant OM as ObpLookupObjectName
    participant Dir as \GLOBAL?? OBJECT_DIRECTORY
    participant Dev as \Device\HarddiskVolume1 (Device type)
    participant Drv as NTFS Driver
    User-&amp;gt;&amp;gt;OM: NtCreateFile(&quot;\??\C:\Users\me\file.txt&quot;)
    OM-&amp;gt;&amp;gt;OM: rewrite \??\ -&amp;gt; \Sessions\\DosDevices\
    OM-&amp;gt;&amp;gt;Dir: lookup &quot;C:&quot;
    Dir--&amp;gt;&amp;gt;OM: SymbolicLink -&amp;gt; \Device\HarddiskVolume1
    OM-&amp;gt;&amp;gt;OM: substitute, re-enter walk
    OM-&amp;gt;&amp;gt;Dev: lookup \Device\HarddiskVolume1
    Dev--&amp;gt;&amp;gt;OM: type=Device, has ParseProcedure
    OM-&amp;gt;&amp;gt;Drv: IopParseDevice with &quot;\Users\me\file.txt&quot;
    Drv-&amp;gt;&amp;gt;Drv: IRP_MJ_CREATE: walk MFT, find file
    Drv--&amp;gt;&amp;gt;OM: FILE_OBJECT
    OM--&amp;gt;&amp;gt;User: HANDLE
&lt;h3&gt;5.4 The 37-bucket directory hash&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;OBJECT_DIRECTORY&lt;/code&gt; is a 37-bucket open-hash table. The hash function is &lt;code&gt;RtlHashUnicodeString&lt;/code&gt;, applied to each component name. Thirty-seven was the prime Cutler picked in 1993; the constant has not changed in thirty-three years. The folk-knowledge corroboration is in Chapter 8 of &lt;em&gt;Windows Internals 7th Edition Part 1&lt;/em&gt; and in Forshaw&apos;s &lt;em&gt;Windows Security Internals&lt;/em&gt; Chapter 8; Microsoft has never published a Learn-hosted spec for the constant [@nostarch-windows-security-internals].&lt;/p&gt;

The 37-bucket open-hash table (`nt!_OBJECT_DIRECTORY`) that lives at every interior node of the Object Manager tree. Keys are `UNICODE_STRING` component names; the hash is `RtlHashUnicodeString` modulo 37. Each bucket is a linked list of `OBJECT_DIRECTORY_ENTRY` records that point at the next-level `OBJECT_HEADER`. Reading the tree requires `Directory`-`TRAVERSE` rights on the parent.
&lt;p&gt;The 37-bucket constant from 1993 has not changed in thirty-three years. On a 2026 Windows 11 25H2 box with several hundred MSIX packages each owning an &lt;code&gt;\AppContainerNamedObjects\&amp;lt;package-sid&amp;gt;\&lt;/code&gt; subtree, average bucket chains run several entries deep. Collision pressure on the constant is the open problem returned to in Section 9.&lt;/p&gt;
&lt;h3&gt;5.5 The lowbox redirect inside ObpLookupObjectName&lt;/h3&gt;
&lt;p&gt;This is the subsection that earns the second aha moment of the article.&lt;/p&gt;
&lt;p&gt;When the calling thread&apos;s primary token is a lowbox token, &lt;code&gt;ObpLookupObjectName&lt;/code&gt; consults the token&apos;s &lt;code&gt;AppContainerNumber&lt;/code&gt; and package SID &lt;em&gt;before&lt;/em&gt; it begins the walk. Lookups that would otherwise resolve into &lt;code&gt;\BaseNamedObjects&lt;/code&gt; or &lt;code&gt;\RPC Control&lt;/code&gt; are rewritten into &lt;code&gt;\Sessions\&amp;lt;n&amp;gt;\AppContainerNamedObjects\&amp;lt;package-sid&amp;gt;\&lt;/code&gt;. The rewrite happens transparently to the user-mode Win32 caller, which still thinks it asked for &lt;code&gt;\BaseNamedObjects\X&lt;/code&gt;.&lt;/p&gt;

A specialised token type produced by `NtCreateLowBoxToken` that carries a `TOKEN_APPCONTAINER_INFORMATION` block (with a package SID `S-1-15-2-...` and an `AppContainerNumber`). When a process runs under a lowbox token, `ObpLookupObjectName` rewrites every named-object lookup into the per-package directory `\Sessions\\AppContainerNamedObjects\\` before path walking begins.

The user-facing brand for the lowbox-token mechanism. Every UWP / MSIX / Windows Store app runs in an AppContainer. The Windows API surface is unchanged for the app; the Object Manager rewrites every named-object name into a per-package subtree, gating cross-package coordination at the namespace layer. The Microsoft Learn page describes this as &quot;Sandboxing the application kernel objects, the AppContainer environment prevents the application from influencing, or being influenced by, other application processes&quot; [@ms-appcontainer-isolation].
&lt;p&gt;The aha moment is structural. AppContainer is not a &lt;em&gt;containment&lt;/em&gt; mechanism the way you might first picture it. It is a &lt;em&gt;name-translation&lt;/em&gt; mechanism. The lowbox token tells the kernel which directory to rewrite every name into; the sandbox is, at root, a hash-table indirection inside the kernel&apos;s path-walk function. The Edge renderer process cannot name &lt;code&gt;\BaseNamedObjects\GlobalEvent_Foo&lt;/code&gt; because the kernel rewrites that name into &lt;code&gt;\Sessions\1\AppContainerNamedObjects\S-1-15-2-...\Global\GlobalEvent_Foo&lt;/code&gt; before lookup even begins. The &quot;sandbox&quot; is a hash-table redirect.&lt;/p&gt;
&lt;h3&gt;5.6 The Silo OBJECT_TYPE and silo-scoped views&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Silo&lt;/code&gt; is itself a registered &lt;code&gt;OBJECT_TYPE&lt;/code&gt;. Each silo instance carries a silo-scoped &lt;code&gt;RootDirectory&lt;/code&gt;, &lt;code&gt;DosDevicesDirectory&lt;/code&gt;, and &lt;code&gt;ServerSiloGlobals&lt;/code&gt; (with the silo&apos;s own registry-hive root and per-silo &lt;code&gt;BaseNamedObjects&lt;/code&gt; root). &lt;code&gt;PsAttachSiloToCurrentThread&lt;/code&gt; switches the thread&apos;s namespace view; once attached, every Object Manager lookup runs through the silo&apos;s roots instead of the host&apos;s. Job objects, which provide the cgroups-equivalent resource-accounting substrate, are the underlying primitive the Silo type extends [@ms-job-objects]. The structural design history is in Prizmant&apos;s reverse-engineering writeup [@unit42-rev-eng-windows-containers].&lt;/p&gt;

A specialised `Job`-derived kernel object (`OBJECT_TYPE` Silo) introduced in Windows Server 2016 that carries silo-scoped `RootDirectory`, `DosDevicesDirectory`, and `ServerSiloGlobals` fields. A thread attached to a silo via `PsAttachSiloToCurrentThread` sees the silo&apos;s namespace as its root; the silo&apos;s `\GLOBAL??\C:` resolves to the silo&apos;s `\Device\HarddiskVolume*`, which is a different `Device` object from the host&apos;s. Server Silo is the substrate underneath Windows Server Containers and WSL1.
&lt;h3&gt;5.7 The Secure Kernel&apos;s parallel namespace&lt;/h3&gt;
&lt;p&gt;Inside VTL1, the Secure Kernel maintains a separate Object Manager tree with its own root, its own type registry, and its own handle-table machinery. The VTL0 NT kernel cannot enumerate this tree; the only cross-VTL traffic is the narrow ALPC interface each trustlet publishes. Ionescu&apos;s BH2015 inventory (Trustlet IDs 0 through 3 at ship, growing in subsequent releases) is the canonical primary [@ionescu-bh2015-pdf].&lt;/p&gt;

A user-mode process running in Isolated User Mode under the VTL1 Secure Kernel. Each trustlet is signed with both the Windows System Component Verification EKU (1.3.6.1.4.1.311.10.3.6) and the IUM EKU (1.3.6.1.4.1.311.10.3.37), runs at Signature Level 12, and is reachable from VTL0 only through narrow ALPC ports. LSAISO.EXE (Credential Guard), VMSP.EXE (virtual TPM host), and the vTPM provisioning trustlet are the inbox examples.
&lt;h3&gt;5.8 The handle table&lt;/h3&gt;
&lt;p&gt;The namespace is the &lt;em&gt;name&lt;/em&gt; side; the per-process &lt;code&gt;HANDLE_TABLE&lt;/code&gt; is the &lt;em&gt;access&lt;/em&gt; side. Once a handle exists in a process, no name lookup happens on subsequent use; the kernel dereferences the handle through a three-level radix tree indexed by the 32-bit handle value, lands on an &lt;code&gt;OBJECT_HEADER&lt;/code&gt;, and operates on the body. This is why &lt;code&gt;ObRegisterCallbacks&lt;/code&gt; fires on handle &lt;em&gt;creation&lt;/em&gt; and &lt;em&gt;duplication&lt;/em&gt; rather than on every use, and why an inherited handle bypasses the callback entirely. The structural consequence -- that the Object Manager is the gate at name resolution but not at every operation -- comes back in Section 8.&lt;/p&gt;
&lt;p&gt;Now you know the data structure. But what does the actual tree look like in 2026? What does &lt;code&gt;\&lt;/code&gt; contain on a Windows 11 25H2 box, and which security boundary lives in each top-level directory?&lt;/p&gt;
&lt;h2&gt;6. The 2026 top-level directory atlas&lt;/h2&gt;
&lt;p&gt;Open &lt;code&gt;WinObj.exe&lt;/code&gt; as administrator on a Windows 11 25H2 machine and the root directory at &lt;code&gt;\&lt;/code&gt; carries roughly twenty entries. The table below catalogues the load-bearing ones. Each row names the directory, the security boundary it physically realises, and a representative exploit class that has been thrown at it. The driver kit&apos;s Object Directories reference [@ms-object-directories] is Microsoft&apos;s canonical inventory.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Top-level directory&lt;/th&gt;
&lt;th&gt;What it contains&lt;/th&gt;
&lt;th&gt;Which boundary it enforces&lt;/th&gt;
&lt;th&gt;Exploit class&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\ObjectTypes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The ~75 &lt;code&gt;OBJECT_TYPE&lt;/code&gt; singletons (&lt;code&gt;Process&lt;/code&gt;, &lt;code&gt;Thread&lt;/code&gt;, &lt;code&gt;Section&lt;/code&gt;, &lt;code&gt;Key&lt;/code&gt;, &lt;code&gt;File&lt;/code&gt;, &lt;code&gt;Token&lt;/code&gt;, &lt;code&gt;Job&lt;/code&gt;, &lt;code&gt;Silo&lt;/code&gt;, etc.)&lt;/td&gt;
&lt;td&gt;Meta -- the type registry the rest of the namespace depends on&lt;/td&gt;
&lt;td&gt;Type confusion (mitigated by &lt;code&gt;ObHeaderCookie&lt;/code&gt; since Windows 10 1709)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\Device&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Driver-published device objects (&lt;code&gt;\Device\HarddiskVolume*&lt;/code&gt;, &lt;code&gt;\Device\Tcp&lt;/code&gt;, &lt;code&gt;\Device\Tpm&lt;/code&gt;, &lt;code&gt;\Device\NamedPipe&lt;/code&gt;, &lt;code&gt;\Device\Mailslot&lt;/code&gt;, &lt;code&gt;\Device\Vmbus&lt;/code&gt;, &lt;code&gt;\Device\KsecDD&lt;/code&gt;, &lt;code&gt;\Device\CNG&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The I/O Manager&apos;s surface; each driver&apos;s parse procedure consumes residual paths&lt;/td&gt;
&lt;td&gt;Bait-and-switch on &lt;code&gt;\Device&lt;/code&gt; (a low-privilege user redirects a privileged opener through a planted symbolic link)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\Driver&lt;/code&gt;, &lt;code&gt;\FileSystem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Loaded &lt;code&gt;DRIVER_OBJECT&lt;/code&gt; registries&lt;/td&gt;
&lt;td&gt;KMCS / HVCI driver-load gate&lt;/td&gt;
&lt;td&gt;Vulnerable signed-driver class (BYOVD)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\GLOBAL??&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The machine-wide DosDevices view -- where &lt;code&gt;C:&lt;/code&gt; and &lt;code&gt;D:&lt;/code&gt; are symlinks to &lt;code&gt;\Device\HarddiskVolume*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cross-session drive-letter map&lt;/td&gt;
&lt;td&gt;Symlink redirect across session boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\??&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The per-session DosDevices alias, falling through to &lt;code&gt;\GLOBAL??&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Session-scoped drive-letter map&lt;/td&gt;
&lt;td&gt;The HackSys / CVE-2023-35359 worked example: a low-privilege caller plants a &lt;code&gt;DefineDosDevice&lt;/code&gt; remapping that survives into the impersonation-time &lt;code&gt;\??&lt;/code&gt; view, and the SYSTEM-side activation-context resolver opens the redirected path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\BaseNamedObjects&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The global / &lt;code&gt;Global\&lt;/code&gt;-prefixed-only BNO&lt;/td&gt;
&lt;td&gt;Cross-session named-object visibility&lt;/td&gt;
&lt;td&gt;Pre-Vista squatting class (closed by Generation 2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\Sessions\&amp;lt;n&amp;gt;\&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-session subtrees (BNO, DosDevices, WindowStations, AppContainerNamedObjects)&lt;/td&gt;
&lt;td&gt;Session boundary (Generation 2)&lt;/td&gt;
&lt;td&gt;Shatter attacks (closed by Generation 2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\Sessions\&amp;lt;n&amp;gt;\AppContainerNamedObjects\&amp;lt;package-sid&amp;gt;\&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-package UWP / MSIX lowbox namespace&lt;/td&gt;
&lt;td&gt;AppContainer / lowbox boundary (Generation 3)&lt;/td&gt;
&lt;td&gt;Forshaw P0 Issue 1550 arbitrary-directory creation race&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\RPC Control&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every named LRPC ALPC port (every COM call lands here)&lt;/td&gt;
&lt;td&gt;RPC endpoint visibility&lt;/td&gt;
&lt;td&gt;Endpoint squatting against named LRPC ports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\KnownDlls&lt;/code&gt;, &lt;code&gt;\KnownDlls32&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pre-mapped &lt;code&gt;Section&lt;/code&gt; objects for system DLLs&lt;/td&gt;
&lt;td&gt;Loader supply-chain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DefineDosDevice&lt;/code&gt; + &lt;code&gt;\??&lt;/code&gt; symlink-plant trick (closed in NTDLL July 2022, build 19044.1826)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\KernelObjects&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;System-defined events (&lt;code&gt;LowMemoryCondition&lt;/code&gt;, &lt;code&gt;HighMemoryCondition&lt;/code&gt;, etc.)&lt;/td&gt;
&lt;td&gt;Kernel-internal visibility&lt;/td&gt;
&lt;td&gt;None public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\Callback&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;System-defined &lt;code&gt;Callback&lt;/code&gt; objects (&lt;code&gt;ExCallback&lt;/code&gt; slots drivers register against)&lt;/td&gt;
&lt;td&gt;Kernel API extension surface&lt;/td&gt;
&lt;td&gt;Driver-callback abuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\Security&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;LSA-private endpoints&lt;/td&gt;
&lt;td&gt;LSA / authentication isolation&lt;/td&gt;
&lt;td&gt;Credential-theft (the LSAISO trustlet via Generation 4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\Windows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BNO-redirect surface and &lt;code&gt;SharedSection&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Win32 subsystem shared state&lt;/td&gt;
&lt;td&gt;Cross-session Win32 state leakage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\Silos\&amp;lt;id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-container silo subroots on Server SKUs&lt;/td&gt;
&lt;td&gt;Server Silo boundary (Generation 5)&lt;/td&gt;
&lt;td&gt;Siloscape -- symlink retarget out of the silo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\BNOLINKS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The boundary-keyed private-namespace index&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CreatePrivateNamespace&lt;/code&gt; cross-session/cross-package IPC&lt;/td&gt;
&lt;td&gt;None public; the directory itself is RE-derived&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

flowchart LR
    subgraph EdgeRenderer[&quot;Microsoft Edge Renderer (lowbox token)&quot;]
        K32[&quot;CreateMutexW(L&apos;Global\\Foo&apos;)&quot;]
    end
    K32 --&amp;gt;|&quot;NtCreateMutant, OBJECT_ATTRIBUTES&quot;| OB
    subgraph KernelOb[&quot;ObpLookupObjectName&quot;]
        OB[&quot;Read caller token&lt;br /&gt;token.AppContainerNumber&lt;br /&gt;token.PackageSid&quot;]
        OB --&amp;gt;|&quot;rewrite name&quot;| RW[&quot;Rewrite &apos;\\BaseNamedObjects\\Global\\Foo&apos;&lt;br /&gt;to&lt;br /&gt;&apos;\\Sessions\\1\\AppContainerNamedObjects\\&lt;br /&gt;S-1-15-2-...\\Global\\Foo&apos;&quot;]
        RW --&amp;gt; WALK[&quot;walk the rewritten path&quot;]
    end
    WALK --&amp;gt; Dir[&quot;\\Sessions\\1\\AppContainerNamedObjects\\&lt;br /&gt;S-1-15-2-...\\Global\\&lt;br /&gt;(per-package OBJECT_DIRECTORY,&lt;br /&gt;DACL allows only package SID)&quot;]
&lt;p&gt;The &lt;code&gt;\BNOLINKS&lt;/code&gt; directory deserves a separate paragraph because it is not on Microsoft Learn. &lt;code&gt;NtCreatePrivateNamespace&lt;/code&gt; is the kernel-side syscall behind the Win32 &lt;code&gt;CreatePrivateNamespace&lt;/code&gt; API [@ms-createprivatenamespacew]; the caller passes a boundary descriptor built by &lt;code&gt;CreateBoundaryDescriptor&lt;/code&gt; [@ms-createboundarydescriptorw] plus one or more SIDs added via &lt;code&gt;AddSIDToBoundaryDescriptor&lt;/code&gt; [@ms-addsidtoboundarydescriptor]. The kernel materialises one &lt;code&gt;\BNOLINKS&lt;/code&gt; entry per &lt;code&gt;(alias_prefix, boundary_descriptor_hash)&lt;/code&gt; tuple; two callers that pass the same &lt;code&gt;lpAliasPrefix&lt;/code&gt; but different boundary descriptors land on different directories. The native signature is documented in the PHNT-derived NtDoc mirror [@ntdoc-ntcreateprivatenamespace], and the &lt;code&gt;OBJECT_BOUNDARY_DESCRIPTOR&lt;/code&gt; structure layout is at ntdoc.m417z.com/object_boundary_descriptor [@ntdoc-object-boundary-descriptor]. The Win32 Object Namespaces overview [@ms-object-namespaces] is Microsoft&apos;s only published user-mode reference; the &lt;code&gt;\BNOLINKS&lt;/code&gt; directory name itself is reverse-engineering-derived.The &lt;code&gt;\BNOLINKS&lt;/code&gt; directory is documented only through reverse engineering of &lt;code&gt;ntoskrnl.exe&lt;/code&gt; -- via Forshaw&apos;s NtObjectManager and System Informer&apos;s PHNT headers -- not on Microsoft Learn. The user-mode API surface (&lt;code&gt;CreatePrivateNamespace&lt;/code&gt;, &lt;code&gt;CreateBoundaryDescriptor&lt;/code&gt;, &lt;code&gt;AddSIDToBoundaryDescriptor&lt;/code&gt;) is fully documented. The provenance gap is worth flagging when you cite the directory by name.The &lt;code&gt;\KnownDlls&lt;/code&gt; LPE class was, for a decade, the canonical example of how a DACL plus loader-side validation could lock down a supply-chain anchor. Forshaw&apos;s August 2018 P0 post first sketched a &lt;code&gt;DefineDosDevice&lt;/code&gt; + &lt;code&gt;\??&lt;/code&gt; symlink-plant chain that could land a forged &lt;code&gt;Section&lt;/code&gt; object into &lt;code&gt;\KnownDlls&lt;/code&gt;; Clement Labro (itm4n) implemented the attack as the PPLdump tool and wrote companion posts on both itm4n.github.io [@itm4n-lsass-runasppl] and the SCRT team blog [@blog-scrt-bypassing-lsa-protection-in-userland]. The class was closed in NTDLL by Windows 10 21H2 build 19044.1826; itm4n confirms the patch in &lt;em&gt;The End of PPLdump&lt;/em&gt; [@itm4n-the-end-of-ppldump]: &quot;A patch in NTDLL now prevents PPLs from loading Known DLLs.&quot;&lt;/p&gt;
&lt;p&gt;{`
const MAX_DIRECTORY_BUCKETS = 37;&lt;/p&gt;
&lt;p&gt;function rtlHashUnicodeString(name) {
  let h = 0;
  for (const ch of name.toUpperCase()) {
    h = (h * 31 + ch.charCodeAt(0)) &amp;gt;&amp;gt;&amp;gt; 0;
  }
  return h % MAX_DIRECTORY_BUCKETS;
}&lt;/p&gt;
&lt;p&gt;function makeDir() {
  return { buckets: Array(MAX_DIRECTORY_BUCKETS).fill(null).map(() =&amp;gt; []) };
}&lt;/p&gt;
&lt;p&gt;function addChild(dir, name, child) {
  dir.buckets[rtlHashUnicodeString(name)].push({ name, child });
}&lt;/p&gt;
&lt;p&gt;function lookupObjectName(path, root) {
  const components = path.split(&apos;\\&apos;).filter(Boolean);
  let cursor = root;
  for (const comp of components) {
    const bucket = rtlHashUnicodeString(comp);
    const chain = cursor.buckets[bucket];
    const hit = chain.find(e =&amp;gt; e.name.toUpperCase() === comp.toUpperCase());
    console.log(`lookup &apos;${comp}&apos; -&amp;gt; bucket ${bucket}, chain length ${chain.length}, ${hit ? &apos;HIT&apos; : &apos;MISS&apos;}`);
    if (!hit) return null;
    if (hit.child.parseProcedure) {
      const rest = &apos;\\&apos; + components.slice(components.indexOf(comp) + 1).join(&apos;\\&apos;);
      console.log(`  parse-procedure handoff for type &apos;${hit.child.type}&apos;, residual=&apos;${rest}&apos;`);
      return { handedOff: hit.child, residual: rest };
    }
    cursor = hit.child;
  }
  return cursor;
}&lt;/p&gt;
&lt;p&gt;const root = makeDir();
const device = makeDir();
device.parseProcedure = true; device.type = &apos;Device&apos;;
const sessions = makeDir();
addChild(root, &apos;Device&apos;, device);
addChild(root, &apos;Sessions&apos;, sessions);
addChild(root, &apos;BaseNamedObjects&apos;, makeDir());&lt;/p&gt;
&lt;p&gt;lookupObjectName(&apos;\\Device\\HarddiskVolume1\\Users\\me\\file.txt&apos;, root);
`}&lt;/p&gt;
&lt;p&gt;The walk is the algorithm. The 37 is the bucket count Cutler picked in 1993. The parse-procedure handoff is where the I/O Manager and the Configuration Manager and dozens of other subsystems insert themselves into the tree. Now turn the question around: Windows bet on one tree. What did the kernels that did not bet on one tree do, and why?&lt;/p&gt;
&lt;h2&gt;7. How other kernels name kernel objects&lt;/h2&gt;
&lt;p&gt;Three kernels, three different bets. Linux took the namespace and &lt;em&gt;split it into per-resource-class clones&lt;/em&gt; -- one for mounts, one for PIDs, one for IPC, one for the network stack, one for users, one for hostnames, one for cgroups, one for time -- and never built a unified tree. macOS / Darwin gave each task its own &lt;em&gt;Mach port-right namespace&lt;/em&gt; and let &lt;code&gt;launchd&lt;/code&gt; broker named-service lookups. Plan 9 from Bell Labs was the academic ancestor of &quot;every named OS resource is a filesystem path,&quot; and the design Cutler imported into NT.&lt;/p&gt;
&lt;h3&gt;7.1 Linux: per-resource namespaces&lt;/h3&gt;
&lt;p&gt;Linux ships eight namespace types, each governed by a &lt;code&gt;CLONE_NEW*&lt;/code&gt; flag passed to &lt;code&gt;clone()&lt;/code&gt;, &lt;code&gt;unshare()&lt;/code&gt;, or &lt;code&gt;setns()&lt;/code&gt;: mount, PID, network, IPC, user, UTS, cgroup, and time. The &lt;code&gt;namespaces(7)&lt;/code&gt; man page is precise: &quot;A namespace wraps a global system resource in an abstraction that makes it appear to the processes within the namespace that they have their own isolated instance of the global resource&quot; [@man7-namespaces]. Docker, containerd, runc, Kubernetes pods, LXC, and systemd-nspawn all compose these eight flags into a Linux container.&lt;/p&gt;
&lt;p&gt;The strength of the Linux design is per-class composability. A process can be in a fresh mount namespace, a fresh PID namespace, and the host&apos;s network namespace, all at once. The weakness is the absence of a unified type registry: Linux has no equivalent of &lt;code&gt;\ObjectTypes&lt;/code&gt;, no equivalent of the &lt;code&gt;OBJECT_HEADER&lt;/code&gt; reference counting that the kernel applies uniformly to every named object. Each resource class has its own lookup function, its own permission model, and its own ownership story. A bug in any one of them is bounded to that one resource class but is also not shared mitigation across the others.&lt;/p&gt;
&lt;h3&gt;7.2 macOS / Darwin: Mach ports and the bootstrap server&lt;/h3&gt;
&lt;p&gt;Darwin&apos;s kernel-object naming is capability-style. Apple&apos;s archive documentation describes the model directly: &quot;each task consists of a virtual address space, a port right namespace, and one or more threads&quot; [@apple-mach-kernel]. Tasks send messages by holding a &lt;em&gt;port right&lt;/em&gt; -- a per-task index into a kernel-managed table of Mach ports. There is no single hierarchical namespace; ports are sent over Mach messages, and &lt;code&gt;launchd&lt;/code&gt; operates as the bootstrap-server name broker for services that need a stable rendezvous. A separate I/O Registry tree carries device objects.&lt;/p&gt;
&lt;p&gt;The strength of the Mach design is that capabilities cannot be forged; you cannot synthesise a port right out of a string the way you can synthesise a path string under Windows. The weakness is the split namespace: device objects live in the I/O Registry, services live behind &lt;code&gt;launchd&lt;/code&gt;, and the kernel itself has no equivalent of &lt;code&gt;\BaseNamedObjects&lt;/code&gt; as a one-stop shop.&lt;/p&gt;
&lt;h3&gt;7.3 Plan 9 from Bell Labs&lt;/h3&gt;
&lt;p&gt;Plan 9 is the design lineage Cutler imported. In Plan 9, every named operating-system resource -- including processes, network connections, devices, and the window system -- surfaces as a path served over 9P. The single hierarchical namespace was the central claim. Plan 9 never reached commercial scale, but its design idea reached production in three places: NT (1993, via Cutler), Linux&apos;s /proc, /sys, and FUSE (the 1990s onward), and the various capability-OS research projects (KeyKOS, EROS, seL4) that took the lessons in a different direction.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Primitive&lt;/th&gt;
&lt;th&gt;Granularity&lt;/th&gt;
&lt;th&gt;Enforcement point&lt;/th&gt;
&lt;th&gt;Structural / opt-in&lt;/th&gt;
&lt;th&gt;Bypass by privilege&lt;/th&gt;
&lt;th&gt;Inheritance gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Per-Session (NT)&lt;/td&gt;
&lt;td&gt;Logon session&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ObpLookupObjectName&lt;/code&gt; + DACL&lt;/td&gt;
&lt;td&gt;Structural&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SeDebugPrivilege&lt;/code&gt; short-circuit&lt;/td&gt;
&lt;td&gt;Inherited handles cross sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AppContainer Lowbox (NT)&lt;/td&gt;
&lt;td&gt;Package SID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ObpLookupObjectName&lt;/code&gt; rewrite&lt;/td&gt;
&lt;td&gt;Structural&lt;/td&gt;
&lt;td&gt;TCB privileges only&lt;/td&gt;
&lt;td&gt;Brokered handles enter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server Silo (NT)&lt;/td&gt;
&lt;td&gt;Container&lt;/td&gt;
&lt;td&gt;Process-&amp;gt;Silo indirection&lt;/td&gt;
&lt;td&gt;Structural&lt;/td&gt;
&lt;td&gt;KMCS-signed driver&lt;/td&gt;
&lt;td&gt;Host handles cross silos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VBS / IUM Trustlet (NT)&lt;/td&gt;
&lt;td&gt;Trust level (VTL)&lt;/td&gt;
&lt;td&gt;Hypervisor&lt;/td&gt;
&lt;td&gt;Structural&lt;/td&gt;
&lt;td&gt;Hypervisor compromise&lt;/td&gt;
&lt;td&gt;Cross-VTL ALPC only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mandatory Integrity Control (NT)&lt;/td&gt;
&lt;td&gt;IL band&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SeAccessCheckByType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opt-in (per-object SACL)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SeRelabelPrivilege&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inherited handles bypass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ObRegisterCallbacks&lt;/code&gt; (NT)&lt;/td&gt;
&lt;td&gt;Per-type, per-driver&lt;/td&gt;
&lt;td&gt;Object Manager pre-op callback&lt;/td&gt;
&lt;td&gt;Mediation, not partition&lt;/td&gt;
&lt;td&gt;KMCS-signed driver&lt;/td&gt;
&lt;td&gt;Inheritance bypasses callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private Namespace (NT)&lt;/td&gt;
&lt;td&gt;Boundary SID-list&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NtCreatePrivateNamespace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Structural&lt;/td&gt;
&lt;td&gt;All SIDs in caller&apos;s token&lt;/td&gt;
&lt;td&gt;Boundary-keyed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux Namespace&lt;/td&gt;
&lt;td&gt;Per-resource clone&lt;/td&gt;
&lt;td&gt;&lt;code&gt;setns&lt;/code&gt;/&lt;code&gt;unshare&lt;/code&gt;/&lt;code&gt;clone&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Structural&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fork inherits namespace set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mach Port Right&lt;/td&gt;
&lt;td&gt;Per-task&lt;/td&gt;
&lt;td&gt;Capability check on send&lt;/td&gt;
&lt;td&gt;Structural (capabilities)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;host_priv&lt;/code&gt; / kernel&lt;/td&gt;
&lt;td&gt;Inherited rights on fork&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

The Object Manager namespace is not a filesystem. There is no disk persistence, no journal, no FAT or MFT, no inode allocator, no per-file DACL in the filesystem sense. Nothing under `\` survives a reboot. Files-on-disk, registry-keys-in-a-hive, and named pipes are leaves in the OM tree, but the actual filesystem implementation lives in NTFS / ReFS / ExFAT drivers reached through the `Device` type&apos;s parse procedure.&lt;p&gt;What the OM namespace &lt;em&gt;shares&lt;/em&gt; with filesystems is exactly three things: the path-walk algorithm (left-to-right, component-by-component, with one hash-table lookup per component), the per-directory hash table (analogous to the directory-entry hash filesystems use), and the per-object security descriptor (which the SRM enforces at the same point a filesystem would enforce its DACL).&lt;/p&gt;
&lt;p&gt;When you read or write the phrase &quot;Object Manager namespace,&quot; the metaphor that is doing real work is &quot;in-memory directory tree the kernel uses to find named objects,&quot; not &quot;filesystem in the disk-format sense.&quot;
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;

The Windows 2000-era `CreateRestrictedToken` primitive was the wrong layer in 2000 as a standalone sandboxing mechanism -- it could not partition the namespace; it only filtered the caller&apos;s SID set against per-object DACLs. Chromium revived it in 2008 as one of four cooperating layers, and that pattern is the canonical 2026 production sandbox shape. The Chromium design document captures the constraints: &quot;The Windows sandbox is a user-mode only sandbox. There are no special kernel mode drivers... The sandbox is provided as a static library that must be linked to both the broker and the target executables&quot; (Chromium Sandbox Design [@chromium-sandbox-md], FAQ [@chromium-sandbox-faq]).&lt;p&gt;The four layers compose pairwise-orthogonally. The token gates &lt;em&gt;which DACLs&lt;/em&gt; the renderer can satisfy at &lt;code&gt;SeAccessCheck&lt;/code&gt; time; the job object gates &lt;em&gt;which kernel API surface&lt;/em&gt; the renderer can call (UI exceptions, process creation, etc.); the integrity level gates &lt;em&gt;which writes&lt;/em&gt; the renderer can perform across MIC label boundaries; the AppContainer lowbox-rewrites &lt;em&gt;every named-object lookup&lt;/em&gt; into the per-package directory inside &lt;code&gt;ObpLookupObjectName&lt;/code&gt;. A handle that survives all four checks is the only object the renderer can usefully touch. The load-bearing header is &lt;code&gt;sandbox_policy.h&lt;/code&gt;, which declares &lt;code&gt;TargetConfig::SetTokenLevel(TokenLevel initial, TokenLevel lockdown)&lt;/code&gt;, &lt;code&gt;SetJobLevel&lt;/code&gt;, &lt;code&gt;SetIntegrityLevel&lt;/code&gt;, &lt;code&gt;SetDelayedIntegrityLevel&lt;/code&gt;, and &lt;code&gt;SetAppContainerSid&lt;/code&gt;, with one verbatim mutual-exclusion note: &quot;Using an initial token is not compatible with AppContainer&quot; [@chromium-sandbox-policy-h].&lt;/p&gt;
&lt;p&gt;This is the 2026 production sandbox shape every Chromium-based browser inherits (Edge, Chrome, Brave, Vivaldi, Opera), as do Electron-based apps like Visual Studio Code&apos;s renderer processes.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;

The cross-VTL ALPC ports through which a VTL0 process talks to a VTL1 trustlet are still located in VTL0&apos;s `\RPC Control`. An attacker who controls VTL0 can *send* messages to LsaIso even though they cannot *read* LsaIso&apos;s internal state. Oliver Lyak&apos;s December 2022 *Pass-the-Challenge* result is the canonical worked example ([GitHub: ly4k/PassTheChallenge](https://github.com/ly4k/PassTheChallenge)): the trustlet&apos;s pages are never read, but the trustlet&apos;s RPC output exfiltrates the secret. The lesson is that VTL1 isolation is a *page-level* read barrier, not a *protocol-level* containment property. The VBS Trustlets piece in this corpus carries the deeper walkthrough.
&lt;p&gt;Windows bet on one tree; Linux bet on eight clone-flag dimensions; Darwin bet on capability-style port-right tables. Each bet has theoretical limits. What are they?&lt;/p&gt;
&lt;h2&gt;8. What the namespace cannot do&lt;/h2&gt;
&lt;p&gt;The frame for this section comes from James P. Anderson&apos;s 1972 USAF technical report &lt;em&gt;Computer Security Technology Planning Study&lt;/em&gt; (ESD-TR-73-51), Section 4.1.1. Anderson is the named originator of the reference-monitor concept and of the four properties such a monitor must satisfy. Wikipedia preserves the modern acronym verbatim: the reference-validation mechanism must be &quot;&lt;strong&gt;N&lt;/strong&gt;on-bypassable... &lt;strong&gt;E&lt;/strong&gt;valuable... &lt;strong&gt;A&lt;/strong&gt;lways invoked... &lt;strong&gt;T&lt;/strong&gt;amper-proof,&quot; and &quot;according to Ross Anderson, the reference monitor concept was introduced by James Anderson in an influential 1972 paper&quot; [@wikipedia-reference-monitor]. The NIST CSRC mirror hosts the original PDF [@csrc-nist-ande72].&lt;/p&gt;
&lt;p&gt;Saltzer and Schroeder&apos;s 1975 paper &lt;em&gt;The Protection of Information in Computer Systems&lt;/em&gt; [@cs-virginia-saltzer-schroeder] added the &lt;em&gt;complete-mediation principle&lt;/em&gt; -- &quot;every access to every object must be checked for authority&quot; -- and seven other design principles the reference-validation mechanism must satisfy (economy of mechanism, fail-safe defaults, open design, separation of privilege, least privilege, least common mechanism, psychological acceptability).&lt;/p&gt;
&lt;p&gt;Map the Windows Object Manager against the four NEAT properties and the answer is uncomfortable. The namespace partially achieves two (Always-invoked and Tamper-proof), fails Non-bypassable outright, and falls one to two orders of magnitude short of Evaluable.&lt;/p&gt;
&lt;h3&gt;8.1 Always-invoked: provably gapped&lt;/h3&gt;
&lt;p&gt;The namespace achieves always-invoked for &lt;em&gt;name-based opens&lt;/em&gt;. Every &lt;code&gt;Nt*OpenObject*&lt;/code&gt; syscall walks &lt;code&gt;ObpLookupObjectName&lt;/code&gt;; there is no path that returns a handle to a named object without going through the lookup. But the namespace cannot achieve always-invoked for &lt;em&gt;handle inheritance&lt;/em&gt;. A child process inherits handles from &lt;code&gt;CreateProcess(bInheritHandles=TRUE)&lt;/code&gt; without going through the OM at all. The handles already exist in the parent&apos;s &lt;code&gt;HANDLE_TABLE&lt;/code&gt;; the kernel walks the parent&apos;s table, duplicates the entries into the child&apos;s table, and the child has live access. No name-lookup, no &lt;code&gt;ObRegisterCallbacks&lt;/code&gt; callback, no SRM check. As long as the OS API exposes handle inheritance -- and it is too deeply embedded in 33 years of shipping Windows code to remove -- the Object Manager cannot be the &lt;em&gt;sole&lt;/em&gt; reference monitor.&lt;/p&gt;
&lt;h3&gt;8.2 Tamper-proof: bounded, not absolute&lt;/h3&gt;
&lt;p&gt;The Object Manager runs in ring 0, under Kernel-Mode Code Signing (KMCS), and -- on machines with Virtualization-Based Security and Hypervisor-protected Code Integrity (HVCI) enabled -- inside a Hyper-V-enforced code-integrity policy. Any kernel-mode adversary who can load a driver bypasses the OM. KMCS and HVCI raise the cost; they do not eliminate the surface. The Bring-Your-Own-Vulnerable-Driver class of attacks (signed but exploitable drivers) is the running residual class, and the historical pattern is that one or two new vulnerable signed drivers surface every quarter.&lt;/p&gt;
&lt;h3&gt;8.3 Evaluable: provably above threshold&lt;/h3&gt;
&lt;p&gt;A small enough TCB can be machine-verified. The seL4 microkernel is the canonical demonstration: roughly 9,000 lines of C verified end-to-end against a formal specification (~11 person-years for initial functional correctness per Klein et al. SOSP 2009, and approximately 25 person-years for the full suite of subsequent proofs including information-flow and binary verification) [@sel4-project]. The Object Manager subsystem, the Security Reference Monitor, and the parse procedures the Object Manager delegates to (file-system drivers via &lt;code&gt;IopParseDevice&lt;/code&gt;; the registry via &lt;code&gt;CmpParseKey&lt;/code&gt;; ALPC; the I/O manager itself) collectively comprise tens of thousands of lines of C, putting the TCB for &quot;open a named object&quot; at one to two orders of magnitude above the verification threshold any current proof system can handle. The Object Manager is &lt;em&gt;not&lt;/em&gt; evaluable in the formal sense Anderson required.&lt;/p&gt;
&lt;h3&gt;8.4 Non-bypassable: the privilege short-circuit&lt;/h3&gt;
&lt;p&gt;A process holding &lt;code&gt;SeDebugPrivilege&lt;/code&gt; (or any privilege that grants &lt;code&gt;PROCESS_VM_*&lt;/code&gt; rights) can short-circuit per-directory ACLs. The privilege evaluation happens at &lt;code&gt;SeAccessCheck&lt;/code&gt; time, &lt;em&gt;after&lt;/em&gt; &lt;code&gt;ObpLookupObjectName&lt;/code&gt; has resolved the name. The Object Manager will resolve any path the privileged caller asks for; the gate fires, but it lets the call through. The namespace cannot defend against the holder of &lt;code&gt;SeDebugPrivilege&lt;/code&gt;. This is by design -- you want a debugger to be able to attach to anything -- but it is also the structural reason why &quot;lock down the namespace&quot; is not by itself a containment story.&lt;/p&gt;
&lt;h3&gt;8.5 What else the namespace cannot do&lt;/h3&gt;
&lt;p&gt;It cannot prevent in-process memory disclosure -- the Pass-the-Challenge limit covered in the Section 7 aside. It cannot defend against a malicious driver -- KMCS, HVCI, and WDAC gate driver load; the namespace itself trusts already-loaded drivers. It cannot eliminate time-of-check / time-of-use racing during a path walk; the walker walks components one at a time, and any reentrant call into the walker is a TOCTOU surface. The mitigation is per-call -- callers pass &lt;code&gt;OBJ_DONT_REPARSE&lt;/code&gt; on object-attributes, &lt;code&gt;FILE_FLAG_OPEN_REPARSE_POINT&lt;/code&gt; on file opens, or otherwise instruct the path-walker to refuse symbolic-link substitution -- not a structural property of the namespace.&lt;/p&gt;
&lt;h3&gt;8.6 The honest accounting&lt;/h3&gt;
&lt;p&gt;The Object Manager namespace is a &lt;em&gt;coordination&lt;/em&gt; mechanism, not a &lt;em&gt;containment&lt;/em&gt; mechanism. Containment is in the layers above: the session ID, the package SID, the integrity level, the silo ID, the VTL split. The namespace&apos;s job is to make those layers &lt;em&gt;enforceable&lt;/em&gt; by partitioning the path space so the bad open &lt;em&gt;cannot resolve to the privileged object&apos;s name&lt;/em&gt;. The layers above decide which partition the caller is in; the namespace&apos;s only job is &quot;given a path and a caller, find the object.&quot; Anderson 1972 names the &lt;em&gt;kernel mechanism&lt;/em&gt; (the reference-validation mechanism with NEAT properties); Saltzer-Schroeder 1975 names the &lt;em&gt;design principles&lt;/em&gt; the mechanism must satisfy. The Object Manager is the Windows realisation; it inherits both the strengths and the limits.&lt;/p&gt;

The namespace is a coordination mechanism, not a containment mechanism. The containment is in the layers above.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The Object Manager is the coordination layer; the containment is in the partition primitives stacked on top (session ID, package SID, integrity level, silo ID, VTL). The namespace&apos;s only job is &quot;given a path and a caller, find the object.&quot; Every Windows security boundary is a parameter to that one job: a per-directory ACL, a token-keyed name rewrite, or a kernel callback registered against an &lt;code&gt;OBJECT_TYPE&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The provable gaps are real. What is the active research direction in 2026 -- where do attackers and defenders actually meet inside the namespace today?&lt;/p&gt;
&lt;h2&gt;9. Open problems in 2026&lt;/h2&gt;
&lt;p&gt;Five open problems sit in active research as of 2026.&lt;/p&gt;
&lt;h3&gt;9.1 Hash-bucket collision pressure&lt;/h3&gt;
&lt;p&gt;The 37-bucket constant has not changed since 1993. On a 2026 Windows 11 25H2 machine with several hundred MSIX packages, each owning an &lt;code&gt;\AppContainerNamedObjects\&amp;lt;package-sid&amp;gt;\&lt;/code&gt; subtree, average chain lengths inside &lt;code&gt;\Sessions\1\AppContainerNamedObjects&lt;/code&gt; exceed two and routinely run higher under load. The structural impact is small per-lookup (O(chain length) at each component), but it compounds across deep path walks and across the per-VM hot loops in &lt;code&gt;ObpLookupObjectName&lt;/code&gt;. Microsoft has not committed to a larger table or a different structure; the constant remains.&lt;/p&gt;
&lt;h3&gt;9.2 Cross-AppContainer object-directory privacy&lt;/h3&gt;
&lt;p&gt;Per-AppContainer isolation is the AppContainer model&apos;s promise; residual cross-package reads erode it. Forshaw&apos;s Project Zero work between 2017 and 2020 documents specific classes; Windows 11 25H2 DACLs are tighter than Windows 10 RTM, but the impersonation-mediated cases survive. The HackSys / CVE-2023-35359 family covered in Section 4.5 is the current realisation of the cross-AppContainer-plus-impersonation surface, and the same broader resource-planting taxonomy Forshaw described in the 2017 Named Pipe Secure Prefixes post [@tiraniddo-named-pipe-secure-prefixes] is still rediscovered every year.&lt;/p&gt;
&lt;h3&gt;9.3 Silo-escape via routines that ignore silo attachment&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;Siloscape&lt;/em&gt; (June 7, 2021) showed that &lt;code&gt;NtSetInformationSymbolicLink&lt;/code&gt; could retarget a silo-scoped symbolic link at a host-scoped path. Microsoft patched the specific function; the &lt;em&gt;class&lt;/em&gt; -- kernel routines whose path resolution does not honour &lt;code&gt;Process-&amp;gt;Silo-&amp;gt;RootDirectory&lt;/code&gt; -- remains open. Microsoft&apos;s long-standing position is that Server Silo is not a security boundary; Hyper-V Container is the security-boundary product. Container runtimes that depend on Server Silo for tenant isolation are knowingly running outside the supported boundary.&lt;/p&gt;
&lt;h3&gt;9.4 ObRegisterCallbacks erosion under HVCI&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ObRegisterCallbacks&lt;/code&gt; requires a KMCS-signed driver, and on HVCI-enabled machines the binary must additionally be HVCI-compatible. Microsoft has progressively raised the compatibility bar -- preventing unsigned drivers, banning common runtime-patching idioms, and tightening the W^X policy. EDR vendors depend on the surface staying open; if HVCI&apos;s compatibility bar ever excludes the EDR kernel driver pattern, the in-kernel callback layer is at risk. The CrowdStrike Falcon Sensor outage of July 2024 made the brittleness of in-kernel EDR a public conversation. Microsoft&apos;s &lt;em&gt;Defender for Endpoint&lt;/em&gt; and &lt;em&gt;EDR-on-Linux eBPF&lt;/em&gt; projects point at alternative-mediation futures, but in-kernel &lt;code&gt;ObRegisterCallbacks&lt;/code&gt; is still the primary credential-theft sensor.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; As attackers ship Hell&apos;s Gate / Halo&apos;s Gate / direct-syscall stubs to bypass userland EDR hooks, the kernel callback fires regardless. The arms race accordingly shifts to the &lt;em&gt;access-mask-strip vs. impersonate-trusted-parent-PID&lt;/em&gt; layer inside the kernel callback itself, with both sides racing to define the right pre-operation policy for &lt;code&gt;lsass.exe&lt;/code&gt; handle opens. Watch the Microsoft Security Response Center advisories and the EDR-vendor incident postmortems for the bleeding edge.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;9.5 Public benchmark vacuum&lt;/h3&gt;
&lt;p&gt;No peer-reviewed benchmark compares per-call namespace-lookup cost across the Windows Object Manager, Linux namespaces, and Mach ports. Choice of namespace design at the OS level is a multi-decade commitment; the absence of an empirical comparison forces architecture decisions on theoretical-only grounds. The Linux Kernel Test Robot, the Phoronix Test Suite, and various academic systems-conference benchmarks measure adjacent properties (filesystem-call latency, system-call vector cost), but none publishes head-to-head numbers on the named-object-lookup hot path. This is an open invitation to systems researchers.&lt;/p&gt;
&lt;p&gt;Five open problems is a research agenda, not a how-to. How do you actually look at this thing on your own machine?&lt;/p&gt;
&lt;h2&gt;10. Reading the namespace from a live system&lt;/h2&gt;
&lt;p&gt;Three tools cover the operational practice: Sysinternals WinObj, Forshaw&apos;s NtObjectManager PowerShell module, and WinDbg in kernel mode.&lt;/p&gt;
&lt;h3&gt;10.1 WinObj on a live system&lt;/h3&gt;
&lt;p&gt;Download &lt;code&gt;winobj.exe&lt;/code&gt; from Sysinternals [@ms-winobj] and run it as administrator. The left pane is the directory tree; the right pane shows the children of the selected directory with their object types. Navigate to &lt;code&gt;\Sessions\1\BaseNamedObjects&lt;/code&gt; and read off the named events and mutants every Win32 app in your interactive session has created. Navigate to &lt;code&gt;\Sessions\1\AppContainerNamedObjects&lt;/code&gt; and pick an &lt;code&gt;S-1-15-2-...&lt;/code&gt; directory; right-click, choose Properties, and read the security descriptor. You will see a single allow-ACE granting full access only to the package SID itself. That ACE is the entire AppContainer sandbox at the namespace layer.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; WinObj cannot traverse &lt;code&gt;\ObjectTypes&lt;/code&gt;, &lt;code&gt;\Security&lt;/code&gt;, or &lt;code&gt;\Sessions\0\&lt;/code&gt; without administrator rights. Without traversal, the enumerate fails silently and the tree looks empty. Always run elevated, and accept that the tool will show the kernel view, not a per-process view.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;10.2 NtObjectManager PowerShell&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;NtObjectManager&lt;/code&gt; is Forshaw&apos;s PowerShell module that exposes the Object Manager namespace through cmdlets (PowerShell Gallery [@powershellgallery-ntobjectmanager]; GitHub [@p0-sandbox-attacksurface-analysis-tools]). Install with &lt;code&gt;Install-Module NtObjectManager&lt;/code&gt;. Useful commands: &lt;code&gt;Get-ChildItem NtObject:\&lt;/code&gt; walks the root; &lt;code&gt;Get-NtType&lt;/code&gt; lists the registered &lt;code&gt;OBJECT_TYPE&lt;/code&gt; singletons; &lt;code&gt;Get-NtObject \BaseNamedObjects&lt;/code&gt; enumerates the global BNO; &lt;code&gt;Get-NtAlpcPort &apos;\RPC Control&apos;&lt;/code&gt; lists every LRPC endpoint on the machine. The module wraps the same NTDLL syscalls WinObj uses, but in a scripting surface that composes into automation.&lt;/p&gt;
&lt;h3&gt;10.3 WinDbg kernel session&lt;/h3&gt;
&lt;p&gt;In a kernel-mode WinDbg session attached to a target machine (or to a live local kernel via Microsoft&apos;s local-kernel debug mode), &lt;code&gt;!object \&lt;/code&gt; dumps the root directory and its children. &lt;code&gt;dt nt!_OBJECT_HEADER &amp;lt;addr&amp;gt;-30&lt;/code&gt; reads the header preceding any object&apos;s body (the offset 0x30 is the size of &lt;code&gt;OBJECT_HEADER&lt;/code&gt; on x64; subtract that from the body pointer to land on the header -- the field layout is documented in &lt;em&gt;Windows Internals 7th Edition* Chapter 8, Microsoft Press Store [@microsoftpressstore-wininternals7-part1]). `dx -r1 ((nt!_OBJECT_TYPE&lt;/em&gt;)nt!PsProcessType[0]).TypeInfo` walks the Process type&apos;s method table and lists all eight procedure pointers and the WaitObjectFlagOffset, including the parse procedure.&lt;/p&gt;
&lt;h3&gt;10.4 The EDR primitive: an ObRegisterCallbacks driver template&lt;/h3&gt;
&lt;p&gt;The minimal sketch of an in-kernel EDR sensor is four steps. Register an &lt;code&gt;OB_CALLBACK_REGISTRATION&lt;/code&gt; for &lt;code&gt;PsProcessType&lt;/code&gt; with &lt;code&gt;OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE&lt;/code&gt; [@ms-obregistercallbacks]. In the pre-operation callback, examine &lt;code&gt;OperationInformation-&amp;gt;Object&lt;/code&gt;, derive the target process&apos;s PID, and compare it against &lt;code&gt;lsass.exe&lt;/code&gt;. If it matches, strip credential-relevant access bits from &lt;code&gt;OperationInformation-&amp;gt;Parameters-&amp;gt;CreateHandleInformation.DesiredAccess&lt;/code&gt; (or duplicate-handle equivalent). The kernel grants the handle with the reduced rights, the attacker&apos;s &lt;code&gt;PROCESS_VM_READ&lt;/code&gt; is gone before the call returns, and the post-operation callback logs the attempt. The parallel API &lt;code&gt;PsSetCreateProcessNotifyRoutineEx&lt;/code&gt; [@ms-pssetcreateprocessnotifyroutineex] covers process creation, which is the other half of the EDR sensor surface.&lt;/p&gt;

sequenceDiagram
    participant A as Attacker process
    participant NT as nt!NtOpenProcess
    participant OM as Object Manager
    participant EDR as EDR Pre-Op Callback
    participant LSASS as lsass.exe (target)
    A-&amp;gt;&amp;gt;NT: NtOpenProcess(lsass PID, PROCESS_VM_READ | PROCESS_QUERY_INFORMATION)
    NT-&amp;gt;&amp;gt;OM: lookup PsProcessType, target by PID
    OM-&amp;gt;&amp;gt;EDR: fire pre-op callback (handle create)
    EDR-&amp;gt;&amp;gt;EDR: target == lsass.exe?
    EDR-&amp;gt;&amp;gt;EDR: strip PROCESS_VM_READ from DesiredAccess
    EDR--&amp;gt;&amp;gt;OM: granted = PROCESS_QUERY_LIMITED_INFORMATION
    OM--&amp;gt;&amp;gt;NT: HANDLE with reduced access
    NT--&amp;gt;&amp;gt;A: open succeeded (but useless rights)
&lt;p&gt;{`
const PROCESS_VM_READ                   = 0x0010;
const PROCESS_VM_WRITE                  = 0x0020;
const PROCESS_VM_OPERATION              = 0x0008;
const PROCESS_QUERY_INFORMATION         = 0x0400;
const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
const PROCESS_CREATE_THREAD             = 0x0002;
const PROCESS_DUP_HANDLE                = 0x0040;&lt;/p&gt;
&lt;p&gt;function stripForLsass(desired) {
  const STRIPPED =
      PROCESS_VM_READ |
      PROCESS_VM_WRITE |
      PROCESS_VM_OPERATION |
      PROCESS_CREATE_THREAD |
      PROCESS_DUP_HANDLE |
      PROCESS_QUERY_INFORMATION;
  return desired &amp;amp; ~STRIPPED;
}&lt;/p&gt;
&lt;p&gt;const desired = PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | PROCESS_DUP_HANDLE;
console.log(&apos;attacker asked for:&apos;, &apos;0x&apos; + desired.toString(16));
const granted = stripForLsass(desired) | PROCESS_QUERY_LIMITED_INFORMATION;
console.log(&apos;EDR pre-op granted:&apos;, &apos;0x&apos; + granted.toString(16));
`}&lt;/p&gt;

```c
OB_OPERATION_REGISTRATION op = {
    .ObjectType = PsProcessType,
    .Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE,
    .PreOperation = MyPreOp,
    .PostOperation = MyPostOp,
};
OB_CALLBACK_REGISTRATION reg = {
    .Version = OB_FLT_REGISTRATION_VERSION,
    .OperationRegistrationCount = 1,
    .Altitude = RTL_CONSTANT_STRING(L&quot;123456&quot;),
    .OperationRegistration = &amp;amp;op,
};
ObRegisterCallbacks(®, &amp;amp;g_handle);
```
The driver must be KMCS-signed (`IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY`) per the wdm.h documentation; an unsigned image returns `STATUS_ACCESS_DENIED` from `ObRegisterCallbacks`. Two drivers cannot pick the same Altitude; collisions return `STATUS_FLT_INSTANCE_ALTITUDE_COLLISION`.
&lt;p&gt;You can now read the namespace, register an EDR-style callback, and dump the type registry. What are the questions readers ask after they finish reading?&lt;/p&gt;
&lt;h2&gt;11. Frequently asked questions&lt;/h2&gt;


No. The registry is a separate Windows Executive subsystem implemented in `nt!Cm*`, with its own hive on-disk format and its own in-memory hive structures. It hooks into the Object Manager namespace through one and only one mechanism: the `Key` `OBJECT_TYPE` registers a `ParseProcedure` (`CmpParseKey`) that takes over path walking when the namespace walker reaches `\REGISTRY`. The registry is therefore a *consumer* of the Object Manager, but not part of the Object Manager.

Because `\BaseNamedObjects` is the *global* / `Global\`-prefixed-only view, distinct from the per-session BNO at `\Sessions\\BaseNamedObjects`. The Win32 `Local\` prefix routes through `kernel32!BaseGetNamedObjectDirectory` into the per-session BNO; `Global\` routes into the global one [@ms-termserv-kernel-object-namespaces]. Cross-session named-object coordination still needs the global view; per-session isolation lives in the per-session subtree.

Because the lowbox token attached to the UWP app&apos;s process tells `ObpLookupObjectName` to rewrite the path to `\Sessions\\AppContainerNamedObjects\\Global\Foo` before path walking. Two different UWP apps have two different package SIDs and therefore land on two different directories. The Win32 names look the same; the kernel resolves them to different objects.

`\??\C:` is the per-session DosDevices alias; if `C:` is not defined in the current session&apos;s `\??`, the walker falls through to `\GLOBAL??\C:`. `\GLOBAL??\C:` is the machine-wide DosDevices symbolic link to `\Device\HarddiskVolume*` -- the real on-disk volume object. The split matters because the per-session `\??` is where per-session drive-letter remappings (`net use X: \\server\share`, `subst Z: C:\foo`, `DefineDosDevice`) live, and the activation-context resolver class covered in Section 4.5 is the exploit family that lives at this boundary.

Several top-level directories have `Directory`-`TRAVERSE` ACLs that restrict to SYSTEM and the local Administrators group. Without traversal, the directory enumeration silently fails. `\ObjectTypes`, `\Security`, and `\Sessions\0\` are the directories users most often notice as &quot;missing&quot; when running unelevated.

By DACL plus loader-side validation. The directory grants `Directory`-`READ` to everyone but `Directory`-`WRITE` only to SYSTEM and TrustedInstaller. The `Section` objects inside are Authenticode-signed by Microsoft and validated at boot by `smss.exe`. The historical `DefineDosDevice` + `\??` symlink-plant bypass class survived until Windows 10 21H2 build 19044.1826 (July 2022), when an NTDLL patch closed it [@itm4n-the-end-of-ppldump].

`ObRegisterCallbacks` [@ms-obregistercallbacks] and `PsSetCreateProcessNotifyRoutineEx` [@ms-pssetcreateprocessnotifyroutineex] are both fully documented. The HVCI compatibility requirements, the KMCS attestation flow, and the exact policy interactions with Defender for Endpoint&apos;s tamper-protection layer are partly implementation-defined; EDR vendor engineering teams maintain private regression suites against successive Windows feature updates.

When two or more processes that don&apos;t share a session or package must coordinate over a securable directory keyed by a SID-list they agree on at design time. The boundary descriptor is the *agreement primitive*: the kernel requires every SID in the boundary to be in the caller&apos;s token. The namespace&apos;s `OBJECT_DIRECTORY` lives in `\BNOLINKS`, keyed by the alias-prefix string plus a hash of the boundary descriptor&apos;s SID-list (CreatePrivateNamespaceW [@ms-createprivatenamespacew]; Object Namespaces overview [@ms-object-namespaces]; native NtCreatePrivateNamespace [@ntdoc-ntcreateprivatenamespace] and OBJECT_BOUNDARY_DESCRIPTOR [@ntdoc-object-boundary-descriptor] signatures). From inside an AppContainer process the lookup is rewritten into the per-package subtree, so private namespaces are not a substitute for the `windows.applicationModel.*` brokered APIs when cross-package coordination is the goal.


A user-mode structure produced by `CreateBoundaryDescriptor` and populated with `AddSIDToBoundaryDescriptor` (plus the optional `CREATE_BOUNDARY_DESCRIPTOR_ADD_APPCONTAINER_SID` flag). Conceptually the descriptor is a SID-list that the caller and every other participant must share via their tokens. Kernel-side the structure is `OBJECT_BOUNDARY_DESCRIPTOR` (Version, Items, TotalSize, Flags). `NtCreatePrivateNamespace` materialises a directory in `\BNOLINKS` keyed by the `lpAliasPrefix` plus a hash of the boundary descriptor&apos;s SIDs.
&lt;h2&gt;12. Coming back to the WinObj screen&lt;/h2&gt;
&lt;p&gt;Open WinObj one more time. Navigate back to &lt;code&gt;\Sessions\1\AppContainerNamedObjects&lt;/code&gt; and pick the Edge renderer&apos;s &lt;code&gt;S-1-15-2-...&lt;/code&gt; directory. You can now name everything you are looking at. The directory is an &lt;code&gt;_OBJECT_DIRECTORY&lt;/code&gt; instance with 37 hash buckets. You reach it through a token-keyed rewrite that the kernel applies inside &lt;code&gt;ObpLookupObjectName&lt;/code&gt; before path walking begins. Its security descriptor grants &lt;code&gt;GenericAll&lt;/code&gt; only to the package SID. Every EDR loaded on this machine has registered an &lt;code&gt;ObRegisterCallbacks&lt;/code&gt; filter on &lt;code&gt;PsProcessType&lt;/code&gt;, watching for handle creations against &lt;code&gt;lsass.exe&lt;/code&gt;. If you are running on a Server SKU with Windows Server Containers, the directory might also be silo-scoped, with &lt;code&gt;Process-&amp;gt;Silo-&amp;gt;RootDirectory&lt;/code&gt; indirecting your view of the rest of &lt;code&gt;\&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The four pieces of the 1993 Cutler design have shipped without architectural change for thirty-three years. The six generations of partition primitives stacked on top are all simultaneously load-bearing on Windows 11 25H2. The namespace itself is a coordination mechanism, in Anderson 1972&apos;s sense of the reference-validation mechanism, with Saltzer-Schroeder 1975&apos;s complete-mediation principle as the design constraint it must satisfy. Containment lives in the partition layers above it: the session, the package, the integrity level, the silo, and the VTL split. Every other article in this corpus -- the Credential Guard piece, the AppContainer piece, the VBS Trustlets piece, the Hyper-V piece, the App Identity piece, the TPM piece -- quietly assumes this tree underneath them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Every Windows security boundary is a path rewrite, a per-directory ACL, a token-keyed name substitution, or a kernel callback against an &lt;code&gt;OBJECT_TYPE&lt;/code&gt;. The Object Manager is the data structure underneath them all.&lt;/p&gt;
&lt;/blockquote&gt;

**Key terms.** Object Manager (`Ob`), `OBJECT_HEADER`, `OBJECT_TYPE`, `ParseProcedure`, `OBJECT_DIRECTORY`, Lowbox token, AppContainer, Server Silo, Trustlet / IUM, Boundary descriptor, Session 0 isolation, Mandatory Integrity Control, `ObRegisterCallbacks`, KMCS, HVCI, `\BaseNamedObjects`, `\Sessions\\AppContainerNamedObjects`, `\RPC Control`, `\KnownDlls`, `\BNOLINKS`, `\GLOBAL??`, `\??`.&lt;p&gt;&lt;strong&gt;Review questions.&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Why does AppContainer isolation work even when the calling UWP app explicitly asks for &lt;code&gt;Global\X&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;What is the relationship between &lt;code&gt;IopParseDevice&lt;/code&gt;, &lt;code&gt;\Device\HarddiskVolume1&lt;/code&gt;, and &lt;code&gt;IRP_MJ_CREATE&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Which of Anderson 1972&apos;s four NEAT properties does the Object Manager achieve cleanly, and which does it provably fail?&lt;/li&gt;
&lt;li&gt;Why is &lt;code&gt;ObRegisterCallbacks&lt;/code&gt; an enforcement gate only against handle creation and duplication, not against handle use?&lt;/li&gt;
&lt;li&gt;Why does the canonical MS15-090 OM-symlink CVE point at CVE-2015-2428 [@nvd-cve-2015-2428] rather than CVE-2015-2528 or CVE-2015-1463?&lt;/li&gt;
&lt;li&gt;What is the structural difference between &lt;code&gt;\??\C:&lt;/code&gt; and &lt;code&gt;\GLOBAL??\C:&lt;/code&gt;, and which one does the HackSys / CVE-2023-35359 worked example abuse?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Recommended reading.&lt;/strong&gt; Russinovich, Ionescu, and Solomon, &lt;em&gt;Windows Internals, Part 1&lt;/em&gt; (7th edition, Microsoft Press, 2017), Chapter 8 [@microsoftpressstore-wininternals7-part1]. James Forshaw, &lt;em&gt;Windows Security Internals&lt;/em&gt; (No Starch Press, 2024), Chapter 8 [@nostarch-windows-security-internals]. Alex Ionescu, &lt;em&gt;Battle of SKM and IUM&lt;/em&gt;, Black Hat USA 2015 [@ionescu-bh2015-pdf]. The Google Project Zero blog&apos;s symlink mitigations [@p0-symlink-mitigations], arbitrary directory creation [@p0-issue1550], and who contains the containers [@p0-who-contains-containers] posts. James P. Anderson, &lt;em&gt;Computer Security Technology Planning Study&lt;/em&gt; [@csrc-nist-ande72].
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
</content:encoded><category>windows-internals</category><category>object-manager</category><category>kernel</category><category>sandbox</category><category>appcontainer</category><category>security-boundaries</category><category>edr</category><category>vbs</category><author>noreply@paragmali.com (Parag Mali)</author></item></channel></rss>