Root image and layouts
How the iOS root is made, and the three ways it sits on the phone.
The root image
mkiosroot-macos.sh IPSW OUT.img [SIZE] [DEVICE] [EXTRA] (needs sudo, so owners are kept, and GNU rsync) builds the root straight
from your IPSW with macOS’s own APFS and HFS+: one case-sensitive journaled HFS+ volume, no partition map, in a sparse file
(SIZE defaults to 16 GB; root_size 16g in device.conf). It holds the OS, the SystemOS and AppOS cryptexes named by the IPSW’s
BuildManifest.plist, the same links and keybag as the generic script, and what root-tweaks.py changes. EXTRA is a
directory laid over the root first: the files mka stages in out/.../root (our programs, the Mapple app), with mappled’s launchd job
when /usr/libexec/mappled is in it. The Mapple kernel has no kexts, so nothing on the volume needs Apple’s file compression:
the copy reads through macOS’s APFS driver, which decompresses.
mka rootsparse turns the image’s allocated blocks into an Android sparse image, root.img (for fastboot flash), and writes
root.ranges, the list of those blocks that the installer checks the phone’s root against.
Three layouts
lunch picks one (build guide). They differ in where the root is and what the loader does at boot:
| Development | Single | Split | |
|---|---|---|---|
| Product | mapple_spaced | mapple_spaced_single | mapple_spaced_split |
| Root partition | xnusys, made by hand | userdata: the root replaces Android’s data | xnusys, carved off the end of userdata by the installer |
mp_ufsroot | xnusys | userdata (single_cmdline) | xnusys |
Loader flags (device.conf) | loader + dev_loader (-DROOT_ROOM_MIB=0) | loader + single_loader (-DXNU_DEFAULT=1) | loader + split_loader (-DSPLIT_MENU=1) |
| Android | stays; Android boots unless XNU is asked for | replaced: iOS at every boot (your boot image is kept) | stays; a boot menu chooses |
| Installed by | hand | fastboot flash userdata root.img, then the recovery zip | the split zip (split-carve, then the root, then split-chain) |
- Single. With
-DXNU_DEFAULT=1,mtkloaderstarts XNU at every boot when the root partition’s HFS+ volume checks out (root_check) and Android when its headers are wrong; if the partition cannot be read after three tries it stops. Itssingle_cmdlineismp_ufsroot=userdata mp_usblink=0: the USB link is off. - Split.
mapple-install split-carve DISK [--size GIB]carvesxnusys(split_size, 22 GiB on spaced) off the end ofuserdata, zeroinguserdata’s start,metadata, and the new partition’s first MiB, saved-area header and last MiB (so a newxnusysstarts without a boot-args record). Boot is not touched untilsplit-chain, which keeps the boot image inxnusys’s saved area and chains the loader in front of it.split-uninstallputs the boot image and the partition table back. After the first write that matters, a failure puts back what was written. - Development. The volume lies inside the partition and only its headers are checked (
ROOT_ROOM_MIB=0).
The end of the root partition
The last 128 MiB are outside the HFS+ volume (mapple-install.c, mtkroot.h). The loader’s check wants that room after the
volume in the single and split layouts and none in the development layout.
| From the end | Size | What |
|---|---|---|
| 128 MiB | 4 KiB header, then one slot per item | the saved area: the boot image at +1 MiB (a slot of 48 MiB), then a record that chained one |
| 64 MiB | 1 MiB | tail, settings |
| 63 MiB | 15 MiB | tail, firmware: a ustar archive with connsys/ and touch/, taken off your own phone by the zip |
| 48 MiB | 47 MiB | tail, log |
| 1 MiB | not the installer’s: the first 4 KiB block is the boot-args record; HFS+ also writes an alternate volume header 1 KiB before the end |
The loader reads the touch firmware out of the firmware region when the payload carries none.
What the zip checks
mapple-install verify-root RANGES DEV checks both volume headers and the SHA-256 of every allocated block against root.ranges
before anything is written. put and save read back and hash what they write; chain puts the loader in front of the Android kernel in
the boot image (the Android kernel follows at offset 0x40000), the payload after it, and --backup keeps the boot image as it was. mka refuses to
build a payload larger than payload_max (0x3fe0000 on spaced).
Source: tools/mkiosroot-macos.sh, core/mka.sh, installer/mapple-install.c of mapple_build at f7431bb; mtk/loader/mtkroot.h and
mtkloader.c of mapple_loader at 2aa5786; installer/split.c of mapple_splitboot at 406b03a; device.conf of the device repository at 40ba6c3.
Applies to realme 8i (spaced), release 1 · Updated 15 November 2026