Skip to content
Merged
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
21 changes: 19 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,24 @@ jobs:
- name: Install the combined toolchain once
if: inputs.reuse_run_id == ''
run: |
bash ci/install-toolchains recovery m1n1
bash ci/install-toolchains recovery recovery-arm m1n1
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Build static recovery tools
if: inputs.reuse_run_id == ''
run: |
OMA_RECOVERY_TOOLS_WORK="$PWD/out/recovery-tools" bash boot/initramfs/recovery/build-tools
mkdir -p out/components/boot
cp out/recovery-tools/bin/* out/components/boot/
- name: Build the ARMv7 tools and the boards' Cascadia loaders
if: inputs.reuse_run_id == ''
run: |
OMA_RECOVERY_TOOLS_WORK="$PWD/out/recovery-tools-arm" bash boot/initramfs/recovery/build-tools --arch arm
mkdir -p out/components/boot-arm
cp out/recovery-tools-arm/bin/* out/components/boot-arm/
for profile in devices/*/cascadia.toml; do
board=$(basename "$(dirname "$profile")")
python3 boot/cascadia/build --profile "$profile" --output "$PWD/out/components/cascadia/$board"
done
- name: Build quiet loader
if: inputs.reuse_run_id == ''
run: |
Expand Down Expand Up @@ -353,13 +363,20 @@ jobs:
# distribution builds its release from, so it stops copying any of them.
python -m orchard.image.platforms_index out/publication --kernels out/platforms \
--tools out/components/boot --commit "$GITHUB_SHA"
# The same for 32-bit roots, next to it: platforms-armv7.toml.
loaders=()
for loader in out/components/cascadia/*/loader.bin; do
loaders+=(--loader "$(basename "$(dirname "$loader")")=$loader")
done
python -m orchard.image.platforms_index out/publication --architecture arm --kernels out/platforms \
--tools out/components/boot-arm --commit "$GITHUB_SHA" "${loaders[@]}"
# tool-components.tar.gz stays: a distribution build takes its tools from it.
rm out/publication/kernel-components-*.tar.gz out/publication/quiet-pongo.tar.gz
# Source keys of each platform's kernel, m1n1 and the tools: a distribution build
# with the same state uses these instead of building them (orchard.published).
python -m orchard.published keys --output out/publication/component-keys.json apple-16k apple-4k apple-armv7
git archive --format=tar.gz -o out/publication/orchard-source.tar.gz HEAD
(cd out/publication && sha256sum ./*.tar.gz ./*.bin ./*.pyz ./component-keys.json ./platforms.toml > SHA256SUMS)
(cd out/publication && sha256sum ./*.tar.gz ./*.bin ./*.pyz ./component-keys.json ./platforms.toml ./platforms-armv7.toml > SHA256SUMS)
- name: Attest public build
if: toJSON(github.event.repository.private) == 'false'
id: attest
Expand Down
16 changes: 16 additions & 0 deletions boot/cascadia/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
The ARMv7 cache-maintenance and Linux handoff sequence is derived from the
Project Cascadia A5 staging loader:

https://github.com/rzxvx/Project-Cascadia
reference revision: 091d441f19e101699f7e1835a80ccb48d6e54eec
license: GPL-2.0

The local P105 work fixed the executable entry by discarding ARM unwind
metadata and asserting the upload address in the linker script.

This producer keeps those entry safeguards. Hardware addresses come from
the device's profile. Orchard's configuration record replaces the loader's
compiled-in DTB address and Cascadia's overlay header; the initramfs is
copied before the MMU is turned off and the DTB's initrd placeholders are set
in place, as Cascadia's linux_boot.c and assemble.py do. Not yet booted on
hardware in this form.
48 changes: 48 additions & 0 deletions boot/cascadia/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# ARM32 iBEC staging payload (Cascadia loader)

What starts Linux on Apple's 32-bit boards in place of m1n1: a patched iBEC
loads one file at the board's `load_address` and jumps to it. The file is
this loader, the board's zImage with its DTB appended, and an initramfs.

`build --profile devices/BOARD/cascadia.toml --output DIR` builds the loader
with Orchard's kernel toolchain through `buildenv` (orchard does this in
`orchard build`, see `build.prepare_payload_loader`). The output directory
contains `loader.bin`, its ELF and a source/profile/toolchain receipt. The
ELF check requires executable ARM code at the upload address and checks
every allocated section against the raw bytes. ARM unwind tables are
discarded so they cannot displace the entry instruction.

`src/orchard/image/cascadia.py` assembles one board's payload (also by file
path: `--profile --loader [--loader-elf] --kernel --dtb --initramfs
--output`). Kernel bytes stay unchanged. The DTB's `/chosen`
`linux,initrd-start`/`-end`, placeholders in the board's tree, are set in
place to the initramfs copy; nothing else in the tree changes, its boot
arguments included.

The layout is a checked loader followed by padding to 32 KiB, the zImage,
the DTB and the initramfs, each data address aligned to eight bytes. The
loader disables the profile's watchdog, then checks its configuration
checksum and bounds; a rejected image stays stopped for inspection. It
copies the initramfs to the profile's `initrd_address` while iBEC's MMU and
caches are still on, then cleans the caches, turns the MMU off and enters
the zImage with the DTB in `r2`. This is the form Project Cascadia's own
P105 boot used (`linux_boot.c` `copy_overlay`, `assemble.py`
`OVERLAY_PHYS = 0x88000000`). The fixed loader stack uses the gap below
offset `0x7000`; code must finish below `0x6000`, and the configuration
starts at `0x7f00`.

The zImage must carry the v6.12 ARM size table and end on an eight-byte
boundary. Its DTB follows immediately so the appended-DTB decompressor
(`CONFIG_ARM_APPENDED_DTB`) can relocate it. Assembly bounds the inflated
kernel together with the relocated decompressor's workspace below the
initramfs destination; unknown table formats and overlapping layouts are
refused. The receipt describes every byte range and its hash.

Upstream Cascadia also built its runtime into the kernel
(`CONFIG_INITRAMFS_SOURCE`) and copied only machine-local files this way.
orchard copies its whole initrd plus the boot's add-on
(`src/orchard/install_direct.py`), which works with either kernel: with an
initrd built into the kernel, the copy is unpacked over it.

See `NOTICE` for source provenance. This producer contacts no device; the
payload has not been booted on hardware yet.
50 changes: 50 additions & 0 deletions boot/cascadia/build
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-only
"""Build the ARM32 staging loader from source and check its executable entry."""
import argparse
import json
from pathlib import Path
import sys

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'src'))
from orchard import buildenv
from orchard.image.cascadia import digest, profile, verify_loader


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--profile', type=Path, required=True)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
described = profile(args.profile)
out = args.output.resolve()
if out.is_symlink() or (out.exists() and (not out.is_dir() or any(out.iterdir()))):
parser.error('The loader output must be a new or empty directory')
out.mkdir(parents=True, exist_ok=True)
(out / 'cascadia-generated.h').write_text(''.join(
f'#define {name.upper()} {hex(value)}\n' for name, value in sorted(described.items()) if name != 'schema'))
template = (ROOT / 'boot/cascadia/linux-boot.ld.in').read_text()
(out / 'linux-boot.ld').write_text(template.replace('@LOAD_ADDRESS@', hex(described['load_address'])))
image = 'localhost/orchard-hardware-kernel-toolchain:bookworm'
buildenv.build(image, ROOT / 'kernel/Containerfile', ROOT / 'kernel')
buildenv.run(image, ['sh', '/src/boot/cascadia/compile', '/out'],
[(ROOT, '/src', True), (out, '/out', False)])
binary, elf = (out / 'loader.bin').read_bytes(), (out / 'loader.elf').read_bytes()
verify_loader(binary, described, elf)
source = ROOT / 'boot/cascadia'
receipt = {'schema': 1, 'kind': 'cascadia-loader', 'architecture': 'arm',
'loader_format': 'cascadia', 'profile_sha256': digest(args.profile.read_bytes()),
'toolchain': buildenv.identity(image),
'sources': {path.name: digest(path.read_bytes()) for path in sorted(source.iterdir()) if path.is_file()},
'loader': {'bytes': len(binary), 'sha256': digest(binary)},
'elf': {'bytes': len(elf), 'sha256': digest(elf)},
'entry': described['load_address'], 'hardware_boot_verified': False}
(out / 'loader.json').write_text(json.dumps(receipt, indent=2, sort_keys=True) + '\n')
for name in ('linux_boot.o', 'linux_boot_start.o', 'cascadia-generated.h', 'linux-boot.ld'):
(out / name).unlink()
print(f'Loader: {out / "loader.bin"}')


if __name__ == '__main__':
main()
14 changes: 14 additions & 0 deletions boot/cascadia/compile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env sh
# SPDX-License-Identifier: GPL-2.0-only
set -eu
out=$1
src=${ORCHARD_SRC:-$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)}
clang-19 --target=armv7a-none-eabi -march=armv7-a -marm -mfloat-abi=soft \
-ffreestanding -fno-builtin -fno-stack-protector -fno-unwind-tables \
-fno-asynchronous-unwind-tables -Os -Wall -Wextra -Werror \
-I "$out" -c "$src/boot/cascadia/linux_boot.c" -o "$out/linux_boot.o"
clang-19 --target=armv7a-none-eabi -march=armv7-a -marm -I "$out" \
-c "$src/boot/cascadia/linux_boot_start.S" -o "$out/linux_boot_start.o"
ld.lld-19 --build-id=none -T "$out/linux-boot.ld" \
"$out/linux_boot_start.o" "$out/linux_boot.o" -o "$out/loader.elf"
llvm-objcopy-19 -O binary "$out/loader.elf" "$out/loader.bin"
22 changes: 22 additions & 0 deletions boot/cascadia/linux-boot.ld.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* SPDX-License-Identifier: GPL-2.0-only */
ENTRY(_start)
SECTIONS
{
. = @LOAD_ADDRESS@;
.text : {
KEEP(*(.text.start))
*(.text .text.*)
*(.rodata .rodata.*)
*(.data .data.*)
}
ASSERT(. <= @LOAD_ADDRESS@ + 0x6000, "loader overlaps its stack")
. = @LOAD_ADDRESS@ + 0x7f00;
.loader_config : { KEEP(*(.loader_config)) }
ASSERT(SIZEOF(.loader_config) == 68, "loader configuration changed")
.ARM.attributes 0 : { *(.ARM.attributes) }
/DISCARD/ : {
*(.comment) *(.note*) *(.eh_frame*)
*(.ARM.exidx*) *(.ARM.extab*)
}
}
ASSERT(_start == @LOAD_ADDRESS@, "loader entry differs from the upload address")
87 changes: 87 additions & 0 deletions boot/cascadia/linux_boot.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: GPL-2.0-only
/* ARM32 iBEC handoff. See NOTICE for the Cascadia source and local changes. */
typedef unsigned int u32;
typedef unsigned char u8;

