-
Notifications
You must be signed in to change notification settings - Fork 0
Description
On macOS Sonoma 14.4.1 (23E224, Apple Silicon), EntitlementJail can successfully exercise the sandbox extension SPI for issuing and consuming extensions under a dedicated injection-friendly entitlement profile, but sandbox_extension_update_file_by_fileid is not generally usable from userland. The kernel entrypoint for this operation is _syscall_extension_update_file_by_fileid in sandbox.kext, which expects a 24-byte user struct whose first field is a pointer to an 8-byte payload, whose second field is a selector, and whose third field must be zero (or it returns EINVAL). The kernel only reads the payload’s low 32 bits and uses it as a lookup key; EFAULT arises only from malformed pointers/copyin failures, while EINVAL primarily indicates lookup/validation failure.
Static kernel-collection analysis closes the key uncertainty: the value compared against the payload low-32 is an internal id stored at [obj + 0xc0], and this id is constructed in-kernel (class/type bits combined with global counter state and uniqueness checks across a list), not derived from file identity (inode) or token fields. After adding an ABI-matching call variant in EntitlementJail, repeated correlation sweeps show stable outcomes with no EFAULT (call shape confirmed) and near-universal EINVAL for candidates like inode and token numeric fields; the only non-EINVAL case observed is when the payload low-32 equals st_dev, which produces selector-dependent results including OK for selector 2 (and ENOENT for selector 1), but still with no demonstrated access change. Overall, the operation’s success depends on a kernel-minted id that userland cannot presently obtain or predict, leaving update_file_by_fileid blocked as a practical userland maintenance flow on this baseline.
Scope and baseline
- Host baseline: macOS Sonoma 14.4.1 (23E224), Apple Silicon
- world_id: sonoma-14.4.1-23E224-arm64-dyld-2c0602c5
- Evidence sources are static kernel-collection / kext disassembly artifacts (Ghidra) plus EntitlementJail probe runs (no live kernel memory capture).
EntitlementJail: current probe + profile state
Implemented capabilities
EntitlementJail now ships:
-
A
sandbox_extensionprobe (xpc/InProcessProbeCore.swift) that exercises:sandbox_extension_issue_filesandbox_extension_consumesandbox_extension_release- plus wrapper and maintenance symbols for discovery/instrumentation
-
A dedicated entitlement profile:
ProbeService_fully_injectable_extensions(“fully_injectable_extensions”), allowing issuance/consumption while remaining injection-friendly.
Empirical behavior in EntitlementJail (Sonoma 14.4.1)
Issue/consume path (works)
sandbox_extension_issue_filesucceeds underfully_injectable_extensions.sandbox_extension_consumesucceeds (same or different profile).- Token observation: semicolon-delimited tokens; token field 9 matches
stat -f %i(inode) for the target file.
Release path (Sonoma limitation)
- Release calls can return OK, but access is not reliably revoked in-process on 14.4.1; revocation is observed only after process exit.
- Documented as best-effort cleanup.
Update-by-path (sandbox_extension_update_file) behavior
- Returns success when called with path + flags.
- No clear, observable access change has been demonstrated in current tests.
Update-by-fileid (sandbox_extension_update_file_by_fileid) behavior (updated)
- Earlier attempts using token-derived or swapped-argument call variants produced
EINVAL/EFAULTand were not actionable. - After correcting the call shape to match the actual kernel ABI (details below),
EFAULTcan be eliminated (indicating correct copy-in and argument construction). - With the corrected ABI, outcomes are stable across runs and show that some values can pass lookup and proceed further.
Kernel ABI closure: _syscall_extension_update_file_by_fileid (sandbox.kext)
Entry point (sandbox.kext): _syscall_extension_update_file_by_fileid
Evidence: dumps/ghidra/out/14.4.1-23E224/sandbox-kext-function-dump/function_dump.json
User ABI at the syscall boundary (confirmed by copy-in sequence)
Disassembly excerpt (canonical addrs):
-
Copies in a 0x18 (24-byte) struct:
0xfffffe000b421aa4 bl 0xfffffe00086b7eb4; copy-in 0x18 bytes
-
Uses struct field0 as a pointer to an 8-byte payload and copies in 0x8 bytes:
0xfffffe000b421ab8 ldr x0,[sp, #0x80]; field0 (pointer)0xfffffe000b421ac4 bl 0xfffffe00086b7eb4; copy-in 8 bytes from field0
-
Enforces field2 == 0:
0xfffffe000b421b24 ldr x9,[sp, #0x90]; field20xfffffe000b421b28 cbz x9,0xfffffe000b421b3c- else sets
w9=#0x16→EINVAL
-
Passes selector and payload onward:
0xfffffe000b421b3c ldr x21,[sp, #0x88]; field1 (selector)0xfffffe000b421b54 bl 0xfffffe0008757afc
Inferred struct layout (host-specific, Sonoma 14.4.1):
+0x00: pointer to 8-byte payload+0x08: selector (passed onward)+0x10: must be zero (else immediateEINVAL)
EFAULT vs EINVAL split (now grounded)
EFAULT: either copy-in of the 0x18 struct fails or copy-in of the 8-byte payload fails (both via0xfffffe00086b7eb4).EINVAL (0x16): field2 non-zero (fast path), or downstream lookup/validation failure.
Kernel validation and lookup: low-32 payload matched against [obj + 0xc0]
Validation helper: 0xfffffe0008757afc (KC)
Evidence: dumps/ghidra/out/14.4.1-23E224/kernel-collection-addr-window-dump/addr_window_dump_0xfffffe0008757afc.json
Key excerpt:
-
Reads only the low 32 bits of the 8-byte payload:
0xfffffe0008757b20 ldr w0,[x0]
-
Calls internal lookup:
0xfffffe0008757b28 bl 0xfffffe000876a30c
-
Returns
EINVAL (0x16)when lookup fails (null return). -
Uses
selectorafter lookup to pick one of two vtable paths:0xfffffe0008757b3c cmp x22,#0x2
-
Observed secondary error:
0xfffffe0008757be4 mov w20,#0x2d(vtable missing / alternate failure mode)
Internal id lookup: 0xfffffe000876a30c (KC)
Evidence: dumps/ghidra/out/14.4.1-23E224/kernel-collection-addr-window-dump/addr_window_dump_0xfffffe000876a30c.json
Key excerpt (match against +0xc0):
-
Iterates a list and compares payload low-32 (
w22) against an object’sw9 = [obj + 0xc0]:0xfffffe000876a380 ldr w9,[x20, #0xc0]0xfffffe000876a384 cmp w9,w22
-
On match, a refcount-ish update is gated by
w21(second arg to lookup):0xfffffe000876a38c cbz w21, ...0xfffffe000876a390 ldr w8,[x20, #0x9a4]0xfffffe000876a39c str w8,[x20, #0x9a4]
Implication: the payload must supply the correct kernel-internal id equal to [obj + 0xc0]. Tokens, inode, or file metadata only matter if they can be mapped to that internal id.
Provenance closure: [obj + 0xc0] is kernel-constructed (not fileid/token-derived)
This closes the “what is [obj + 0xc0]?” question for Sonoma 14.4.1 using static KC artifacts.
List head used by lookup
From 0xfffffe000876a30c:
0xfffffe000876a354 adrp x8,-0x1fff42830000xfffffe000876a358 ldr x20,[x8, #0xfa0]
Computed list head address:
0xfffffe000bd7dfa0(page0xfffffe000bd7d000+0xfa0)
Addr xrefs (from addr_lookup.json) show reads/writes to 0xfffffe000bd7dfa0 by:
0xfffffe000876a30c(lookup)0xfffffe0008765ed4(writersandbox_extension_update_file_by_fileidblocked by access to kernel information #1)0xfffffe0008a1ef8c(writer QuarantineLab bundle resolution error #2)- plus additional list-manipulation helpers
Writer #1: FUN_fffffe0008765ed4 (direct, same list head)
Evidence:
dumps/ghidra/out/14.4.1-23E224/kernel-collection-addr-window-disasm/0xfffffe0008765fc4/addr_window_disasm.json
Store site:
0xfffffe0008765fc4 stp w13,w8,[x20, #0xc0]
Decisive provenance one step upstream:
-
Loads a “type-ish” field:
0xfffffe0008765f3c ldr w8,[x16, #0x18]
-
Reads a 16-bit global counter:
0xfffffe0008765f44 ldrh w10,[x9, #0xd80]
-
Constructs the id in
w13:0xfffffe0008765f5c mov w11,#0x180000000xfffffe0008765f60 add w11,w11,w8, LSL #0x180xfffffe0008765f70 bfxil w13,w10,#0x0,#0x10
-
Ensures uniqueness by scanning existing list entries and comparing:
- existing
[+0xc0]vs proposedw13 - existing
[+0xc4]vsw8 - on collision: increments and retries
- existing
-
Finally stores:
[obj + 0xc0] = w13(constructed id)[obj + 0xc4] = w8(type-ish)
Interpretation: [obj + 0xc0] is a kernel-minted id assembled from a class prefix (0x18000000), a type component (w8 << 24), and a low-16 counter component, with collision checking across the list.
Writer #2: FUN_fffffe0008a1ef8c (same list head, different constant)
Evidence:
dumps/ghidra/out/14.4.1-23E224/kernel-collection-addr-window-disasm/0xfffffe0008a1f1b0/addr_window_disasm.json
Store site:
0xfffffe0008a1f1b0 stp w11,w10,[x8, #0xc0]
Provenance:
-
Loads a global counter:
0xfffffe0008a1f1a8 ldr w10,[x10, #0xc0c]
-
Uses a different class prefix constant:
0xfffffe0008a1f1ac mov w11,#0x22000000
-
Stores:
[obj + 0xc0] = 0x22000000[obj + 0xc4] = counter
Interpretation: there are at least two object “classes” in the same list head namespace, with distinct high-bit prefixes and id constructions.
Bottom line from provenance
-
The id matched in
0xfffffe000876a30c([obj + 0xc0]) is constructed in-kernel and is not directly derived from:- token string fields
- inode (
st_ino) - or other ordinary file metadata
-
Even if some userland-visible value correlates with an id in a given session, the generating inputs include kernel-internal state (global counters / class/type fields).
Updated EntitlementJail finding: corrected ABI produces stable outcomes; one st_dev-correlated path returns OK
EntitlementJail added a Sonoma-14.4.1 ABI-matching call variant and selector control:
-
New call variant:
payload_ptr_selector-
Builds the 0x18 struct with:
field0 = pointer to 8-byte payloadfield1 = selectorfield2 = 0(forced)
-
-
Added
--selectorflag. -
Ran correlation sweep in a single XPC session under
fully_injectable_extensions:- issue → consume → update
-
Result: call shape is confirmed (no
EFAULT), and outcomes are stable across 5 runs.
Candidate → outcome matrix (stable across 5 runs)
All candidates are interpreted as payload low-32 values (the only bytes the kernel uses in the lookup).
-
consume_rc(value 0):- sel0
EINVAL, sel1EINVAL, sel2EINVAL
- sel0
-
st_dev(value 16777233 /0x01000011):- sel0
EINVAL - sel1
ENOENT(errno 2) - sel2
OK
- sel0
-
st_ino(value 114566121 /0x06D…):- sel0
EINVAL, sel1EINVAL, sel2EINVAL
- sel0
-
token_field_0 low32(value 2584418386):- sel0
EINVAL, sel1EINVAL, sel2EINVAL
- sel0
-
token_field_5(value 26 /0x1a):- sel0
EINVAL, sel1EINVAL, sel2EINVAL
- sel0
-
token_field_7(value 1):- sel0
EINVAL, sel1EINVAL, sel2EINVAL
- sel0
-
token_field_10(value 83 /0x53):- sel0
EINVAL, sel1EINVAL, sel2EINVAL
- sel0
Interpretation consistent with kernel path
-
Since the call shape is correct (no
EFAULT) and results are stable, remainingEINVALoutcomes are best explained as lookup misses in0xfffffe000876a30c(payload low-32 did not match any[obj + 0xc0]in the list). -
The
st_devcandidate producing non-EINVALresults (includingOKunder selector 2) implies:- for that payload value, the lookup is likely succeeding (otherwise it would be
EINVAL), and subsequent selector-dependent logic determines the final return (ENOENTvsOK).
- for that payload value, the lookup is likely succeeding (otherwise it would be
-
No access change has been observed tied to the
OKcase so far; the operational meaning of “success” remains unclear in userland-visible behavior.
Relationship to prior hypotheses and remaining loose ends
Earlier “token/inode should be enough” is now ruled out for this host
-
Kernel lookup key is
[obj + 0xc0], and provenance shows it is kernel-constructed. -
The sweep confirms that:
- inode (
st_ino) does not match - tested token numeric fields do not match
- inode (
Earlier +0x150 sandbox-adjacent stores remain unlinked to this path
Previously identified KC stores to [x23 + 0x150] (e.g., 0xfffffe000b021978 str x0,[x23, #0x150]) are still not tied to the update_file_by_fileid lookup path, which definitively uses +0xc0.
Current conclusion (Sonoma 14.4.1)
- The correct kernel ABI for
update_file_by_fileidis now identified and implementable from userland (0x18 struct + payload pointer + selector + field2==0). - The lookup key is payload low-32 matched against a kernel-maintained object list’s
[obj + 0xc0]. - Static KC analysis identifies writers that construct
[obj + 0xc0]from class/type components and global counters, i.e., kernel-minted rather than fileid/token-derived. - With the correct ABI, userland can drive the call without
EFAULT, and can sometimes obtain non-EINVALresults; one observed case (st_devwith selector 2) returnsOK, but no access change has been demonstrated. - Without a way to obtain or predict the runtime
[obj + 0xc0]id(s), there is still no general end-to-end userland method to useupdate_file_by_fileidas a reliable maintenance operation.
Practical stopping point and what would decide the remaining questions
-
To make
update_file_by_fileidgenerally usable from userland, one of the following must become true:- a userland-visible API exposes the relevant internal id (or a stable mapping to it), or
- the id construction can be derived from userland-observable quantities (currently contradicted by static provenance), or
- kernel instrumentation/debugging can capture the runtime id(s) for correlation and mapping.
-
The single observed
st_devcorrelation (selector 2 returning OK) is now the most informative anomaly:- it suggests there is at least one reachable object id equal to that payload value in the tested environment, but it does not yet establish a general derivation rule nor a user-visible effect.