Much of the Windows kernel functionality is exposed via kernel objects. Processes, threads, events, desktops, semaphores, and many other object types exist. Some object types can have string-based names, which means they can be “looked up” by that name. In this post, I’d like to consider some subtleties that concern object names.
Let’s start by examining kernel object handles in Process Explorer. When we select a process of interest, we can see the list of handles in one of the bottom views:

Handles view in Process Explorer
However, Process Explorer shows what it considers handles to named objects only by default. But even that is not quite right. You will find certain object types in this view that don’t have string-based names. The simplest example is processes. Processes have numeric IDs, rather than string-based names. Still, Process Explorer shows processes with a “name” that shows the process executable name and its unique process ID. This is useful information, for sure, but it’s not the object’s name.
Same goes for threads: these are displayed, even though threads (like processes) have numeric IDs rather than string-based names.
If you wish to see all handles in a process, you need to check the menu item Show Unnamed Handles and Mappings in the View menu.
Object Name Lifetime
What is the lifetime associated with an object’s name? This sounds like a weird question. Kernel objects are reference counted, so obviously when an object reference count drops to zero, it is destroyed, and its name is deleted as well. This is correct in part. Let’s look a bit deeper.
The following example code creates a Notepad process, and puts it into a named Job object (error handling omitted for brevity):
PROCESS_INFORMATION pi;
STARTUPINFO si = { sizeof(si) };
WCHAR name[] = L"notepad";
::CreateProcess(nullptr, name, nullptr, nullptr, FALSE, 0,
nullptr, nullptr, &si, &pi);
HANDLE hJob = ::CreateJobObject(nullptr, L"MyTestJob");
::AssignProcessToJobObject(hJob, pi.hProcess);
After running the above code, we can open Process Explorer, locate the new Notepad process, double-click it to get to its properties, and then navigate to the Job tab:

We can clearly see the job object’s name, prefixed with “\Sessions\1\BaseNamedObjects” because simple object names (like “MyTestJob”) are prepended with a session-relative directory name, making the name unique to this session only, which means processes in other sessions can create objects with the same name (“MyTestJob”) without any collision. Further details on names and sessions is outside the scope of this post.
Let’s see what the kernel debugger has to say regarding this job object:
lkd> !process 0 1 notepad.exe PROCESS ffffad8cfe3f4080 SessionId: 1 Cid: 6da0 Peb: 175b3b7000 ParentCid: 16994 DirBase: 14aa86d000 ObjectTable: ffffc2851aa24540 HandleCount: 233. Image: notepad.exe VadRoot ffffad8d65d53d40 Vads 90 Clone 0 Private 524. Modified 0. Locked 0. DeviceMap ffffc28401714cc0 Token ffffc285355e9060 ElapsedTime 00:04:55.078 UserTime 00:00:00.000 KernelTime 00:00:00.000 QuotaPoolUsage[PagedPool] 214720 QuotaPoolUsage[NonPagedPool] 12760 Working Set Sizes (now,min,max) (4052, 50, 345) (16208KB, 200KB, 1380KB) PeakWorkingSetSize 3972 VirtualSize 2101395 Mb PeakVirtualSize 2101436 Mb PageFaultCount 4126 MemoryPriority BACKGROUND BasePriority 8 CommitCharge 646 Job ffffad8d14503080 lkd> !object ffffad8d14503080 Object: ffffad8d14503080 Type: (ffffad8cad8b7900) Job ObjectHeader: ffffad8d14503050 (new version) HandleCount: 1 PointerCount: 32768 Directory Object: ffffc283fb072730 Name: MyTestJob
Clearly, there is a single handle to the job object. The PointerCount value is not the real reference count because of the kernel’s tracking of the number of usages each handle has (outside the scope of this post as well). To get the real reference count, we can click the PointerCount DML link in WinDbg (the !truref
command):
kd> !trueref ffffad8d14503080 ffffad8d14503080: HandleCount: 1 PointerCount: 32768 RealPointerCount: 3
We have a reference count of 3, and since we have one handle, it means there are two references somewhere to this job object.
Now let’s see what happens when we close the job handle we’re holding:
::CloseHandle(hJob);
Reopening the Notepad’s process properties in Process Explorer shows this:

Running the !object
command again on the job yields the following:
lkd> !object ffffad8d14503080 Object: ffffad8d14503080 Type: (ffffad8cad8b7900) Job ObjectHeader: ffffad8d14503050 (new version) HandleCount: 0 PointerCount: 1 Directory Object: 00000000 Name: MyTestJob
The handle count dropped to zero because we closed our (only) existing handle to the job. The job object’s name seem to be intact at first glance, but not really: The directory object is NULL, which means the object’s name is no longer visible in the object manager’s namespace.
Is the job object alive? Clearly, yes, as the pointer (reference) count is 1. When the handle count it zero, the Pointer Count is the correct reference count, and there is no need to run the !truref
command. At this point, you should be able to guess why the object is still alive, and where is that one reference coming from.
If you guessed “the Notepad process”, then you are right. When a process is added to a job, it adds a reference to the job object so that it remains alive if at least one process is part of the job.
We, however, have lost the only handle we have to the job object. Can we get it back knowing the object’s name?
hJob = ::OpenJobObject(JOB_OBJECT_QUERY, FALSE, L"MyTestJob");
This call fails, and GetLastError
returns 2 (“the system cannot find the file specified”, which in this case is the job object’s name). This means that the object name is destroyed when the last handle of the object is closed, even if there are outstanding references on the object (the object is alive!).
This the job object example is just that. The same rules apply to any named object.
Is there a way to “preserve” the object name even if all handles are closed? Yes, it’s possible if the object is created as “Permanent”. Unfortunately, this capability is not exposed by the Windows API functions like CreateJobObject
, CreateEvent
, and all other create functions that accept an object name.
Quick update: The native NtMakePermanentObject
can make an object permanent given a handle, if the caller has the SeCreatePermanent privilege. This privilege is not granted to any user/group by default.
A permanent object can be created with kernel APIs, where the flag OBJ_PERMANENT
is specified as one of the attribute flags part of the OBJECT_ATTRIBUTES
structure that is passed to every object creation API in the kernel.
A “canonical” kernel example is the creation of a callback object. Callback objects are only usable in kernel mode. They provide a way for a driver/kernel to expose notifications in a uniform way, and allow interested parties (drivers/kernel) to register for notifications based on that callback object. Callback objects are created with a name so that they can be looked up easily by interested parties. In fact, there are quite a few callback objects on a typical Windows system, mostly in the Callback object manager namespace:

Most of the above callback objects’ usage is undocumented, except three which are documented in the WDK (ProcessorAdd, PowerState, and SetSystemTime). Creating a callback object with the following code creates the callback object but the name disappears immediately, as the ExCreateCallback API returns an object pointer rather than a handle:
PCALLBACK_OBJECT cb;
UNICODE_STRING name = RTL_CONSTANT_STRING(L"\\Callback\\MyCallback");
OBJECT_ATTRIBUTES cbAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name,
OBJ_CASE_INSENSITIVE);
status = ExCreateCallback(&cb, &cbAttr, TRUE, TRUE);
The correct way to create a callback object is to add the OBJ_PERMANENT
flag:
PCALLBACK_OBJECT cb;
UNICODE_STRING name = RTL_CONSTANT_STRING(L"\\Callback\\MyCallback");
OBJECT_ATTRIBUTES cbAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name,
OBJ_CASE_INSENSITIVE | OBJ_PERMANENT);
status = ExCreateCallback(&cb, &cbAttr, TRUE, TRUE);
A permanent object must be made “temporary” (the opposite of permanent) before actually dereferencing it by calling ObMakeTemporaryObject
.
Aside: Getting to an Object’s Name in WinDbg
For those that wonder how to locate an object’s name give its address. I hope that it’s clear enough… (watch the bold text).
lkd> !object ffffad8d190c0080 Object: ffffad8d190c0080 Type: (ffffad8cad8b7900) Job ObjectHeader: ffffad8d190c0050 (new version) HandleCount: 1 PointerCount: 32770 Directory Object: ffffc283fb072730 Name: MyTestJob lkd> dt nt!_OBJECT_HEADER ffffad8d190c0050 +0x000 PointerCount : 0n32770 +0x008 HandleCount : 0n1 +0x008 NextToFree : 0x00000000`00000001 Void +0x010 Lock : _EX_PUSH_LOCK +0x018 TypeIndex : 0xe9 '' +0x019 TraceFlags : 0 '' +0x019 DbgRefTrace : 0y0 +0x019 DbgTracePermanent : 0y0 +0x01a InfoMask : 0xa '' +0x01b Flags : 0 '' +0x01b NewObject : 0y0 +0x01b KernelObject : 0y0 +0x01b KernelOnlyAccess : 0y0 +0x01b ExclusiveObject : 0y0 +0x01b PermanentObject : 0y0 +0x01b DefaultSecurityQuota : 0y0 +0x01b SingleHandleEntry : 0y0 +0x01b DeletedInline : 0y0 +0x01c Reserved : 0 +0x020 ObjectCreateInfo : 0xffffad8c`d8e40cc0 _OBJECT_CREATE_INFORMATION +0x020 QuotaBlockCharged : 0xffffad8c`d8e40cc0 Void +0x028 SecurityDescriptor : 0xffffc284`3dd85eae Void +0x030 Body : _QUAD lkd> db nt!ObpInfoMaskToOffset L10 fffff807`72625e20 00 20 20 40 10 30 30 50-20 40 40 60 30 50 50 70 . @.00P @@`0PPp lkd> dx (nt!_OBJECT_HEADER_NAME_INFO*)(0xffffad8d190c0050 - ((char*)0xfffff807`72625e20)[(((nt!_OBJECT_HEADER*)0xffffad8d190c0050)->InfoMask & 3)]) (nt!_OBJECT_HEADER_NAME_INFO*)(0xffffad8d190c0050 - ((char*)0xfffff807`72625e20)[(((nt!_OBJECT_HEADER*)0xffffad8d190c0050)->InfoMask & 3)]) : 0xffffad8d190c0030 [Type: _OBJECT_HEADER_NAME_INFO *] [+0x000] Directory : 0xffffc283fb072730 [Type: _OBJECT_DIRECTORY *] [+0x008] Name : "MyTestJob" [Type: _UNICODE_STRING] [+0x018] ReferenceCount : 0 [Type: long] [+0x01c] Reserved : 0x0 [Type: unsigned long]
One thought on “Kernel Object Names Lifetime”