OBJECT_HEADER

Every object that the Object Manager ever creates has an OBJECT_HEADER immediately before it. The Object Manager sees the object itself as an opaque region of memory that the Object Manager has been asked to provide, to manage and even to guard, but is not to interpret. Everything that the Object Manager knows of the object for the Object Manager’s purposes is reached through the header.

Documentation Status

The OBJECT_HEADER is not documented. Microsoft’s only known publication of a C-language declaration is in NTOSP.H from the Enterprise edition of the Windows Driver Kit (WDK) for Windows 10 Version 1511. Since this header is in a subdirectory of a subdirectory named “um”, as if to suggest user-mode programming, and the OBJECT_HEADER cannot be visible to user-mode programs, the disclosure is here thought to have been unintended.

Access

The OBJECT_HEADER points to other structures and is itself preceded by other sorts of header. Being able to inspect the OBJECT_HEADER and these other structures by sight can be very useful when debugging. For just the simplest example, if the !object command can show an object’s security descriptor, then I suspect I won’t be the only one who keeps forgetting the incantation. It’s anyway so much easier to remember that a pointer to the security descriptor (albeit with slight alteration) is the last member of the OBJECT_HEADER and is therefore the pointer immediately before the object.

Variability

For a structure that might be an implementation detail for the Object Manager, with no need to be known even elsewhere in the kernel, let alone externally, the OBJECT_HEADER is surprisingly stable. The earliest versions differ—indeed, the OBJECT_HEADER is barely recognisable in version 3.10—but the modern form was well-settled as early as version 3.51. There has been internal reorganisation since then, especially for Windows 7, but it has been done by finding ways to squeeze more in, as if the structure’s size is for all practical effect treated as architectural:

Version Size (x86) Size (x64)
3.10 0x18  
3.50 and higher 0x20 0x38

This size is of the OBJECT_HEADER as a structure, not as a header. If the header is understood as just what the Object Manager places immediately before the object, then the header is 0x18 or 0x30 bytes in 32-bit and 64-bit Windows, respectively, except for being just 0x10 bytes in version 3.10. Note that the header’s size is a multiple of the natural alignment for memory allocation. Callers who ask the Object Manager to create an object of a given type expect the object to have this alignment. If the memory allocation can instead begin with the Object Manager’s own header, then the header too must have this alignment. In practice, the memory allocation begins with one of the header’s headers, which all have this alignment.

Layout

The sizes above and the names and types in the table below are from type information in the public symbol files for the kernel, starting with Windows 2000 SP3. Names are known with slightly less certainty for version 4.0 from the output of the !dso command as implemented by the debugger extension USEREXTS.DLL from the Windows NT 4.0 Device Driver Kit (DDK).

Offset (x86) Definition Versions
0x00 (3.10) unknown dword 0x0B0B0B0B 3.10 only
0x00 (3.50)
ULONG ObjectBodySize : 24;
3.50 only
0x03 (3.50)
ULONG Flags : 8;
3.50 only

The name ObjectBodySize is proposed for what version 3.50 has in the first three bytes. It is taken directly from the ObCreateObjectType argument that is later known to be named ObjectBodySize. That the ObjectBodySize is formally a bit field is almost certain from the binary code. That the Flags are too would be natural and is suggested by some access that tests the whole dword against flags as immediate data. How exactly these two are defined within this first dword, e.g., wrapped in a structure or in union with an integral type, may never be known. The Flags persist as a UCHAR in all later versions.

Offset (x86) Offset (x64) Definition Versions
0x00 0x00
union {
    struct {
        LONG_PTR PointerCount;
        LONG_PTR HandleCount;
    };
    LIST_ENTRY Entry;
};
3.51 to 4.0
LONG_PTR PointerCount;
5.0 and higher
0x04  
union {
    LONG_PTR HandleCount;
    SINGLE_LIST_ENTRY *SEntry;
};
5.0 only
0x08
union {
    LONG_PTR HandleCount;
    PVOID NextToFree;
};
5.1 and higher
0x08 0x10
OBJECT_TYPE *Type;
3.51 to 6.0
EX_PUSH_LOCK Lock;
6.1 and higher

Before version 3.51, the PointerCount, HandleCount and Type are in a separate structure (see below), apparently so that the two counts are in non-paged memory even when the object and its header are not. In all versions that have these members in the OBJECT_HEADER itself, the PointerCount is at offset 0x00 and the HandleCount is at offsets 0x04 and 0x08 (for 32-bit and 64-bit Windows, respectively), but this is a little disguised because either or both of them are in union with one sort or another of list entry.

