One PACIA instruction describes an authentication-code calculation, not why a pointer remains verifiable across a process switch. XNU pointer authentication also depends on boot configuration, shared-region state, scheduling, exception transitions, and controlled helper interfaces that preserve compatible signing and checking contexts.
This analysis follows state flows from a 2021 iPhone 12 kernel study, cross-checked against Apple XNU 7195.60.75 and its design notes. The sample's complete build identity is missing. Offsets, private control bits, and boot constants therefore remain sample-specific; public fast and slow switching paths should not be spliced into a single universal instruction sequence.
Three kinds of value answer different questions
| Object | Question answered | Distinction to preserve |
|---|---|---|
APIAKey / APIBKey / APDAKey / APDBKey and related registers |
Which hardware authentication material does the operation select? | Low and high writes do not describe the entire hardware derivation |
jop_pid / rop_pid / srk_jop_key software fields |
Which context does a task or shared region require? | A CPU cache copy is not an additional PAC instruction key |
| Modifier / discriminator | For which use or storage location is the signature valid? | It is not the complete signing key |
A fixed software constant does not reveal all effective signing material. Apple's fixed-version design notes describe an optional implementation with a per-boot diversifier and KERNKey differentiation between kernel and userspace. Constant architectural-register writes can coexist with other signing inputs; the applicable implementation still determines a particular chip's behavior.
Read boot constants in write order
| Register pair | Low | High |
|---|---|---|
| APIBKey | K |
K+1 |
| APDBKey | K+2 |
K+3 |
| Private pair | K+4 |
K+5 |
| APIAKey | K+6 |
K+7 |
| APDAKey | K+8 |
K+9 |
| APGAKey | K+10 |
K+11 |
The middle private pair lacks confirmed complete semantics and is left unnamed. K+6 is 0xFEEDFACEFEEDFAD5, also seen as a default software state value on later paths.
For S3_4_C15_C0_4, AND #0x2 tests bit 1, while ORR #0x1 and ORR #0x4 set bits 0 and 2. Bit numbering starts at zero; confusing the third bit with bit 3 changes the control condition.
0x3454593d | 0x40000000 = 0x7454593d, with added bit 30 corresponding to SCTLR_EL1.EnIB. That describes this write only. Later paths or platform extensions may control other authentication classes, so it does not establish that they remain disabled.
Reuse a shared-region entry before selecting a new value
shared_region_key_alloc looks up shared_region_id. A match reuses the software entry and increments its reference count. An inheriting caller must also match the stored value against inherited_key. This is separate from selecting a value for a newly allocated entry.
| Condition | Stored value |
|---|---|
diversify_user_jop disabled |
Default 0xFEEDFACEFEEDFAD5 |
| Enabled with inheritance | inherited_key |
| Enabled, no inheritance, nonempty identifier | Retry random generation until nonzero |
| Other observed default path | Retain the default |
The 0x28 entry stride accommodates links, an identifier pointer, a 64-bit software value, and reference-count storage. It is not a stable ABI. Shared regions allow eligible processes to use compatible signing contexts; they do not expose kernel registers to ordinary userspace.
Fixed modifiers 0x2ABE and 0x9BF6 are useful landmarks for tracing authenticated indirect calls. Repeated constants alone establish neither authentication collisions nor predictable randomness. Check the signing schema, target pointer, and reachable call path together.
jophash covers an explicit set of fields
h = PACGA(pc, state_address)
h = PACGA(cpsr & 0xFFFFFFFFDFFFFFFF, h)
h = PACGA(lr, h)
h = PACGA(x16, h)
h = PACGA(x17, h)
state.jophash = hThis is a PACGA operand-dependency chain, not its cryptographic implementation. PC and the state address begin the chain, followed by CPSR, LR, X16, and X17. Other fields do not automatically receive the same protection merely because they occupy the same structure.
The CPSR mask clears bit 29, the carry flag. Both public signing and checking macros apply this transformation so a system-call return can change carry without re-signing. Omitting the mask creates the false expectation that a carry-only change must mismatch.
| Offset | Name | Type | Size |
|---|---|---|---|
| 0x00 | padding | 0x68 | |
| 0x68 | lr | 8 | |
| 0x70 | padding | 8 | |
| 0x78 | pc | 8 | |
| 0x80 | cpsr | 4 | |
| 0x84 | padding | 4 | |
| 0x88 | jophash | 8 |
Only identified fields from the sample are shown; gaps are not filled with a guessed complete structure. ml_check_kernel_signed_state recomputes with the same field order and state address. machine_stack_attach signs initial kernel-stack state, supplying a valid value for later context-switch checks.
Changing signed state is more than writing a field and re-signing. The public design disables interrupts before loading and checking sensitive values, mutates and re-signs them in registers, writes them back, and restores interrupts. An unintended spill during that window would reintroduce a memory-tampering opportunity.
Scheduling and exception transitions are different switches
| Path | Main observation in the sample | Source-comparison limit |
|---|---|---|
| Scheduling a new thread | Compare software state with the CPU cache and update B keys if needed | Track rop_pid state and its cache together |
| Entering the kernel from userspace | Prepare kernel-side A context and sign saved state | Actual registers depend on fast or slow A-key switching |
| Returning to userspace | Check saved state before restoring user context | Keep this separate from kernel-internal exception returns |
B-key writes place value through value+3 into the low and high APIB and APDB parts. Matching cached state skips redundant writes. The sample's exception path shows direct A-register writes, while the fixed source's REPROGRAM_JOP_KEYS also supports a fast path through KERNKey registers. Configuration selects these implementations; every exception does not necessarily rewrite all architectural keys.
Saved-state signing and checking must use paired contexts. Collapsing scheduling, user exceptions, and kernel exceptions into one abstract key update hides the execution state in which a signature was produced.
Helper instructions do not define the public input set
pmap_sign_user_ptr saves interrupt and authentication state, selects a userspace context within a controlled window, performs the operation, and restores state. The sample clears or sets private-register bit 2, not bit 1. Its role in kernel/userspace signing should be derived from the implementation rather than calling it a lock bit from the function name alone.
Signing dispatch accepts key number 0 for PACIA and 2 for PACDA. An authentication assembly helper may also contain AUTIB and AUTDB, but instruction presence does not establish that a higher-level interface accepts those keys. The checked XNU 7195.60.75 pmap_auth_user_ptr_internal accepts only ptrauth_key_asia and ptrauth_key_asda before calling its helper.
Trace accepted parameters through the complete call path. Reachability of B-key branches in another historical build needs its own entry checks. Seeing B-key authentication also does not prove that the call has just rewritten B-key registers.
ABI aliases do not promise identical values in every process
| ABI name | Base key | Number |
|---|---|---|
ptrauth_key_process_independent_code |
ptrauth_key_asia |
0 |
ptrauth_key_process_dependent_code |
ptrauth_key_asib |
1 |
ptrauth_key_process_independent_data |
ptrauth_key_asda |
2 |
ptrauth_key_process_dependent_data |
ptrauth_key_asdb |
3 |
A keys have process-independent aliases and B keys process-dependent aliases. A helper that updates only A keys does not reverse those definitions. C function pointers and C++ vtable pointers use the corresponding A code and data keys; return addresses use the B code-key role.
These names describe ABI roles, not an unconditional promise that arbitrary processes use identical values. Shared regions, inheritance, and platform implementation still matter. Current Clang documentation explicitly limits the cross-process interpretation. A signing schema also includes its discriminator and address-diversification rule.
Offline checks cover arithmetic and dependencies only
The model represents PACGA as nested tuples to track inputs. It generates no real PAC and proves nothing about cryptographic collisions. Addresses and random outputs are illustrative; new-entry selection omits queue reuse and concurrency.
MASK64 = (1 << 64) - 1
K = 0xFEEDFACEFEEDFACF
values = [(K + i) & MASK64 for i in range(12)]
assert values[6] == 0xFEEDFACEFEEDFAD5
assert values[11] == 0xFEEDFACEFEEDFADA
assert (0x3454593D | 0x40000000) == 0x7454593D
assert [i for i in (0, 1, 2) if ((1 | 4) >> i) & 1] == [0, 2]
CARRY = 1 << 29
assert MASK64 & ~CARRY == 0xFFFFFFFFDFFFFFFF
def transcript(address, pc, cpsr, lr, x16, x17):
h = ("PACGA", pc, address)
for value in (cpsr & ~CARRY, lr, x16, x17):
h = ("PACGA", value, h)
return h
state = [0x1000, 0x2000, 0x40000000, 0x3000, 4, 5]
baseline = transcript(*state)
for i in range(6):
changed = state.copy()
changed[i] ^= 1
assert transcript(*changed) != baseline
changed = state.copy()
changed[2] ^= CARRY
assert transcript(*changed) == baseline
def fresh_value(diversify, inherit, identifier, inherited, random_values):
if not diversify: return values[6]
if inherit: return inherited
if not identifier: return values[6]
return next(x for x in random_values if x != 0)
assert fresh_value(False, True, "example", 7, iter(())) == values[6]
assert fresh_value(True, True, "example", 7, iter(())) == 7
assert fresh_value(True, False, "", 7, iter(())) == values[6]
assert fresh_value(True, False, "example", 7, iter((0, 0, 9))) == 9
print("PASS: constants; 6 transcript dependencies; carry exclusion; 4 new-entry choices")The checks pass for boot increments, six dependency inputs, carry exclusion, and four new-entry choices. They establish which values participate and which branch selects state, not hardware behavior on arm64e.
Keep architectural registers, saved software values, signing schemas, and call reachability distinct. A complete authentication explanation shows that signing and checking use compatible fields, addresses, and execution contexts.
Official references
Fixed-version files document the public design and implementation. Current Clang and Apple developer documentation cross-check terminology and ABI roles, not sample-specific offsets.