XXNU Source Handbook Download offline version from here
Part 02 · Chapter 02.04

The task and its shared read-only data

A task owns the Mach resources shared by its threads: an address space, IPC namespace, ledgers, policy and thread membership. In this checkout, its writable structure occupies the second part of a combined process/task allocation. Selected task fields instead live in a separate proc_ro object, shared with the process on the ordinary BSD creation path. Following those two allocations explains why obtaining a task pointer, validating its protected fields and keeping its storage alive are different operations.

Key structures · Process and protected data · Writing protected fields · Runtime objects · Task capabilities

This chapter follows xnu-12377.121.6: ordinary process-associated tasks, corpse tasks and the kernel bootstrap exception. The process transition chapter supplies the surrounding fork, exec, exit and wait protocols; the focus here is storage, access and mutation.

Continue through thread and thread_ro for the thread's task association, IPC spaces and entries for itk_space, and ports and kobjects for the task's exported capability objects and referenced conversion.

Inspect the live task_ro_data layout for the selected source profile.

1. Writable task state and the protected companion

Inspect the task layout in the offline HTML handbook to select a configuration and follow nested field offsets. These offsets describe the task object itself; the combined process/task allocation is explained below.

struct task begins with its mutex and reference count. Neither the whole structure nor all of its security-related state is in a read-only zone. Its bsd_info_ro member points to struct proc_ro; the latter contains a task_data group declared with the type name task_ro_data. That group is part of the RO allocation, not a separately allocated task_ro. Task declaration, RO pointer, protected layout.

Writable field group State to recognize Synchronization or lifetime distinction
Object state lock, ref_count, active, ipc_active, halting, task_uniqueid References protect lifetime. active and ipc_active describe different readiness/termination stages. The task ID is distinct from the process unique ID. Fields.
Address space and threads map, threads, counts, suspension counts, pset_hint The task mutex protects the selected creation/termination decisions; global list changes use tasks_threads_lock. The VM map has its own lifetime and locking. Fields, thread creation.
IPC itk_lock_data, task-port array, exception actions, special rights, itk_space The IPC mutex is separate from the task mutex and coordinates port enable/disable and lazy publication. Fields, enable.
Accounting and ownership ledger, Recount state, counters, semaphore list, task_objq Counters and subordinate objects have their own APIs; task_objq_lock explicitly protects the owned-object queue. Fields, owned-object queue.
Policy and flags t_flags, t_procflags, requested/effective policy, security_config t_flags is documented as task-lock protected, while t_procflags is proc-lock protected. Initialization writes happen before external readiness. Do not assume every field uses the task mutex. Flag declarations, proc flags, policy, security configuration.
Coalition membership coalition[], task_coalition[] Membership is adopted during task creation and unset during final release; the declaration identifies coalition locking rather than the task mutex. Contract.

task_tokens is another distinct allocation. It points to task_token_data, whose UID and GID pairs are packed into atomic 64-bit fields, with separate atomic audit/PID fields. Its signed pointer and atomic data are not an alternative name for proc_ro. Creation allocates and initializes it separately; final task release frees it separately. Token representation, allocation and initialization, free.

The protected task group is small. Its exact declaration is proc_ro.h, lines 84–92:

	__xnu_struct_group(task_ro_data, task_data, {
#ifdef CONFIG_MACF
		struct task_filter_ro_data task_filters;
#endif
		uint32_t t_flags_ro;                               /* RO-protected task flags (see osfmk/kern/task.h) */

		/* This is not inherited on fork/exec, must be re-evaluated */
		task_control_port_options_t task_control_port_options;
	});

t_flags_ro includes such state as TFRO_CORPSE, TFRO_PLATFORM, TFRO_FILTER_MSG and TFRO_FREEZE_EXCEPTION_PORTS. Some definitions or uses depend on macOS, pointer authentication or Exclaves configuration. The two MAC filter members hold pointers to masks; protecting those pointer fields does not establish the contents or lifetime of the pointed-to masks. Flag definitions, mask types.