#include "cascadia-generated.h"

struct config {
u8 magic[4];
u32 version, header_size, load_address, kernel_offset;
u32 dtb_offset, dtb_size, initrd_offset, initrd_size;
u32 initrd_address, image_size, watchdog_address, l2_address;
u32 ram_start, ram_end, initrd_limit, crc32;
};

/* The assembler fills the dynamic fields and checksum before upload. */
__attribute__((section(".loader_config"), used))
volatile const struct config cascadia_config = {
{ 'C', 'B', '3', '2' }, 1, sizeof(struct config), LOAD_ADDRESS,
KERNEL_OFFSET, 0, 0, 0, 0, INITRD_ADDRESS, 0, WATCHDOG_ADDRESS,
L2_ADDRESS, RAM_START, RAM_END, INITRD_LIMIT, 0,
};

static int range(u32 start, u32 size, u32 first, u32 end)
{
return start >= first && start < end && size && size <= end - start;
}

static u32 checksum(void)
{
volatile const u8 *p = (volatile const u8 *)&cascadia_config;
u32 crc = ~0u, i, bit;
for (i = 0; i < sizeof(struct config) - sizeof(u32); i++) {
crc ^= p[i];
for (bit = 0; bit < 8; bit++)
crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1)));
}
return ~crc;
}

