#603

Issues with .NET MAUI applications

Openprockallsymsopened 24d ago

Interpreter/JIT apps (.NET MAUI, Mono) do not run on research guests

Measured 2026-10-06 on a research guest (iPhone17,3, iOS 26.0 / 23A341, kernel xnu-12377.42.6~55 RELEASE_ARM64_VRESEARCH1, roothide bootstrap present, Launchpad/bundle 2.2.5). Subject: a stock .NET MAUI app built with the Mono interpreter — the main binary exports interp_to_native_trampoline, native_to_interp_trampoline, mono_jit_call and carries " interp" strings. No application-specific identifiers, paths, or data are retained in this note.

Symptom

The app installs (via installd or apps.install) and spawns — it becomes frontmost and gets a pid — then is SIGKILLed in under a second on the first JIT page it executes:

Exception Type:    EXC_BAD_ACCESS (SIGKILL)
Exception Subtype: KERN_PROTECTION_FAILURE
Termination:       namespace CODESIGNING, code 2, indicator "Invalid Page"
codeSigningFlags:  0x2200000D  (CS_VALID|CS_GET_TASK_ALLOW|CS_INSTALLER|CS_DYLD_PLATFORM|CS_SIGNED; no CS_DEBUGGED)

The faulting frame is inside the app's own Mono runtime. This reads like a signing bug but is not one — it is the kernel refusing to execute a dynamically generated (unsigned) page.

Root cause (measured in-process)

A diagnostic dylib was injected into the live app via the existing SystemHook → <jbroot>/usr/lib/TweakLoader.dylib load path (SystemHook dlopens that exact path for every app process, so dropping a name-gated stub there runs code inside the target). It exercised the JIT primitives directly:

Probe (inside the app)Result
mmap(RW X, MAP_ANON) plain RWXmmap succeeds — the kernel W^X relaxation works
execute an instruction written into that RWX pageSIGKILL — TXM blocks exec of unsigned pages
mmap(RW X, MAP_ANON|MAP_JIT)mmap succeeds
pthread_jit_write_protect_supported_np()returns 1
pthread_jit_write_protect_np (dlsym, incl. explicit libsystem_pthread handle)NULL — not exported
pthread_jit_write_with_callback_np (dlsym)NULL — not exported
linking pthread_jit_write_protect_np directlydylib fails to load (undefined symbol)

Two independent walls, either of which is fatal to Mono's interpreter:

  1. Plain RWX execution is TXM-blocked. The W^X hook (see Research/KernelCustomFirmwarePatches/26.5_jb_hook_fixes.md) lets you map RWX memory, but TXM still refuses to execute a page whose contents are not covered by a valid signature → CODESIGNING "Invalid Page".
  2. The sanctioned MAP_JIT path is unusable from userland. MAP_JIT mapping works and ..._supported_np() claims support, but the write-protect toggle functions are not exported by libsystem_pthread on this PCC userland, so a MAP_JIT region can never be flipped writable to populate it. Mono has no way to emit code into a MAP_JIT region.

Net: there is no userland path to runtime JIT for a third-party process on this guest.

Why it presents as ".NET MAUI"

MAUI / .NET-iOS apps routinely ship with the Mono interpreter (all Debug builds, and Release builds with UseInterpreter=true). The interpreter emits native trampolines at runtime → needs to execute freshly written code → needs JIT. Full-AOT builds (interpreter disabled) need no JIT and install/run normally here. The same failure class applies to any JIT-dependent app (emulators, Flutter debug, heavy JSC use).

Ruled out (all tested, not assumed)

  • Entitlements (ad-hoc re-sign): get-task-allow, dynamic-codesigning, and com.apple.developer.cs.allow-jit + ...web-browser-engine.webcontent.
    • allow-jit alone → AMFI at exec: "allow-jit disallowed without …webcontent".
    • allow-jit+webcontent → installd refuses ("only allowed on app extensions …com.apple.web-browser-engine.content"). Installing via apps.install (which bypasses installd) lets it spawn, but TXM still kills it at runtime; codeSigningFlags unchanged.
    • dynamic-codesigning → installs + spawns, no runtime JIT grant → same kill.
  • csops(CS_OPS_SET_STATUS) to set CS_DEBUGGED on self → returns success but the kernel filters the bit (flags unchanged).
  • ptrace(PT_TRACE_ME) → EPERM.
  • debugserver / idevicedebug / frida-spawn (which would set CS_DEBUGGED) → all need the Developer Disk Image; on iOS 17+ the personalized DDI personalizes but fails to mount on the CFW guest (Failed to mount … at /System/Developer). frida is not installed on the guest (open :27042 earlier was only an iproxy local listener).

Open questions

  1. Is running JIT/interpreter apps in scope for research guests? If not, worth documenting as a known limitation — the CODESIGNING "Invalid Page" symptom is easily misread as a signing bug.
  2. If in scope, the guest needs either:
    • the pthread JIT write-protect API restored/exposed (pthread_jit_write_protect_np / ..._with_callback_np) so MAP_JIT is usable, or
    • a kernel patch letting TXM permit unsigned-page execution for a get-task-allow process (e.g. honoring CS_DEBUGGED, which currently cannot be set from userland here).
  3. A kernel-call bridge exists on the guest (kcall10, sysent[439]), so a userland JIT-enable helper is conceivable — but it needs the exact kernel primitive that satisfies TXM on this A18/TXM part, and whether CS_DEBUGGED alone suffices under TXM here is unverified (the kernel's check_jit_entitlement path, ret 0xA0, gates on com.apple.developer.cs.allow-jit + ...web-browser-engine.webcontent).

Potential Workaround

Build/test a full-AOT (interpreter-disabled) .NET MAUI build — no JIT, installs and runs normally via ideviceinstaller / apps.install.

Activity

0

No activity yet.