XXNU Source Handbook Download offline version from here
Part 12 · Chapter 12.04

IOKit object runtime, class registration, and collection ownership

This chapter follows an object from allocation through collection membership and final release, then connects those rules to class lookup and module lifetime. Read the libkern introduction first; registry planes and properties applies these contracts to the registry, and service notifications applies them to retained callback snapshots. The implementations studied here are the kernel C++ runtime in this checkout, including KERNEL_PRIVATE allocator macros; they do not describe the separately supplied userspace DriverKit runtime. OSMetaClass.h.

1. Construction, initialization, and destruction are separate steps

An OSObject begins with retainCount = 1. The generated concrete constructor calls the superclass constructor with its metaclass and increments that concrete metaclass's instance count. The generated MetaClass::alloc() performs new className; it does not call init(). OSMakeShared<T>() similarly calls OSTypeAlloc(T) and adopts the initial reference with OSNoRetain. A typical factory must therefore check initialization separately; OSArray::withCapacity() and OSCollectionIterator::withCollection() illustrate the cleanup convention. OSObject.cpp, constructor macros, OSSharedPtr.h, OSArray.cpp, OSCollectionIterator.cpp.

The macros make the boundary visible: constructing a class records its existence, while initialization remains a caller responsibility. OSDefineBasicStructors.

#define OSDefineBasicStructors(className, superclassName)       \
	OSObject * className ::MetaClass::alloc() const             \
	{ return new className; }                                   \
	className :: className () : superclassName (&gMetaClass)    \
	{ gMetaClass.instanceConstructed(); }

The default OSObject::init() succeeds, with optional tracking. The final reference-release path invokes virtual free(), and the base OSObject::free() decrements the most-derived metaclass's instance count before delete this. Consequently subclasses normally release owned resources in their free() override and eventually call the superclass implementation; calling delete directly would bypass the reference-release protocol and the base free() accounting. This is an implementation consequence of the visible paths, rather than a claim that C++ forbids direct deletion. init/free, final release.

IOKit object runtime, class registration, and collection ownership diagram
IOKit object runtime, class registration, and collection ownership diagram · Open SVG
Mermaid source
flowchart TD
    A[Generated MetaClass alloc] --> B[new class: allocation + constructor]
    B --> C[Initial reference 1; instanceConstructed]
    C --> D{Caller init succeeds?}
    D -- no --> E[Release initial owner]
    D -- yes --> F[Publish retained references]
    F --> G[Owners release references]
    E --> H[Final release marks freeing]
    G --> H
    H --> I[Virtual subclass free]
    I --> J[OSObject free: instanceDestructed]
    J --> K[delete and matching allocator free]

The generated kernel-private allocator creates a KALLOC_TYPE_DEFINE view and calls OSObject_typed_operator_new/delete. The implementation uses the typed external entry point when the requested size fits the view. A subclass that inherits an allocator without declaring its own structor macros can request a larger size; that case falls back to KHEAP_DEFAULT, with a corresponding sized-free fallback. IOTRACKING can route both operations through the tracking allocator instead. The non-KERNEL_PRIVATE macro uses the base OSObject allocator; the separate WithZone macros use a class zone when available. These are concrete configuration distinctions, so “every OSObject allocation has a unique typed zone” is not a supported conclusion. allocator macros, allocation/deallocation, typed allocation policy.

2. The reference word includes collection membership

retainCount is treated as two 16-bit quantities inside one atomic update: the low half is the ordinary reference count, and a retain tagged with OSTypeID(OSCollection) increments the high half as well. A collection reference therefore contributes to both total ownership and the collection-membership count. getRetainCount() exposes only the low half. The extra bucket detects a class of bad releases: after decrement, the low count must not be less than the collection count. It is a diagnostic invariant, not a garbage collector or a complete use-after-free detector. OSObject.cpp.

taggedTryRetain() computes a CAS update; the tag changes the increment: taggedTryRetain.

