Skip to content
This repository was archived by the owner on Oct 30, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ framing are informed by Corellium's Project Sandcastle and Paul Praschl's
Gen2 mtprops and MTFW HBPP item conversions (mtfw.c, makez2fw.c):
https://github.com/Pauli1Go/HoolockLinux-linux-firmware
Reference revision: 944541e67b4c603df328c77d30093551de802c17.
convert_gen1() ports the legacy Gen1 bootstrap from Corellium LLC's
GPL-2.0-or-later hx-touchd/mtfw/mtfw.c (Copyright (C) 2020 Corellium LLC):
https://github.com/corellium/projectsandcastle
Reference revision: 03db9c6ae04141eb940f3b9f56d446f50d57fadf.
114 changes: 114 additions & 0 deletions docs/n61-nfs-link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Experimental iPhone 6 NFS host link

The N61 RAM experiment uses a USB network link to a private NFS root. Its host
address is `10.61.0.1/30`; the phone uses `10.61.0.2`. This command manages the
macOS host address for that link. The NFS server and any forwarding process
remain separate host resources.

Stage the RAM runtime with `--handoff manual` and the exact `nfs-root.json`
from the private NFS root. If the root is on a Linux server, copy that identity
file to the Mac without changing its bytes and compare the SHA-256 values. Use
the Mac copy for every host-link command in this session.

With the iPhone in DFU, run `orchard devices` and verify its ECID, chip `0x7000`
(28672) and board `0x06` (6) against the selected phone. Set `N61_LOCATION` in
the Mac shell to the reported numeric USB location. Boot the matching RAM image
with `orchard run /path/to/staged-ram-directory` and retain the same cable and
location through the transition. Stop to inspect the connection if the location
changes or another boot device appears.

Set `N61_ROOT_IDENTITY` to the Mac copy of `nfs-root.json` and
`N61_LINK_STATE` to a new private receipt path. Inspect the resulting gadget:

```sh
orchard nfs-link inspect --root-identity "$N61_ROOT_IDENTITY" --location "$N61_LOCATION"
```

The result includes `endpoint.serial_port` and `endpoint.boot_id`. Set
`N61_SERIAL_PORT` to the reported port and open it from the Mac:

```sh
screen "$N61_SERIAL_PORT" 115200
```

Run the following command in the phone's rescue shell:

```sh
cat /proc/sys/kernel/random/boot_id
```

Compare the result with `endpoint.boot_id`. Keep the serial session open and
use a second Mac terminal for host commands. Set `N61_BOOT_ID` there to the
matching UUID, along with `N61_LOCATION`, `N61_ROOT_IDENTITY` and
`N61_LINK_STATE` from the first terminal. Then configure the host address:

```sh
orchard nfs-link apply --root-identity "$N61_ROOT_IDENTITY" \
--location "$N61_LOCATION" --boot-id "$N61_BOOT_ID" --state "$N61_LINK_STATE"
```

The command verifies the root, boot UUID, USB location, serial port and network
interface before requesting administrator access. It rejects conflicting routes,
an unowned existing address and ambiguous boot-device discovery. If address
setup fails after the intent is recorded, inspect the host and the receipt before
another attempt.

Start the NFS server and any required forwarding process for this session.
For the qualified network settings, the phone reaches the configured NFSv4.0
export through `10.61.0.1:32061`. For a server bound to loopback on a separate
Linux host, one SSH terminal can forward the port after the host address exists:

```sh
ssh -N -o ExitOnForwardFailure=yes \
-L 10.61.0.1:32061:127.0.0.1:32061 "$NFS_SERVER_SSH"
```

Set `NFS_SERVER_SSH` to the server's SSH destination and add its port and key
options when needed. Verify the listening address or forward reaches
the intended server and that the export exposes the staged root with the same
identity bytes and root ownership. Keep the server and any forward running
throughout the Linux session.

Recheck the selected gadget and the owned host address using the same receipt:

```sh
orchard nfs-link inspect --root-identity "$N61_ROOT_IDENTITY" \
--location "$N61_LOCATION" --boot-id "$N61_BOOT_ID" --state "$N61_LINK_STATE"
```

The result should report `already-owned`. On the checked serial shell, compare
`/proc/sys/kernel/random/boot_id` with `N61_BOOT_ID` again. When the link and
export are ready, request handoff once:

```sh
[ ! -e /run/orchard/nfs-handoff-attempted ] &&
[ ! -e /run/orchard/nfs-handoff ] &&
touch /run/orchard/nfs-handoff
```

The runtime makes one mount attempt and keeps the RAM rescue shell available
if handoff fails. Inspect the failure and device state before another boot.

After shutting down the Linux session and disconnecting its NFS gadget, retire
the address owned by that receipt:

```sh
orchard nfs-link cleanup --root-identity "$N61_ROOT_IDENTITY" --state "$N61_LINK_STATE"
```

Cleanup refuses to run while any Apple `05ac:4144` experimental gadget remains
connected, including the RAM-only shell. It checks the recorded interface and
hardware address before removing an alias. If the interface has disappeared,
the receipt is retired without changing another interface. The NFS server,
forwarding processes and other host exports remain under their owners' control.

This gadget advertises the profile, root UUID and boot UUID. It does not yet
expose the device ECID or implement Orchard's serial-pinned SSH bootstrap and
device protocol. The operator must retain the verified DFU location throughout
the transition. To qualify full recovery, the RAM runtime needs to parse the
chosen `unique-chip-id` from the live ADT and bind it to the selected ECID,
root UUID and kernel boot UUID. It must provision an ephemeral client key over
the checked serial endpoint, return its boot-scoped SSH host key over that same
endpoint, and answer the shared recovery status request through the pinned SSH
session. The host must check the same device and boot at each transition.
Treat this link as a bring-up interface until those checks pass on the phone.
16 changes: 16 additions & 0 deletions docs/ram-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,22 @@ base producers remain reusable, including the charging-worker cache. A changed
base producer does not inherit that compatibility. Cache reuse still checks the
complete file inventory and hashes.

Loader workspaces are separated by device, the complete m1n1 recipe and the build
environment identity. The source tree recorded after the initial build identifies
that recipe's baseline. Tracked and untracked source edits select their own cached
loader without changing the checkout's Git index. A workspace without a recorded
baseline uses its source tree identity as well. Loader builds sharing a workspace
wait for one another, even when they use different component caches. If a nested
submodule has local changes, build the loader explicitly and pass its path with
`--loader`.

The RAM build receipt records the CLI and hardware package content hashes, plus
the Git head and dirty state when those packages come from tracked checkouts. It
records the kernel component receipt and a copy of the device firmware recipe
when those are available. Explicit older kernel inputs without a receipt retain
their image and selected DTB hashes. The receipt states whether the firmware
recipe supplied files to the RAM image.

Developer receipts are not publisher signatures or hardware qualification.
Explicit component paths are trusted inputs: callers must keep kernel, DTB and
modules coherent. Never automatically retry uncertain boot or storage mutations.
11 changes: 8 additions & 3 deletions src/orchard_cli/apple/boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,14 @@ def run_process_group(argv, *, timeout, **kwargs):
with subprocess.Popen(argv, start_new_session=True, **kwargs) as process:
try:
returncode = process.wait(timeout=timeout)
if returncode:
raise subprocess.CalledProcessError(returncode, argv)
except BaseException:
os.killpg(process.pid, signal.SIGKILL)
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
raise
if returncode:
raise subprocess.CalledProcessError(returncode, argv)


