# Jailbreak Kernel Hooks — cloudOS 26.5 Fixes

_Last updated: 2026-06-16_

## What this is

The jailbreak (`jb`/`exp`) firmware variants apply a set of small kernel edits ("hooks")
that switch off specific security checks so the jailbreak can do its job. Each hook
finds its spot in the kernel by looking for a recognizable code pattern — **not** by a
fixed address — because Apple's kernel layout shifts with every release.

When we moved to **cloudOS 26.5**, Apple recompiled the kernel and several of those
patterns changed shape. Nine hooks could no longer find their spot, so they **silently
did nothing**. The build still reported "success" (it only fails if *zero* patches
apply), so the breakage only showed up at runtime — starting with `sudo` not working.

This document lists each broken hook, why it matters, and how we re-anchored it so it
works on 26.5 **and** older releases.

## The guiding principle for every fix

We never hardcode an address. For each hook we picked an anchor that is **stable across
versions**:

- a **string** the kernel still contains (e.g. a panic message),
- a **standard constant** that Apple won't change (an error code, a privilege ID, a
  protection bit), or
- a **relationship** between functions (who calls whom).

…and then we require the match to be **unique** — if the pattern ever becomes
ambiguous, the hook refuses to patch rather than guess wrong. The result is the same
patch lands on 26.5 and on the earlier kernels we derived the originals from.

---

## The hooks

### 1. `sudo` / setuid (cred-label on exec)
- **The hook:** re-applies "run as root" when a setuid binary is launched.
- **Why it's needed:** the jailbreak's files live on a partition mounted "no setuid," so
  without this hook `sudo` and every root helper run as a normal user.
- **What broke & the fix:** the hook located a helper function by an exact instruction
  sequence whose last instruction's offset changed (an internal struct field moved). We
  now match the stable part of that sequence and accept *any* value for the field that
  moved, instead of pinning the old one.
- **Symptom if missing:** `sudo` fails with "effective uid is not 0 … nosuid".

### 2. Process security policy
- **The hook:** forces the "are you allowed to inspect/control this other process?" check
  to always say yes.
- **Why it's needed:** jailbreak tools enumerate, inspect, and manage other processes;
  the stock check denies that across different users.
- **What broke & the fix:** the old code found the check via a switch-table size number
  that changed, and on 26.5 it accidentally matched the wrong function. We now find it by
  the one thing that's unique to it: it asks for the **"query other users' processes"
  privilege**, a fixed ID Apple keeps stable. Only this function uses it.
- **Symptom if missing:** process listing/inspection tools get "permission denied."

### 3. `proc_pidinfo` pid-0 guards
- **The hook:** removes two early checks that block asking about the kernel/PID 0.
- **Why it's needed:** some kernel-introspection tooling queries PID 0.
- **What broke & the fix:** this code got folded into a much larger function, so the old
  "look at the start of the function" approach missed it. We now search for the exact
  little guard sequence (it ends right before the kernel's `EINVAL` default and a switch),
  which is unique kernel-wide — so we no longer need to find the enclosing function at all.
- **Symptom if missing:** introspecting the kernel task returns "permission denied."

### 4. Task conversion
- **The hook:** lets you obtain another process's control handle.
- **Why it's needed:** this is the foundation for injecting into, debugging, or
  controlling other apps (`task_for_pid`-style operations).
- **What broke & the fix:** the compiler swapped the order of the two values in a
  comparison, so the old pattern didn't match. The fix accepts the comparison in **either
  order**. Everything else about the pattern is unchanged.
- **Symptom if missing:** in the *harder* cases — Developer Mode off, or targeting Apple's
  own system processes — getting a handle is rejected. Everyday use (a root, entitled tool
  with Developer Mode on attaching to a regular app) still works without this hook, which
  is why tools like frida-server kept running. See "How much did the broken hooks actually
  matter?" below.

### 5. Secure boot-root policy
- **The hook:** forces the "is this boot filesystem approved?" answer to success.
- **Why it's needed:** lets the device boot from a custom/ramdisk root instead of only an
  Apple-approved one.
- **What broke & the fix:** Apple inserted extra code that pushed the surrounding
  landmarks out of the window the old matcher searched. We re-anchored on the actual
  decision: the single spot that picks between "success" and the **"not privileged" error
  code** (a fixed IOKit constant). That's unambiguous and doesn't depend on the
  surrounding code.
- **Symptom if missing:** a custom root gets torn down during boot.

### 6. Mount policy
- **The hook:** removes two gates that block the mounts the jailbreak needs.
- **Why it's needed:** the jailbreak has to remount the system read-write and mount its
  own bootstrap; the stock policy rejects this.
- **What broke & the fix:** one gate was unchanged; the other was recompiled — two
  instructions got merged into one and the branch direction flipped. We now match the
  underlying check (a load of a status byte followed by a test of the same role bit)
  regardless of how it's spelled, and clear that byte.
- **Symptom if missing:** the jailbreak can't set up its filesystem (`/var/jb`), which can
  stall first-boot setup entirely.

### 7. Persona validation (process spawn)
- **The hook:** removes two rejections in the path that launches a process under a
  different identity ("persona").
- **Why it's needed:** jailbreak components launch helpers under specific identities.
- **What broke & the fix:** a trailing landmark the old matcher relied on was recompiled.
  We re-anchored on the two sibling "reject if empty" checks themselves (they both jump to
  the same rejection), which is the actual gate and is stable.
- **Symptom if missing:** launching a process with a custom persona is rejected.

### 8. Writable + executable memory (W^X)
- **The hook:** lets a memory region be both writable and executable (RWX).
- **Why it's needed:** required for JIT, runtime code generation, and many hooking
  frameworks that write code then run it.
- **What broke & the fix:** this one is compiled two different ways depending on the
  release. On 26.1/26.3 there's an explicit "skip the downgrade" branch; on 26.5 the
  downgrade became a runtime mask that strips one permission bit. We handle **both**: on
  the older shape we make the skip-branch unconditional; on the newer shape we widen the
  mask to keep **all** requested bits. The widen is strictly more permissive — it can only
  allow what was being blocked, never break a case that already worked. (This dual-shape
  handling was added specifically so the fix is backward-compatible — see below.)
- **Symptom if missing:** code that needs a *single* page that is writable and executable
  at the same time (some in-process JITs) fails. Tools that write code while a page is
  writable and then flip it to execute-only — like frida's out-of-process injection — are
  **not** affected, because that's still allowed under W^X.

### 9. Kernel-call syscall (`kcall10`)
- **The hook:** turns an unused syscall into a "call a kernel function from userspace"
  helper the jailbreak uses to drive the kernel.
- **Why it's needed:** it's the jailbreak's convenient bridge for kernel operations.
- **What broke & the fix:** finding the syscall table relied on locating an unimplemented
  "not supported" stub, but on 26.5 that stub looks different. We now find the table by
  its **shape** — the long run of fixed-size syscall entries that all point into kernel
  code — which doesn't depend on any one stub. The table is unmistakable (hundreds of
  entries; nothing else looks like it).
