XXNU Source Handbook Offline reading instructions
Part 04 · Chapter 04.30

Typed allocation: from C layouts to zones and VM ranges

kalloc_type attaches a compiler description of a C/C++ memory layout to an allocation callsite. Startup code turns that description into a zone or heap choice. The runtime then uses that precomputed choice, the requested size, and accounting state to select storage. This reduces which allocations can reuse the same storage, but the implementation deliberately groups types: it does not create one allocator for every C type. The fixed and variable paths also have different free-time checks. Start with the declarations in kalloc.h, then follow startup assignment and runtime dispatch.

This page follows the implementation in this checkout. Kernel allocation explains the public interface choices; the zone allocator explains element storage, caching, and reclamation; kernel virtual allocation continues the large-allocation path through kmem. Four distinct identifiers recur below:

Identifier What it describes What it does not promise
kt_zv.zv_name / kt_name An emitted string such as site.struct request; used to group metadata records during startup A globally unique C++ dynamic type or an allocation identity
kt_signature, kt_sig_hdr, kt_sig_type Compiler descriptions of pointer/data/PAC placement in 8-byte granules Member names, object lifetime, ownership, or arbitrary program semantics
z_sig_eq A group of fixed-size zones whose allocations may be cross-freed by the typed free path One unique layout signature per group
High 16 bits of kt_flags A compact VM range/direction choice derived from variable-type signatures A unique type ID stored with, and checked against, every object

These roles are visible in metadata emission, signature equivalence assignment, typed free, and VM hash construction.

1. A source expression emits a metadata record

The interface has three allocation shapes. The spelling of the shape matters even when two expressions request the same number of bytes:

Expression Metadata section Runtime size Main internal path
kalloc_type(T, flags) __kalloc_type sizeof(T) Fixed view → kalloc_type_impl_internal → zalloc_ext
kalloc_type(T, count, flags) __kalloc_var sizeof(T) * count, with overflow checking Variable view → kalloc_ext → zone or kalloc_large
kalloc_type(H, T, count, flags) __kalloc_var sizeof(H) rounded up to _Alignof(T), plus sizeof(T) * count Same variable dispatch, with separate header/element signatures

The macros generate the static view at the callsite; free macros generate their own corresponding view. kalloc_type(T, flags) and kalloc_type(T, 1, flags) therefore belong to different allocation families. The API documents that the matching free form must be used. API contract, size arithmetic and fixed macros, array and header-array macros.

The fixed emission is small enough to show in full. It creates an array containing one view, records a stringized type name, and leaves the runtime-assigned fields zero-initialized. The live_support section attribute is part of the emitted section declaration; this is not a manually maintained registry. Fixed metadata macro.

#define _KALLOC_TYPE_DEFINE(var, type, flags)                       \
	__kalloc_no_kasan                                               \
	__PLACE_IN_SECTION(KALLOC_TYPE_SEGMENT ", __kalloc_type, "      \
	    "regular, live_support")                                    \
	struct kalloc_type_view var[1] = { {                            \
	    .kt_zv.zv_name = "site." #type,                             \
	    .kt_flags = KALLOC_TYPE_ADJUST_FLAGS(flags, type),          \
	    .kt_size = sizeof(type),                                    \
	    .kt_signature = KALLOC_TYPE_EMIT_SIG(type),                 \
	} };                                                            \
	KALLOC_TYPE_SIZE_CHECK(sizeof(type));

KALLOC_TYPE_SEGMENT is __DATA_CONST for the kernel build and __DATA for __BUILDING_XNU_LIBRARY__. The header documents that views become protected by lockdown and cannot be initialized dynamically. Startup is the mutation phase: it fills zone pointers, accounting pointers, hashes, and processed flags before ordinary allocation consumes them. Segment choice, view lifetime contract, assignment bodies.

The fixed record embeds a zone_view: zv_zone is the assigned zone, zv_stats is the accounting destination, and zv_name identifies the emitted type expression. kt_zearly records the shared initial-allocation zone; kt_zsig identifies the signature-equivalence zone. Startup packs the fixed-size class index into the top byte of kt_size; accessors mask off that byte when retrieving the original size. The other 24 bits are the size, not a per-object allocation count. Fixed structure, class packing, size accessors.

The variable record instead carries kt_size_hdr, kt_size_type, kt_sig_hdr, kt_sig_type, kt_heap_start, kt_stats, and linkage for the heap's view list. kt_heap_start is the first zone ID of a contiguous size-class family. Although the structure still has a kt_zones[] field, the assignment and allocation bodies examined here use kt_heap_start; they do not consult that array for per-size routing. Variable structure, assignment, runtime access.

For array sizes, kt_size uses checked multiply-and-add. Overflow or a result with the high size bit set produces a sentinel size which kalloc_large rejects. Header-plus-array allocation explicitly rounds the header to the element alignment before this calculation. There is no unchecked sizeof(T) * count in these wrappers. Arithmetic, header-array use, large rejection.

2. What the compiler signature classifies