def boot(device, assets, manifest, progress, *, profile, chip, board, product):
Expand All @@ -73,6 +76,8 @@ def boot(device, assets, manifest, progress, *, profile, chip, board, product):
if not executable:
raise ValueError('palera1n is required on the host to boot Pongo')
usb.selected(device['location'], device['ecid'], chip=chip, board=board, mode='dfu')
pongo.preflight()
if device['mode'] == 'dfu':
progress.phase('Booting Pongo')
progress.phase('Wait for the Apple logo to appear. If it does not disappear after a few seconds, '
'disconnect and reconnect the cable at the iPad once.')
Expand Down
2 changes: 2 additions & 0 deletions src/orchard_cli/apple/usb.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ def devices():
ports = []
while descendants:
child = descendants.pop()
if child.get('IOObjectClass') == 'IOUSBHostDevice':
continue
descendants.extend(child.get('IORegistryEntryChildren', []))
if child.get('IOObjectClass') == 'IOSerialBSDClient':
port = child.get('IOCalloutDevice')
Expand Down
61 changes: 51 additions & 10 deletions src/orchard_cli/apple/z2fw.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
The file is the same for every unit of a model: apple_z2 fills each
calibration record from the DT properties m1n1 copies from the ADT at boot.

Three input forms:
Input forms:
- convert(): an mtprops profile whose "Constructed Firmware" is one
preconstructed HBPP14 packet (J71b).
- convert_packets(): an mtprops profile whose "Constructed Firmware" is an
array of preconstructed packets (iPhone 7 Plus); makez2fw's Gen2 path.
- convert_mtfw(): an MTFW image (IM4P payload) listing HBPP load items per
personality (iPad 7); makez2fw's HBPP item path.
- convert_gen1(): a legacy Z2 mtprops data packet (iPhone 6), with the
Gen1 bootstrap from Corellium Project Sandcastle.

convert_packets() and convert_mtfw() are ports of Paul Praschl's (Pauli1Go)
GPL-2.0-or-later makez2fw (mtfw.c, makez2fw.c), which builds on Corellium's
Expand All @@ -36,7 +38,8 @@
PROVIDERS = {'multi-touch-calibration': 0, 'orb-gap-cal': 1, 'orb-force-cal': 2,
'shape-dynamic-accel-cal': 3, 'multitouch-to-display-offset': 4,
'firefly-calibration': 5, 'pressure-effect-correction': 6,
'firefly-p100-calibration': 7, 'ext-multi-touch-calibration': 8}
'firefly-p100-calibration': 7, 'ext-multi-touch-calibration': 8,
'prox-calibration': 9}


def uint(value, name, maximum=0xffffffff):
Expand Down Expand Up @@ -68,21 +71,21 @@ def from_xe32(data):
return lo | hi << 16


def decode_blob(packet):
def decode_blob(packet, *, hbpp_version=0x104):
if len(packet) < 20 or packet[:4] != b'\x18\xe1\x30\x01':
raise ValueError('Not a prefixed HBPP14 blob')
words_minus_one = struct.unpack_from('>H', packet, 4)[0]
size = (words_minus_one + 1) * 4
raise ValueError('Not a prefixed HBPP blob')
words = struct.unpack_from('>H', packet, 4)[0]
size = (words + (hbpp_version >= 0x104)) * 4
if len(packet) != 16 + size:
raise ValueError('HBPP14 length mismatch or extra trailing packets')
raise ValueError('HBPP length mismatch or extra trailing packets')
if sum(packet[4:10]) != struct.unpack_from('>H', packet, 10)[0]:
raise ValueError('HBPP14 header checksum mismatch')
raise ValueError('HBPP header checksum mismatch')
payload = swapped(packet[12:-4])
if sum(payload) & 0xffffffff != from_xe32(packet[-4:]):
raise ValueError('HBPP14 payload checksum mismatch')
raise ValueError('HBPP payload checksum mismatch')
address = from_xe32(packet[6:10])
if address + size > 0x100000000:
raise ValueError('HBPP14 address range overflow')
raise ValueError('HBPP address range overflow')
return address, payload


Expand Down Expand Up @@ -307,6 +310,44 @@ def container(records):
return output


