The Object Manager Namespace: The Hierarchical Filesystem Underneath Every Windows Security Boundary
A bottom-up tour of the Windows Object Manager namespace, the 1993 Cutler-era kernel data structure that every Windows security boundary quietly assumes.
Permalink1. The path that isn't a path
Open WinObj.exe as administrator on any Windows 11 25H2 machine (Windows 11 version history). For about ten seconds the screen looks like a filesystem. The root is named \. Below it sit folders called \Device, \BaseNamedObjects, \Sessions, \RPC Control, \KnownDlls, and \ObjectTypes. 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's Winternals [1], and the current build is a Microsoft-signed Sysinternals binary whose navigation surface has not been redesigned in three decades [2].
This tree is not a filesystem. There is no disk persistence; nothing under \ 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 Key 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 [4]. Its top-level directories are catalogued in the driver kit's Object Directories reference [5].
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 "subsystem implemented as part of the Windows Executive which manages Windows resources... each [resource] reside[s] in a namespace for categorization" [6].
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 lsass.exe -- is physically realised 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 OBJECT_TYPE. The boundaries you read about elsewhere are the policies; this tree is the mechanism.
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?
2. Where the namespace came from
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: "RSX-11M, VAXELN, VMS, and MICA" [7]. 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 [8]. Cutler brought a small team of DEC veterans with him.
The Object Manager is one of that team's earliest design decisions. The architectural bet was to unify every named kernel object under one filesystem-shaped tree, with each type carrying a parse procedure so a single family of syscalls (NtCreateFile, NtOpenSection, NtOpenProcess, 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's choice was, at heart, a Plan-9-style "every named resource is a filesystem path" idea, imported into a Windows shell.
Windows NT 3.1 shipped on July 27, 1993. It was "Microsoft's first 32-bit operating system," supported on IA-32, DEC Alpha, and MIPS [9]. 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 [8]. The four pieces this article will rebuild from scratch -- the OBJECT_HEADER that prefixes every object in memory, the OBJECT_TYPE singleton that owns each type's method table, the ParseProcedure that delegates path resolution to the owning subsystem, and the OBJECT_DIRECTORY hash table that maps names to objects -- were all in the NT 3.1 kernel. None of them has been rearchitected since.
That same year, Microsoft Press published Inside Windows NT, written by technical writer Helen Custer with a Foreword by Cutler himself. The book's Object Manager chapter is the canonical pre-2000 description of the namespace, cited on the Sysinternals WinObj page [2] as "Helen Custer's Inside Windows NT provides a good overview of the Object Manager namespace." Custer's book has been out of print for two decades, but the citation chain through Russinovich's tool is durable.
Three years later, in 1996, Russinovich and Cogswell co-founded Winternals and released WinObj 1.0 [1]. WinObj was the first publicly distributed tool to walk \ from user mode, using the native NtOpenDirectoryObject and NtQueryDirectoryObject syscalls that the Object Manager exposed through NTDLL [2]. The following year, Russinovich's October 1997 Windows IT Pro column "Inside the Object Manager" gave the namespace its first treatment in the trade press. The original URL did not survive changes to TechTarget'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 "Mark's October 1997 [WindowsITPro Magazine] column, 'Inside the Object Manager'."
The line of book-length internals references that began with Custer continued through Inside Windows 2000 (third edition) and the Windows Internals 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 [10]; its Chapter 8 is the current canonical reference for the Object Manager. James Forshaw's April 2024 Windows Security Internals [11] is the contemporary companion that ties the namespace into the access-check pipeline.
The 1993 design assumed a single global namespace. One process tree, one \BaseNamedObjects, one \Windows\WindowStations\WinSta0, one \?? view of DOS device letters. Everyone shared everything. Did that assumption survive the Internet?
3. The pre-Vista namespace and how it broke
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.
The most public failure was the shatter attack. In August 2002 a researcher named Chris Paget published a paper titled "Exploiting design flaws in the Win32 API for privilege escalation." Wikipedia's article on the disclosure preserves the chronology: "Shatter attacks became a topic of intense conversation in the security community in August 2002 after the publication of Chris Paget's paper" [12]. The proof-of-concept was about thirty lines. As an unprivileged interactive user, Paget sent a WM_TIMER window message to a service's hidden window in the same \Windows\WindowStations\WinSta0 (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.
Microsoft's initial response, preserved in the Wikipedia article, was that "the flaw lies in the specific, highly privileged service": 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 property of the namespace: as long as services and users shared a window station and a \BaseNamedObjects, any service that ever called a Windows API processing a message from its message queue was reachable from any logged-in user.
The third class was symbolic-link redirection. The pre-Vista Object Manager exposed two kinds of user-creatable symbolic link: object-manager symbolic links inside \?? (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.
Forshaw's 2015 Project Zero post on the symbolic-link hardening generation is the canonical taxonomy: "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" [14]. His worked example for the Internet Explorer 11 EPM sandbox is CVE-2015-0055 [15], described in the post as "an information disclosure issue in the IE EPM sandbox which abused symbolic links to bypass a security check."
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 one 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's backward-compatibility hack for legacy services that drew GUIs into Session 0. ISDS displayed a "An interactive service has requested attention" 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.That fix took five years to ship. Windows Vista RTM was released on November 8, 2006 and General Availability arrived on January 30, 2007 [16]. 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.
4. Six generations of namespace isolation
The namespace itself has not been rearchitected since 1993. What has evolved, in six discrete generations between 1993 and 2026, is the set of partition primitives 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.
Diagram source
flowchart LR
G1["Gen 1
NT 3.1, Jul 1993
Single global namespace"] --> G2
G2["Gen 2
Vista, Jan 2007 / SP1, Feb 2008
Session 0 + MIC + ObRegisterCallbacks"] --> G3
G3["Gen 3
Windows 8, Oct 2012
AppContainer / Lowbox / per-package directory"] --> G4
G4["Gen 4
Windows 10 RTM, Jul 2015
VBS / IUM secure-kernel namespace"] --> G5
G5["Gen 5
Windows Server 2016, Oct 2016
Server Silos / silo-scoped views"] --> G6
G6["Gen 6
MS15-090, Aug 2015 ->
symbolic-link class hardening"] 4.1 Generation 2 -- Session 0 isolation, integrity levels, ObRegisterCallbacks
Vista shipped three mechanisms in one release window because the structural failure had three faces.
The first was Session 0 isolation. From Vista forward, services run in Session 0 alone; the first interactive logon starts at Session 1. Each session gets its own subtree at \Sessions\<n>\BaseNamedObjects, \Sessions\<n>\Windows\WindowStations, and \Sessions\<n>\DosDevices. The Win32 Local\ prefix routes through kernel32!BaseGetNamedObjectDirectory into the per-session BNO; Global\ routes into the shared \BaseNamedObjects [17]. The Wikipedia Shatter article preserves the architectural fix verbatim: "Local user logins were moved from Session 0 to Session 1, thus separating the user's processes from system services that could be vulnerable" [12]. After Vista an interactive user could no longer SendMessage(WM_TIMER) into a service's hidden window because the user and the service no longer shared a window station.
The second mechanism was Mandatory Integrity Control. Vista introduced a new ACE type, SYSTEM_MANDATORY_LABEL_ACE, attached to every object'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's level against the object's level after path resolution succeeds [18]. MIC is not a namespace partition. A Low-IL process and a Medium-IL process resolve the same \BaseNamedObjects directory; only the open is denied at the leaf. The structural property MIC adds is that the leaf check is unbypassable from user mode; the check fires regardless of which DACL the object carries.
The third mechanism was ObRegisterCallbacks. Microsoft's wdm.h documentation records the API's first ship date verbatim: "Available starting with Windows Vista with Service Pack 1 (SP1) and Windows Server 2008" [19]. The API lets a KMCS-signed driver intercept handle creation and handle duplication on PsProcessType, PsThreadType, and the desktop object type. The registration carries an Altitude (a FltMgr-style collision key) and an array of OB_OPERATION_REGISTRATION records [20]. Pre-operation callbacks can strip access-mask bits before the handle is granted; post-operation callbacks fire for logging. The parallel API PsSetCreateProcessNotifyRoutineEx [21] 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 lsass.exe.
4.2 Generation 3 -- AppContainer and the lowbox token
Windows 8 shipped on October 26, 2012 [22]. Modern / UWP apps downloaded from the Microsoft Store needed a sandbox finer-grained than per-session BNO. The Vista path rewriting in kernel32!BaseGetNamedObjectDirectory 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.
Each UWP / MSIX process runs under a special token type, the AppContainer / LowBox token (referred to in kernel code as the lowbox token), created by NtCreateLowBoxToken. The token carries a TOKEN_APPCONTAINER_INFORMATION block that names the process's package SID (S-1-15-2-...) and an AppContainerNumber. Inside ObpLookupObjectName, before the path is walked, the kernel checks whether the caller's token is a lowbox token; if it is, lookups of \BaseNamedObjects\X, \RPC Control\X, and other rewriteable paths get redirected into \Sessions\<n>\AppContainerNamedObjects\<package-sid>\X. 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's wording is precise: AppContainer works by "sandboxing the application kernel objects, the AppContainer environment prevents the application from influencing, or being influenced by, other application processes" [3].
"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's AppContainerNamedObjects object directory to support redirecting BaseNamedObjects and RPC endpoints by the OS." -- James Forshaw, Project Zero Issue 1550 [23]
The residual class the AppContainer model has not closed is the one Forshaw's August 30, 2018 Project Zero post [23] documents: because the SYSTEM-side AppInfo service has to write into the user'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 -- "SYSTEM-privileged directory creation in user-controllable territory" -- is the worked example of why "the kernel rewrites the name" is an isolation property only when the SYSTEM helpers also use the rewrite.
4.3 Generation 4 -- VBS trustlets and the IUM secure-kernel namespace
Windows 10 RTM shipped on July 29, 2015 [24]. 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 (securekernel.exe) maintains its own root, its own type registry, and its own handle-table machinery. The VTL0 NT kernel can see trustlet processes -- the per-trustlet user-mode containers running in Isolated User Mode (IUM) -- but it cannot reach into their secure-side state.
Alex Ionescu's Black Hat USA 2015 talk Battle of SKM and IUM [25] 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.
4.4 Generation 5 -- Server Silos and the silo-scoped namespace
Windows Server 2016 shipped on October 12, 2016 [26]. 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 Server Silo: a new OBJECT_TYPE registered alongside Job, Process, and Thread, that carries its own RootDirectory, DosDevicesDirectory, and ServerSiloGlobals. A process attached to a silo via PsAttachSiloToCurrentThread sees the silo's namespace as its root; the silo's \GLOBAL??\C: resolves to the silo's \Device\HarddiskVolume*, which is a different Device object from the host's. Job objects [27] provide the cgroups-equivalent resource-accounting dimension; the Silo type builds on top.
The canonical reverse-engineering reference is Daniel Prizmant's July 2020 Unit 42 writeup, which spells out the architecture: "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" [28].
The companion piece, Prizmant's June 2021 Siloscape [29], is the first known malware family that escapes the silo boundary: Prizmant named the malware "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." James Forshaw's April 2021 Project Zero post Who Contains the Containers? [30] is the four-LPE companion disclosure. Microsoft'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's silo, is the security-boundary product.
4.5 Generation 6 -- the symbolic-link hardening continuum
The cross-cutting hardening generation closes the symlink subclass that recurred in Generations 1, 3, and 5. MS15-090 shipped on August 11, 2015 [31] and "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." The bulletin's canonical Object Manager CVE is CVE-2015-2428 [32], described verbatim as the case where the "Object Manager in Microsoft Windows... does not properly constrain impersonation levels during interaction with object symbolic links that originated in a sandboxed process." Subsequent Windows 10 builds added OBJ_DONT_REPARSE, an open-time flag that disables symbolic-link substitution for callers willing to opt in, and post-Siloscape patches in 2021 closed NtSetInformationSymbolicLink retargeting from inside a silo.
The residual subclass MS15-090 did not close was the per-session \?? DosDevices remapping path under impersonation. A low-privileged process whose token is impersonated by a SYSTEM service can plant a DefineDosDevice remapping that survives into the impersonation-time \?? 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's Activation Context Hell -- DosDevices Remapping Attack under Impersonation [36], which targets the CSRSS / SxS activation-context resolver and shipped as CVE-2023-35359 [37], with the closely-related CVE-2022-22047 [38] covering the underlying CSRSS surface. The mitigation has to live inside the impersonation-aware \?? resolver in the SYSTEM caller, not at the symlink-creation gate.
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?
5. The four load-bearing primitives
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 OBJECT_HEADER that prefixes every named object in memory, an OBJECT_TYPE singleton that owns each type's method table, a ParseProcedure that delegates path resolution to the owning subsystem when needed, and an OBJECT_DIRECTORY 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.
Diagram source
flowchart TB
OD["OBJECT_DIRECTORY
(37-bucket hash table)"] -->|"hash(name) % 37"| OH
OH["OBJECT_HEADER
(PointerCount, HandleCount,
TypeIndex, InfoMask,
SecurityDescriptor, Body offset)"] -->|"TypeIndex XOR
ObHeaderCookie"| OT
OT["OBJECT_TYPE singleton
(in nt!ObTypeIndexTable)"] -->|"TypeInfo"| TI
TI["TYPE_INFO method table
(Dump, Open, Close, Delete,
ParseProcedure,
Security, QueryName, ...)"]
OH -->|"Body[]"| BODY["Type-specific body
(EPROCESS, FILE_OBJECT,
SECTION_OBJECT, ...)"] 5.1 OBJECT_HEADER
Every named kernel object lives in non-paged pool. Immediately before each object's typed body sits an OBJECT_HEADER, a 0x30-byte (48-byte on x64) structure that the Object Manager owns. PointerCount and HandleCount are the two reference counts: the former tracks raw kernel-mode pointer references, the latter tracks user-mode handles. TypeIndex is a single byte that indexes into the nt!ObTypeIndexTable to find the object's type singleton; since Windows 10 1709, the byte is XOR-obfuscated against the per-boot nt!ObHeaderCookie so that simple type confusion is non-trivial.
InfoMask is a bitmap of optional sub-headers that may precede the main header: OBJECT_HEADER_NAME_INFO for named objects, OBJECT_HEADER_QUOTA_INFO for objects that charge a quota block, OBJECT_HEADER_HANDLE_INFO for objects that need per-process handle accounting. SecurityDescriptor is a tagged pointer to the object's DACL/SACL. Body[] is the offset at which the type-specific payload begins; for a process object that payload is an EPROCESS, for a file it is a FILE_OBJECT, and so on. The canonical reference is Chapter 8 of Windows Internals 7th Edition Part 1 [10].
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[].
TypeIndex XOR-with-cookie is one of the smallest kernel hardening changes Microsoft has shipped: a single byte that prevents a poisoned OBJECT_HEADER from naming an arbitrary type after a heap-corruption primitive. The cookie is per-boot and lives in nt!ObHeaderCookie. The hardening is documented in Windows Internals 7th Edition Chapter 8 [10] and in Geoff Chappell's reverse-engineering studies; Microsoft has not, as of 2026, published a Learn-hosted reference for the cookie itself.
5.2 OBJECT_TYPE
OBJECT_TYPE is the per-type singleton. There is exactly one OBJECT_TYPE per registered kernel type, and they live in \ObjectTypes. On Windows 11 25H2 the count sits at roughly seventy-five: Type, Directory, SymbolicLink, Token, Job, Process, Thread, Section, Key, File, Event, Mutant, Semaphore, Timer, WindowStation, Desktop, Device, Driver, IoCompletion, ALPC Port, EtwRegistration, Silo, and dozens more.
The per-type singleton (nt!_OBJECT_TYPE) that owns each kernel type'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.
The TypeInfo field on each OBJECT_TYPE carries eight procedure pointers and one offset field (WaitObjectFlagOffset). The most consequential is the ParseProcedure. When ObpLookupObjectName is walking a path component-by-component, and a step lands on an object whose OBJECT_TYPE defines a ParseProcedure, the OM hands the residual path and the desired access to that procedure, which becomes the namespace authority below that point. That is how the registry's Key type, the I/O Manager's Device 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 [6].
5.3 The parse procedure
ObpLookupObjectName walks \Foo\Bar\Baz\...\Leaf left-to-right. At each component the walker does one of three things. The common case is a hash-table lookup in the current OBJECT_DIRECTORY's 37 buckets to find the child object by name. The second case is SymbolicLink substitution: if the child object's type is SymbolicLink, the walker substitutes the link target and re-enters the walk at the substitution. The third and most consequential case is parse-procedure handoff. If the child object's OBJECT_TYPE has a non-null ParseProcedure, the walker stops, hands the residual path string to that procedure, and lets it decide what to do.
The load-bearing method pointer on each OBJECT_TYPE'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's owning driver via IRP_MJ_CREATE) and CmpParseKey (for the Key type, which walks the registry hive).
IopParseDevice is the parse procedure for the Device type. When the walker reaches \Device\HarddiskVolume1 and is asked to continue with \Users\me\file.txt, the I/O Manager builds an IRP_MJ_CREATE 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 FILE_OBJECT, which the Object Manager packages into a handle.
CmpParseKey is the parse procedure for the Key type. When the walker reaches \REGISTRY and is asked to continue with \MACHINE\Software\Microsoft\Windows, the Configuration Manager takes over and walks the in-memory hive structures.
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 FILE_OBJECT or KEY bodies back to the OM.
Diagram source
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->>OM: NtCreateFile("??\C:\Users\me\file.txt")
OM->>OM: rewrite ??\ -> \Sessions<n>\DosDevices
OM->>Dir: lookup "C:"
Dir-->>OM: SymbolicLink -> \Device\HarddiskVolume1
OM->>OM: substitute, re-enter walk
OM->>Dev: lookup \Device\HarddiskVolume1
Dev-->>OM: type=Device, has ParseProcedure
OM->>Drv: IopParseDevice with "\Users\me\file.txt"
Drv->>Drv: IRP_MJ_CREATE: walk MFT, find file
Drv-->>OM: FILE_OBJECT
OM-->>User: HANDLE 5.4 The 37-bucket directory hash
OBJECT_DIRECTORY is a 37-bucket open-hash table. The hash function is RtlHashUnicodeString, 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 Windows Internals 7th Edition Part 1 and in Forshaw's Windows Security Internals Chapter 8; Microsoft has never published a Learn-hosted spec for the constant [11].
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.
\AppContainerNamedObjects\<package-sid>\ subtree, average bucket chains run several entries deep. Collision pressure on the constant is the open problem returned to in Section 9.
5.5 The lowbox redirect inside ObpLookupObjectName
This is the subsection that earns the second aha moment of the article.
When the calling thread's primary token is a lowbox token, ObpLookupObjectName consults the token's AppContainerNumber and package SID before it begins the walk. Lookups that would otherwise resolve into \BaseNamedObjects or \RPC Control are rewritten into \Sessions\<n>\AppContainerNamedObjects\<package-sid>\. The rewrite happens transparently to the user-mode Win32 caller, which still thinks it asked for \BaseNamedObjects\X.
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\<n>\AppContainerNamedObjects\<package-sid>\ 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 "Sandboxing the application kernel objects, the AppContainer environment prevents the application from influencing, or being influenced by, other application processes" [3].
The aha moment is structural. AppContainer is not a containment mechanism the way you might first picture it. It is a name-translation 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's path-walk function. The Edge renderer process cannot name \BaseNamedObjects\GlobalEvent_Foo because the kernel rewrites that name into \Sessions\1\AppContainerNamedObjects\S-1-15-2-...\Global\GlobalEvent_Foo before lookup even begins. The "sandbox" is a hash-table redirect.
5.6 The Silo OBJECT_TYPE and silo-scoped views
Silo is itself a registered OBJECT_TYPE. Each silo instance carries a silo-scoped RootDirectory, DosDevicesDirectory, and ServerSiloGlobals (with the silo's own registry-hive root and per-silo BaseNamedObjects root). PsAttachSiloToCurrentThread switches the thread's namespace view; once attached, every Object Manager lookup runs through the silo's roots instead of the host's. Job objects, which provide the cgroups-equivalent resource-accounting substrate, are the underlying primitive the Silo type extends [27]. The structural design history is in Prizmant's reverse-engineering writeup [28].
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's namespace as its root; the silo's \GLOBAL??\C: resolves to the silo's \Device\HarddiskVolume*, which is a different Device object from the host's. Server Silo is the substrate underneath Windows Server Containers and WSL1.
5.7 The Secure Kernel's parallel namespace
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's BH2015 inventory (Trustlet IDs 0 through 3 at ship, growing in subsequent releases) is the canonical primary [25].
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.
5.8 The handle table
The namespace is the name side; the per-process HANDLE_TABLE is the access 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 OBJECT_HEADER, and operates on the body. This is why ObRegisterCallbacks fires on handle creation and duplication 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.
Now you know the data structure. But what does the actual tree look like in 2026? What does \ contain on a Windows 11 25H2 box, and which security boundary lives in each top-level directory?
6. The 2026 top-level directory atlas
Open WinObj.exe as administrator on a Windows 11 25H2 machine and the root directory at \ 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's Object Directories reference [5] is Microsoft's canonical inventory.
| Top-level directory | What it contains | Which boundary it enforces | Exploit class |
|---|---|---|---|
\ObjectTypes | The ~75 OBJECT_TYPE singletons (Process, Thread, Section, Key, File, Token, Job, Silo, etc.) | Meta -- the type registry the rest of the namespace depends on | Type confusion (mitigated by ObHeaderCookie since Windows 10 1709) |
\Device | Driver-published device objects (\Device\HarddiskVolume*, \Device\Tcp, \Device\Tpm, \Device\NamedPipe, \Device\Mailslot, \Device\Vmbus, \Device\KsecDD, \Device\CNG) | The I/O Manager's surface; each driver's parse procedure consumes residual paths | Bait-and-switch on \Device (a low-privilege user redirects a privileged opener through a planted symbolic link) |
\Driver, \FileSystem | Loaded DRIVER_OBJECT registries | KMCS / HVCI driver-load gate | Vulnerable signed-driver class (BYOVD) |
\GLOBAL?? | The machine-wide DosDevices view -- where C: and D: are symlinks to \Device\HarddiskVolume* | Cross-session drive-letter map | Symlink redirect across session boundary |
\?? | The per-session DosDevices alias, falling through to \GLOBAL?? | Session-scoped drive-letter map | The HackSys / CVE-2023-35359 worked example: a low-privilege caller plants a DefineDosDevice remapping that survives into the impersonation-time \?? view, and the SYSTEM-side activation-context resolver opens the redirected path |
\BaseNamedObjects | The global / Global\-prefixed-only BNO | Cross-session named-object visibility | Pre-Vista squatting class (closed by Generation 2) |
\Sessions\<n>\ | Per-session subtrees (BNO, DosDevices, WindowStations, AppContainerNamedObjects) | Session boundary (Generation 2) | Shatter attacks (closed by Generation 2) |
\Sessions\<n>\AppContainerNamedObjects\<package-sid>\ | Per-package UWP / MSIX lowbox namespace | AppContainer / lowbox boundary (Generation 3) | Forshaw P0 Issue 1550 arbitrary-directory creation race |
\RPC Control | Every named LRPC ALPC port (every COM call lands here) | RPC endpoint visibility | Endpoint squatting against named LRPC ports |
\KnownDlls, \KnownDlls32 | Pre-mapped Section objects for system DLLs | Loader supply-chain | DefineDosDevice + \?? symlink-plant trick (closed in NTDLL July 2022, build 19044.1826) |
\KernelObjects | System-defined events (LowMemoryCondition, HighMemoryCondition, etc.) | Kernel-internal visibility | None public |
\Callback | System-defined Callback objects (ExCallback slots drivers register against) | Kernel API extension surface | Driver-callback abuse |
\Security | LSA-private endpoints | LSA / authentication isolation | Credential-theft (the LSAISO trustlet via Generation 4) |
\Windows | BNO-redirect surface and SharedSection | Win32 subsystem shared state | Cross-session Win32 state leakage |
\Silos\<id> | Per-container silo subroots on Server SKUs | Server Silo boundary (Generation 5) | Siloscape -- symlink retarget out of the silo |
\BNOLINKS | The boundary-keyed private-namespace index | CreatePrivateNamespace cross-session/cross-package IPC | None public; the directory itself is RE-derived |
Diagram source
flowchart LR
subgraph EdgeRenderer["Microsoft Edge Renderer (lowbox token)"]
K32["CreateMutexW(L'Global\Foo')"]
end
K32 -->|"NtCreateMutant, OBJECT_ATTRIBUTES"| OB
subgraph KernelOb["ObpLookupObjectName"]
OB["Read caller token
token.AppContainerNumber
token.PackageSid"]
OB -->|"rewrite name"| RW["Rewrite '\BaseNamedObjects\Global\Foo'
to
'\Sessions\1\AppContainerNamedObjects\
S-1-15-2-...\Global\Foo'"]
RW --> WALK["walk the rewritten path"]
end
WALK --> Dir["\Sessions\1\AppContainerNamedObjects\
S-1-15-2-...\Global\
(per-package OBJECT_DIRECTORY,
DACL allows only package SID)"] The \BNOLINKS directory deserves a separate paragraph because it is not on Microsoft Learn. NtCreatePrivateNamespace is the kernel-side syscall behind the Win32 CreatePrivateNamespace API [39]; the caller passes a boundary descriptor built by CreateBoundaryDescriptor [40] plus one or more SIDs added via AddSIDToBoundaryDescriptor [41]. The kernel materialises one \BNOLINKS entry per (alias_prefix, boundary_descriptor_hash) tuple; two callers that pass the same lpAliasPrefix but different boundary descriptors land on different directories. The native signature is documented in the PHNT-derived NtDoc mirror [42], and the OBJECT_BOUNDARY_DESCRIPTOR structure layout is at ntdoc.m417z.com/object_boundary_descriptor [43]. The Win32 Object Namespaces overview [44] is Microsoft's only published user-mode reference; the \BNOLINKS directory name itself is reverse-engineering-derived.
\BNOLINKS directory is documented only through reverse engineering of ntoskrnl.exe -- via Forshaw's NtObjectManager and System Informer's PHNT headers -- not on Microsoft Learn. The user-mode API surface (CreatePrivateNamespace, CreateBoundaryDescriptor, AddSIDToBoundaryDescriptor) is fully documented. The provenance gap is worth flagging when you cite the directory by name.
The \KnownDlls 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's August 2018 P0 post first sketched a DefineDosDevice + \?? symlink-plant chain that could land a forged Section object into \KnownDlls; Clement Labro (itm4n) implemented the attack as the PPLdump tool [45] and wrote companion posts on both itm4n.github.io [46] and the SCRT team blog [47]. The class was closed in NTDLL by Windows 10 21H2 build 19044.1826; itm4n confirms the patch in The End of PPLdump [48]: "A patch in NTDLL now prevents PPLs from loading Known DLLs."
const MAX_DIRECTORY_BUCKETS = 37;
function rtlHashUnicodeString(name) {
let h = 0;
for (const ch of name.toUpperCase()) {
h = (h * 31 + ch.charCodeAt(0)) >>> 0;
}
return h % MAX_DIRECTORY_BUCKETS;
}
function makeDir() {
return { buckets: Array(MAX_DIRECTORY_BUCKETS).fill(null).map(() => []) };
}
function addChild(dir, name, child) {
dir.buckets[rtlHashUnicodeString(name)].push({ name, child });
}
function lookupObjectName(path, root) {
const components = path.split('\\').filter(Boolean);
let cursor = root;
for (const comp of components) {
const bucket = rtlHashUnicodeString(comp);
const chain = cursor.buckets[bucket];
const hit = chain.find(e => e.name.toUpperCase() === comp.toUpperCase());
console.log(`lookup '${comp}' -> bucket ${bucket}, chain length ${chain.length}, ${hit ? 'HIT' : 'MISS'}`);
if (!hit) return null;
if (hit.child.parseProcedure) {
const rest = '\\' + components.slice(components.indexOf(comp) + 1).join('\\');
console.log(` parse-procedure handoff for type '${hit.child.type}', residual='${rest}'`);
return { handedOff: hit.child, residual: rest };
}
cursor = hit.child;
}
return cursor;
}
const root = makeDir();
const device = makeDir();
device.parseProcedure = true; device.type = 'Device';
const sessions = makeDir();
addChild(root, 'Device', device);
addChild(root, 'Sessions', sessions);
addChild(root, 'BaseNamedObjects', makeDir());
lookupObjectName('\\Device\\HarddiskVolume1\\Users\\me\\file.txt', root); Press Run to execute.
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?
7. How other kernels name kernel objects
Three kernels, three different bets. Linux took the namespace and split it into per-resource-class clones -- 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 Mach port-right namespace and let launchd broker named-service lookups. Plan 9 from Bell Labs was the academic ancestor of "every named OS resource is a filesystem path," and the design Cutler imported into NT.
7.1 Linux: per-resource namespaces
Linux ships eight namespace types, each governed by a CLONE_NEW* flag passed to clone(), unshare(), or setns(): mount, PID, network, IPC, user, UTS, cgroup, and time. The namespaces(7) man page is precise: "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" [49]. Docker, containerd, runc, Kubernetes pods, LXC, and systemd-nspawn all compose these eight flags into a Linux container.
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's network namespace, all at once. The weakness is the absence of a unified type registry: Linux has no equivalent of \ObjectTypes, no equivalent of the OBJECT_HEADER 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.
7.2 macOS / Darwin: Mach ports and the bootstrap server
Darwin's kernel-object naming is capability-style. Apple's archive documentation describes the model directly: "each task consists of a virtual address space, a port right namespace, and one or more threads" [50]. Tasks send messages by holding a port right -- 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 launchd operates as the bootstrap-server name broker for services that need a stable rendezvous. A separate I/O Registry tree carries device objects.
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 launchd, and the kernel itself has no equivalent of \BaseNamedObjects as a one-stop shop.
7.3 Plan 9 from Bell Labs
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'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.
| Primitive | Granularity | Enforcement point | Structural / opt-in | Bypass by privilege | Inheritance gap |
|---|---|---|---|---|---|
| Per-Session (NT) | Logon session | ObpLookupObjectName + DACL | Structural | SeDebugPrivilege short-circuit | Inherited handles cross sessions |
| AppContainer Lowbox (NT) | Package SID | ObpLookupObjectName rewrite | Structural | TCB privileges only | Brokered handles enter |
| Server Silo (NT) | Container | Process->Silo indirection | Structural | KMCS-signed driver | Host handles cross silos |
| VBS / IUM Trustlet (NT) | Trust level (VTL) | Hypervisor | Structural | Hypervisor compromise | Cross-VTL ALPC only |
| Mandatory Integrity Control (NT) | IL band | SeAccessCheckByType | Opt-in (per-object SACL) | SeRelabelPrivilege | Inherited handles bypass |
ObRegisterCallbacks (NT) | Per-type, per-driver | Object Manager pre-op callback | Mediation, not partition | KMCS-signed driver | Inheritance bypasses callback |
| Private Namespace (NT) | Boundary SID-list | NtCreatePrivateNamespace | Structural | All SIDs in caller's token | Boundary-keyed |
| Linux Namespace | Per-resource clone | setns/unshare/clone | Structural | CAP_SYS_ADMIN | Fork inherits namespace set |
| Mach Port Right | Per-task | Capability check on send | Structural (capabilities) | host_priv / kernel | Inherited rights on fork |
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?
8. What the namespace cannot do
The frame for this section comes from James P. Anderson's 1972 USAF technical report Computer Security Technology Planning Study (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 "Non-bypassable... Evaluable... Always invoked... Tamper-proof," and "according to Ross Anderson, the reference monitor concept was introduced by James Anderson in an influential 1972 paper" [54]. The NIST CSRC mirror hosts the original PDF [55].
Saltzer and Schroeder's 1975 paper The Protection of Information in Computer Systems [56] added the complete-mediation principle -- "every access to every object must be checked for authority" -- 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).
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.
8.1 Always-invoked: provably gapped
The namespace achieves always-invoked for name-based opens. Every Nt*OpenObject* syscall walks ObpLookupObjectName; 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 handle inheritance. A child process inherits handles from CreateProcess(bInheritHandles=TRUE) without going through the OM at all. The handles already exist in the parent's HANDLE_TABLE; the kernel walks the parent's table, duplicates the entries into the child's table, and the child has live access. No name-lookup, no ObRegisterCallbacks 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 sole reference monitor.
8.2 Tamper-proof: bounded, not absolute
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.
8.3 Evaluable: provably above threshold
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) [57]. The Object Manager subsystem, the Security Reference Monitor, and the parse procedures the Object Manager delegates to (file-system drivers via IopParseDevice; the registry via CmpParseKey; ALPC; the I/O manager itself) collectively comprise tens of thousands of lines of C, putting the TCB for "open a named object" at one to two orders of magnitude above the verification threshold any current proof system can handle. The Object Manager is not evaluable in the formal sense Anderson required.
8.4 Non-bypassable: the privilege short-circuit
A process holding SeDebugPrivilege (or any privilege that grants PROCESS_VM_* rights) can short-circuit per-directory ACLs. The privilege evaluation happens at SeAccessCheck time, after ObpLookupObjectName 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 SeDebugPrivilege. This is by design -- you want a debugger to be able to attach to anything -- but it is also the structural reason why "lock down the namespace" is not by itself a containment story.
8.5 What else the namespace cannot do
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 OBJ_DONT_REPARSE on object-attributes, FILE_FLAG_OPEN_REPARSE_POINT on file opens, or otherwise instruct the path-walker to refuse symbolic-link substitution -- not a structural property of the namespace.
8.6 The honest accounting
The Object Manager namespace is a coordination mechanism, not a containment mechanism. Containment is in the layers above: the session ID, the package SID, the integrity level, the silo ID, the VTL split. The namespace's job is to make those layers enforceable by partitioning the path space so the bad open cannot resolve to the privileged object's name. The layers above decide which partition the caller is in; the namespace's only job is "given a path and a caller, find the object." Anderson 1972 names the kernel mechanism (the reference-validation mechanism with NEAT properties); Saltzer-Schroeder 1975 names the design principles the mechanism must satisfy. The Object Manager is the Windows realisation; it inherits both the strengths and the limits.
The namespace is a coordination mechanism, not a containment mechanism. The containment is in the layers above.
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's only job is "given a path and a caller, find the object." 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
OBJECT_TYPE.
The provable gaps are real. What is the active research direction in 2026 -- where do attackers and defenders actually meet inside the namespace today?
9. Open problems in 2026
Five open problems sit in active research as of 2026.
9.1 Hash-bucket collision pressure
The 37-bucket constant has not changed since 1993. On a 2026 Windows 11 25H2 machine with several hundred MSIX packages, each owning an \AppContainerNamedObjects\<package-sid>\ subtree, average chain lengths inside \Sessions\1\AppContainerNamedObjects 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 ObpLookupObjectName. Microsoft has not committed to a larger table or a different structure; the constant remains.
9.2 Cross-AppContainer object-directory privacy
Per-AppContainer isolation is the AppContainer model's promise; residual cross-package reads erode it. Forshaw'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 [13] is still rediscovered every year.
9.3 Silo-escape via routines that ignore silo attachment
Siloscape (June 7, 2021) showed that NtSetInformationSymbolicLink could retarget a silo-scoped symbolic link at a host-scoped path. Microsoft patched the specific function; the class -- kernel routines whose path resolution does not honour Process->Silo->RootDirectory -- remains open. Microsoft'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.
9.4 ObRegisterCallbacks erosion under HVCI
ObRegisterCallbacks 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'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's Defender for Endpoint and EDR-on-Linux eBPF projects point at alternative-mediation futures, but in-kernel ObRegisterCallbacks is still the primary credential-theft sensor.
9.5 Public benchmark vacuum
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.
Five open problems is a research agenda, not a how-to. How do you actually look at this thing on your own machine?
10. Reading the namespace from a live system
Three tools cover the operational practice: Sysinternals WinObj, Forshaw's NtObjectManager PowerShell module, and WinDbg in kernel mode.
10.1 WinObj on a live system
Download winobj.exe from Sysinternals [2] 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 \Sessions\1\BaseNamedObjects and read off the named events and mutants every Win32 app in your interactive session has created. Navigate to \Sessions\1\AppContainerNamedObjects and pick an S-1-15-2-... 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.
10.2 NtObjectManager PowerShell
NtObjectManager is Forshaw's PowerShell module that exposes the Object Manager namespace through cmdlets (PowerShell Gallery [58]; GitHub [59]). Install with Install-Module NtObjectManager. Useful commands: Get-ChildItem NtObject:\ walks the root; Get-NtType lists the registered OBJECT_TYPE singletons; Get-NtObject \BaseNamedObjects enumerates the global BNO; Get-NtAlpcPort '\RPC Control' 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.
10.3 WinDbg kernel session
In a kernel-mode WinDbg session attached to a target machine (or to a live local kernel via Microsoft's local-kernel debug mode), !object \ dumps the root directory and its children. dt nt!_OBJECT_HEADER <addr>-30 reads the header preceding any object's body (the offset 0x30 is the size of OBJECT_HEADER on x64; subtract that from the body pointer to land on the header -- the field layout is documented in Windows Internals 7th Edition Chapter 8, Microsoft Press Store [10]). dx -r1 ((nt!_OBJECT_TYPE*)nt!PsProcessType[0]).TypeInfo walks the Process type's method table and lists all eight procedure pointers and the WaitObjectFlagOffset, including the parse procedure.
10.4 The EDR primitive: an ObRegisterCallbacks driver template
The minimal sketch of an in-kernel EDR sensor is four steps. Register an OB_CALLBACK_REGISTRATION for PsProcessType with OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE [19]. In the pre-operation callback, examine OperationInformation->Object, derive the target process's PID, and compare it against lsass.exe. If it matches, strip credential-relevant access bits from OperationInformation->Parameters->CreateHandleInformation.DesiredAccess (or duplicate-handle equivalent). The kernel grants the handle with the reduced rights, the attacker's PROCESS_VM_READ is gone before the call returns, and the post-operation callback logs the attempt. The parallel API PsSetCreateProcessNotifyRoutineEx [21] covers process creation, which is the other half of the EDR sensor surface.
Diagram source
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->>NT: NtOpenProcess(lsass PID, PROCESS_VM_READ | PROCESS_QUERY_INFORMATION)
NT->>OM: lookup PsProcessType, target by PID
OM->>EDR: fire pre-op callback (handle create)
EDR->>EDR: target == lsass.exe?
EDR->>EDR: strip PROCESS_VM_READ from DesiredAccess
EDR-->>OM: granted = PROCESS_QUERY_LIMITED_INFORMATION
OM-->>NT: HANDLE with reduced access
NT-->>A: open succeeded (but useless rights) 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;
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 & ~STRIPPED;
}
const desired = PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | PROCESS_DUP_HANDLE;
console.log('attacker asked for:', '0x' + desired.toString(16));
const granted = stripForLsass(desired) | PROCESS_QUERY_LIMITED_INFORMATION;
console.log('EDR pre-op granted:', '0x' + granted.toString(16)); Press Run to execute.
What this looks like in real driver code (skeleton)
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"123456"),
.OperationRegistration = &op,
};
ObRegisterCallbacks(®, &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.
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?
11. Frequently asked questions
Frequently asked questions
Is the Object Manager namespace the same as the registry?
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.
Why is \BaseNamedObjects not just \Sessions\1\BaseNamedObjects on Windows 11?
Because \BaseNamedObjects is the global / Global\-prefixed-only view, distinct from the per-session BNO at \Sessions\<n>\BaseNamedObjects. The Win32 Local\ prefix routes through kernel32!BaseGetNamedObjectDirectory into the per-session BNO; Global\ routes into the global one [17]. Cross-session named-object coordination still needs the global view; per-session isolation lives in the per-session subtree.
Why can a UWP app call CreateMutex('Global\\Foo') and not collide with another app?
Because the lowbox token attached to the UWP app's process tells ObpLookupObjectName to rewrite the path to \Sessions\<n>\AppContainerNamedObjects\<package-sid>\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.
What is the difference between \??\C: and \GLOBAL??\C:?
\??\C: is the per-session DosDevices alias; if C: is not defined in the current session'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.
Why does WinObj need administrator rights?
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 "missing" when running unelevated.
How is \KnownDlls enforced?
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 [48].
Are the EDR kernel callbacks documented?
ObRegisterCallbacks [19] and PsSetCreateProcessNotifyRoutineEx [21] are both fully documented. The HVCI compatibility requirements, the KMCS attestation flow, and the exact policy interactions with Defender for Endpoint's tamper-protection layer are partly implementation-defined; EDR vendor engineering teams maintain private regression suites against successive Windows feature updates.
When should I use CreatePrivateNamespace instead of \Global\ BNO plus a DACL?
When two or more processes that don'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's token. The namespace's OBJECT_DIRECTORY lives in \BNOLINKS, keyed by the alias-prefix string plus a hash of the boundary descriptor's SID-list (CreatePrivateNamespaceW [39]; Object Namespaces overview [44]; native NtCreatePrivateNamespace [42] and OBJECT_BOUNDARY_DESCRIPTOR [43] 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's SIDs.
12. Coming back to the WinObj screen
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's sense of the reference-validation mechanism, with Saltzer-Schroeder 1975'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.
Every Windows security boundary is a path rewrite, a per-directory ACL, a token-keyed name substitution, or a kernel callback against an
OBJECT_TYPE. The Object Manager is the data structure underneath them all.
References
- Mark Russinovich. Wikipedia. https://en.wikipedia.org/wiki/Mark_Russinovich ↩
- WinObj. Microsoft Learn / Sysinternals. https://learn.microsoft.com/en-us/sysinternals/downloads/winobj ↩
- AppContainer Isolation. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/secauthz/appcontainer-isolation ↩
- Managing Kernel Objects. Microsoft Learn. https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/managing-kernel-objects ↩
- Object Directories. Microsoft Learn. https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/object-directories ↩
- Object Manager. Wikipedia. https://en.wikipedia.org/wiki/Object_Manager ↩
- Dave Cutler. Wikipedia. https://en.wikipedia.org/wiki/Dave_Cutler ↩
- Architecture of Windows NT. Wikipedia. https://en.wikipedia.org/wiki/Architecture_of_Windows_NT ↩
- Windows NT 3.1. Wikipedia. https://en.wikipedia.org/wiki/Windows_NT_3.1 ↩
- (2017). Windows Internals, Part 1: System Architecture, Processes, Threads, Memory Management, and More (7th Edition). Microsoft Press. https://www.microsoftpressstore.com/store/windows-internals-part-1-system-architecture-processes-9780735684188 ↩
- (2024). Windows Security Internals. No Starch Press. https://nostarch.com/windows-security-internals ↩
- Shatter attack. Wikipedia. https://en.wikipedia.org/wiki/Shatter_attack ↩
- (2017). Named Pipe Secure Prefixes. tiraniddo.dev. https://www.tiraniddo.dev/2017/11/named-pipe-secure-prefixes.html ↩
- (2015). Windows 10hh Symbolic Link Mitigations. Google Project Zero. https://projectzero.google/2015/08/windows-10hh-symbolic-link-mitigations.html ↩
- (2015). CVE-2015-0055 -- Internet Explorer Elevation of Privilege. NIST National Vulnerability Database. https://nvd.nist.gov/vuln/detail/CVE-2015-0055 ↩
- Windows Vista. Wikipedia. https://en.wikipedia.org/wiki/Windows_Vista ↩
- Kernel Object Namespaces. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/termserv/kernel-object-namespaces ↩
- Mandatory Integrity Control. Wikipedia. https://en.wikipedia.org/wiki/Mandatory_Integrity_Control ↩
- ObRegisterCallbacks function (wdm.h). Microsoft Learn. https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-obregistercallbacks ↩
- OB_CALLBACK_REGISTRATION structure (wdm.h). Microsoft Learn. https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_ob_callback_registration ↩
- PsSetCreateProcessNotifyRoutineEx function (ntddk.h). Microsoft Learn. https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/nf-ntddk-pssetcreateprocessnotifyroutineex ↩
- Windows 8. Wikipedia. https://en.wikipedia.org/wiki/Windows_8 ↩
- (2018). Windows Exploitation Tricks: Exploiting Arbitrary Object Directory Creation. Google Project Zero. https://projectzero.google/2018/08/windows-exploitation-tricks-exploiting.html ↩
- Windows 10 version history. Wikipedia. https://en.wikipedia.org/wiki/Windows_10_version_history ↩
- (2015). Battle of SKM and IUM: How Windows 10 Rewrites OS Architecture. Black Hat USA 2015. https://github.com/tpn/pdfs/blob/master/Battle%20of%20SKM%20and%20IUM%20-%20How%20Windows%2010%20Rewrites%20OS%20Architecture%20-%20Alex%20Ionescu%20-%202015%20(blackhat2015).pdf ↩
- Windows Server 2016. Wikipedia. https://en.wikipedia.org/wiki/Windows_Server_2016 ↩
- Job Objects. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects ↩
- (2020). What I Learned From Reverse Engineering Windows Containers. Palo Alto Networks Unit 42. https://unit42.paloaltonetworks.com/what-i-learned-from-reverse-engineering-windows-containers/ ↩
- (2021). Siloscape: First Known Malware Targeting Windows Containers. Palo Alto Networks Unit 42. https://unit42.paloaltonetworks.com/siloscape/ ↩
- (2021). Who Contains the Containers?. Google Project Zero. https://projectzero.google/2021/04/who-contains-containers.html ↩
- (2015). Microsoft Security Bulletin MS15-090. Microsoft Learn. https://learn.microsoft.com/en-us/security-updates/securitybulletins/2015/ms15-090 ↩
- (2015). CVE-2015-2428 -- Windows Object Manager Elevation of Privilege. NIST National Vulnerability Database. https://nvd.nist.gov/vuln/detail/CVE-2015-2428 ↩
- (2015). CVE-2015-2528 -- Windows Task Management Elevation of Privilege. NIST National Vulnerability Database. https://nvd.nist.gov/vuln/detail/CVE-2015-2528 ↩
- (2015). CVE-2015-1463 -- ClamAV denial of service. NIST National Vulnerability Database. https://nvd.nist.gov/vuln/detail/CVE-2015-1463 ↩
- (2018). CVE-2018-0824 -- Microsoft COM for Windows RCE. NIST National Vulnerability Database. https://nvd.nist.gov/vuln/detail/CVE-2018-0824 ↩
- (2023). Activation Context Hell: DosDevices Remapping Attack under Impersonation. HackSys Inc. https://hacksys.io/blogs/activation-context-hell-dosdevices-remapping-attack-under-impersonation ↩
- (2023). CVE-2023-35359 -- Windows Kernel Elevation of Privilege. NIST National Vulnerability Database. https://nvd.nist.gov/vuln/detail/CVE-2023-35359 ↩
- (2022). CVE-2022-22047 -- Windows CSRSS Elevation of Privilege. NIST National Vulnerability Database. https://nvd.nist.gov/vuln/detail/CVE-2022-22047 ↩
- CreatePrivateNamespaceW function (namespaceapi.h). Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/api/namespaceapi/nf-namespaceapi-createprivatenamespacew ↩
- CreateBoundaryDescriptorW function (namespaceapi.h). Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/api/namespaceapi/nf-namespaceapi-createboundarydescriptorw ↩
- AddSIDToBoundaryDescriptor function (namespaceapi.h). Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/api/namespaceapi/nf-namespaceapi-addsidtoboundarydescriptor ↩
- NtCreatePrivateNamespace. NtDoc (m417z mirror of PHNT). https://ntdoc.m417z.com/ntcreateprivatenamespace ↩
- OBJECT_BOUNDARY_DESCRIPTOR. NtDoc (m417z mirror of PHNT). https://ntdoc.m417z.com/object_boundary_descriptor ↩
- Object Namespaces. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/sync/object-namespaces ↩
- PPLdump. GitHub. https://github.com/itm4n/PPLdump ↩
- (2021). Do you really know about LSA Protection (RunAsPPL)?. itm4n blog. https://itm4n.github.io/lsass-runasppl/ ↩
- (2021). Bypassing LSA Protection in Userland. SCRT Team Blog. https://blog.scrt.ch/2021/04/22/bypassing-lsa-protection-in-userland/ ↩
- (2022). The End of PPLdump. itm4n blog. https://itm4n.github.io/the-end-of-ppldump/ ↩
- namespaces(7) -- overview of Linux namespaces. man7.org. https://man7.org/linux/man-pages/man7/namespaces.7.html ↩
- Mach Overview -- Kernel Programming Guide. Apple Developer Archive. https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/Mach/Mach.html ↩
- Chromium Sandbox Design. Chromium Project. https://chromium.googlesource.com/chromium/src/+/main/docs/design/sandbox.md ↩
- Chromium Sandbox FAQ. Chromium Project. https://chromium.googlesource.com/chromium/src/+/main/docs/design/sandbox_faq.md ↩
- sandbox_policy.h. Chromium Project. https://chromium.googlesource.com/chromium/src/+/refs/heads/main/sandbox/win/src/sandbox_policy.h ↩
- Reference Monitor. Wikipedia. https://en.wikipedia.org/wiki/Reference_monitor ↩
- (1972). Computer Security Technology Planning Study (ESD-TR-73-51). USAF / NIST CSRC mirror. https://csrc.nist.gov/files/pubs/conference/1998/10/08/proceedings-of-the-21st-nissc-1998/final/docs/early-cs-papers/ande72.pdf ↩
- (1975). The Protection of Information in Computer Systems. https://www.cs.virginia.edu/~evans/cs551/saltzer/ ↩
- seL4 Microkernel. seL4 Foundation. https://sel4.systems/ ↩
- NtObjectManager PowerShell Module. PowerShell Gallery. https://www.powershellgallery.com/packages/NtObjectManager ↩
- sandbox-attacksurface-analysis-tools. GitHub. https://github.com/googleprojectzero/sandbox-attacksurface-analysis-tools ↩
- Windows NT. Wikipedia. https://en.wikipedia.org/wiki/Windows_NT ↩