The PointerCount is of how many times the object has been referenced but not dereferenced. After creation, an object is referenced through such functions as ObReferenceObjectByPointer, ObReferenceObjectByHandle or ObReferenceObjectByName. A reference is required for a kernel-mode user of an object to be sure that the object persists.

The HandleCount is of how many times the object has been opened but not closed. After a handle is created for an object, more are opened through such functions as ObOpenObjectByPointer or ObOpenObjectByName. A handle is required for access from user mode.

The various list entries, i.e., Entry, SEntry and NextToFree, matter only when the counts no longer can. When all of an object’s references are balanced by dereferences, the object has nobody who cares for its continued existence and so the object can be deleted. Although ObDereferenceObject was in the early years—up to and including the DDK for Windows NT 4.0—documented as being callable only at PASSIVE_LEVEL, its implementation even in version 3.10 anticipates being called at higher IRQL. Though dereferencing is immediate, deleting can be deferred. In version 5.1 and higher, deferred deletion can even be forced from within the kernel and in version 6.0 from outside (since ObDereferenceObjectDeferDelete is exported). When deferring an object’s deletion, the Object Manager puts the OBJECT_HEADER into a list. Both counts should be zero and since the object should now be unknown outside the Object Manager, neither count can now change. The space they occupied is available to reuse for linking into the list. The early versions have a double-linked list, inserting at the tail and removing from the head, so that deferred deletions are picked up in the order of their final dereferences. Such ordering is unnecessary: just as nobody should now care about the object’s continued existence, nobody should care how soon it is destroyed. Version 5.0 changes to a single-linked list, which version 5.1 simplifies. Ordinarily, NextToFree points directly to the OBJECT_HEADER for the next object to delete. It can, of course, be NULL at the end. Less obviously, it can be 1 as an implementation detail in how the list of objects to delete is processed without the earlier versions’ repeated acquisition and release of a lock.

Offset (x86) Offset (x64) Definition Versions
0x04 (3.50);
0x0C
0x18
UCHAR NameInfoOffset;
3.50 to 6.0
UCHAR TypeIndex;
6.1 and higher
0x05 (3.50);
0x0D
0x19
UCHAR HandleInfoOffset;
3.50 to 6.0
UCHAR TraceFlags;
6.1 only
union {
    UCHAR TraceFlags;
    struct {
        /*  bit fields, follow link  */
    };
};
6.2 and higher
0x06 (3.50);
0x0E
0x1A
UCHAR QuotaInfoOffset;
3.50 to 6.0
UCHAR InfoMask;
6.1 and higher
0x07 (3.50);
0x0F
 
UCHAR CreatorInfoOffset;
3.50 only
0x1B
UCHAR Flags;
3.51 to 6.2
union {
    UCHAR Flags;
    struct {
        /*  bit fields, follow link  */
    };
};
6.3 and higher
  0x1C
ULONG Spare;
6.2 to 1511
ULONG Reserved;
1607 and higher

The NameInfoOffset, HandleInfoOffset, QuotaInfoOffset and CreatorInfoOffset correspond to the header’s headers (listed in the next paragraph). Each is ordinarily the offset from the start of the OBJECT_HEADER back to the corresponding structure, else is zero if the corresponding structure is not present. As noted above, these offsets must be multiples of eight, even for 32-bit Windows, and so at least three bits in each byte are wasted. Version 6.0 uses this to squeeze a little more meaning into the QuotaInfoOffset: its low two bits are an early implementation of the TraceFlags.

The plain purpose to retaining these structures selectively is to reduce how much memory the Object Manager adds for each object. More savings come from knowing that the header’s headers, whichever are present, are always in a particular order:

Since the OBJECT_HEADER_CREATOR_INFO, if present at all, can only end where the OBJECT_HEADER begins, the CreatorInfoOffset can only be either zero or the structure’s (known) size, and so version 4.0 did away with the waste of keeping a whole byte for it. Indeed, the offsets to each structure can be calculated just from knowing which other structures are present. This needs only one bit for each of the possible structures. Version 6.1 built this into the InfoMask and thus recovered two bytes.

The more notable new use that was opened by this recovery is the TypeIndex since it brought the saving of not keeping a whole pointer for the Type. That each type of object has a 0-based TypeIndex is ancient, just not for the Object Manager’s own purposes. It is instead a sequence number that is convenient to report in the SYSTEM_OBJECTTYPE_INFORMATION structure when objects are enumerated through ZwQuerySystemInformation. and later as the ObjectTypeIndex in the SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX when enumerating handles.