def convert_gen1(profile, *, packet, touch_maximum, prox_maximum, fll_mval, ref_clk_div, clk32_clock_enable):
"""Legacy Z2 data-form mtprops with dynamic per-unit calibration.

Corellium projectsandcastle 03db9c6ae04141eb940f3b9f56d446f50d57fadf,
hx-touchd/mtfw/mtfw.c:169-219 defines this Gen1 command order. INIT
preserves its unacknowledged exchanges; SEND requests a separate ACK.
Calibration records replace its SysCfg loads at the same positions.
The N61 packet uses full payload-word counts, before HBPP 0x104.
Board recipes supply the clock values from Apple's matching kernel
personality; the legacy mtprops packet contains no Firmware Config.
"""
if packet != 'Z2' or profile.get('PreconstructedBootloadPacketType') != packet:
raise ValueError('Unexpected Gen1 packet type')
payload = profile.get('Constructed Firmware')
if not isinstance(payload, bytes):
raise ValueError('Expected data-form Gen1 constructed firmware')
if 'Firmware Config' in profile:
raise ValueError('Gen1 firmware with an unsupported config')
address, firmware = decode_blob(payload, hbpp_version=0x103)
if address != 0:
raise ValueError('Unexpected Gen1 firmware load address')
providers = []
records = [(INIT, b'\x19\xc1')] + [(INIT, HBPP_INIT + b'\x18\xe1' * 6)] * 3
records.append(calibration_record('prox-calibration', 0x10009600, prox_maximum, providers))
records.append(calibration_record('multi-touch-calibration', 0x10009000, touch_maximum, providers))
records.append((SEND, payload))
boot = [(0x10003060, uint(fll_mval, 'FLL multiplier')),
(0x1000305c, uint(ref_clk_div, 'reference clock divider')), (0x10003058, 6),
(0x10003000, 3), (0x10003518, uint(clk32_clock_enable, '32K clock enable'))]
records += [(SEND, register_packet(address, 0xffffffff, value)) for address, value in boot]
records += [(SEND, REQUEST_CALIBRATION), (INIT, bytes.fromhex('1d5334001000000100000045'))]
return container(records), {'packet': packet, 'generation': 1,
'firmware_packet_preserved_verbatim': True,
'firmware_packet_bytes': len(payload), 'firmware_payload_bytes': len(firmware),
'firmware_address': address, 'calibration': providers,
'register_operations': len(boot), 'command_sequence': [c for c, p in records]}


def convert_packets(profile, config, *, packet):
"""mtprops whose constructed firmware is an array of preconstructed packets.

Expand Down
7 changes: 4 additions & 3 deletions src/orchard_cli/boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def boot_ram(output, *, yes=False):
"""Boot once via existing USB/Pongo transport. No recovery API required."""
from orchard_hardware import device as hardware_device
from .apple import usb
from .apple.boot import wait_mode
from .apple.boot import run_process_group, wait_mode
from .state import operation_lock
from .transports import pongo

Expand Down Expand Up @@ -64,11 +64,12 @@ def boot_ram(output, *, yes=False):
confirm_boot(mismatch=mismatch)
verify_ram(output)
usb.selected(device['location'], ecid, chip=chip, board=board, mode=device['mode'])
pongo.preflight()
if device['mode'] == 'dfu':
print('Starting Pongo once. Inspect any uncertain failure before retrying.')
with (output / 'palera1n.log').open('wb') as log:
subprocess.run([executable, '-p', '-k', str(output / 'pongo.bin')],
check=True, timeout=120, stdout=log, stderr=subprocess.STDOUT)
run_process_group([executable, '-p', '-k', str(output / 'pongo.bin')],
timeout=120, stdout=log, stderr=subprocess.STDOUT)
wait_mode(device, 'pongo', chip=chip, board=board, product='', seconds=60)
usb.selected(device['location'], ecid, chip=chip, board=board)
pongo.upload(output / 'linux.bin', device['location'], receipt['files']['linux.bin'])
Expand Down
Loading