2. One combined allocation, two structure addresses

The BSD side defines proc_struct_size = sizeof(struct proc) and proc_alignment = _Alignof(struct proc). The Mach side supplies the task alignment and rounds the two portions before creating proc_task_zone. The zone's element size is the sum of those rounded portions. Initial sizes, task alignment.

The allocation geometry is explicit in task.c, lines 1243–1252:

static inline void
task_zone_init(void)
{
	proc_struct_size = roundup(proc_struct_size, task_alignment);
	task_struct_size = roundup(sizeof(struct task), proc_alignment);
	proc_and_task_size = proc_struct_size + task_struct_size;

	proc_task_zone = zone_create_ext("proc_task", proc_and_task_size,
	    ZC_ZFREE_CLEARMEM | ZC_SEQUESTER, ZONE_ID_PROC_TASK, NULL); /* sequester is needed for proc_rele() */
}

For a zone-element base address B, let P be the rounded proc_struct_size and T the rounded task_struct_size. The process begins at B, the task at B + P, and the requested element extent is P + T. proc_get_task_raw(p) adds P; task_get_proc_raw(t) subtracts it. These are kernel virtual-address calculations, not PID lookup or physical-address translation. The sizes depend on the compiled structures and configuration, so no fixed byte offset is assumed here. Raw conversion bodies.

The proc_task zone is created with clear-on-free and sequestering flags, not ZC_READONLY. The separately defined proc_ro zone uses ZC_READONLY | ZC_ZFREE_CLEARMEM. The SECURITY_READ_ONLY_LATE declaration of the zone handle variable does not make the combined elements read-only. Combined-zone handle, RO-zone definition. The RO-zone guide explains configuration-dependent enforcement.

The task and its shared read-only data diagram
The task and its shared read-only data diagram · Open SVG
Mermaid source
flowchart TD
    B["One proc_task element at B"] -->|"offset 0"| P["proc at B"]
    B -->|"offset proc_struct_size"| T["task at B + P"]
    P -->|"p_proc_ro"| R["Separate proc_ro element"]
    T -->|"bsd_info_ro"| R
    R -->|"pr_proc"| P
    R -->|"pr_task"| T
    T -->|"task_tokens"| K["Separate writable task_token_data"]
    H["thread"] -->|"t_tro"| TR["Separate thread_ro"]
    TR -->|"tro_task"| T

This diagram selects an ordinary live process/task pair; the first two arrows show layout, and the remaining arrows show pointers. A thread also has BSD/credential links in thread_ro, omitted here. The kernel and corpse cases below deliberately do not satisfy every association shown. Thread protected links.

Raw arithmetic does not prove that the neighboring process is initialized or logically associated. proc_task(p) tests P_LHASTASK; get_bsdtask_info(t) checks the raw process location and returns it only when TF_HASPROC is set. Their setters validate the fixed neighboring address and change association flags. set_bsdtask_info() does not assign bsd_info_ro. None of these helpers acquires a task or process reference. Process association, task-side read, task-side setter, flag access.

3. Constructing the task and attaching protected state

task_create_internal() initializes a supplied child_task. It does not allocate the combined structure itself. The ordinary caller already has a process at the front of an allocated zone element and a process-only RO object: forkproc() calls proc_ro_alloc(child_proc, &proc_ro_data, NULL, NULL), then fork_create_child() obtains the raw task neighbor and passes that existing RO object to task creation. Process allocation, initial RO object, task creation call.