static u32 big_endian(volatile const u8 *p)
{
return ((u32)p[0] << 24) | ((u32)p[1] << 16) |
((u32)p[2] << 8) | p[3];
}

u32 cascadia_main(void)
{
volatile const struct config *c = &cascadia_config;
volatile const u8 *dtb, *from;
volatile u8 *to;
u32 i;

if (WATCHDOG_ADDRESS) {
volatile u32 *wdt = (volatile u32 *)WATCHDOG_ADDRESS;
for (i = 0; i < 4; i++)
wdt[i] = 0;
}
if (c->magic[0] != 'C' || c->magic[1] != 'B' ||
c->magic[2] != '3' || c->magic[3] != '2' ||
c->version != 1 || c->header_size != sizeof(struct config) ||
c->load_address != LOAD_ADDRESS || c->kernel_offset != KERNEL_OFFSET ||
c->watchdog_address != WATCHDOG_ADDRESS || c->l2_address != L2_ADDRESS ||
c->ram_start != RAM_START || c->ram_end != RAM_END ||
c->initrd_limit != INITRD_LIMIT ||
checksum() != c->crc32)
return 0;
if (!range(LOAD_ADDRESS, c->image_size, RAM_START, RAM_END) ||
c->dtb_offset < KERNEL_OFFSET || c->dtb_size < 40 ||
c->dtb_offset >= c->image_size ||
c->dtb_size > c->image_size - c->dtb_offset ||
c->initrd_offset < c->dtb_offset + c->dtb_size ||
c->initrd_offset >= c->image_size ||
c->initrd_size > c->image_size - c->initrd_offset ||
c->initrd_address != INITRD_ADDRESS ||
!range(INITRD_ADDRESS, c->initrd_size, RAM_START, INITRD_LIMIT) ||
INITRD_ADDRESS < LOAD_ADDRESS + c->image_size)
return 0;
dtb = (volatile const u8 *)(LOAD_ADDRESS + c->dtb_offset);
if (big_endian(dtb) != 0xd00dfeedu || big_endian(dtb + 4) != c->dtb_size)
return 0;
from = (volatile const u8 *)(LOAD_ADDRESS + c->initrd_offset);
to = (volatile u8 *)INITRD_ADDRESS;
for (i = 0; i < c->initrd_size; i++)
to[i] = from[i];
return LOAD_ADDRESS + c->dtb_offset;
}
Loading
Loading