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
45 changes: 45 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ on:
reuse_package_run_id:
description: Exact prior run supplying the unchanged hardware wheel (sdist is refreshed)
required: false
verify_n61_kernel:
description: Build the N61 4 KiB kernel on an ARM64 runner (full compile, up to 6 hours)
required: false
type: boolean
default: false
permissions:
contents: read
actions: read
Expand Down Expand Up @@ -54,6 +59,7 @@ jobs:
fi
python -m pip install build
python -m build "${args[@]}"
python ci/audit_packages.py dist/*
- uses: actions/upload-artifact@v4
with:
name: hardware-packages
Expand Down Expand Up @@ -150,6 +156,45 @@ jobs:
compression-level: 0
if-no-files-found: error
retention-days: 2
kernel-n61-check:
if: github.event_name == 'workflow_dispatch' && inputs.verify_n61_kernel == true
needs: native-smoke
runs-on: ubuntu-24.04-arm
container: debian:bookworm
timeout-minutes: 360
defaults:
run:
shell: bash
env:
ORCHARD_BUILD_NATIVE: '1'
ORCHARD_BUILD_ENV_ID: debian-bookworm-arm64-llvm19
steps:
- run: apt-get update && apt-get install -y git ca-certificates python3 python3-venv
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: hardware-packages
path: out/packages
- name: Install the kernel toolchain and hardware wheel
run: |
bash kernel/install-toolchain
python3 -m venv .venv
.venv/bin/python -m pip install out/packages/*.whl
echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH"
- name: Build the N61 kernel, modules and device trees
run: |
bash kernel/build n61 --work-dir "$PWD/out/n61-work" --output "$PWD/out/n61-kernel"
dpkg-query -W > out/n61-toolchain-packages.lock
tar -C out -czf out/n61-kernel-check.tar.gz n61-kernel n61-toolchain-packages.lock
- uses: actions/upload-artifact@v4
with:
name: n61-kernel-check
path: out/n61-kernel-check.tar.gz
compression-level: 0
if-no-files-found: error
retention-days: 2
tools:
if: github.ref_type == 'tag' || github.event_name == 'workflow_dispatch'
needs: native-smoke
Expand Down
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ recursive-include docs *.md
recursive-include .github/workflows *.yml
prune boot/m1n1/.work
prune boot/quiet/.work
prune kernel/.work
prune .work
global-exclude __pycache__ *.py[cod] .DS_Store
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ Hardware support and image-building resources for Linux on Apple devices. This r

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

The iPhone 6 (`iPhone7,2`, N61AP) has an experimental 4 KiB RAM-boot recipe.
An earlier build reached a tethered desktop with touch. Its
[bring-up instructions](docs/n61-bringup.md) describe the sources and the
physical checks required for the reconciled build.

## Use

For installing and starting a published system, use the [Orchard CLI](https://github.com/OrchardLinux/cli). Its release requirements select compatible CLI and hardware wheels automatically.
Expand Down
119 changes: 119 additions & 0 deletions boot/initramfs/nfs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# NFS RAM startup

This shared hardware runtime starts a systemd root over a dedicated USB link.
It owns configfs USB, the network link, root validation and the PID 1 handoff.
The distro builds its signed package tree and decides which desktop to start.
Host software supplies the interface and NFS export. This path performs no
internal-storage operation.

The first consumer is experimental N61. Its existing 4 KiB kernel includes
ECM, NCM, ACM, configfs, the Apple USB PHY/controller and NFSv4.0 as built-ins.
The root still carries the matching `/usr/lib/modules/<release>` tree for
other drivers. Starting a desktop needs a fresh RAM boot: the original N61
diagnostic `/init` has a foreground shell loop without a PID 1 handoff hook.

On 2026-10-28, N61 completed the PID 1 handoff over ECM to an Arch NFS root
running systemd 262.1. Systemd reported `running` with zero failed units.
The desktop user had a PAM/logind session and user D-Bus socket; labwc and
Quickshell produced a desktop ready marker. A `grim` capture showed the
Omarchy wallpaper and top bar. SSH worked over the same ECM link.
The initial NFS boot preceded physical touch qualification. A later complete
payload accepted taps and swipes. On 2026-10-29, the recorded reconciled
combination reached the desktop twice with distinct boot IDs. Touch worked on
both boots, and the first covered vertical edges and on-screen keyboard input.
Internal storage remains unqualified. Installation is disabled.

## Root identity and staging

Prepare the aarch64 distro root on a filesystem that preserves Linux file
names, numeric ownership and modes. Copy the exact built kernel module tree
into `/usr/lib/modules/<kernel_release>`. Leave `/dev`, `/proc`, `/sys` and
`/run` as real directories. The root's regular, root-owned
`/etc/orchard/nfs-root.json` contains exactly:

```json
{
"schema": 1,
"kind": "orchard.nfs-root",
"profile": "n61",
"architecture": "aarch64",
"kernel_release": "7.3.0-rc1-orchard-apple-4k",
"root_id": "06106106-1061-4061-8061-061061061061",
"image_sha256": "<64 lowercase hex digits from the public image archive>",
"image_receipt_sha256": "<64 lowercase hex digits from its public receipt>"
}
```

Mint a fresh canonical UUID for each private root. The image digests bind it
to the distro's audited input. The RAM builder embeds the SHA-256 of these
exact JSON bytes, so host and runtime agree without a JSON interpreter in RAM.
The running kernel release and device-tree compatible must also match. The
runtime inspects the systemd ELF header before executing the new root.
The trusted host controls the writable export after this check.

```sh
python -m orchard_hardware.boot.initramfs.nfs.stage \
--receipt /path/to/root/etc/orchard/nfs-root.json \
--busybox /path/to/static-aarch64-busybox \
--output /path/to/new-ram-root \
--server 10.61.0.1 --address 10.61.0.2/30 \
--export /n61-root --port 32061 --function ecm --handoff manual \
> /path/to/nfs-runtime-receipt.json
```

The output uses native `orchard build DIRECTORY` layout, with `init`,
`orchard.toml` and a `rootfs` overlay. Pass the same `--busybox` file to that
build, together with the existing N61 kernel, m1n1 and qualified Pongo inputs.
Keep the resulting RAM image and staging
receipt private because they select a specific writable root. The builder
requires a fresh output path and a static AArch64 BusyBox. ECM is the default;
NCM can be selected after the host's driver has been qualified. The generated
MAC addresses derive from the two IPv4 addresses and appear in the runtime
receipt.

Use the pinned hardware BusyBox recipe, which supplies `awk`, `mount`, `stat`,
`sha256sum`, `od`, `timeout`, `setsid` and `switch_root`. Mount tracking reads
`/proc/self/mountinfo` with `awk`, including while `/proc` lives under
`/newroot`; the pinned BusyBox omits the `mountpoint` applet. Failure recovery
tracks the retired rescue process explicitly so its replacement starts even
while the old PID still exists.

The N61 link reserves host `10.61.0.1`, device `10.61.0.2/30` and NFS TCP port
`32061`. A dedicated userspace NFS server can expose a Linux volume at the
NFSv4 pseudo path `/n61-root`, with `sec=sys`, writable access, numeric IDs and
root ownership preserved. The runtime explicitly requests NFSv4.0 over TCP,
so rpcbind and mountd ports are outside its protocol. Keep P105's existing
network, exports and host services unchanged.
Before arming handoff, verify through the NFS client that the root metadata
and systemd retain UID/GID `0:0`; NFSv4 owner mapping must preserve those IDs.

## Handoff and rescue

The default `--handoff manual` leaves the physical ACM shell running after
configuring USB. Create `/run/orchard/nfs-handoff` through that shell when the
host interface and export are ready. `--handoff auto` arms that request during
startup. PID 1 attempts the mount once with a 60-second initial timeout and
refuses a mismatched root. Network failure can still stall later I/O to a hard
NFS mount; the physical rescue tools remain in RAM.

After validation, PID 1 moves `/dev`, `/proc`, `/sys` and `/run` into the new
root and calls BusyBox `switch_root`. The new PID 1 starts systemd. A transient
`orchard-acm-rescue.service` under `/run/systemd/system` runs the copied static
BusyBox and shell from `/run/orchard/rescue`. Its physical USB root access has
the same trusted-host scope as the RAM experiment. A runtime NetworkManager
configuration keeps `usb0` unmanaged so desktop services preserve the NFS
transport. Configfs keeps ownership of the existing composite gadget.

The experimental USB PID is `05ac:4144`. Its descriptor publishes `PROFILE`,
the selected `ROOT` UUID and the live kernel `BOOT` UUID. This minimal runtime
does not read ECID from the ADT; it does not qualify as the installed recovery
protocol. The boot operator must retain the verified target's physical USB
location when selecting its ACM port.

Before this path can present itself as normal Orchard recovery, adapt the
protected identity flow for N61: read and validate its live ADT, publish the
ECID and current boot UUID through the verified serial connection, and retain
that identity across the NFS handoff. Provision the host's public key through
that connection and pin the boot-scoped SSH host key received there. Recheck
the identity after handoff and exercise access retirement and failure recovery.
Until those checks pass, this remains a bring-up runtime.
1 change: 1 addition & 0 deletions boot/initramfs/nfs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Shared RAM startup for an explicitly identified NFS systemd root."""
16 changes: 16 additions & 0 deletions boot/initramfs/nfs/acm-rescue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/run/orchard/rescue/busybox sh
# Keep every executable and the shell history on the RAM mount.
BB=/run/orchard/rescue/busybox
export PATH=/run/orchard/rescue HOME=/run/orchard/rescue TERM=linux
export HISTFILE=/run/orchard/rescue/.ash_history
cd /run/orchard/rescue
while :; do
if [ -c /dev/ttyGS0 ]; then
"$BB" setsid "$BB" sh -i </dev/ttyGS0 >/dev/ttyGS0 2>&1 &
shell=$!
echo "$shell" > /run/orchard/rescue/shell.pid
wait "$shell" || true
"$BB" rm -f /run/orchard/rescue/shell.pid
fi
"$BB" sleep 1
done
91 changes: 91 additions & 0 deletions boot/initramfs/nfs/handoff
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/bin/busybox sh
# Only PID 1 can retire the initial RAM filesystem and enter systemd.
set -eu
export PATH=/bin:/sbin:/usr/bin:/usr/sbin
[ "$$" -eq 1 ] || { echo 'NFS handoff requires PID 1' >&2; exit 1; }
. /etc/orchard/nfs.conf
rescue_retired=false
is_mounted() {
# Debian's static BusyBox omits mountpoint. /proc itself can be moved
# during handoff, so inspect whichever proc mount is still reachable.
if [ -r /proc/self/mountinfo ]; then
table=/proc/self/mountinfo
elif [ -r /newroot/proc/self/mountinfo ]; then
table=/newroot/proc/self/mountinfo
else
return 1
fi
awk -v target="$1" '$5 == target { found = 1 } END { exit !found }' "$table"
}
failed() {
echo 'NFS handoff stopped; physical RAM rescue remains available.'
for name in run sys proc dev; do
if is_mounted "/newroot/$name" && ! is_mounted "/$name"; then
mount --move "/newroot/$name" "/$name" || true
fi
done
echo failed > /run/orchard/nfs-handoff-state
if [ "$rescue_retired" = true ] || ! kill -0 "$(cat /run/orchard/rescue.pid)" 2>/dev/null; then
# A retired child may still answer kill -0 while exiting or as a
# zombie. Its replacement must start even while that PID exists.
if [ "$rescue_retired" = true ]; then
kill -KILL "$(cat /run/orchard/rescue.pid)" 2>/dev/null || true
fi
/run/orchard/rescue/busybox sh /run/orchard/rescue/acm-rescue &
echo "$!" > /run/orchard/rescue.pid
fi
while :; do sleep 60; done
}
trap failed EXIT
[ ! -e /run/orchard/nfs-handoff-attempted ]
touch /run/orchard/nfs-handoff-attempted
/sbin/orchard-nfs-validate --board
options="vers=4.0,proto=tcp,port=$NFS_PORT,addr=$NFS_SERVER,hard,timeo=50,retrans=2"
timeout -s KILL 60 mount -t nfs4 -o "$options" "$NFS_SERVER:$NFS_EXPORT" /newroot
/sbin/orchard-nfs-validate /newroot
mkdir -p /run/systemd/system/sysinit.target.wants
cat > /run/systemd/system/orchard-acm-rescue.service <<'UNIT'
[Unit]
Description=Orchard physical USB RAM rescue
DefaultDependencies=no
Before=basic.target
IgnoreOnIsolate=yes

[Service]
Type=simple
ExecStart=/run/orchard/rescue/busybox sh /run/orchard/rescue/acm-rescue
Restart=always
RestartSec=1
TimeoutStopSec=2
KillMode=control-group
UNIT
ln -s ../orchard-acm-rescue.service /run/systemd/system/sysinit.target.wants/orchard-acm-rescue.service
# Keep an NFS-rooted interface outside NetworkManager's DHCP/reconnection policy.
mkdir -p /run/NetworkManager/conf.d
cat > /run/NetworkManager/conf.d/90-orchard-nfs.conf <<'NETWORK'
[keyfile]
unmanaged-devices=interface-name:usb0
NETWORK
cat > /run/orchard/rescue/systemd-init <<'INIT'
#!/run/orchard/rescue/busybox sh
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
export HOME=/root USER=root LOGNAME=root TERM=linux
[ "$$" -eq 1 ] || exit 1
echo /sbin/modprobe > /proc/sys/kernel/modprobe
exec /usr/lib/systemd/systemd
INIT
chmod 755 /run/orchard/rescue/systemd-init
echo ready > /run/orchard/nfs-handoff-state
# Stop the pre-handoff shell; systemd starts its RAM-owned replacement.
if [ -f /run/orchard/rescue.pid ]; then
rescue_retired=true
rescue=$(cat /run/orchard/rescue.pid)
kill "$rescue" 2>/dev/null || true
# The foreground shell was spawned in its own session.
if [ -f /run/orchard/rescue/shell.pid ]; then
shell=$(cat /run/orchard/rescue/shell.pid)
/bin/busybox kill -KILL "-$shell" 2>/dev/null || true
fi
fi
for name in dev proc sys run; do mount --move "/$name" "/newroot/$name"; done
exec /bin/busybox switch_root /newroot /run/orchard/rescue/systemd-init
41 changes: 41 additions & 0 deletions boot/initramfs/nfs/init
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/bin/busybox sh
# Shared NFS RAM PID 1. Internal storage is outside this startup path.
set -eu
export PATH=/bin:/sbin:/usr/bin:/usr/sbin HOME=/root TERM=linux
/bin/busybox --install -s /bin
startup_failed() {
echo 'NFS startup stopped; LCD RAM rescue is available.'
while :; do /bin/busybox sh -i || true; /bin/busybox sleep 1; done
}
trap startup_failed EXIT
mount -t proc -o nosuid,nodev,noexec proc /proc
mount -t sysfs -o nosuid,nodev,noexec sysfs /sys
mount -t devtmpfs -o nosuid devtmpfs /dev
mount -t tmpfs -o mode=0755,nosuid,nodev tmpfs /run
mkdir -p /dev/pts /newroot /run/orchard/rescue
chmod 700 /run/orchard /run/orchard/rescue
mount -t devpts -o mode=0620,ptmxmode=0666 devpts /dev/pts
exec </dev/console >/dev/console 2>&1
echo /bin/false > /proc/sys/kernel/modprobe
. /etc/orchard/nfs.conf
cp /bin/busybox /run/orchard/rescue/busybox
cp /sbin/orchard-acm-rescue /run/orchard/rescue/acm-rescue
chmod 755 /run/orchard/rescue/busybox /run/orchard/rescue/acm-rescue
/run/orchard/rescue/busybox --install -s /run/orchard/rescue
if ! /sbin/orchard-nfs-validate --board; then
echo 'NFS runtime identity rejected; leaving the LCD rescue shell.'
while :; do sh -i || true; sleep 1; done
fi
/sbin/orchard-nfs-usb
ip link set lo up
ip address add "$NFS_ADDRESS" dev usb0
ip link set usb0 up
/run/orchard/rescue/busybox sh /run/orchard/rescue/acm-rescue &
echo "$!" > /run/orchard/rescue.pid
echo "$NFS_PROFILE: USB network ready; NFS root $NFS_ROOT_ID."
if [ "$NFS_HANDOFF" = auto ]; then
touch /run/orchard/nfs-handoff
fi
echo 'Create /run/orchard/nfs-handoff to start the configured NFS root.'
while [ ! -f /run/orchard/nfs-handoff ]; do sleep 1; done
exec /sbin/orchard-nfs-handoff
Loading