The important construction stages are:

  1. Initialize task reference accounting, instantiate the ledger and allocate the fault counter. The ordinary reference model starts with two references: one for the caller and one for being alive. Development/debug reference-group accounting has additional bookkeeping. Constructor entry, initial count, reference initialization variants, ordinary implementation.
  2. Fork the parent map when inherit_memory is true and TF_CORPSE_FORK is clear; otherwise create a pmap/map and optionally inherit map limits. A null pmap/map path releases the fault counter, ledger and reference bookkeeping before returning KERN_RESOURCE_SHORTAGE. The caller still owns the combined allocation; the RO attachment has not happened yet. Map choice and failure cleanup.
  3. Initialize the task mutex, thread queue, counts, active/halting state and flags. Build a writable stack-local task_ro_data template. Selected RO flags are copied explicitly from the parent, rather than inheriting all bits. Initial state and inheritance.
  4. Allocate token data, prepare filter pointers and initialize control-port options to TASK_CONTROL_PORT_OPTIONS_INVALID. With an existing RO record, attach the task through proc_ro_ref_task; without one, allocate a task-only RO record. This happens before IPC/importance initialization. Tokens, filters and attachment.
  5. Complete policy, machine, coalition and resource initialization, link the task into the global task list under tasks_threads_lock, and set its allocation hold. Returning success still does not enable user task-port access. Later initialization, list publication and hold.

The attachment branch is task.c, lines 1633–1638:

	/* must set before task_importance_init_from_parent: */
	if (proc_ro != NULL) {
		new_task->bsd_info_ro = proc_ro_ref_task(proc_ro, new_task, &task_ro_data);
	} else {
		new_task->bsd_info_ro = proc_ro_alloc(NULL, NULL, new_task, &task_ro_data);
	}

Despite its name, proc_ro_ref_task() does not increment a reference count. It rejects a non-null existing pr_task, copies the old RO record into a stack-local value, fills pr_task and task_data, then copies that whole value back with zalloc_ro_update_elem(). It preserves the process group while initializing the task group before the new task becomes externally usable. This whole-record update is not a general concurrent field-update protocol. Attachment body.

proc_ro_alloc() similarly starts with a zeroed local record, allocates from ZONE_ID_PROC_RO, populates the local record from whichever process/task arguments are supplied, and copies the template through the RO API. A task-only call leaves the process group and pr_proc zero. Allocation body.

Publication is more than constructor success

IPC initialization allocates the task's IPC space and control/name ports, initially without a task kobject association; read/inspect ports are lazy. On the fork path, the caller subsequently sets up the map owner, resolves control-port policy, enables task IPC, ends process construction barriers and clears the task's return wait. The first thread initially waits in the return protocol. IPC initialization, first-thread wait, fork completion.

The task and its shared read-only data diagram
The task and its shared read-only data diagram · Open SVG
Mermaid source
flowchart TD
    A["Allocate combined element and process RO record"] --> B["Initialize task and attach task_data to that RO record"]
    B --> C["Publish global task membership and set TASK_HOLD"]
    C --> D["Associate BSD halves and create waiting first thread"]
    D --> E["Set map owner and control-port defaults"]
    E --> F["ipc_task_enable checks readiness and connects ports"]
    F --> G["Finish process barriers and clear return wait"]
    G --> H["Release creation references"]

This is the selected successful fork path, not all rollback branches or a guarantee that the child survives construction. The fork wrapper can terminate the child if the parent died during the operation. ipc_task_enable() itself takes the IPC mutex, rejects a task already made inactive, verifies map/control-option setup, marks ipc_active, and enables the port associations. Kernel/corpse exceptions appear in those assertions. Late parent-death branch, enable checks.

Task-only, corpse and kernel variants

Variant Allocation and protected-record behavior
Ordinary BSD child, including the exec-copy construction path Uses the child's combined allocation and existing process-only RO record. fork_create_child() chooses memory inheritance for fork, marks exec copies with TPF_EXEC_COPY, and passes return-wait flags. Caller.
Corpse fork Allocates a zeroed combined element, obtains its task half and calls task creation with no process RO argument, TF_CORPSE_FORK and TFRO_CORPSE. The process half is storage, not an initialized BSD process. Even though the caller passes inherit_memory = TRUE, the corpse flag selects the fresh-map constructor branch; map/thread duplication follows separately. Caller and subsequent duplication, map predicate.
Kernel bootstrap task_init() allocates kernproc/kernel_task together and passes no RO object, so task creation allocates a task-only record. Later bsd_init() separately allocates kernproc->p_proc_ro with both backreferences. Its preceding set_bsdtask_info() changes association state, not the task's RO pointer. Thus the ordinary shared-record diagram is not universal. Task bootstrap, BSD bootstrap, setter body.

