Skip to content
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
55 changes: 35 additions & 20 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,16 @@ jobs:
runs-on: ubuntu-24.04-arm
container: debian:bookworm
timeout-minutes: 360
strategy:
matrix:
device: [j71b, j71t] # Keep in sync with DEVICES in the tools and assets jobs.
defaults:
run:
shell: bash
env:
ORCHARD_BUILD_NATIVE: '1'
ORCHARD_BUILD_ENV_ID: debian-bookworm-arm64-llvm19
DEVICE: ${{ matrix.device }}
steps:
- run: apt-get update && apt-get install -y git ca-certificates python3 python3-venv
- uses: actions/checkout@v4
Expand All @@ -135,18 +139,18 @@ jobs:
echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH"
- name: Build kernel, modules and device tree once
run: |
bash kernel/build j71b --output "$PWD/out/components/kernel"
dpkg-query -W > out/components/kernel-toolchain-packages.lock
bash kernel/build "$DEVICE" --output "$PWD/out/$DEVICE/components/kernel"
dpkg-query -W > "out/$DEVICE/components/kernel-toolchain-packages.lock"
- name: Archive components and corresponding kernel source
run: |
tar -C out -czf out/kernel-components.tar.gz components
tar -C kernel/.work/j71b/source --exclude=.git -czf out/kernel-source.tar.gz .
tar -C out -czf "out/kernel-components-$DEVICE.tar.gz" "$DEVICE/components"
tar -C "kernel/.work/$DEVICE/source" --exclude=.git -czf "out/kernel-source-$DEVICE.tar.gz" .
- uses: actions/upload-artifact@v4
with:
name: hardware-kernel
name: hardware-kernel-${{ matrix.device }}
path: |
out/kernel-components.tar.gz
out/kernel-source.tar.gz
out/kernel-components-${{ matrix.device }}.tar.gz
out/kernel-source-${{ matrix.device }}.tar.gz
compression-level: 0
if-no-files-found: error
retention-days: 2
Expand All @@ -163,6 +167,7 @@ jobs:
ORCHARD_BUILD_NATIVE: '1'
ORCHARD_BUILD_ENV_ID: debian-bookworm-arm64-llvm19-rust1.90.0
RUSTUP_TOOLCHAIN: '1.90.0'
DEVICES: j71b j71t
steps:
- name: Reuse unchanged successful outputs
if: inputs.reuse_run_id != ''
Expand Down Expand Up @@ -194,17 +199,22 @@ jobs:
- name: Build static recovery tools
if: inputs.reuse_run_id == ''
run: |
# The static tools are device-independent; every device bundle carries them.
OMA_RECOVERY_TOOLS_WORK="$PWD/out/recovery-tools" bash boot/initramfs/j71b/recovery/build-tools
mkdir -p out/components/boot
cp out/recovery-tools/bin/* out/components/boot/
- name: Build quiet loader
for device in $DEVICES; do
mkdir -p "out/$device/components/boot"
cp out/recovery-tools/bin/* "out/$device/components/boot/"
done
- name: Build quiet loaders
if: inputs.reuse_run_id == ''
run: |
bash boot/m1n1/build j71b --output "$PWD/out/components/quiet/m1n1.bin" \
--work-dir "$PWD/out/loader-work" --extra-patch "$PWD/boot/quiet/m1n1.patch" \
--extra-cflags '-DQUIET_BOOT'
dpkg-query -W > out/components/tools-toolchain-packages.lock
tar -C out -czf out/tool-components.tar.gz components
for device in $DEVICES; do
bash boot/m1n1/build "$device" --output "$PWD/out/$device/components/quiet/m1n1.bin" \
--work-dir "$PWD/out/loader-work/$device" --extra-patch "$PWD/boot/quiet/m1n1.patch" \
--extra-cflags '-DQUIET_BOOT'
dpkg-query -W > "out/$device/components/tools-toolchain-packages.lock"
done
tar -C out -czf out/tool-components.tar.gz $(for device in $DEVICES; do echo "$device/components"; done)
- uses: actions/upload-artifact@v4
with:
name: hardware-tools
Expand Down Expand Up @@ -289,15 +299,20 @@ jobs:
pattern: hardware-*
path: out/downloads
merge-multiple: true
- name: Assemble public component bundle without rebuilding
- name: Assemble public component bundles without rebuilding
env:
DEVICES: j71b j71t
run: |
mkdir -p out/publication
tar -xzf out/downloads/kernel-components.tar.gz -C out
tar -xzf out/downloads/tool-components.tar.gz -C out
tar -xzf out/downloads/quiet-pongo.tar.gz -C out/components/quiet
python ci/package_components.py --directory out/components --output out/publication/j71b-components.tar.gz
for device in $DEVICES; do
tar -xzf "out/downloads/kernel-components-$device.tar.gz" -C out
tar -xzf out/downloads/quiet-pongo.tar.gz -C "out/$device/components/quiet"
python ci/package_components.py --device "$device" --directory "out/$device/components" \
--output "out/publication/$device-components.tar.gz"
done
cp out/downloads/*.whl out/downloads/*.tar.gz out/publication/
rm out/publication/kernel-components.tar.gz out/publication/tool-components.tar.gz out/publication/quiet-pongo.tar.gz
rm out/publication/kernel-components-*.tar.gz out/publication/tool-components.tar.gz out/publication/quiet-pongo.tar.gz
git archive --format=tar.gz -o out/publication/hardware-source.tar.gz HEAD
(cd out/publication && sha256sum ./*.whl ./*.tar.gz > SHA256SUMS)
- name: Attest public build
Expand Down
6 changes: 4 additions & 2 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ HW=$(python -c 'from pathlib import Path; import orchard_hardware; print(Path(or
"$HW/boot/m1n1/build" j71b --output "$PWD/out/m1n1.bin"
```

Replace `j71b` with `j71t` for the iPad 5 Wi-Fi. Image producers take the same device as `--device` (default `j71b`): `orchard_hardware.image.recovery`, `orchard_hardware.image.launcher`, `ci/package_components.py`, `ci/launcher_inputs.py` and `boot/quiet/build.py`; `orchard_hardware.image.runtime.files()` takes it as `profile`.

Choose new output paths. Producers use pinned upstream revisions, hardware patches and public toolchains. Their `.work` cache lives beside the installed producer, so use a writable virtual environment. Kernel/module builds enforce matching kernel releases. See [kernel/README.md](kernel/README.md) and [recovery tools](boot/initramfs/j71b/recovery/README.md) for individual producers.

The m1n1 backend is unbranded by default. A distro can pass `--extra-patch FILE`, `--include-header FILE`, and `--extra-cflags FLAGS` for its own presentation.
Expand All @@ -35,12 +37,12 @@ The m1n1 backend is unbranded by default. A distro can pass `--extra-patch FILE`
- `image/runtime.py`: modules, hardware/touch scripts, device API and integrity lists.
- `image/prepare_runtime.py`: Python/ELF dependency staging and recovery root-stream receipt.
- `image/recovery.py`: recovery assembly, newc packing and m1n1 framing.
- `runtime/j71b`: archive containment, staging cleanup, storage fencing and PID-1 handoff.
- `runtime/j71b`: archive containment, staging cleanup, storage fencing and PID-1 handoff, shared by all devices; `runtime/<device>` adds hardware preparation and platform checks.
- `devices/j71b/touch/runtime`: device activation scripts.

Image builders pass a `--policy-dir` containing `image_policy.py` and supporting modules. The policy is staged into the image, not imported on the host. It supplies `ready`, `validate_root`, `validate_handoff`, `verify_privileges`, `verify_session_access`, `diagnostics`, and `INIT`; diagnostic retirement also uses `stop_diagnostics` and `DIAGNOSTICS_EXECUTABLE`. Desktop and init policy belong to the image, not the shared hardware runtime.

Recovery and installed roots receive identical public runtime files and integrity lists. Runtime paths are `/run/orchard`, `/etc/orchard`, and `/usr/libexec/orchard-j71b`; the command endpoint is `/usr/bin/orchard-device`.
Recovery and installed roots receive identical public runtime files and integrity lists. Runtime paths are `/run/orchard`, `/etc/orchard`, and `/usr/libexec/orchard-<device>` (for example `orchard-j71b`); the command endpoint is `/usr/bin/orchard-device`.

## Shared launcher and publication

Expand Down
8 changes: 8 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,13 @@ Reference revision: 944541e67b4c603df328c77d30093551de802c17.
Original copyright notices and per-file licenses remain applicable. This is
selective reuse and adaptation, not a replacement of the pinned base kernel.

The J71t (iPad 5 Wi-Fi, S8003) kernel patches, configuration and m1n1 patches
come from the omarchy-ipad5 iPad 5 port (https://github.com/brkosebay/omarchy-ipad5;
kernel source https://github.com/brkosebay/linux/tree/a9/orchard-j71t), which builds on the same HoolockLinux base and adapts
Paul Praschl's (Pauli1Go) T8010 PCIe/ANS, SmartIO, Wi-Fi and Bluetooth work to
A9. The video decoder's CAVLC table is generated from GPL-2.0 Imagination
Technologies/Texas Instruments source, whose notices are kept. Original
copyright notices and per-file licenses remain applicable.

m1n1: Asahi Linux Contributors, MIT (LICENSES/m1n1.txt).
Wireless regulatory database: LICENSES/wireless-regdb.txt.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ Hardware support and image-building resources for Linux on Apple devices. This r

## Supported hardware

Currently: iPad 6 Wi-Fi (`iPad7,5`, J71b), experimental. Boot is tethered and GPU acceleration is not available.
Currently, all experimental, tethered and without GPU acceleration:

- iPad 6 Wi-Fi (`iPad7,5`, J71b, A10 T8010).
- iPad 5 Wi-Fi (`iPad6,11`, J71t, A9 S8003), tested on one unit. The Samsung S8000 variant (J71s) is not supported, and there is no shared launcher for J71t yet.

## Use

Expand All @@ -13,7 +16,7 @@ For installing and starting a published system, use the [Orchard CLI](https://gi
For building components or another image, follow [BUILD.md](BUILD.md). All commands run from this repository or installed package resources; no sibling source checkout is needed.

- [Kernel producers](kernel/README.md)
- [Recovery tools](boot/initramfs/j71b/recovery/README.md)
- [Recovery tools](boot/initramfs/j71b/recovery/README.md) and [J71t recovery](boot/initramfs/j71t/recovery/README.md)
- [GitHub builds and release ordering](docs/ci.md)
- [Shared launcher build and publication](boot/quiet/README.md)
- [Device protocol](https://github.com/OrchardLinux/cli/blob/main/docs/device-protocol.md)
Expand Down
19 changes: 19 additions & 0 deletions boot/initramfs/j71t/recovery/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# J71t recovery

J71t (iPad 5 Wi-Fi, S8003) uses the same RAM recovery as [J71b](../../j71b/recovery/README.md). Only the files here are device-specific:

- `init`: checks for `apple,j71t` and starts `/usr/libexec/orchard-j71t/handoff.py`.
- `usb-gadget`: presents `J71t Recovery`, which the CLI adapter matches in Linux mode.
- `boot.json`, `boot-verbose.json`: boot arguments, including the NVMe write policy below.

BusyBox, Dropbear, `authorize-key`, `start-ssh`, `capture-adt` and `udhcpd.conf` come from the J71b recovery directory unchanged. The device API in `runtime/j71b/` is shared; J71t adds `runtime/j71t/phase2-hardware` and `j71t-check-platform`.

## Storage policy

The J71t kernel keeps NVMe read-only unless `nvme.write_window=NSID:FIRST-LAST` is set at boot. Only NVMe Write commands inside that LBA range reach the controller; trim, Write Zeroes, Format, Sanitize, firmware and namespace commands stay blocked, and namespaces 2-7 (Apple system namespaces) stay read-only whatever the window says.

Orchard selects the target partition after boot, so recovery opens the whole of namespace 1 (`1:0-4294967295`) and relies on the same block-layer fence as J71b: `phase2-hardware` sets every `nvme0n1p*` read-only, and the device API makes only the confirmed target writable while it formats or mounts it. The GPT and the iPadOS partition are therefore protected by the block layer, as on J71b, not by the kernel's command filter.

The runtime accepts any window confined to namespace 1. Someone who knows their Linux partition's LBA range can build recovery with that narrower window in `boot.json` and keep the kernel-level fence; writes to a target outside it then fail with I/O errors and nothing outside the window is written.

The kernel also holds the PCIe host and the ANS controller (`CONFIG_NVME_APPLE_ANS_EARLY_USB`) until `phase2-hardware` releases them, after the private firmware mixin has been written.
3 changes: 3 additions & 0 deletions boot/initramfs/j71t/recovery/boot-verbose.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"bootargs": "chosen.bootargs=j71t.recovery=1 oma.verbose=1 console=tty0 loglevel=7 ignore_loglevel fbcon=map:0 nvme.write_window=1:0-4294967295 pcie_apple_h9p.ans_boot_mode=handoff nvme.apple_ans_rdy_timeout=5 nvme_core.io_timeout=5 nvme_core.admin_timeout=10 nvme_core.max_retries=1\n"
}
3 changes: 3 additions & 0 deletions boot/initramfs/j71t/recovery/boot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"bootargs": "chosen.bootargs=j71t.recovery=1 loglevel=7 fbcon=map:9 vt.global_cursor_default=0 nvme.write_window=1:0-4294967295 pcie_apple_h9p.ans_boot_mode=handoff nvme.apple_ans_rdy_timeout=5 nvme_core.io_timeout=5 nvme_core.admin_timeout=10 nvme_core.max_retries=1\n"
}
30 changes: 30 additions & 0 deletions boot/initramfs/j71t/recovery/init
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/busybox sh
# RAM-only J71t recovery. No internal storage is mounted or written.
set -eu
export PATH=/bin:/sbin:/usr/bin:/usr/sbin
cd /
/bin/busybox --install -s
mount -t proc -o nodev,noexec,nosuid proc /proc
mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
mount -t devtmpfs -o mode=0755,nosuid dev /dev
mount -t tmpfs -o mode=0755,nosuid,nodev tmpfs /run
mkdir -p /dev/pts /run/recovery /run/orchard
mount -t devpts -o mode=0620,ptmxmode=0666 devpts /dev/pts
exec </dev/console >/dev/console 2>&1

[ "$(tr '\000' '\n' < /proc/device-tree/compatible | head -1)" = apple,j71t ] || {
echo 'Wrong device for J71t recovery image'
exec sh
}
/sbin/recovery-usb
ip address add 172.16.42.1/30 dev usb0
ip link set usb0 up
udhcpd -f /etc/udhcpd.conf &
echo 'J71t recovery: USB serial ready; provision an SSH key before SSH starts.'
(
while getty -n -l /bin/sh ttyGS0 115200 vt100; do sleep 1; done
) &
# Only PID 1 may retire the RAM userspace. A separate writer must first install
# and verify a new sibling root, then arm this one-shot request in /run.
while [ ! -e /run/orchard/handoff.json ]; do sleep 1; done
exec /usr/bin/python3 /usr/libexec/orchard-j71t/handoff.py
23 changes: 23 additions & 0 deletions boot/initramfs/j71t/recovery/usb-gadget
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/sh
# USB NCM networking and ACM serial, restricted to the attached host.
set -eu
mkdir -p /sys/kernel/config
mount -t configfs -o nodev,noexec,nosuid configfs /sys/kernel/config
g=/sys/kernel/config/usb_gadget/linux-recovery
[ ! -e "$g" ]
identity=$(/usr/bin/python3 /usr/libexec/orchard-j71t/identity.py)
mkdir "$g"
echo 0x05ac > "$g/idVendor"
echo 0x4142 > "$g/idProduct"
mkdir -p "$g/strings/0x409" "$g/configs/c.1/strings/0x409"
echo 'J71t Linux' > "$g/strings/0x409/manufacturer"
echo 'J71t Recovery' > "$g/strings/0x409/product"
printf '%s\n' "$identity" > "$g/strings/0x409/serialnumber"
echo 'USB network and serial' > "$g/configs/c.1/strings/0x409/configuration"
echo 500 > "$g/configs/c.1/MaxPower"
mkdir "$g/functions/ncm.usb0" "$g/functions/acm.usb0"
ln -s "$g/functions/ncm.usb0" "$g/configs/c.1/ncm.usb0"
ln -s "$g/functions/acm.usb0" "$g/configs/c.1/acm.usb0"
set -- /sys/class/udc/*
[ "$#" -eq 1 ] && [ -e "$1" ]
printf '%s\n' "${1##*/}" > "$g/UDC"
74 changes: 74 additions & 0 deletions boot/m1n1/patches/j71t/0001-a9-a10-nvme-hmb.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
kboot: Populate the A9/A10 NVMe HMB region