Starting with version 6.1, pointers to the created OBJECT_TYPE structures are kept in an array and the TypeIndex truly is an index. Instead of keeping a pointer in every OBJECT_HEADER just to find the the OBJECT_TYPE, the Object Manager keeps just the 1-byte index. The space that had been taken by the pointer is reclaimed so that for the first time, objects can be locked independenty of one another.

For reasons that are not yet understood well enough to write about with confidence, version 10.0 obfuscates the TypeIndex as kept in the OBJECT_HEADER.

Offset (x86) Offset (x64) Definition Versions
0x04 (3.10);
0x08 (3.50);
0x10
 
OBJECT_CREATE_INFORMATION *ObjectCreateInfo;
3.10 only
0x20
union {
    OBJECT_CREATE_INFORMATION *ObjectCreateInfo;
    PVOID QuotaBlockCharged;
};
3.50 and higher
0x08 (3.10);
0x0C (3.50)
  unknown pointer to non-paged header, see below 3.10 to 3.50
0x10 (3.50);
0x14
0x28
PVOID SecurityDescriptor;
3.50 and higher
0x0C (3.10)   unknown dword 0x1B1B1B1B 3.10 only
0x14 (3.50)   unaccounted dword 3.50 only
0x10 (3.10);
0x18
0x30
QUAD Body;
all

As noted above, the structure is the header and then some. Formally, the object begins at the structure’s Body. Given a C-language definition of the OBJECT_HEADER, locating a given object’s header could be conveniently wrapped into an inline routine:

FORCELINE 
OBJECT_HEADER *OBJECT_TO_OBJECT_HEADER (PVOID Object)
{
    return CONTAINING_RECORD (Object, OBJECT_HEADER, Body);
}

which is essentially the macro that the NTOSP.H disclosure confirms is what Microsoft’s programmers have been using.

Non-Paged Object Header

Both versions 3.10 and 3.50 split the OBJECT_HEADER in two. One part immediately precedes the object and is here taken as the original OBJECT_HEADER, but what later become the PointerCount, HandleCount and Type are in a separate structure. Microsoft’s name for this separate structure is not known (and perhaps never will be). That it is separate is because the object, and thus also its header, may be in paged pool but these versions require that the two counts be in non-paged pool.

Offset Definition Versions
0x00 (3.10 to 3.50)
PVOID Object;
3.10 to 3.50
0x04 (3.10 to 3.50)
LONG PointerCount;
3.10 only
union {
    struct {
        LONG PointerCount;
        LONG HandleCount;
    };
    LIST_ENTRY Entry;
};
3.50 only
0x08 (3.10)
LONG HandleCount;
3.10 only
0x0C (3.10 to 3.50)
OBJECT_TYPE *Type;
3.10 to 3.50

The name Object is proposed as obvious for a pointer back to the object. The other members have direct counterparts in the OBJECT_HEADER as known for later versions.

Why are the counts in non-paged pool even when the object is not? One reason, here thought to be the main reason, is that these versions increment and decrement by using the ExInterlockedIncrementLong and ExInterlockedDecrementLong functions. These require a spin lock which they may acquire to protect the operation from concurrent access by another process. The lock and the count must therefore be resident. Both functions were already documented as obsolete as long ago as the Device Driver Kit (DDK) for Windows NT 3.51. Contemporaneous documentation of what were then the new InterlockedIncrement and InterlockedDecrement functions is explicit that these can “be safely used on pageable data” (but is only implicit that the old functions cannot). Even for version 3.10, the old functions are almost certainly redefined by macro to lose the spin lock and to operate on the count by using the undocumented Exi386InterlockedIncrementLong and Exi386InterlockedDecrementLong. The separate existence of a non-paged part to the header therefore looks to be a side-effect of using the documented ExInterlockedIncrementLong and ExInterlockedDecrementLong for portability.

Another reason for a separate structure that is necessarily in non-paged pool may be incidental. As already noted, if the dereference that brings the PointerCount to zero occurs at high IRQL, then the object’s deletion is deferred. In version 3.10, it is queued as a work item. The WORK_QUEUE_ITEM must be in non-paged pool. Version 3.10 simply repurposes the non-paged header (which is conveniently the right size). Whether this repurposing is modelled in the structure’s definition is, of course, not known. It anyway doesn’t survive even to version 3.50. Instead of wastefully queueing a separate work item for each object that is to be deleted, version 3.50 puts all such objects into a double-linked list and has one statically allocated work item drain the list.