The corpse caller explicitly distinguishes raw allocation ownership from initialized-task ownership. Before success it can free corpse_proc directly; after task construction and IPC setup it clears that local pointer, and later errors terminate/deallocate the task instead. Ownership transfer, cleanup. Ordinary task termination asserts that the task is not kernel_task; its teardown is not a model for freeing kernel bootstrap records. Termination entry.

4. Obtaining a task and reading its protected fields

Route Actual operations What the caller obtains
Current execution current_task() calls get_threadtask(current_thread()); get_threadtask() reads get_thread_ro(th)->tro_task A pointer supplied by the current thread's association, with no new task reference from the accessor. Current task, thread-to-task.
Referenced thread get_thread_ro() validates the RO zone and tro_owner backlink, then get_threadtask() returns tro_task The thread's lifetime protects its thread_ro. Thread construction takes an internal task reference and final thread destruction releases it. Validation, lifetime contract, task retain, task release.
Process association proc_task(p) tests the process association flag and returns the fixed neighbor No new reference; a caller needing independent task lifetime explicitly takes one before releasing its process reference. Accessor, example transfer.
Task port conversion Under the active port's lock, validate its flavor, kobject association, corpse options and applicable policy; the referenced wrapper calls task_reference_grp() A referenced task, or null. The helper ending in _noref does not retain it. Validation and reference wrapper.

The capability chapter covers names, flavor compatibility, self fast paths and reference-consuming output conversions. A user port name is not a task address, and a reference obtained from a port conversion is not proof that all task operations remain permitted.

Once the task's lifetime is established, task_get_ro() follows bsd_info_ro, calls the configuration-dependent RO-zone validator and verifies the backreference. The exact accessor is task.c, lines 10360–10371:

proc_ro_t
task_get_ro(task_t t)
{
	proc_ro_t ro = (proc_ro_t)t->bsd_info_ro;

	zone_require_ro(ZONE_ID_PROC_RO, sizeof(struct proc_ro), ro);
	if (__improbable(proc_ro_task(ro) != t)) {
		panic_proc_ro_task_backref_mismatch(t, ro);
	}

	return ro;
}

This accessor neither takes a reference nor locks a field. It does not check active, restore a vanished process association or turn a stale task pointer into a valid one. task_require() similarly validates the combined allocation's zone using the raw process base; it is not a lookup-and-retain operation. Task zone check. The protected-update chapter separates zone/backreference checks from lifetime and concurrency guarantees.

current_task_early() is a separate boot-time convenience: before the early-boot phase it can return null when the initial thread has no t_tro. The ordinary current-task accessor has no such null-bootstrap path. Early accessor.

5. Updating writable fields and RO fields

A normal task-field update can take the task mutex and assign directly. For example, task_set_thread_limit() asserts that the task is not kernel_task. For a limit within TASK_MAX_THREAD_LIMIT, it writes task_thread_limit while holding the task lock; an out-of-range value leaves the field unchanged. This helper returns void, not a rejection status. Concrete writable update.

Protected flags use a different operation. task_ro_flags_set() and task_ro_flags_clear() perform an atomic OR or AND through the RO update API. The exact bodies are task.c, lines 10379–10391:

void
task_ro_flags_set(task_t task, uint32_t flags)
{
	zalloc_ro_update_field_atomic(ZONE_ID_PROC_RO, task_get_ro(task),
	    t_flags_ro, ZRO_ATOMIC_OR_32, flags);
}

void
task_ro_flags_clear(task_t task, uint32_t flags)
{
	zalloc_ro_update_field_atomic(ZONE_ID_PROC_RO, task_get_ro(task),
	    t_flags_ro, ZRO_ATOMIC_AND_32, ~flags);
}