Copy the runtime /arm-io/nvme-mmu0 "sart-region" (base, size) that
iBoot sets up for the NVMe host memory buffer into the reserved-memory
node the DT aliases as "nvme-hmb". Linux's Apple ANS NVMe glue and the
H8P/H9P PCIe host bridge use it for the NVMMU SART window.

Based on Paul Praschl's T8010 version (Pauli1Go/m1n1 ca0ce2ff43), but
a missing or zero sart-region only leaves the node empty instead of
refusing the kernel handoff, and Linux then keeps NVMe off. iBoot fills
it on J71t (S8003).

diff --git a/src/kboot.c b/src/kboot.c
index 2840651..9383c1a 100644
--- a/src/kboot.c
+++ b/src/kboot.c
@@ -456,6 +456,48 @@ static int dt_set_memory(void)
return 0;
}

+static int dt_set_nvme_hmb(void)
+{
+ const void *prop;
+ u64 region[2];
+ u32 len = 0;
+ int adt_node;
+ int fdt_node;
+ fdt64_t reg[2];
+
+ /* Only DTs that ask for it (A9/A10 PCIe NVMe) have the nvme-hmb alias */
+ fdt_node = fdt_path_offset(dt, "nvme-hmb");
+ if (fdt_node < 0)
+ return 0;
+
+ /*
+ * iBoot puts the NVMe host memory buffer it set up in the runtime ADT.
+ * If it is missing, leave the node empty: Linux then keeps the NVMe
+ * controller untouched instead of failing the boot.
+ */
+ adt_node = adt_path_offset(adt, "/arm-io/nvme-mmu0");
+ if (adt_node < 0) {
+ printf("FDT: no /arm-io/nvme-mmu0, NVMe HMB left empty\n");
+ return 0;
+ }
+
+ prop = adt_getprop(adt, adt_node, "sart-region", &len);
+ if (prop && len == sizeof(region))
+ memcpy(region, prop, sizeof(region));
+ if (!prop || len != sizeof(region) || !region[0] || !region[1]) {
+ printf("FDT: unusable nvme-mmu0.sart-region (len %u), NVMe HMB left empty\n", len);
+ return 0;
+ }
+
+ fdt64_st(&reg[0], region[0]);
+ fdt64_st(&reg[1], region[1]);
+ if (fdt_setprop(dt, fdt_node, "reg", reg, sizeof(reg)))
+ bail("FDT: failed to set NVMe HMB region\n");
+
+ printf("FDT: NVMe HMB at 0x%lx size 0x%lx\n", region[0], region[1]);
+ return 0;
+}
+
static int dt_set_serial_number(void)
{

@@ -2976,6 +3018,8 @@ int kboot_prepare_dt(void *fdt)
return -1;
if (dt_set_pcie_tunables())
return -1;
+ if (dt_set_nvme_hmb())
+ return -1;
if (dt_set_display())
return -1;
if (dt_set_gpu(dt))
68 changes: 68 additions & 0 deletions boot/m1n1/patches/j71t/0002-a9-a10-wifi-calibration.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
kboot: Wi-Fi calibration for A9/A10, whose wlan node sits under a UART

On A9/A10 (PCIe BCM4355) the ADT has no /arm-io/wlan. The WLAN
properties, including "wifi-calibration-msf", are on the "wlan"
(wlan-pcie-uart,bcm4355) node under /arm-io/uart2, and there is no
"wifi-antenna-sku-info". Fall back to that node, copy the calibration
into the DT's wifi0 "brcm,cal-blob" as on Apple silicon, and keep the
DT's own apple,antenna-sku when the ADT has none instead of failing.

The MAC address already comes through dt_set_mac_addresses()
(/chosen mac-address-wifi0 -> wifi0 local-mac-address).

diff --git a/src/kboot.c b/src/kboot.c
index 9383c1a..9240ce2 100644
--- a/src/kboot.c
+++ b/src/kboot.c
@@ -1041,6 +1041,17 @@ static int dt_set_nvram(void)
static int dt_set_wifi(void)
{
int anode = adt_path_offset(adt, "/arm-io/wlan");
+ bool on_uart = false;
+
+ if (anode < 0) {
+ /*
+ * A9/A10 (PCIe BCM4355): the WLAN properties (calibration,
+ * module-instance) sit on the "wlan" node under the UART it uses,
+ * and there is no wifi-antenna-sku-info.
+ */
+ anode = adt_path_offset(adt, "/arm-io/uart2/wlan");
+ on_uart = anode >= 0;
+ }

if (anode < 0) {
/*
@@ -1052,7 +1063,8 @@ static int dt_set_wifi(void)
}

uint8_t info[16];
- if (ADT_GETPROP_ARRAY(adt, anode, "wifi-antenna-sku-info", info) < 0)
+ bool have_sku = ADT_GETPROP_ARRAY(adt, anode, "wifi-antenna-sku-info", info) >= 0;
+ if (!have_sku && !on_uart)
bail("ADT: Failed to get wifi-antenna-sku-info\n");

const char *path = fdt_get_alias(dt, "wifi0");
@@ -1063,9 +1075,11 @@ static int dt_set_wifi(void)
if (node < 0)
return 0;

- char antenna[8];
- memcpy(antenna, &info[8], sizeof(antenna));
- fdt_setprop_string(dt, node, "apple,antenna-sku", antenna);
+ if (have_sku) {
+ char antenna[8];
+ memcpy(antenna, &info[8], sizeof(antenna));
+ fdt_setprop_string(dt, node, "apple,antenna-sku", antenna);
+ }

u32 len;
const u8 *cal_blob = adt_getprop(adt, anode, "wifi-calibration-msf", &len);
@@ -1076,6 +1090,8 @@ static int dt_set_wifi(void)
}

fdt_setprop(dt, node, "brcm,cal-blob", cal_blob, len);
+ if (on_uart)
+ printf("FDT: Wi-Fi calibration (%u bytes) from the uart2 wlan node\n", len);

return 0;
}
Loading