Loading content...
In Windows, virtually every system resource—files, processes, threads, registry keys, mutexes, events, pipes, and dozens more—is represented as a kernel object. Applications cannot directly access these objects; instead, they interact with them through handles, opaque identifiers that serve as indirect references to kernel resources.
This handle-based architecture is fundamental to Windows security and resource management. Unlike Unix file descriptors (which are simple integer indices), Windows handles are part of a sophisticated object management system that provides:
Understanding handles is essential for any Windows developer. Handle mismanagement leads to resource leaks, security vulnerabilities, and system instability.
By the end of this page, you will understand the Windows kernel object architecture, how handle tables work, the relationship between handles and kernel objects, handle inheritance and duplication, security implications of handle access rights, and proper handle lifecycle management patterns.
Windows implements a unified object model where all kernel resources are derived from a common base structure. This object-oriented design (implemented in C) provides consistent behavior across all resource types.
The Object Header:
Every kernel object begins with a standard header structure that contains metadata about the object:
┌─────────────────────────────────────────────────────────────┐
│ OBJECT_HEADER │
├─────────────────────────────────────────────────────────────┤
│ PointerCount (8 bytes) - Referenced by kernel pointers │
│ HandleCount (8 bytes) - Number of handles to this obj │
│ Lock - Synchronization primitive │
│ TypeIndex (1 byte) - Index into ObpObjectTypes │
│ TraceFlags (1 byte) - Debugging/tracing flags │
│ InfoMask (1 byte) - Which optional headers exist │
│ Flags (1 byte) - Object flags │
│ SecurityDescriptor - Points to security info │
│ ObjectType - Points to OBJECT_TYPE │
├─────────────────────────────────────────────────────────────┤
│ [Optional: OBJECT_HEADER_NAME_INFO] │
│ [Optional: OBJECT_HEADER_CREATOR_INFO] │
│ [Optional: OBJECT_HEADER_HANDLE_INFO] │
│ [Optional: OBJECT_HEADER_QUOTA_INFO] │
│ [Optional: OBJECT_HEADER_PROCESS_INFO] │
├─────────────────────────────────────────────────────────────┤
│ OBJECT BODY │
│ (Type-specific data: FILE_OBJECT, EPROCESS, etc.) │
└─────────────────────────────────────────────────────────────┘
Reference Counting:
Kernel objects use two types of reference counts:
The object is destroyed only when both counts reach zero. This dual-counting allows the kernel to hold references to objects that user mode has "closed"—essential for operations like I/O completion.
Object Types:
The OBJECT_TYPE structure defines the behavior for each object category:
| Object Type | Examples Created By | Description |
|---|---|---|
| Process | CreateProcess | Represents a running program with address space |
| Thread | CreateThread | Unit of execution within a process |
| File | CreateFile | Open file, device, or pipe handle |
| Section | CreateFileMapping | Memory-mapped file section |
| Event | CreateEvent | Signalable synchronization object |
| Mutex | CreateMutex | Mutually exclusive lock |
| Semaphore | CreateSemaphore | Counting synchronization object |
| Key | RegOpenKey | Registry key handle |
| Token | OpenProcessToken | Security token for access checks |
| Timer | CreateWaitableTimer | Timer that can signal events |
| Desktop | CreateDesktop | Interactive display surface |
| WindowStation | CreateWindowStation | Container for desktops |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
// Simplified view of OBJECT_TYPE structure// (Actual structure is more complex and version-dependent) typedef struct _OBJECT_TYPE { // Type list linkage LIST_ENTRY TypeList; // Type name (e.g., L"Process", L"File", L"Event") UNICODE_STRING Name; // Default security descriptor for this type PVOID DefaultSecurityDescriptor; // Type-specific access mask (what rights are valid) ACCESS_MASK ValidAccessMask; // Type procedures - similar to virtual function table OBJECT_TYPE_PROCEDURES TypeInfo; // Type-specific key for indexing ULONG TypeIndex; // Statistics ULONG TotalNumberOfObjects; ULONG TotalNumberOfHandles; ULONG HighWaterNumberOfObjects; ULONG HighWaterNumberOfHandles; } OBJECT_TYPE, *POBJECT_TYPE; // Type procedures - callbacks for object operationstypedef struct _OBJECT_TYPE_PROCEDURES { // Called before handle is created OB_OPEN_METHOD OpenProcedure; // Called when handle is closed OB_CLOSE_METHOD CloseProcedure; // Called when object is deleted OB_DELETE_METHOD DeleteProcedure; // Parse object name (for named objects) OB_PARSE_METHOD ParseProcedure; // Query object name OB_QUERYNAME_METHOD QueryNameProcedure; // Security query callback OB_SECURITY_METHOD SecurityProcedure; // Called when dumping object in KD OB_DUMP_METHOD DumpProcedure; } OBJECT_TYPE_PROCEDURES;Named kernel objects live in the Object Manager namespace, starting at the root . You can browse this namespace using tools like WinObj from Sysinternals. Paths like \Device\HarddiskVolume1 and \BaseNamedObjects\MyMutex show how objects are organized hierarchically.
Each Windows process has its own handle table—a data structure that maps handle values to kernel object references. Understanding this structure clarifies how handles work and why they behave the way they do.
Handle Value Format:
A handle is a process-specific index into the handle table. The format is:
63 30 29 28 27 26 2 1 0
┌──────────┬───┬───┬──────────────────┬───┐
│ Reserved │ P │ I │ Table Index │ 0 │
└──────────┴───┴───┴──────────────────┴───┘
P = Protected from close (HANDLE_FLAG_PROTECT_FROM_CLOSE)
I = Inherit flag (HANDLE_FLAG_INHERIT)
Table Index = Actual index into handle table (multiplied by 4)
Handle values are always multiples of 4 (the bottom 2 bits are zero), and certain values are reserved:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// Handle table entry structure (simplified, x64)typedef struct _HANDLE_TABLE_ENTRY { union { // When handle is in use: struct { ULONG_PTR ObjectPointerBits : 44; // Pointer to OBJECT_HEADER ULONG_PTR GrantedAccessBits : 25; // Access rights for this handle ULONG_PTR NoRightsUpgrade : 1; // Security flag ULONG_PTR Spare1 : 1; ULONG_PTR Protectable : 1; // Can set PROTECT_FROM_CLOSE ULONG_PTR Inherit : 1; // Inheritable handle ULONG_PTR Attributes : 3; // Object attributes }; // When handle is free: struct { ULONG_PTR NextFreeTableEntry; // Link to next free entry }; };} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY; // The handle table itself is structured in levelstypedef struct _HANDLE_TABLE { ULONG_PTR TableCode; // Points to table levels PEPROCESS QuotaProcess; // Process owning this table HANDLE UniqueProcessId; // Owner process ID EX_PUSH_LOCK HandleLock; // Synchronization LIST_ENTRY HandleTableList; // Links all process handle tables ULONG NextHandleNeedingPool;// Next index needing allocation ULONG ExtraInfoPages; // Additional info pages volatile ULONG HandleCount; // Number of handles in use ULONG Flags; // Table flags // ... more fields} HANDLE_TABLE, *PHANDLE_TABLE; /* * Handle table levels (for scalability): * * Level 0: Single table - up to 256 handles * Level 1: One pointer table pointing to up to 256 Level-0 tables * Level 2: One pointer table pointing to Level-1 tables * * This allows up to 16 million handles per process while only * allocating memory for tables actually in use. */While the handle table structure supports millions of handles, there are practical limits. By default, a process can have up to 16,777,216 handles. The system-wide limit depends on available kernel memory. Use Performance Monitor or Task Manager to monitor handle counts—a steadily increasing count often indicates a handle leak.
From Handle to Object:
When the kernel needs to access an object given a handle, it performs these steps:
This process is called handle translation and happens for every handle-based operation.
Handle creation, management, and cleanup are critical operations that every Windows developer must understand thoroughly.
Handle Creation:
Handles are created when you call "Create" or "Open" functions:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
#include <windows.h> void HandleCreationExamples() { HANDLE hProcess, hThread, hFile, hEvent, hMutex, hMapping; // ==== Process Handle ==== // Create new process - returns handles to process AND thread STARTUPINFOW si = { sizeof(si) }; PROCESS_INFORMATION pi; if (CreateProcessW(L"notepad.exe", NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { hProcess = pi.hProcess; // Handle to new process hThread = pi.hThread; // Handle to main thread // MUST close both handles when done! } // Open existing process - requires process ID hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, // Access rights FALSE, // Handle not inheritable 1234 // Process ID ); // ==== File Handle ==== hFile = CreateFileW( L"C:\\data\\file.txt", GENERIC_READ | GENERIC_WRITE, // Access rights FILE_SHARE_READ, // Share mode NULL, // Security attributes CREATE_ALWAYS, // Creation disposition FILE_ATTRIBUTE_NORMAL, // Flags and attributes NULL // Template file ); // ==== Synchronization Objects ==== // Event - signalable flag hEvent = CreateEventW( NULL, // Security attributes TRUE, // Manual reset (vs auto-reset) FALSE, // Initial state not signaled L"MyEventName" // Optional name for sharing ); // Mutex - mutual exclusion lock hMutex = CreateMutexW( NULL, // Security attributes FALSE, // Don't initially own L"MyMutexName" // Optional name ); // ==== Memory-Mapped File ==== hMapping = CreateFileMappingW( hFile, // File to map (or INVALID_HANDLE_VALUE for pagefile) NULL, // Security attributes PAGE_READWRITE, // Protection flags 0, // Max size high DWORD 1024 * 1024, // Max size low DWORD (1 MB) L"MyMappingName"// Optional name ); // All these handles must eventually be closed!}Access Rights:
When you create or open a handle, you specify access rights that determine what operations you can perform. Access rights are fundamental to Windows security:
| Right | Value | Meaning |
|---|---|---|
| DELETE | 0x00010000 | Delete the object |
| READ_CONTROL | 0x00020000 | Read security descriptor |
| WRITE_DAC | 0x00040000 | Modify DACL (permissions) |
| WRITE_OWNER | 0x00080000 | Change owner |
| SYNCHRONIZE | 0x00100000 | Wait on the object |
| STANDARD_RIGHTS_ALL | 0x001F0000 | All standard rights |
| Object Type | Common Rights | Description |
|---|---|---|
| Process | PROCESS_ALL_ACCESS | All process operations |
| Process | PROCESS_VM_READ | Read process memory |
| Process | PROCESS_TERMINATE | Terminate process |
| File | GENERIC_READ | Read file data |
| File | GENERIC_WRITE | Write file data |
| File | FILE_APPEND_DATA | Append only |
| Thread | THREAD_ALL_ACCESS | All thread operations |
| Thread | THREAD_SUSPEND_RESUME | Suspend/resume thread |
Always request only the access rights you actually need. Requesting PROCESS_ALL_ACCESS when you only need PROCESS_QUERY_INFORMATION will fail in many security contexts where the broader access would be denied. This is a common source of bugs when applications are run with limited privileges.
Handles can be shared between processes through two mechanisms: inheritance (parent to child) and duplication (between any processes).
Handle Inheritance:
When a process creates a child process, inheritable handles from the parent can be copied to the child's handle table:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
#include <windows.h>#include <stdio.h> void HandleInheritanceExample() { // Create a pipe - one handle for reading, one for writing HANDLE hRead, hWrite; // Security attributes to make handles inheritable SECURITY_ATTRIBUTES sa = { .nLength = sizeof(SECURITY_ATTRIBUTES), .lpSecurityDescriptor = NULL, .bInheritHandle = TRUE // <-- Key: makes handles inheritable }; if (!CreatePipe(&hRead, &hWrite, &sa, 0)) { return; } // Make only the read handle inheritable // (Don't want child to have write access) SetHandleInformation( hWrite, HANDLE_FLAG_INHERIT, 0 // Clear the inherit flag ); // Prepare child process startup STARTUPINFOW si = { sizeof(si) }; // Redirect child's stdin to our read handle si.dwFlags = STARTF_USESTDHANDLES; si.hStdInput = hRead; // Child inherits this si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); si.hStdError = GetStdHandle(STD_ERROR_HANDLE); PROCESS_INFORMATION pi; // Create child with handle inheritance enabled BOOL success = CreateProcessW( L"child.exe", NULL, NULL, NULL, TRUE, // <-- bInheritHandles = TRUE 0, NULL, NULL, &si, &pi ); if (success) { // Write data to pipe - child can read it via stdin const char* message = "Hello from parent!"; DWORD written; WriteFile(hWrite, message, strlen(message), &written, NULL); // Wait for child to finish WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } CloseHandle(hRead); CloseHandle(hWrite);}Handle Duplication:
For sharing handles between unrelated processes, or with more control than inheritance provides, use DuplicateHandle:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
#include <windows.h> // Scenario: Process A wants to give Process B access to a file // In Process A:void ShareHandleToProcessB(HANDLE hFile, DWORD processIdB) { HANDLE hProcessB = OpenProcess( PROCESS_DUP_HANDLE, // Need this right to duplicate into target FALSE, processIdB ); if (!hProcessB) { return; } HANDLE hDuplicated; BOOL success = DuplicateHandle( GetCurrentProcess(), // Source process hFile, // Handle to duplicate hProcessB, // Target process &hDuplicated, // Receives handle value in target GENERIC_READ, // Can reduce access rights FALSE, // Not inheritable 0 // Options ); if (success) { // hDuplicated is the handle value valid in Process B // Need to communicate this value to Process B somehow // (e.g., via shared memory, message, command line) printf("Handle for Process B: %p\n", (void*)hDuplicated); } CloseHandle(hProcessB);} // Duplicate within same process (useful for different access rights)HANDLE DuplicateWithReducedAccess(HANDLE hOriginal, DWORD newAccess) { HANDLE hNew; if (DuplicateHandle( GetCurrentProcess(), hOriginal, GetCurrentProcess(), &hNew, newAccess, // Reduced access FALSE, 0)) { return hNew; } return NULL;} // Close a handle in another processBOOL CloseRemoteHandle(DWORD targetProcessId, HANDLE targetHandle) { HANDLE hProcess = OpenProcess(PROCESS_DUP_HANDLE, FALSE, targetProcessId); if (!hProcess) return FALSE; // DUPLICATE_CLOSE_SOURCE closes the source handle BOOL success = DuplicateHandle( hProcess, targetHandle, NULL, // No destination process NULL, // No destination handle 0, FALSE, DUPLICATE_CLOSE_SOURCE // Just close, don't duplicate ); CloseHandle(hProcess); return success;}DuplicateHandle requires PROCESS_DUP_HANDLE access to the target process. This is a powerful right—with it, you can inject handles into any process. Many security boundaries rely on restricting this access right. Additionally, you cannot increase access rights when duplicating—only maintain or reduce them.
Proper handle cleanup is one of the most important aspects of Windows programming. Failing to close handles leads to resource leaks that can accumulate and eventually crash applications or even the system.
The Golden Rule:
Every successful handle-creating function call must be matched by a
CloseHandle()call.
Closing Handles:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
#include <windows.h> // Basic handle cleanupvoid BasicCleanup() { HANDLE hFile = CreateFileW(L"test.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); if (hFile != INVALID_HANDLE_VALUE) { // Use the file... // ALWAYS close when done CloseHandle(hFile); }} // RAII-style cleanup pattern for Ctypedef struct { HANDLE* pHandle; HANDLE invalidValue; // Different for different handle types} HandleGuard; void ReleaseHandle(HandleGuard* guard) { if (guard && guard->pHandle && *guard->pHandle != NULL && *guard->pHandle != guard->invalidValue) { CloseHandle(*guard->pHandle); *guard->pHandle = guard->invalidValue; }} // Using the guard patternvoid SafeCleanupExample() { HANDLE hFile = INVALID_HANDLE_VALUE; HandleGuard fileGuard = { &hFile, INVALID_HANDLE_VALUE }; hFile = CreateFileW(L"test.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { return; // fileGuard.pHandle is invalid, nothing to close } // Do work that might throw or have multiple return paths... // Cleanup at the end ReleaseHandle(&fileGuard);} // For C++, use proper RAII#ifdef __cplusplusclass HandleWrapper { HANDLE m_handle; HandleWrapper(const HandleWrapper&) = delete; HandleWrapper& operator=(const HandleWrapper&) = delete;public: explicit HandleWrapper(HANDLE h = NULL) : m_handle(h) {} ~HandleWrapper() { if (m_handle && m_handle != INVALID_HANDLE_VALUE) { CloseHandle(m_handle); } } HANDLE get() const { return m_handle; } HANDLE* addressof() { return &m_handle; } void reset(HANDLE h = NULL) { if (m_handle && m_handle != INVALID_HANDLE_VALUE) { CloseHandle(m_handle); } m_handle = h; } HANDLE release() { HANDLE h = m_handle; m_handle = NULL; return h; } explicit operator bool() const { return m_handle && m_handle != INVALID_HANDLE_VALUE; }};#endifSpecial Close Functions:
Not all handles are closed with CloseHandle(). Some object types require specific functions:
| Handle Type | Close Function | Notes |
|---|---|---|
| Most kernel objects | CloseHandle() | Files, processes, threads, events, mutexes... |
| Registry keys | RegCloseKey() | Do NOT use CloseHandle() |
| Find handles | FindClose() | From FindFirstFile() |
| GDI objects | DeleteObject() | Brushes, pens, fonts... |
| Device contexts | DeleteDC() or ReleaseDC() | Depends on how obtained |
| Windows | DestroyWindow() | Window handles (HWND) |
| Sockets | closesocket() | Winsock socket handles |
| Service handles | CloseServiceHandle() | SCM handles |
Handles are security boundaries—they determine what a process can do with a kernel object. Understanding handle security is essential for writing secure Windows applications.
Access Rights Checked at Open Time:
One of the most important security concepts in Windows is that access rights are checked when a handle is created, not when it's used:
This "open-time check" model has important implications:
Handle Protection:
Handles can be marked protected to prevent accidental or malicious closure:
1234567891011121314151617181920212223242526272829303132333435
#include <windows.h> void HandleProtectionExample() { HANDLE hEvent = CreateEventW(NULL, TRUE, FALSE, L"MyEvent"); if (!hEvent) return; // Protect handle from being closed SetHandleInformation( hEvent, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE // Set the flag ); // Now CloseHandle will FAIL if (!CloseHandle(hEvent)) { // GetLastError() returns ERROR_INVALID_HANDLE // Handle is still valid! OutputDebugStringW(L"Cannot close protected handle\n"); } // To close, first remove protection SetHandleInformation( hEvent, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0 // Clear the flag ); // Now we can close CloseHandle(hEvent);} // Why protect handles?// 1. Prevent accidental close in complex codebases// 2. Ensure critical resources remain available// 3. Debug handle lifetime issues (closing protected handle crashes in debugger)Handle leaks have security implications beyond resource exhaustion. An inherited or leaked handle to a privileged object (like an elevated process token) can allow privilege escalation. Security reviewers should audit handle creation, inheritance, and cleanup paths carefully.
Handle bugs are notoriously difficult to debug. Windows provides several tools and techniques for investigating handle issues:
Common Handle Problems:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
#include <windows.h>#include <stdio.h> // Enable handle tracing in Windows (requires Application Verifier)// Or use !htrace command in WinDbg // Manual handle tracking for debugging#ifdef _DEBUGtypedef struct { HANDLE handle; const char* file; int line; const char* function;} HandleRecord; #define MAX_TRACKED_HANDLES 1000HandleRecord g_handles[MAX_TRACKED_HANDLES];int g_handleCount = 0;CRITICAL_SECTION g_handleLock; void InitHandleTracking() { InitializeCriticalSection(&g_handleLock);} void TrackHandle(HANDLE h, const char* file, int line, const char* func) { EnterCriticalSection(&g_handleLock); if (g_handleCount < MAX_TRACKED_HANDLES) { g_handles[g_handleCount].handle = h; g_handles[g_handleCount].file = file; g_handles[g_handleCount].line = line; g_handles[g_handleCount].function = func; g_handleCount++; } LeaveCriticalSection(&g_handleLock);} void UntrackHandle(HANDLE h) { EnterCriticalSection(&g_handleLock); for (int i = 0; i < g_handleCount; i++) { if (g_handles[i].handle == h) { g_handles[i] = g_handles[--g_handleCount]; break; } } LeaveCriticalSection(&g_handleLock);} void DumpLeakedHandles() { EnterCriticalSection(&g_handleLock); for (int i = 0; i < g_handleCount; i++) { printf("Leaked handle %p: %s:%d in %s\n", g_handles[i].handle, g_handles[i].file, g_handles[i].line, g_handles[i].function); } LeaveCriticalSection(&g_handleLock);} // Wrapper macros#define TRACK_HANDLE(h) TrackHandle(h, __FILE__, __LINE__, __FUNCTION__)#define UNTRACK_HANDLE(h) UntrackHandle(h) #else#define TRACK_HANDLE(h)#define UNTRACK_HANDLE(h)#endif // Query handle informationvoid PrintHandleInfo(HANDLE h) { DWORD flags; if (GetHandleInformation(h, &flags)) { printf("Handle %p flags: 0x%X\n", (void*)h, flags); if (flags & HANDLE_FLAG_INHERIT) printf(" - Inheritable\n"); if (flags & HANDLE_FLAG_PROTECT_FROM_CLOSE) printf(" - Protected from close\n"); } // Get the object type (requires NtQueryObject - Native API) // ... (omitted for brevity)}Debugging Tools:
Process Explorer (Sysinternals):
WinDbg / !handle:
!handle - List all handles in the process!handle <handle_value> f - Full info for specific handle!htrace - Enable/query handle tracingApplication Verifier:
In WinDbg, use '!htrace -enable' to start tracking all handle operations. Then use '!htrace <handle>' to see the full history of a specific handle—when it was created, by what code, and if/when it was closed. This is invaluable for debugging handle leaks and use-after-close bugs.
The Windows handle system is a sophisticated mechanism for managing kernel object access. Understanding handles is fundamental to writing correct, secure, and performant Windows applications.
What's Next:
With understanding of how handles work, we'll next examine the Windows Registry—the centralized configuration database that uses a handle-based access model for storing and retrieving system and application configuration data.
You now understand the Windows handle system, from kernel object architecture through handle tables, creation, inheritance, duplication, and proper lifecycle management. This knowledge is essential for writing robust Windows applications and understanding Windows security.