The atomic operation protects the selected word against lost read-modify-write updates; it does not atomically publish a collection of other fields or retain the task. task_set_filter_msg_flag(task, flag) selects those helpers for TFRO_FILTER_MSG, and the getter tests the same RO word. Selected public helper.

Control-port options use a field copy, not those bitwise flag helpers. The static setter passes a local opts value to zalloc_ro_update_field(). Its caller, task_set_ctrl_port_default(), asserts that task IPC is not yet active, derives options from the task's IPC policy, the MAC movable-port decision and configured policy bits, stores them, then refreshes task/thread control-port exposure. ipc_task_enable() later asserts valid options for ordinary tasks. Setter, policy and ordering, enable assertion.

Read this ordering as part of the field's meaning. The constructor initializes options to INVALID; it does not copy the parent's value. The static setter's comment explains that changing the stored value after startup cannot make already immovable control ports movable again. Therefore a successful RO write is not itself a complete port-policy transition. Initial value, non-inheritance declaration, setter restriction.

With CONFIG_MACF, trap and kobject filter setters copy the pointer field using &mask; they do not copy the mask's bytes or establish its ownership in these bodies. Constructor inheritance also copies the parent's filter pointers for non-corpse tasks. The MAC provider's mask production/lifetime policy is outside this walkthrough. Filter accessors, inheritance.

These calls identify the object, field and operation. Follow writing protected fields for macro expansion, source/destination checks and the architecture backend. Here, RO means a protected ordinary mapping with controlled mutations when RO enforcement is enabled, not an immutable C object. Configuration can select a different enforcement path; no task API alone proves a particular device's page permissions.

6. Logical termination, final task release and shared storage

Three separate ownership mechanisms appear in this path:

Mechanism What it keeps alive or delays
task->ref_count and task reference APIs The task object and resources retained until final release; logical termination can already dismantle operational resources. A new thread and a successful referenced port conversion can add references; callers must release theirs. Reference implementation, termination teardown.
BSD process references and p_waitref Process operation/reaping lifetime, with the final wait-reference release scheduling deferred SMR reclamation. They are not acquired merely by holding a task reference. Process wait release, explicit corpse-side process lookup.
P_REF_PROC_HOLD / P_REF_TASK_HOLD in the process half Whether each side still holds the combined allocation. These bits are not the task reference count and are not a reference count inside proc_ro. Bit declarations, hold helpers.

task_terminate_internal() holds the task, marks it inactive and disables task IPC, requests termination of its threads, tears down operational resources and moves it to the terminated-task list. It finally releases the task's active reference. Other references can keep the task allocated after this logical death. The VM map and IPC space must exist until the termination function returns, as its comment explicitly requires for port conversions. Inactive and IPC transition, remaining teardown/list transfer, active-reference release.

BSD exit clears both ordinary process/task association flags and the exiting thread's process linkage before later final reclamation. This does not immediately erase pr_task from the RO record. BSD detach.

At zero task references, task_deallocate_internal() requires no task threads, no BSD association, an inactive IPC space, an inactive task and zero active threads. It removes terminated-list membership and releases IPC, map, ledger, accounting, coalition and other task resources. The importance-inheritance branch can disconnect its last remaining reference before the count reaches zero. Last-reference gate and assertions, resource release.

The final task-side actions free reference bookkeeping and token data, atomically clear the RO record's pr_task, and release P_REF_TASK_HOLD. Clearing the backreference does not zero the whole task group or free the RO record immediately. Final task actions, atomic backlink clear.

On the process side, the last p_waitref release schedules proc_wait_free() in the smr_proc_task domain; that callback releases P_REF_PROC_HOLD. Each hold-release helper atomically clears its own bit and checks the old value of the opposite bit. The side that observes no opposite hold calls proc_free(), which releases the credential retained by the supplied RO record, frees that RO record, then frees the combined element. Deferred callback, two-sided free protocol.