bool
OSObject::taggedTryRetain(const void *tag) const
{
	volatile UInt32 *countP = (volatile UInt32 *) &retainCount;
	UInt32 inc = 1;
	UInt32 origCount;
	UInt32 newCount;

	// Increment the collection bucket.
	if ((const void *) OSTypeID(OSCollection) == tag) {
		inc |= (1UL << 16);
	}

Two low-count values have special meaning. 0xffff marks an object being freed: try-retain fails, ordinary tagged-retain panics, and a release seeing that value returns. 0xfffe pegs an overflowing count in non-debug builds, deliberately leaking instead of wrapping to a false final reference; debug builds panic. The final release CAS writes 0xffff before invoking free(), so concurrent reference updates observe the terminal marker rather than a conventional zero. None of this grants permission to call try-retain through arbitrary freed storage: the caller still needs a protocol that keeps the memory address valid while attempting the operation. retain handling, release handling.

The release operation takes a when threshold, normally one. It subtracts the tagged amount and chooses the terminal value if the remaining low half is below that threshold. This matters when reading specialized subclasses or release(when): the implementation is not simply an unconditional atomic decrement followed by a test for zero. OSObject.cpp, release wrappers.

OSSharedPtr supplies ordinary intrusive retain/release; OSTaggedSharedPtr<T, OSCollection> supplies collection-tagged operations. OSRetain means acquire an additional reference; OSNoRetain means adopt an existing one. An rvalue OSDynamicPtrCast transfers the source reference on successful cast using detach(), whereas the lvalue form acquires another reference. The wrapper's reset(p, retain) retains p before releasing its old object, which makes self-replacement safe at that level. retainers and casts, intrusive reset.

Operation Ownership result Implementation
OSMakeShared<T>() Adopts new/OSTypeAlloc's initial reference; does not initialize the object OSSharedPtr.h
OSSharedPtr(p, OSRetain) Adds an ordinary reference retainer
array->setObject(p) Adds a collection-tagged reference on successful insertion OSArray.cpp, array storage type
array->getObject(i) Returns a borrowed pointer OSArray.cpp
array->removeObject(i) Drops the array's reference after moving remaining entries OSArray.cpp
OSCollectionIterator::withCollection(c) Retains c; returned elements remain borrowed iterator initialization, next

3. Collections own members but do not serialize callers

OSArray::setObject(index, object) rejects null objects and indices beyond count, ensures capacity before mutation, increments the update stamp, shifts entries with move operations, and retains the inserted object. merge() checks count overflow before extending storage. Replacement retains the new value through the shared-pointer helper. Removing an element first moves its owner into a local variable, shifts subsequent entries, and releases the removed object when that local goes out of scope. OSArray.cpp.

A dictionary owns both keys and values. Its normal symbol-key lookup compares interned symbol identities. The body uses binary search when kSort is enabled and a linear scan otherwise, despite the introductory header's simpler statement that dictionaries use linear search. The onlyAdd variant refuses replacement; regular replacement updates the stamp and resets the value. New entries grow capacity first, then install retained key and value owners. OSDictionary.cpp, dictionary fields, header description.

The mutation stamp supports invalidation detection, not mutual exclusion. OSCollection::haveUpdated() increments updateStamp; if kImmutable is set, it normally panics, with the kOSRegistryModsMode debug option reporting a backtrace and continuing. OSArray::setOptions() and OSDictionary::setOptions() propagate changed options into child collections. This explains why publishing a nested collection into a registry property can change the allowed operations on that same collection object retained elsewhere. haveUpdated, array options, dictionary options, registry setProperty.

The check is intentionally visible here because it is sometimes mistaken for a lock: OSCollection::haveUpdated.

void
OSCollection::haveUpdated()
{
	if (fOptions & kImmutable) {
		if (!(gIOKitDebug & kOSRegistryModsMode)) {
			panic("Trying to change a collection in the registry");
		} else {
			OSReportWithBacktrace("Trying to change a collection in the registry");
		}
	}
	updateStamp++;
}

OSCollectionIterator retains the collection and lazily initializes its iteration storage. isValid() compares the stored stamp to the current stamp. getNextObject() returns null both when invalid and when exhausted; callers that need a complete scan must distinguish those cases with isValid(). Reset reinitializes storage and captures a new stamp. A concurrent writer can still race the storage access itself, so a stamp check does not make unsynchronized enumeration safe. OSDictionary's documented concurrency restriction and the absence of locking in these operations make the required external synchronization explicit. iterator lifecycle, getNextObject, dictionary restriction.

For callback iteration, OSCollection::iterateObjects() uses a small local iterator and compares the update stamp after each callback. Its Boolean result means the stamp stayed unchanged; a callback may have requested an early stop. It does not mean every member was visited. That distinction is useful when reviewing a helper that returns the iteration result as a higher-level success condition. OSCollection.cpp.

4. Copying the container graph is not cloning every object

OSArray::withArray() and OSDictionary::withDictionary() make new containers whose elements retain the original objects. copyCollection() goes further for nested collections: it creates a cycle dictionary if necessary, checks whether the original collection was already copied, creates a shallow container copy, records original-address-to-copy identity, then recursively replaces child collections. Non-collection objects remain shared. The address keys in the private cycle dictionary are internal identity tokens, not ordinary external registry property names. array copy, array recursive copy, dictionary recursive copy, base lookup.

The crucial order is “record this copy, then recurse,” which prevents repeated descent through the same collection cycle: OSArray::copyCollection.

		newArray = OSArray::withArray(this);
		if (!newArray) {
			continue;
		}

		// Insert object into cycle Dictionary
		cycleDict->setObject((const OSSymbol *) this, newArray.get());

		for (unsigned int i = 0; i < count; i++) {
			OSCollection *coll =
			    OSDynamicCast(OSCollection, static_cast<OSObject *>(
				    const_cast<OSMetaClassBase *>(
					    newArray->array[i].get())));

			if (coll) {
				OSSharedPtr<OSCollection> newColl = coll->copyCollection(cycleDict);
				if (!newColl) {
					return ret;
				}

				newArray->replaceObject(i, newColl.get());
			}
		}

Conceptually, if two properties refer to the same nested array, a successful graph copy can preserve that shared nested identity through the cycle dictionary. If they refer to an OSData, that data object is retained, not byte-cloned by this routine. This is a reading example derived from the dispatch on OSDynamicCast(OSCollection, ...), not output from an executed test. A custom collection subclass that does not override the base copy routine cannot be assumed to provide deep-copy behavior; the base fallback can return itself or only a prior cycle-dictionary result. OSArray.cpp, OSCollection.cpp.

Serialization uses a related but separate identity table. Array and dictionary serializers call previouslySerialized(this) before descending. The XML path emits an IDREF for an object already in the serializer's tag array; the binary case dispatches to binarySerialize. This preserves reference identity in the serialized representation, but does not itself lock the source graph. For registry properties, a separate snapshot step provides the intended ownership boundary. OSSerialize.cpp, array serializer, dictionary serializer, registry serialization.

5. Metaclasses are registered in a module-load transaction

An OSMetaClass records class size, superclass link, and initially a raw class-name string stored in the className slot. It cannot yet be treated as an OSSymbol. preModLoad() takes sStalledClassesLock, allocates a stalled-class array, and leaves that lock held until postModLoad(). Constructors append metaclasses to that array; the array grows in fixed increments. checkModLoad() verifies the handle and stored result. The constructor logs a missing preModLoad() rather than silently treating registration as complete. constructor, load setup.

postModLoad() performs the decisive registration sequence under sAllClassesLock: it checks for duplicate names, computes the maximum class depth, and only after the duplicate-check pass converts names to symbols and inserts metaclasses into the global class dictionary. It resolves the owning OSKext and calls addClass(), while deliberately storing an unretained pointer to the kext. Duplicate handling differs by target: the OSX branch logs the duplicate; the other branch invokes panic. The second pass can still fail in addClass(), so the first-pass duplicate check should not be described as a general all-or-nothing rollback guarantee. OSMetaClass.cpp.

At completion it releases temporary kext/name references, frees the stalled storage, clears the global stalled pointer, and unlocks sStalledClassesLock. A metaclass destructor removes its name from the class dictionary, removes it from its owning kext, releases the symbol only after that operation, and removes itself from the stalled list if construction is being unwound. This handles the distinction between a still-raw class name and an installed symbol. post-load cleanup, destructor.

Named allocation needs a separate pin: ordinary OSMetaClass::retain/release are empty overrides. copyMetaClassWithName() increments reserved->retain while holding sAllClassesLock; allocClassWithName() calls meta->alloc() while that pin is held and then calls releaseMetaClass(). removeClasses() refuses removal if a class has either instances or this special retain count. Merely obtaining a raw pointer with getMetaClassWithName() does not acquire that pin. empty overrides, lookup/allocation, removal check.

The named allocation handoff is short enough to inspect whole: allocClassWithName.

OSObject *
OSMetaClass::allocClassWithName(const OSSymbol * name)
{
        const OSMetaClass * meta;
        OSObject          * result;

        result = NULL;
        meta = copyMetaClassWithName(name);
        if (meta) {
                result = meta->alloc();
                meta->releaseMetaClass();
	}

        return result;
}

6. Casts, instance counts, and service-instance enumeration

OSDynamicCast ultimately asks whether the requested metaclass appears in the object's getMetaClass()->superClassLink chain. safeMetaCast returns null for a null input or incompatible object; requiredMetaCast still returns null for null input but panics on an incompatible non-null object. This is the IOKit class metadata path, rather than a lookup through the registry or an ownership operation. A successful raw cast does not retain the object. safe/required casts, metaclass chain.

Instance accounting has a subtle aggregate meaning. instanceConstructed() propagates to the superclass only when the current class transitions from zero to one; instanceDestructed() propagates only when one transitions to zero. A superclass therefore counts its own direct instances plus nonempty immediate descendant branches, recursively, rather than counting every descendant object. The useful unload predicate is zero versus nonzero. OSKext::hasOSMetaClassInstances() checks that predicate across the module's classes, and autounload also checks references, policy flags and request callbacks. instance counters, module predicate, autounload gates.

The transition-based propagation is explicit: instanceConstructed/instanceDestructed.

void
OSMetaClass::instanceConstructed() const
{
        // if ((0 == OSIncrementAtomic(&(((OSMetaClass *) this)->instanceCount))) && superClassLink)
        if ((0 == OSIncrementAtomic(&instanceCount)) && superClassLink) {
                superClassLink->instanceConstructed();
	}
}

/*********************************************************************
*********************************************************************/
void
OSMetaClass::instanceDestructed() const
{
        if ((1 == OSDecrementAtomic(&instanceCount)) && superClassLink) {
                superClassLink->instanceDestructed();
	}

        if (((int)instanceCount) < 0) {
                OSKext * myKext = reserved->kext;

                OSKextLog(myKext, kOSMetaClassLogSpec,
                    // xxx - this phrasing is rather cryptic
                    "OSMetaClass: Class %s - bad retain (%d)",
                    getClassName(), instanceCount);
	}
}

The service-instance index is a different mechanism. OSMetaClass::addInstance() lazily creates an ordered set, installs that set into its superclass's instance tree, and inserts the object. removeInstance() tears down empty sets. applyToInstances() walks this hierarchy under sInstancesLock; its helper uses a 24-level local traversal stack, allocating larger storage if sDeepestClass requires it. Named traversal obtains a retained set under sAllClassesLock before acquiring the instances lock. instance-tree operations.

For IOService, the insertion happens at first publication in doServiceMatch(), and removal happens on detach when the service is marked inactive. It is therefore incorrect to equate this index with every constructed OSObject. copyExistingServices() uses it to obtain matching service candidates; the new notification chapter follows that path through replay. publication, detach, candidate enumeration.

Reading route and inspection limits

Start with OSDefineBasicStructors, OSObject::taggedRelease, and OSObject::free; then inspect one array insertion/removal and OSCollectionIterator::isValid. Continue into copyCollection, metaclass preModLoad/postModLoad, and finally copyMetaClassWithName/removeClasses. That route separates memory ownership, mutable-container synchronization, and executable-class lifetime before introducing the service-instance index.

For dictionary-key identity and its separate backing lifetime, continue with symbol interning. It follows ordinary/huge lookup, duplicate creation, no-copy/permanent strings, pool resizing, module-unload adjustment and SMR-zone destruction. The symbol's deferred cleanup deliberately differs from the ordinary OSObject::free() route above.

Source inspected Depth here Limits
OSObject.cpp, OSMetaClass.h, OSSharedPtr.h, intrusive-pointer reset Allocation, initialization, reference transition and final-free bodies Tracking internals and every macro variant are peripheral
OSMetaClass.cpp, selected OSKext.cpp predicates Registration, casts, pins, counters and instance-tree traversal Full module load/unload and linker cleanup remain in libkern/kext overview
OSArray.cpp, OSDictionary.cpp, OSCollection.cpp, OSCollectionIterator.cpp Mutation, tagged ownership, copies, options, enumeration Other container algorithms are not exhaustively reviewed
OSSerialize.cpp, registry/service call sites Identity handling and consumers of container ownership Binary wire format, parser validation and userspace runtime are outside this chapter

This is a source review, not an executed allocation, unload, or concurrent-mutation test. Error paths are described where inspected; the chapter does not claim that collection cycles are automatically reclaimed or that reference counts replace caller locking.

Search the handbook

Search every chapter, interface and code excerpt.

    Diagram

    Source preview

    Open full file