- **Symptom if missing:** the jailbreak loses its kernel-call bridge.

---

## How much did the broken hooks actually matter?

Short version: **the device still booted and most jailbreak tooling kept working.** The
broken hooks did not brick the jailbreak — which is exactly why the problem was easy to
miss. These hooks are *conditional* gates (they only block in specific situations), not
master on/off switches, and well-built tools are already designed to live within iOS's
restrictions.

A concrete example: **frida-server kept working** even with several of these hooks missing.
- Frida injects from *outside* the target process and never needs a page that is writable
  and executable at the same time — it writes code while a page is writable, then flips it
  to read+execute. So it never needed the W^X hook (#8). (Frida even has a built-in
  fallback for when simultaneous write+execute isn't allowed, which is the normal iOS case.)
- Frida's way of grabbing another process (`task_for_pid`) only trips the task-conversion
  hook (#4) in narrow cases — Developer Mode off, or targeting Apple's own system
  processes. Its normal use (running as root, with the right entitlement, Developer Mode
  on, attaching to a regular app) goes straight through.
- What frida and similar tools actually rely on is the **code-signing / AMFI relaxation** —
  and those hooks were applying correctly the whole time.

So the realistic impact of the breakage was **targeted malfunctions, not a dead
jailbreak**:
- **`sudo` / setuid was broken** (the symptom that kicked this off) — a hard, obvious failure.
- **A Sileo bug** — see below.
- Reduced robustness for the harder cases these hooks exist to cover (Developer Mode off,
  acting on Apple platform binaries, anything that genuinely needs a single
  write+execute page, etc.).

The lesson for testing: **"it boots and most things work" is not evidence the hooks are
fine.** A few of them can silently do nothing while the device looks completely healthy —
which is the whole reason the cross-version patch check (and, ideally, an on-device
functional check) matter.

### A Sileo bug also got fixed
Restoring the hooks also fixed a bug in **Sileo** (the jailbreak's package manager) that
was present on the affected build — it started working again once the full hook set was
back. We have **not** isolated *which* hook was responsible (it wasn't deliberately tracked
down). Worth pinning down if it recurs. Given what Sileo does — it runs as root, spawns
`apt`/`dpkg` and package maintainer scripts, and renders package pages — the most likely
candidates are the **process-spawn / persona** hook (#7), the **setuid** hook (#1), or the
**process-info** hooks (#2 / #3). Treat that as a lead, not a conclusion.

---

## How we verified it

- One test covers everything: **`make test_jb_patches`** (script: `tests/test_jb_kernel_patches.sh`)
  runs the whole JB kernel patch layer — every hook, including all the Sandbox ops hooks —
  over **every** cloudOS version the README lists as supported (26.1 / 26.3 / 26.4, and the
  current 26.5), and requires each kernel to apply with **0 failures** and emit every
  expected patch. (`QUICK=1` limits it to the local/newest kernel for a fast dev loop.)
  Result: **every version passes, 0 failures.**
- This cross-version run caught a real regression — the first W^X fix only worked on 26.5;
  we then made it handle both the old and new code shapes (see hook #8). On the oldest
  kernel (26.1) the generic anchors even land on the exact same locations the original
  hand-written patches used, which is strong evidence the new logic is correct.
- Every patched location was cross-checked against the actual kernel (via an IDA
  database) to confirm we're editing the right instruction.
- The full app builds and signs cleanly (`make build`).

## What still needs a human

These checks ran in "dry-run" mode (we confirm the right edits would be made). The final
confirmation — building real firmware and checking on a device — has **not** been run
here:

1. `make fw_prepare && make fw_patch_jb` — build the jailbreak firmware end-to-end and
   confirm the jailbreak layer reports 0 failures.
2. On the booted jailbroken device, run `sudo id` and confirm it returns `uid=0`.
3. Open **Sileo** and install/remove a package — confirm the previously-seen Sileo bug is
   gone (and, if you can, note which behavior changed so we can finally attribute it to a
   specific hook).

## Where to look in the code

- The hooks live in `sources/FirmwarePatcher/Kernel/JBPatches/` (one file per hook) with
  shared helpers in `sources/FirmwarePatcher/Kernel/KernelJBPatcherBase.swift`.
- Deeper, per-hook reverse-engineering notes are in `research/kernel_patch_jb/` and the
  patch inventory is `research/0_binary_patch_comparison.md`.