The task and its shared read-only data diagram
The task and its shared read-only data diagram · Open SVG
Mermaid source
flowchart TD
    T["Logical task termination"] --> R["Task references eventually reach zero"]
    R --> D["Destroy task resources and clear RO pr_task"]
    D --> TH["Atomically clear TASK_HOLD"]
    P["Final BSD wait-reference release"] --> S["Deferred smr_proc_task callback"]
    S --> PH["Atomically clear PROC_HOLD"]
    TH --> C["This release sees the other hold absent?"]
    PH --> C
    C -->|"no"| W["Keep combined element and RO record"]
    C -->|"yes"| F["Release credential, free RO record, free combined element"]

The diagram describes the ordinary shared record. The two releases can occur in either order. Retaining the combined bytes for the process side does not preserve an already destroyed task's locks, map or token data. Conversely, a task reference can preserve the task after its BSD association has disappeared; it does not authorize using the raw process half as a live process. Task-only corpses have no initialized BSD lifetime to await: their task-side release can free the supplied task-only RO record and combined storage when no process hold exists. Task-side release, task-only construction.

7. Worked access, mutation and lifetime example

Take an ordinary child task t, process p and shared record r, after construction has completed. Let B be the combined allocation base and P = proc_struct_size. Then p = B, t = B + P, p->p_proc_ro = r and t->bsd_info_ro = r. These are illustrative symbols, not fixed offsets or measured addresses.

  1. A thread's tro_task points to t. current_task() returns that pointer through the validated thread_ro route; it does not increment t->ref_count. A separate caller arriving through the referenced port conversion obtains its own task reference instead. The caller's chosen route must keep t alive before reading r.
  2. task_get_ro(t) verifies the RO zone and r->pr_task == t. Suppose the selected starting flag word is only TFRO_PLATFORM, numerically 0x00000080. task_set_filter_msg_flag(t, TRUE) issues an atomic OR with TFRO_FILTER_MSG, 0x00004000, giving 0x00004080. Clearing that same flag issues an AND with its complement and restores 0x00000080. The unrelated platform bit survives both operations. These values illustrate the helpers; they do not claim that those are a running task's only policy flags. Bits, selected setter, atomic operations.
  3. Later, BSD detaches the task and the task terminates. An independent task reference can keep t and r available during the remaining task lifetime, but cannot make get_bsdtask_info(t) return a process again. The final task release destroys its resources, sets r->pr_task to null and clears the task allocation hold.
  4. If the process hold still exists at that moment, both allocation blocks remain until the process's deferred SMR callback clears it. If the process callback ran first, the task hold kept the blocks allocated, and final task release frees them. In either order, after the last task reference is released, the old t must no longer be used—even if the process side temporarily retains the combined allocation.

This example separates ordinary field access, protected mutation and storage retention. A successful backreference check is useful only inside a valid lifetime; an atomic flag update is useful only for the selected field; a retained allocation is not proof that every object previously stored in it remains initialized.

8. Source scope and limits

The reviewed bodies cover combined-zone construction, ordinary/corpse/kernel task creation, selected task field groups, thread and port access routes, RO attachment/validation, flag/control-option/filter updates, task termination and the shared allocation's final release. The layout and some fields vary with MACH_BSD, CONFIG_MACF, development/debug reference tracking, pointer authentication, memory-status features and platform selection. The representation is useful on ARM64/mobile paths without assuming a universal structure size or every optional field.

This is not a field-by-field account of the VM map, IPC space, ledger, coalition, token-update policy or every task operation. External MAC decisions and hardware/trusted-monitor enforcement are separate boundaries. Continue with task capabilities, process transitions, runtime lifecycles, SMR publication and RO update backends for those adjoining paths. No kernel execution or runtime behavior was tested for this source walkthrough.

Search the handbook

Search every chapter, interface and code excerpt.

    Diagram

    Source preview

    Open full file