TXM Jailbreak Patch Analysis
Analysis of 6 logical TXM jailbreak patches (11 instruction modifications) applied by txm_dev.py on the RESEARCH variant
of TXM from iPhone17,3 / PCC-CloudOS 26.x.
TXM Execution Model
TXM runs at a guested exception level (GL) under SPTM's supervision:
SPTM dispatches selector calls into TXM. TXM cannot execute SPTM code
(instruction fetch permission fault). TXM must return to SPTM via svc #0.
Return Path: TXM → SPTM
All TXM functions return through this chain:
Trampoline at 0x60000 (__TEXT_BOOT_EXEC)
Return Trap Stub at 0x49B40 (__TEXT_EXEC)
x16 carries a return code that SPTM uses to identify which TXM operation completed.
Return Helper at 0x26C04 (__TEXT_EXEC)
Error Handler
Error Handler at 0x25924
Panic Format
TXM [Error]: CodeSignature: selector: 24 | 0xA1 | 0x30 | 1
This is a kernel-side message (not in TXM binary). The kernel receives the
non-zero return from the svc #0 trap and formats the error:
selector: <selector_num> | <low_byte> | <mid_byte> | <high_byte>
For 0x000130A1: low=0xA1, mid=0x30, high=0x1 → | 0xA1 | 0x30 | 1
Why ret/retab Fails
Historical reference: These three failed attempts document why patching TXM functions to return early via normal return instructions does not work. The only viable path is to let the function proceed through the normal
svc #0return chain (see Return Path above).
Attempt 1: mov x0, #0; retab replacing PACIBSP
Result: [TXM] Unhandled synchronous exception at pc 0x...6C84
RETAB tries to verify the PAC signature on LR. Since PACIBSP was replaced, LR was never signed. RETAB detects the invalid PAC → exception.
Attempt 2: mov x0, #0; ret replacing PACIBSP
Result: [TXM] Unhandled synchronous exception at pc 0x...FA88 (SPTM space)
ret strips PAC and jumps to clean LR, which points to SPTM code (the caller).
TXM cannot execute SPTM code → instruction fetch permission fault (ESR EC=0x20, IFSC=0xF).
Attempt 3: pacibsp; mov x0, #0; retab
Result: Same permission fault — RETAB succeeds (PAC valid), but the return address is in SPTM space. TXM still cannot execute there.
Conclusion: No form of ret/retab works because the caller is SPTM
and TXM cannot return to SPTM via normal returns. The only way back is svc #0.
Address Mapping
Conversion: VA = file_offset - 0x1c000 + 0xFFFFFFF017020000 (for __TEXT_EXEC)
TXM Selector Dispatch
All TXM operations enter through a single dispatch function (sub_FFFFFFF01702AE80),
a large switch on the selector number (1-51). Each case validates arguments and calls
a dedicated handler. Relevant selectors:
The dispatcher passes raw page pointers through sub_FFFFFFF0170280A4 (a bounds
validator that returns the input pointer unchanged) before calling handlers.
Patch 1-2: CodeSignature Hash Comparison Bypass (selector 24)
Error: TXM [Error]: CodeSignature: selector: 24 | 0xA1 | 0x30 | 1
Addresses
selector24 Full Control Flow (0x026C80 - 0x026E7C)
The selector 24 handler is the entry point for all CodeSignature validation. Understanding its full control flow is essential context for why the NOP approach was chosen over earlier redirect-based patches.
Existing Success Path
The function already has a success path at 0x026D30 (reached by case 5 when flag is set):
Historical note: An earlier approach tried redirecting to this success path by patching 2 instructions at
0x26CBC(mov x19, x0/b #0x26D30). This was replaced with the more surgical NOP approach below because the redirect did not properly handle the hash validation return value.
Error Codes
Function: sub_FFFFFFF0170353B8 --- CS hash flags validator
Call chain: selector 24 → sub_FFFFFFF017024834 (CS handler) →
sub_FFFFFFF0170356F8 (CS validation pipeline) → sub_FFFFFFF017035A00
(multi-step validation, step 4 of 8) → sub_FFFFFFF0170353B8
Decompiled (pre-patch)
What sub_FFFFFFF0170335F8 does
Extracts hash flags from the CodeSignature blob header. Reads bswap32(*(blob + 12))
into the output parameter (the flags bitmask). Bit 1 of the flags indicates whether
a code hash is present.
What sub_FFFFFFF017033718 does
Locates the hash data within the CodeSignature blob. Validates blob header version
(bswap32(*(blob+8)) >> 9 >= 0x101), then follows a length-prefixed string pointer
at offset 48 to find the hash data. Returns the hash data pointer via output param.
Effect of NOP
With sub_FFFFFFF0170335F8 NOPed, v6 stays at its initialized value of 0.
This means (v6 & 2) >> 1 = 0 (hash-present flag is cleared). As long as
sub_FFFFFFF017033718 returns a non-null hash pointer (v7 != 0), the comparison
becomes (1 == 0) → false, so the 0x130A1 error is skipped. The function
falls through to the version checks which return success for version <= 5.
This effectively bypasses CodeSignature hash validation --- the hash data exists in the blob but the hash-present flag is suppressed, so the consistency check passes.
txm_dev.py dynamic finder: patch_selector24_force_pass()
Scans for mov w0, #0xa1 as a unique anchor to locate the CS hash validator function,
finds PACIBSP to determine function start, then matches the pattern
LDR X1,[Xn,#0x38] / ADD X2,... / BL / LDP within it. NOPs the LDR X1
(arg setup) and the BL hash_flags_extract (call).
UUID Canary Verification
To confirm which TXM variant is loaded during boot, XOR the last byte of LC_UUID:
Panic log TXM UUID: line confirmed canary ...655C → patched research TXM IS loaded.
The problem was exclusively in the selector24 patch logic (the earlier redirect approach
did not properly handle the hash validation return value).
Patch 3: get-task-allow Force True (selector 41)
Error: TXM [Error]: selector: 41 | 29
Address
Function: sub_FFFFFFF017023558 --- selector 41 handler
Call chain: selector 41 → sub_FFFFFFF0170280A4 (ptr validation) →
sub_FFFFFFF017023558
Decompiled (pre-patch)
Assembly at patch site
Effect
Replaces the entitlement lookup call with a constant 1. The subsequent TBNZ W0, #0
always takes the branch to the success path, which sets *(manifest + 0x30) = 1
(the get-task-allow flag byte). Every process now has get-task-allow, enabling
debugging via task_for_pid and LLDB attach.
What sub_FFFFFFF017022A30 does
Universal entitlement lookup function. When a1 != 0, it resolves the manifest's
entitlement dictionary and searches for the named key via sub_FFFFFFF017036294.
Returns a composite status word where bit 0 indicates the entitlement was found.
txm_dev.py dynamic finder: patch_get_task_allow_force_true()
Searches for string refs to "get-task-allow", then scans forward for the pattern
BL X / TBNZ w0, #0, Y. Patches the BL to MOV X0, #1.
Patch 4: selector 42|29 Shellcode (Debug Mapping Gate)
Error: TXM [Error]: selector: 42 | 29
Addresses
Context: Dispatcher case 42
Shellcode (at zero-filled code cave in __TEXT_EXEC)
Why this is needed
Selector 42's handler sub_FFFFFFF017023368 checks the get-task-allow byte early:
Selector 41 (patch 3) sets this byte during entitlement validation, but there are code paths where selector 42 can be called before selector 41 has run for a given manifest. The shellcode ensures the flag is always set at the dispatch level before the handler even sees it.
sub_FFFFFFF0170280A4 --- pointer validator
Since the validator returns the pointer unchanged, x20 (raw arg) and the validated
pointer both refer to the same object. The shellcode's STRB W0, [X20, #0x30]
writes to the correct location.
txm_dev.py dynamic finder: patch_selector42_29_shellcode()
- Finds the "debugger gate function" via string refs to
"com.apple.private.cs.debugger" - Locates the dispatch stub by matching
BTI j / MOV X0, X20 / BL / MOV X1, X21 / MOV X2, X22 / BL debugger_gate / B - Finds a zero-filled code cave via
_find_udf_cave()near the stub - Emits the branch + shellcode + branch-back
Patch 5: Debugger Entitlement Force True (selector 42)
Error: TXM [Error]: selector: 42 | 37
Address
Function: sub_FFFFFFF017023368 --- selector 42 handler (debug memory mapping)
Assembly at patch site
Decompiled (pre-patch)
Effect
Replaces the entitlement lookup with MOV W0, #1. The TBNZ W0, #0 always
branches to the success path, bypassing both the entitlement check and the
fallback flag check. This allows any process to create debug memory mappings
regardless of whether it has com.apple.private.cs.debugger.
txm_dev.py dynamic finder: patch_debugger_entitlement_force_true()
Searches for string refs to "com.apple.private.cs.debugger", then matches
the pattern: mov x0, #0 / mov x2, #0 / bl X / tbnz w0, #0, Y. Patches the BL
to MOV W0, #1.
Patch 6: Developer Mode Bypass
Address
Function: sub_FFFFFFF017023A20 --- developer mode configuration
Called during TXM initialization to determine and store the developer mode state.
The result is stored in byte_FFFFFFF017070F24, which is the gate flag checked by
selector 41 (sub_FFFFFFF017023558).
Assembly at patch site
Decompiled (pre-patch)
Effect
NOPing the TBNZ makes execution always fall through to MOV W20, #1, forcing
developer mode enabled regardless of the system policy byte. Without this:
- The
TBNZwould jump toloc_FFFFFFF017023A6C(the normal path) - The normal path checks xART availability, device tree flags, and user configuration
- On PCC VMs, this can result in developer mode being disabled
Developer mode is a prerequisite for selectors 41 and 42 --- the selector 41
handler returns error 27 immediately if byte_FFFFFFF017070F24 is not set:
txm_dev.py dynamic finder: patch_developer_mode_bypass()
Searches for string refs to "developer mode enabled due to system policy configuration", then scans backwards for a tbz/tbnz/cbz/cbnz instruction
matching w9, #0. NOPs it.
Patch Dependency Chain
The patches have a logical ordering --- later patches depend on earlier ones:
Boot-time flow
- TXM initializes →
sub_FFFFFFF017023A20runs → Patch 6 forces devmode ON - Process loads → selector 24 validates CodeSignature → Patches 1-2 skip hash check
- Process requests entitlements → selector 41 → Patch 3 grants get-task-allow
- Debugger attaches → selector 42 → Patch 4 pre-sets flag + Patch 5 grants debugger ent
- Debug mapping succeeds → LLDB can attach to any process
Summary Table
Total: 6 logical patches, 11 instruction modifications (counting shellcode), enabling:
- CodeSignature bypass (patches 1-2)
- Universal get-task-allow (patches 3-4)
- Universal debugger entitlement (patch 5)
- Forced developer mode (patch 6)