The header defines the interface to __builtin_xnu_type_signature and __builtin_xnu_type_summary. A signature describes the layout in 8-byte granules. Each granule's encoding is the bitwise OR of its contained member categories. The declared category values are padding 0, ordinary pointer 1, scalar data 2, currently unused dual 4, and PAC pointer 8. This permits mixed encodings: a granule containing both pointer and scalar categories differs from either pure category. Compiler contract and encodings.

The summary is a presence bitmap of granule encodings. KT_SUMMARY_GRANULE_TO_IDX(g) contributes 1UL << g, and only the low 16 summary bits are used for these layout checks. KT_SUMMARY_MASK_DATA permits padding and scalar-data granules; KT_SUMMARY_MASK_PTR permits padding, ordinary-pointer, and PAC-pointer granules. A type qualifies only if no summary bit lies outside the relevant allowed mask. The test is thus about every granule, not whether the type contains at least one pointer. Summary masks, classification test.

KALLOC_TYPE_HAS_OVERLAPS uses another mask to detect granules containing different categories. “Overlapping” here includes different-category members sharing a granule; it does not by itself establish overlapping C storage or an invalid union. The compiler implementation is outside this repository, so the guide treats the header as the compiler/kernel contract rather than claiming to verify the compiler's handling of every language feature. Overlap definition.

The flags are constructed from that summary and compile-time size tests. For the header-plus-element form, the data and pointer predicates must hold for both types. KT_VM is set by sizeof(T) > KHEAP_MAX_SIZE, or sizeof(H) + sizeof(T) > KHEAP_MAX_SIZE for the two-type form. It is a compile-time metadata classification, not the runtime array length test. Classification macros, adjusted flags.

#define KALLOC_TYPE_ADJUST_FLAGS(flags, ...)                                 \
	KALLOC_TYPE_CAST_FLAGS((flags | KT_CHANGED | KT_CHANGED2 |               \
	(KALLOC_TYPE_CHECK(KT_SUMMARY_MASK_DATA, __VA_ARGS__)? KT_DATA_ONLY: 0) |\
	(KALLOC_TYPE_CHECK(KT_SUMMARY_MASK_PTR, __VA_ARGS__)? KT_PTR_ARRAY: 0) | \
	(KALLOC_TYPE_VM_SIZE_CHECK(__VA_ARGS__)? KT_VM : 0)))

The flags relevant to the rest of the path are:

Flag Meaning in the inspected path
KT_DATA_ONLY Route compiler-classified data/padding storage to the private data allocation family
KT_PTR_ARRAY Qualifies a variable view for the special pointer-only heap pool; it is also present on qualifying fixed views, but the fixed policy does not use that pool
KT_VM Skip ordinary boot-time zone/signature assignment for this compile-time-large view
KT_CHANGED, KT_CHANGED2 Required metadata-version markers checked while ingesting records
KT_PROCESSED The startup parser saw the record; this is set even for views subsequently skipped because of KT_VM
KT_PRIV_ACCT, KT_SHARED_ACCT, KT_DEFAULT Choose private or shared statistics according to the assignment code and kt options
KT_NOEARLY Fixed-view request to bypass shared initial storage; assignment requires private accounting
KT_HASH Upper 16 bits reserved for the variable-view VM placement hint

See flag definitions, version validation, parser filtering, and fixed accounting setup.

Two compile-time choices prevent misleading interpretations of the metadata. First, KALLOC_TYPE_EMIT_SIG emits an empty string for data-only or compile-time-large views. Such views are intentionally not part of the ordinary signature sorting. Second, a pointer-bearing header followed by a data-only trailing array is rejected by KALLOC_TYPE_TRAILING_DATA_CHECK. Callers must choose an appropriate allocation design instead of silently treating a large byte tail as pointer-bearing typed storage. Signature omission, trailing-data check.

