A deep dive into the Windows kernel. This post breaks down how kernel objects are managed and why user-mode applications use handles instead of direct pointers. We'll explore the entire lifecycle, from creation and reference counting to how drivers safely interact with these core components.

Alright, let's get started! Just a quick note: we're going to be building on ideas we've already explored, like processes, threads, and system calls. If those sound new to you, I'd definitely recommend going back and giving those blogs a read first. And as always, for a really deep understanding, I suggest picking up 'Windows Kernel Programming.'" book.
A kernel object is just a structured block of memory in kernel space managed by Object manager. everything in windows such as process, threads,files, registry keys, mutexes, etc. is represented as kernel object. let's see an example when you open notepad the kernel creates a process object to represent it. this object store information like process ID, and it's memory map.
These objects are stored in kernel space, which is not directly accessible by user-mode program. this ensures security and stability user programs cannot modify kernel data structure directly but why?
User-mode processes run at ring 3,the least privileged level, while the kernel run at ring 0, the most privileged level. Allowing user mode to access kernel memory directly would risk system to crashes or most dangerous privilege escalation.
instead windows use handles, safe, indirect references that point to kernel object without exposing their actual memory address. so when you use a handle you're making a system request. the kernel then validates you're request, permissions, and preforms the operation on your behalf in safe way in ring 0.
okay but how they work??
I like learn by example so let's take an example, Every process gets its own private handle table, which is managed by the kernel. Think of this table like a private inventory list for all the system resources that the process is currently using. This table is stored in kernel memory, so your app can't mess with it directly. Now, let's say your user-mode code runs this line:
1HANDLE hFile = CreateFile("C:\\test.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
2From your application's perspective, you just called a function and got a HANDLE back. But under the hood, a carefully orchestrated sequence of events just happened:
CreateFile function (which lives in a user-mode DLL like kernel32.dll) immediately triggers a system call.GENERIC_READ), which specifies what you're allowed to do with this handle.0x74 or 0x98. This handle is basically just an index into your process's handle table. The system then switches back from kernel mode to user mode, and the function returns this handle value to your variable.So we know the handle is an index, but what does the kernel do with it? When you call a function like ReadFile and pass it your hFile, the kernel needs to translate that handle back into the actual kernel object to read or modify. This is where the internal structures come into play.
Let's get more precise. Deep in the kernel, every running process is represented by a large structure called _EPROCESS. Think of it as the master blueprint for your process. One of the most important fields inside _EPROCESS is a pointer named ObjectTable. As you can guess, this points to the handle table for that specific process.
This handle table (of type _HANDLE_TABLE) is essentially an array of _HANDLE_TABLE_ENTRY structures. Each entry in this table corresponds to a single handle and holds the critical information linking your user-mode handle to the kernel-mode object.
A valid _HANDLE_TABLE_ENTRY stores the following structure
OBJECT_HEADER). This is how the kernel finds the object.CreateFile, you asked for GENERIC_READ. The kernel stored that permission in the handle entry. for more info seeSo, when you call ReadFile(hFile, ...):
hFile._EPROCESS structure to find its ObjectTable._HANDLE_TABLE_ENTRY in that table.CreateFile example, yes. If you had opened it only with GENERIC_WRITE, this ReadFile call would fail.This is why the handle is just a token. The real connection the pointer and the permissions is stored securely in the kernel-side handle table, completely shielded from your user-mode application.
Handle values are multiples of 4 (for alignment and internal reasons). The first valid handle is 0x4; 0x0 is never valid.
If a function like CreateMutex or OpenFile fails, it returns NULL or INVALID_HANDLE_VALUE (-1), depending on the API.
A handle is only meaningful within the process that created it. If Process A has a handle 0x74, that same value means something completely different (or nothing at all) in Process B's handle table.
You can't just pass handle values between processes. to share access to the same kernel object, you must use DuplicateHandle(). This function tells the kernel to create a new handle in the target process's table that points to the same underlying object.
Every kernel object has a pointer count. This number tracks how many references (both handles and direct kernel pointers) point to it.
CreateFile, the object is created and its pointer count becomes 1.CloseHandle, the count is decreased by 1.DuplicateHandle, the count increases.When the pointer count drops to zero, the Object Manager knows nothing is using the object anymore and safely deletes it, freeing its memory.
Kernel-mode code, like a driver, can work with objects in two ways:
ObReferenceObjectByHandleA driver can't just trust a handle it gets from a user-mode app. To safely convert that handle into a kernel-mode pointer, it must call ObReferenceObjectByHandle.
This essential function does three things:
DesiredAccess).The increment is critical it ensures the object won't be deleted while the driver is using it
1NTSTATUS ObReferenceObjectByHandle(
2 HANDLE Handle,
3 ACCESS_MASK DesiredAccess,
4 POBJECT_TYPE ObjectType,
5 KPROCESSOR_MODE AccessMode,
6 PVOID *Object,
7 POBJECT_HANDLE_INFORMATION HandleInformation
8);
9ObDereferenceObjectWhen the driver is finished with the object, it must call ObDereferenceObject.This function decrements the object's pointer count. If the driver forgets this call, the count will never reach zero, and the object will be stuck in memory until the system reboots. This is a classic resource leak.
Every kernel object has a type, defined by a global "type object" like IoFileObjectType or PsProcessType. This type object simply tells the Object Manager the rules for objects of that kind: how to create them, how to delete them, and what their default behaviors are.
In Windows, some kernel objects (e.g., Mutexes, Semaphores, Events) can have names, which allow them to be accessed or opened using functions like OpenMutex or CreateEvent with a name parameter. However, not all objects have names like process and threads they identified by ID.
When a named object created by CreateMutex the name you provide isn't the final name. it's prefixed base on context. For session-based object the prefix is
\\Sessions\\<sessionID>\\BaseNamedObjects\\ if the session was 0 (non-interactive session used for system services) it’s \\BaseNamedObjects\\.
For AppContainer process he prefix includes a unique AppContainer SID, like \\Sessions\\<sessionID>\\AppContainerNamedObjects\\{AppContainerSID}\\.
What if you want to create an object that can be seen by processes across all sessions? For example, maybe you have a single-instance application and you want to ensure only one copy can run on the entire system, regardless of which user tries to launch it.
For this, you use the Global\\ prefix.If you create a mutex named "Global\\MySystemWideMutex", the Object Manager places it in a global location that is visible from all sessions. Its actual name becomes: \\BaseNamedObjects\\MySystemWideMutex This puts it in the same top-level directory used by system services in Session 0, making it globally unique across the entire system. (Note: This requires special privileges, as you are creating an object outside your own session's sandbox).
The Object Manager, a component of the Windows Executive, maintains this hierarchy in memory. Only named objects appear in this structure, visible via the WinObj tool (run as administrator), as shown in the first image.
Left pane displays a tree as we can see we are in opening this path
\Sessions\1\BaseNamedObject, the right pane lists objects with columns for name type and sysmLink example:
IBrowserEmulationSharedMemoryMutex (Type: Mutant, i.e., a Mutex)..NET CLR Data_Perf_Library_Lock_PID_13c4 (name)026f78c61dc4b69848b2e0e3823fc51e2eb7f4901D4C740001184 (Type: Event, possibly a unique identifier for an event object).This view helps administrators or developers inspect named kernel objects and their relationships.
if client code wants to terminate a process, it must call the OpenProcess function first, to obtain a handle to the required process with an access mask of (at least) PROCESS_TERMINATE, otherwise there is no way to terminate the process with that handle. If the call succeeds, then the call to TerminateProcess is bound to succeed.Here’s a user-mode example for terminating a process given its process ID
1bool KillProcess(DWORD pid) {
2 //
3 // open a powerful-enough handle to the process
4 //
5 HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
6 if (!hProcess)
7 return false;
8 //
9 // now kill it with some arbitrary exit code
10 //
11 BOOL success = TerminateProcess(hProcess, 1);
12 //
13 // close the handle
14 //
15 CloseHandle(hProcess);
16 return success != FALSE;
17}The Decoded Access column provides a textual description of the access mask (for some object types), making it easier to identify the exact access allowed for a particular handle.
Double-clicking a handle entry (or right-clicking and selecting Properties) shows some of the object’s properties. image down shows a screenshot of an example event object properties.
The properties in the image include the object’s name (if any), its type, a short description, its address in kernel memory, the number of open handles, and some specific object information, such as the state and type of the event object shown. Note that the References shown do not indicate the actual number.
of outstanding references to the object (it does prior to Windows 8.1). A proper way to see the actual reference count for the object is to use the kernel debugger’s !trueref command, as shown here down and we will look into it more in future.
1lkd> !object 0xFFFFA08F948AC0B0
2Object: ffffa08f948ac0b0 Type: (ffffa08f684df140) Event
3 ObjectHeader: ffffa08f948ac080 (new version)
4 HandleCount: 2 PointerCount: 65535
5 Directory Object: ffff90839b63a700 Name: ShellDesktopSwitchEventWe’ll take a closer look at the attributes of objects and the kernel debugger in later on.
Published Oct 29, 2025
CreateFilehFile