#define KALLOC_TYPE_TRAILING_DATA_CHECK(hdr_ty, elem_ty)     \
	_Static_assert((KALLOC_TYPE_IS_DATA_ONLY(hdr_ty) ||  \
	    !KALLOC_TYPE_IS_DATA_ONLY(elem_ty)),             \
	"cannot allocate data-only array of " #elem_ty       \
	" contiguously to " #hdr_ty)

These are layout classifications. As a consequence, an integer used by a program to encode an address is still described through its declared scalar type; the allocator does not inspect stored values to discover semantic pointers. Likewise, data-only types are not separated from each other by their type names: their destination is the private data family. This conclusion follows from the compiler-category contract and the unconditional data redirection in kalloc_type_handle_data_view_fixed.

3. Startup discovers and transforms the records

kalloc_init runs at STARTUP(ZALLOC, STARTUP_RANK_THIRD, ...). It computes heap size classes; creates private-data, shared-data, and EARLY heaps; allocates a 256 KiB scratch buffer; processes fixed views; clears and reuses the scratch buffer for variable views; and releases it. Fixed initialization also chooses the boot-random signature-hash seed, builds the fixed direct lookup table, sorts surviving views, constructs group boundaries, and creates/assigns zones. Scratch size, fixed initialization, startup sequence.

The parser is sensitive to kernel-collection format. With KCFormatStatic, it consumes coalesced section boundaries. For KCFormatFileset, it traverses LC_FILESET_ENTRY images in the primary collection and locates each image's appropriate section. The KCFormatKCGEN branch processes the kernel and traverses the prelinked extension Mach-O layout, applying slide handling where requested. Unsupported dynamic/unknown collection formats cause the parser to panic. The KCGEN source contains historical armv7 commentary, but the concrete branch should be read together with its compilation environment rather than generalized to every ARM platform. Collection parsing.

For every record, kalloc_type_view_copy validates the required changed-version bits and marks it processed. A KT_VM record is then skipped. A data-only record is assigned directly to the data family and skipped. Only remaining records receive the ordinary hash/index processing and enter the scratch pointer array. This ordering matters: KT_PROCESSED does not prove that a view has an assigned typed zone, and an oversized variable element does not acquire the usual signature hash merely because its view was processed. Filtering and transformations.

		kalloc_type_flags_t kt_flags = kalloc_type_func(type, get_flags, cur,
		    kext_uuid);
		kalloc_type_func(type, mark_processed, cur);
		/*
		 * Skip views that go to the VM
		 */
		if (kalloc_type_from_vm(kt_flags)) {
			cur += kalloc_type_func(type, view_sz);
			continue;
		}

The flow is:

Typed allocation: from C layouts to zones and VM ranges diagram
Typed allocation: from C layouts to zones and VM ranges diagram · Open SVG
Mermaid source
flowchart TD
    A[Static callsite metadata] --> B[Collection parser and version validation]
    B --> C[Set KT_PROCESSED]
    C --> D{KT_VM?}
    D -->|yes| E[Leave ordinary zone assignment unset]
    D -->|no| F{KT_DATA_ONLY?}
    F -->|yes| G[Assign private-data family]
    F -->|no| H{Fixed or variable?}
    H -->|fixed| I[Size class + prefix signature groups + name groups]
    I --> J[Budgeted fixed zones and signature equivalence]
    H -->|variable| K[Signature hash + pointer-only or exact signature-pair groups]
    K --> L[Assign heap start]
    J --> M[Runtime view]
    L --> M
    G --> M
    E --> M

The two branches are implemented by kalloc_type_view_init_fixed and kalloc_type_view_init_var. They run once for the ingested metadata; runtime allocation does not sort type descriptions.

4. Fixed allocations: size classes, layout groups, and a finite zone budget

Build the grouping keys

Fixed types use kt_zone_cfg, a separate class table from the ordinary heap sequence. For example, its small classes include 80, 160, 224, 288, 368, 400, and 576 bytes, which are not all present in the regular variable-heap progression. The table extends through 16 KiB, with 24 and 32 KiB entries on arm64; KASAN_CLASSIC adjusts some class capacities for guards. Startup builds a direct lookup table and packs the resulting class index into each surviving fixed view. Fixed size classes, lookup construction and packing.

kalloc_type_cmp_fixed orders records by size class, then lexicographic signature, then emitted name. The next pass creates groups within each size class. Despite a comment using “substring,” its actual comparison is a prefix test: the previous signature must match the beginning of the current one. The pass then remembers the current signature, so successive longer signatures can remain in the same group. It does not search for a signature at an arbitrary offset. Comparator, group builder.

The decisive branch and the update to the comparison prefix are shown here. Prefix grouping.

		if (strncmp(c_sig, p_sig, strlen(p_sig)) != 0) {
			/*
			 * Unique signature detected. Update counts and advance index
			 */
			unique_sig++;
			total_sig++;

			*kt_skip_list = i;
			kt_skip_list++;
			p_sig = c_sig;
			p_name = c_name;
			continue;
		}
		/*
		 * Need this here as we do substring matching for signatures so you
		 * want to track the longer signature seen rather than the substring
		 */
		p_sig = c_sig;

The two per-class “frequency” arrays are counts of the parsed metadata's groups, not observations of runtime allocation rates. kt_freq_list counts signature groups. kt_freq_list_total records additional emitted-name groups beyond one per signature group (total_sig - unique_sig). Repeated allocation/free callsites with the same signature and emitted name therefore do not consume budget as independent types. More exactly, the implementation counts name changes in the sorted regions; it is not building a language-level global symbol table. Count updates.

Divide the zone budget

The default fixed-zone budget is 120 for watch targets or KASAN_CLASSIC, and 260 otherwise. kt_fixed_zones can supply the tunable value. These numbers are a budget for the new fixed typed zones, not the total number of all XNU zones or a promise that each C type gets one. The policy splits the budget into 70% for signature grouping and the remainder for additional separation among name groups. Configuration, tunables, policy entry.

	uint16_t zbudget_sig = (uint16_t) ((7 * zone_budget) / 10);
	uint16_t zbudget_type = zone_budget - zbudget_sig;

The signature-budget distribution first grants up to two zones per populated size class, capped by that class's group count. It caps the overall available budget by the total count, then distributes remaining zones proportionally to counts beyond those minima. An accumulated remainder distributes integer leftovers; the per-class result is capped by its count. The name-separation distribution uses the same routine with minimum zero. A subsequent pass borrows from the three largest name-zone allocations to give zones to classes with additional names but no such zone. These are static balancing heuristics, not per-type reservation guarantees. Distribution, redistribution.

The policy calls make the different minima explicit. Budget application.

	wasted_zones += kalloc_type_distribute_budget(freq_list, kt_zones_sig,
	    zbudget_sig, 2);
	wasted_zones += kalloc_type_distribute_budget(freq_total_list,
	    kt_zones_type, zbudget_type, 0);
	kalloc_type_redistribute_budget(freq_total_list, kt_zones_type);

For each populated fixed size class, kalloc_type_create_zone_for_size creates n_zones_sig + n_zones_type zones named kalloc.type<index>.<size>, marked ZC_KALLOC_TYPE. With the default shared-fixed setting, it also finds an EARLY zone of exactly that element size, creating an additional shared zone when the regular EARLY heap does not have that exact class. This shared zone is separate from the typed-zone budget. Creation and shared zone, typed-zone flag handling.

Assign groups with shuffling

kalloc_type_create_zones_fixed shuffles signature-group indices, then assigns the resulting groups round-robin over signature zones. If more signature groups exist than signature zones, distinct groups share a signature zone. The randomized order varies which groups collide; it does not remove collisions. Each signature zone is assigned its own zone ID as its z_sig_eq value. Outer shuffle and assignment, equivalence initialization.

Within a signature group, kalloc_type_build_skip_list locates emitted-name runs. kalloc_type_zones_for_type apportions the size class's additional type zones according to the group's share of those names, with a carry used for integer rounding. The denominator passed by the caller includes both the additional-name count and the signature-group count. A group receiving no extra zones stays entirely in the signature zone. A group with one name and an extra zone uses its first extra zone. Otherwise, names are spread across the extra zones plus the signature zone, with a fresh permutation for each complete cycle. Name boundaries, apportionment, inner assignment.

The inner loop's permutation affects assignment, not each subsequent allocation. Name-to-zone selection.

		if (zidx == 0) {
			kmem_shuffle(shuffle_buf, n_zones);
		}

		shuffled_zidx = shuffle_buf[zidx];
		zone = shuffled_zidx == 0 ? sig_zone : type_zones[shuffled_zidx - 1];
		kalloc_type_assign_zone_fixed(kt_type_start, kt_type_end, zone, sig_zone,
		    early_zone);

Every extra type zone receives the z_sig_eq of its group's signature zone. Consequently, if two different signature groups collided in that signature zone, their extra zones also belong to the same equivalence class. kalloc_type_assign_zone_fixed stores the assigned zone, signature zone, EARLY zone, and statistics in each view and validates this relationship. This is the structure the free path later recognizes. Extra-zone equivalence, view assignment.

A conceptual example, not measured boot output: suppose three 16-byte layouts describe (pointer, scalar), (pointer, scalar), and (scalar, pointer). The first two can have different emitted names while sharing a layout signature; the third belongs to another signature group. Budget and shuffle decisions may separate the first two into different assigned zones, and may still put the third into the same broader equivalence group if signature zones are reused. Their ordinary small allocations can also initially use the same EARLY size class. The example illustrates the algorithm's possible relationships; it does not predict any particular build's assigned zone numbers. Grouping, finite assignment, runtime EARLY choice.

5. Variable allocations: whole heap families and special classes

A variable allocation chooses a heap family at startup and a size class at runtime. Ordinary heap classes start at 16 and 32 bytes, then follow two classes per power-of-two interval, such as 48/64, 96/128, and 192/256. KHEAP_MAX_SIZE is 16 KiB on x86_64 and 32 KiB otherwise in these definitions. A sufficiently long array therefore leaves its heap's zones and enters kalloc_large, even though its element's metadata was an ordinary small type. Heap geometry, zone lookup, zone/VM dispatch.

kalloc_type_heap_init creates the variable heap zone families before parsing views, because data and pointer-only views can be redirected while processing. It also creates shared statistics for KHEAP_KT_VAR. In kalloc_type_heap_array, indices zero and one are fake bookkeeping entries for private and shared data; indices two and three are pointer-only heaps; index four is the first flexible heap. The fake data entries link views but do not replace the separately initialized data heaps. Heap IDs, heap initialization, data bookkeeping.

Data-only fixed and variable views bypass signature-based separation and use KHEAP_DATA_PRIVATE. “Private” describes the intended security-domain sharing policy, not private accounting or an exclusive heap for the C type. Shared data requires the dedicated shared-data APIs; the typed compiler flags do not automatically infer that an object will be shared with userspace or a device. Data handlers, runtime heap choice, explicit-sharing contract, external data APIs.

For remaining variable views, sorting places pointer-only views first, then orders by header signature, element signature, and emitted name. kalloc_type_handle_parray_var walks the pointer-only prefix and assigns each detected name group to one of the two pointer heaps using kmem_get_random16(1). A KT_PTR_ARRAY type can be a record composed only of pointer/padding/PAC granules; the name of the flag should not be read as requiring C array syntax in the type definition. Fixed pointer-only views still use the fixed policy described above. Comparator, pointer-heap assignment, summary mask.

The remaining views are grouped by exact equality of the header and element signature strings. An absent header is treated as an empty string. Unlike fixed grouping, this pass does not use prefix matching, and it does not include runtime count in the key. Arrays of a particular element type at different lengths therefore share their startup classification and select different size buckets within the chosen heap. Variable group construction, runtime size selection.

The flexible-heap split

The default kt_var_heaps is six. The exact indexing arithmetic deserves attention because these are pool indices, not six independent C type IDs:

  1. Heap creation starts at index two and creates six families, indices 2 through 7.
  2. fixed_heaps = KT_VAR__FIRST_FLEXIBLE_HEAP - 1, which is 3, and flex_heap_count = kt_var_heaps - fixed_heaps - 1, which is 2.
  3. A permutation of the two ordinary flexible choices is built; one randomly selected choice is appended again, giving that choice twice as many positions in the assignment cycle.
  4. A signature group assigned to that selected choice is subdivided by emitted-name runs. Each run randomly remains in the selected heap or is assigned to heap index kt_var_heaps, which is 6 with defaults.
  5. Other signature groups stay together in their selected flexible heap. With these defaults, typed non-pointer views use indices 4/5 and the split target 6. Index 7 is also created; the separate kheap_init_var path can select existing flexible families including it.

This is a calculation from the current constants and bodies, not observed assignments on a running system. It does not justify arbitrary values for the tunables. Default configuration, heap creation, split helpers, assignment arithmetic, custom heap initialization.

The final dispatch of each signature group is explicit. Flexible assignment.

	for (uint32_t i = 1; i <= unique_sig; i++) {
		uint32_t heap_id = shuffle_buf[i % (flex_heap_count + 1)] +
		    fixed_heaps + 1;
		uint32_t start = kt_skip_list_start[i - 1];
		uint32_t end = kt_skip_list_start[i];

		assert(heap_id <= kt_var_heaps);
		if (heap_id == split_heap) {
			kalloc_type_split_heap(start, end, heap_id);
			continue;
		}
		kalloc_type_assign_zone_var(&kt_buffer[start].ktv_var,
		    &kt_buffer[end].ktv_var, heap_id);
	}

kalloc_type_assign_zone_var stores only the chosen kh_zstart as the view's kt_heap_start, optionally allocates private statistics, and links the view into that heap's list. It does not reserve a separate set of pages for the view. The same family serves all of its assigned types. The generic heap mechanism also uses existing families: a named KALLOC_HEAP_DEFINE view is not evidence that a new physical heap was created. Variable assignment, heap-view setup.

6. Runtime selection and the shared initial-allocation phase

A fixed kernel-private allocation needs no size search. kalloc_type_impl_internal gets the assigned zone and statistics from its view. If Z_SET_NOT_EARLY is present or the current CPU's statistics have zs_alloc_not_early set, it allocates from the assigned zone. Otherwise, it allocates from kt_zearly, carrying Z_SET_NOT_EARLY to the zone implementation so the shared-allocation threshold can be evaluated. Fixed dispatch.

kalloc_type_impl_internal(kalloc_type_view_t kt_view, zalloc_flags_t flags)
{
	zone_stats_t zs = kt_view->kt_zv.zv_stats;
	zone_t       z  = kt_view->kt_zv.zv_zone;
	zone_stats_t zs_cpu = zpercpu_get(zs);

	if ((flags & Z_SET_NOT_EARLY) ||
	    os_atomic_load(&zs_cpu->zs_alloc_not_early, relaxed)) {
		return zalloc_ext(z, zs, flags).addr;
	}

	assert(!zone_is_data_kheap(zone_security_config(z).z_kheap_id));
	return zalloc_ext(kt_view->kt_zearly, zs, flags | Z_SET_NOT_EARLY).addr;
}

The term EARLY does not mean “only before startup finishes.” zone_share_always starts true and is cleared by zalloc_iokit_lockdown. After that, the uncached zalloc_item path can set zs_alloc_not_early when cumulative bytes allocated through the statistics object reach a threshold. This is cumulative allocation traffic, not currently live bytes: the code tests zs_mem_allocated, not allocations minus frees. Once one CPU's statistic reaches the threshold, the implementation sets the flag across all CPU slots of that statistics object. Initial state, lockdown and first-process changes, threshold transition.

	if (__improbable(!zone_share_always &&
	    !os_atomic_load(&zs->zs_alloc_not_early, relaxed))) {
		if (flags & Z_SET_NOT_EARLY) {
			vm_size_t shared_threshold = zalloc_get_shared_threshold(zone, esize);

			if (zs->zs_mem_allocated >= shared_threshold) {
				zpercpu_foreach(zs_cpu, zstats) {
					os_atomic_store(&zs_cpu->zs_alloc_not_early, 1, relaxed);
				}
			}
		}
	}

The threshold is zone_early_thres_mul * page_size / 4 for elements up to 512 bytes, zone_early_thres_mul * element_size * 8 below 2048 bytes, and zone_early_thres_mul * chunk_elements * element_size for larger elements. The multiplier begins at five and is set to one by zalloc_first_proc_made. The transition happens on the allocation path that contains this check, not by a periodic scan of every view. Threshold function, multiplier, first-process hook.

Accounting scope therefore affects routing scope. Fixed views normally point to the assigned zone's statistics; views sharing those statistics share the transition state. KT_PRIV_ACCT creates separate per-view statistics, and KT_NOEARLY is accepted only with private accounting in the fixed assignment body. Data heaps have their no-EARLY bit set at creation and never take this pointer-storage sharing path. Fixed statistics, data setup. The ordinary kalloc_type macros request KT_SHARED_ACCT; the KT_OPTIONS_ACCT switch only changes records carrying KT_DEFAULT. Macro default, accounting condition.

For a variable view, the low bit of the metadata pointer distinguishes it from a plain kalloc_heap argument. kalloc_ext recovers the view, chooses the logical heap from flags, obtains kt_heap_start or its fallback, extracts the hash, and resolves statistics. Processed non-data views use KHEAP_KT_VAR; unprocessed non-data views use KHEAP_DEFAULT; data-only views use KHEAP_DATA_PRIVATE. The actual typed family comes from the view's kt_heap_start, which is distinct from that logical heap identity. Pointer tagging, logical heap choice, variable dispatch.

kalloc_use_early_heap can replace the selected non-data family with the EARLY heap while its statistics still have a zero transition bit. Unlike the fixed allocation body's explicit flag check, this helper tests statistics directly; setting Z_SET_NOT_EARLY in a variable request is not itself a bypass of this helper. It then selects the size class or takes the large path. Normal processed variable views share KHEAP_KT_VAR statistics unless private statistics were requested, so their shared-phase state can cover many views and heaps. EARLY helper, statistics fallback, variable statistics creation.

7. Large allocations: signatures become a limited placement hint

Variable views which survive the startup filters get a hash derived from their element signature and optional header signature. kalloc_type_set_type_hash feeds a boot-random seed through Jenkins update operations, then applies the VM range/direction masks and kmem_adjust_range_id. The resulting range, KMEM_HASH_SET marker, and direction bit occupy the high half of kt_flags. Count is not part of this hash; increasing an array's length does not create a new type classification. Hash seed, hash construction.

	assert(sig_ty != NULL);
	hash = os_hash_jenkins_update(sig_ty, strlen(sig_ty),
	    kalloc_type_hash_seed);
	if (sig_hdr) {
		hash = os_hash_jenkins_update(sig_hdr, strlen(sig_hdr), hash);
	}
	os_hash_jenkins_finish(hash);
	hash &= (KMEM_RANGE_MASK | KMEM_DIRECTION_MASK);

	*kt_flags = *kt_flags | kalloc_hash_adjust(hash, 16);

There is a precise implementation detail in that excerpt: the call to os_hash_jenkins_finish(hash) discards its return value. The helper takes hash by value and returns the finalized result, so the value subsequently masked in this checkout is the update state, not the returned finalized value. This describes the current code rather than inferring an intended alternative. Finalization helper.

static inline uint32_t
os_hash_jenkins_finish(uint32_t hash)
{
	hash += (hash << 3);
	hash ^= (hash >> 11);
	hash += (hash << 15);

	return hash;
}

Fixed views have an intentionally empty hash hook. For processed oversized non-data fixed views, the external allocation fallback chooses KHEAP_KT_VAR, whose base hash is zero, allowing VM backtrace-based placement. An unprocessed non-data fixed view instead falls back to KHEAP_DEFAULT, which is initialized as a heap view with its own boot-random hash. Data-only views take the private-data branch before either of those non-data choices. Likewise, variable views skipped for KT_VM never execute the signature hash hook. One must follow the actual fallback heap and flags before describing placement as “type hashed” or “backtrace hashed.” Fixed no-op hook, external fixed fallback, data-first heap classification, default heap declaration, heap hash assignment, VM fallback.

kalloc_large checks the request before asking VM for storage. Z_NOFAIL on a large request panics; Z_NOWAIT returns failure; excessive sizes and array-encoding limits are rejected. It derives data/private/shared and other VM flags, resolves the VM accounting tag, and calls kmem_alloc_guard. kalloc_guard marks the request atomic and carries the VM tag, type hash, and a hash of the owner context. These are different concepts: a VM accounting tag is not a type-layout hash, and neither is a memory-tagging tag. Large allocation, guard construction, optional memory-tagging policy.

At the VM boundary, private/shared data flags select their data ranges. A nonzero type hash supplies the pointer range bits and placement direction; when the hash is zero, kmem_apply_security_policy obtains a backtrace hash. Early startup and maps other than kernel_map bypass that ordinary kernel-range policy. This finite set of destinations permits collisions by design. It is placement information, not a unique object type checked by every free. Continue with kernel virtual allocation for guard objects, size-class slabs, physical population, and address reuse. Security-policy entry, large free handoff.

Typed allocation: from C layouts to zones and VM ranges diagram
Typed allocation: from C layouts to zones and VM ranges diagram · Open SVG
Mermaid source
flowchart LR
    V[Variable metadata view] --> F[Flags choose logical heap]
    V --> H[Assigned heap start]
    V --> T[Signature hash in flags]
    F --> R[Runtime dispatch]
    H --> R
    R --> E{Shared phase?}
    E -->|yes| Z[EARLY family]
    E -->|no| S[Assigned family]
    Z --> Q{Size has a zone class?}
    S --> Q
    Q -->|yes| A[zalloc_ext]
    Q -->|no| L[kalloc_large]
    T --> L
    L --> G[kmem_guard: hash + tag + context]
    G --> K[kmem_alloc_guard]
    K --> P[VM range and direction policy]

This diagram omits failure checks but preserves the separate sources of heap identity, zone start, and VM placement hash. kalloc_ext, kalloc_large.

8. Freeing verifies allocator relationships, not nominal C identity

The free macros first apply KALLOC_TYPE_ASSERT_COMPATIBLE_POINTER. The compiler builtin accepts the specified type or a compatible signature; void * is explicitly a wildcard. The wrappers also load and clear the caller's pointer expression before calling the implementation. These checks and pointer clearing help callers, but they are not an unforgeable allocation-time type stored in each object. Compatibility contract, free wrappers.

Fixed free

kfree_type_impl_internal reads the actual zone ID from the allocation's zone-page metadata. If the view targets a data heap, or the allocation is already in the assigned zone, the function frees through the view; the ordinary zone free still validates that pointer. Otherwise it compares actual and expected signature-equivalence IDs. A match frees through the actual zone. Failing that, it attempts the EARLY zone; the ordinary zone free machinery must still validate that destination. Thus the fallback is not permission to free any address, but it intentionally accepts storage from the view's shared initial zone. Complete typed-free body, zone free mechanics.

	if (zone_is_data_kheap(zsflags_z.z_kheap_id) ||
	    zone_has_index(z, zidx_meta)) {
		return (zfree)(&kt_view->kt_zv, ptr);
	}
	zsflags_zsig = zone_security_config(zsig);
	if (zsflags_meta.z_sig_eq == zsflags_zsig.z_sig_eq) {
		z = zone_array + zidx_meta;
		return (zfree)(z, ptr);
	}

	return (zfree)(kt_view->kt_zearly, ptr);

The important limit is the equivalence comparison: the check does not compare emitted names or signature strings at free time. An assigned extra zone is narrower placement than the whole signature-equivalence class accepted by the free path. Types assigned to the same physical zone or equivalence class cannot be distinguished by this test. Private accounting can also differ from actual storage ownership, so the selected free route matters when interpreting statistics. Assignment relationship, typed free.

kalloc_type_require(T, ptr) is a separate and stricter check than this typed-free routing. It emits a fixed view and calls zone_require on the assigned zone. zone_require checks that exact zone membership; it does not reproduce the signature-equivalence and EARLY alternatives above. It also does not prove that an address denotes a live allocation if its zone storage remains mapped. Use its actual contract rather than treating it as a general runtime C type assertion. Require macro and scope, zone_require.

Variable free

kfree_ext first handles null, rejects absurd sizes, and distinguishes a zone-sized free from a large free. For the zone case it recovers the actual zone and bucket size from the pointer, accounts for a possible guarded-allocation offset, and rejects a supplied size extending beyond the bucket. kfree_zone then resolves the expected broad heap category from the view, while retaining the actual zone for the free. Size and provenance recovery, zone free.

For typed data allocations, which resolve to the base private-data heap, the actual heap ID must match. Calls through the base shared-data heap similarly require its exact heap ID. Other heap pointers use the branch accepting their logical heap ID or EARLY; this branch also includes custom data heap views because the preceding test compares the heap pointer with the two data bases. All ordinary variable typed families use KHEAP_ID_KT_VAR, so this code does not validate that the actual zone belongs to this particular view's kt_heap_start. It does not recompute a signature hash on free. The source explicitly discusses cross-frees between processed and unprocessed views and the resulting accounting inaccuracies. Heap validation.

	zsflags = zone_security_config(z);
	if (kheap == KHEAP_DATA_PRIVATE || kheap == KHEAP_DATA_SHARED) {
		if (kheap->kh_heap_id != zsflags.z_kheap_id) {
			kfree_heap_confusion_panic(kheap, data, size, z);
		}
	} else {
		if ((kheap->kh_heap_id != zsflags.z_kheap_id) &&
		    (zsflags.z_kheap_id != KHEAP_ID_EARLY)) {
			kfree_heap_confusion_panic(kheap, data, size, z);
		}
	}

After validation, it zeroes the entire bucket and calls zfree_ext on the actual zone. The supplied requested size need not equal the original request to pass the bucket-bound check; it must satisfy the actual checked size and ownership constraints. For a VM-backed allocation, kfree_large delegates to kmem_free_guard with atomic/context guard information and no type hash. The VM guard checks are covered in kernel virtual allocation, not by an assertion that this pointer has a particular C type. Zone zero/free, large free.

Reallocation preserves another useful limit. krealloc_ext computes the new destination family and size class, but if an existing zone allocation has the same bucket size it can retain that allocation without requiring old_z == new_z. Therefore a resize within one size bucket need not migrate an old EARLY allocation into the subsequently preferred typed family. If a move is needed, it allocates from the new destination, copies the retained prefix, and releases the old allocation under the requested failure semantics. Destination calculation and same-bucket path, move/free path.

9. What the isolation provides, and where sharing remains

Boundary Concrete mechanism Limits visible in this checkout
Data versus pointer-bearing storage KT_DATA_ONLY redirects to private-data zones/VM flags All qualifying data types share the data family; declared type classification does not inspect stored values
Fixed layouts and type names Size grouping, prefix layout grouping, budgeted signature zones, extra name zones, startup shuffling Multiple layouts can share a signature zone; multiple names can share an assigned zone; free accepts an equivalence class
Pointer-only variable storage Two special heap families selected by name groups Many pointer-only types share these families; initial shared routing still applies
Other variable layouts Exact header/element signature groups assigned to a small flexible pool, with one split by name Heaps are shared; allocation count is not an identity key; frees validate broad heap category
Initial low-volume storage EARLY zone/family until statistics transition Unrelated non-data types of a compatible size can initially share storage, including after boot
Large allocation placement Type- or heap-derived hash, or backtrace fallback, selects finite VM range/direction Hash collisions and fallback policies; no unique nominal-type check on free
Pointer lifetime Free clears the caller's pointer expression and allocator paths validate ownership Other aliases can remain; this is not automatic reference management or a proof against all use-after-free

The mechanisms in this table come from data filtering, fixed assignment, variable assignment, shared-phase routing, fixed free, variable free, and VM placement. Page allocation, zone virtual-address sequestering, quarantine, and memory tagging are additional mechanisms with their own configuration and reuse rules; see zone allocator and kernel virtual allocation.

Several build distinctions affect the concrete result. Kernel-private fixed calls statically require a type no larger than KALLOC_SAFE_ALLOC_SIZE, 16 KiB, even where the heap maximum is larger. External fixed wrappers can encounter a null assigned zone, either because the view was not ingested or because it is compile-time large, and fall back through kalloc_ext. KASAN changes some size classes and the fixed-zone budget. VM allocation's KMA_KOBJECT policy has an x86 compatibility exception, and Z_MAY_COPYINMAP can move x86 data requests to VM earlier than the normal heap maximum. Size checks, fixed compile-time enforcement, external fallback, class adjustments, copyin-map lookup, large VM flags.

The private-data mapping restriction has a separate policy mode: restricted_data_mode defaults to none on x86_64, telemetry on OSX/TV targets, and enforcement otherwise. In the VM allocation path, __kmem_mapping_type requests PMAP_MAPPING_TYPE_RESTRICTED for private data only in enforce mode. This is separate from private accounting and heap classification. Mode definitions and defaults, VM mapping selector.

Small data-zone allocations follow a different selector in this checkout. Final zone creation puts both private and shared data heaps in Z_SUBMAP_IDX_DATA without consulting restricted_data_mode, and zone_mapping_type returns PMAP_MAPPING_TYPE_DEFAULT for that submap. Zone expansion explicitly supplies this result to kernel_memory_populate_object_and_unlock; the presence of KMA_DATA in the separate zone flags does not make it use the VM path's selector. Therefore the VM private-data enforcement branch must not be generalized to all private-data pages. Data-heap predicate, final zone placement, zone mapping selector, population handoff. Downstream frame-type enforcement also depends on platform configuration; allocator mapping boundaries and ARM64 pmap lifecycle follow that boundary.

10. Reading and verification route

For a fixed callsite, follow _KALLOC_TYPE_DEFINE → kalloc_type_view_copy → kalloc_type_cmp_fixed → kalloc_type_create_iterators_fixed → kalloc_type_apply_policy → kalloc_type_create_zones_fixed → kalloc_type_impl_internal → kfree_type_impl_internal. Keep the three zone pointers and the statistics pointer visible while reading. For an array, substitute KALLOC_TYPE_VAR_DEFINE_*, exact signature-pair grouping, heap assignment, and kalloc_ext; then follow the size threshold into kalloc_large. These symbol paths are anchored in metadata emission, fixed startup, variable startup, and runtime dispatch.

The tree contains useful checks but they were not run for this documentation review. run_kalloc_type_test checks assigned sizes, exercises budget policy with synthetic counts, checks for typed zones, and tests data classification. tests/kalloc.c invokes the debug sysctl test; tests/verify_kalloc_config.c compares runtime zone names with reported element sizes. These do not establish one-zone-per-type isolation, and their existence is not evidence of a measured zone assignment in this guide. Kernel test, test wrapper, configuration test.

Sources inspected Depth Limits
kalloc.h Metadata structures, compiler contract, fixed/variable macros, size arithmetic, classification and compatibility checks Compiler builtin implementation and produced Mach-O records were not available for execution/inspection here
kalloc.c Collection parsing, data/pointer classification, sorting, budget and shuffle algorithms, heap assignment, allocation/free/reallocation boundaries, tests No boot, workload measurements, or exhaustive tuning combinations were exercised
zalloc_internal.h Budgets, heap IDs, heap metadata, security configuration interfaces Full zone page/cache/reclaim mechanics are in the zone guide
zalloc.c and zalloc.h Fixed typed dispatch/free, exact-zone requirement, statistics transition, startup hooks, typed-zone marking This page does not audit every sanitizer, memory-tagging, or zone-validation path
vm_kern.c, hash.h Type-hash range/direction consumption, shuffle helper, and hash update/finalization bodies Guard objects, map placement, and VM recycling are covered by the kernel virtual allocation guide
Allocator tests, configuration test Test coverage and entry-point survey Tests were read, not executed; conceptual examples are not test output

Search the handbook

Search every chapter, interface and code excerpt.

    Diagram

    Source preview

    Open full file