Skip to content
Open
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
7 changes: 7 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ 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.

The on-device ARMv7 handoff utility in boot/ios-launcher/kloader preserves
winocm's ios-kexec-utils source, distributed under GPL-2.0-or-later:
https://github.com/axi0mX/ios-kexec-utils
Reference revision: a1bbca8a8f2847d9c53070e76e0b5d093bda5993.
The original files are byte-pinned. A separate Orchard patch bounds the active
I/O/search paths and permits one sleep request; source.json records provenance.
14 changes: 14 additions & 0 deletions boot/cascadia/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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 and DTB editor add
bounded external-initramfs placement; that extension requires a fresh
hardware boot before it is described as working.
43 changes: 43 additions & 0 deletions boot/cascadia/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# ARM32 iBEC staging payload

`build --profile devices/p105/cascadia.toml --output DIR` builds a loader
with Orchard's kernel toolchain through `buildenv`. 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.

`python -m orchard.image.cascadia --profile devices/p105/cascadia.toml
--loader DIR/loader.bin --loader-elf DIR/loader.elf --kernel zImage
--dtb s5l8942x-p105.dtb --initramfs recovery.cpio.gz --bootargs 'rdinit=/init'
--output linux.bin` assembles one device's payload. Kernel bytes remain
unchanged. The DTB editor preserves properties while updating `/chosen`
boot arguments and its initrd addresses, then reserves that destination.

The layout is a checked loader followed by padding to 32 KiB, the zImage,
the edited DTB and the supplied initramfs, with each data address aligned to
eight bytes. The assembly entry disables the profile's watchdog before stack
setup; the C entry also clears it before validation. A rejected image remains
stopped for inspection. Validation checks the configuration checksum, bounds
and relocation alignment while touching only the uploaded low-memory image.
The loader then flushes caches and disables the inherited MMU/cache state
before copying the initramfs to reserved high RAM. Word copies handle the
aligned body and byte copies preserve the exact final tail. A barrier precedes
the Linux handoff with the DTB in `r2`. High RAM therefore needs no inherited
iBEC virtual mapping.
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 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. It records
`architecture = "arm"` and `initramfs_encoding = "cascadia-dtb"`, allowing a
distribution to reject an image assembled with another architecture or
payload format. This external-initramfs extension awaits hardware testing;
the earlier P105 boot embedded its initramfs in the kernel.

See `NOTICE` for source provenance. This producer contacts no device.
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.cascadia_payload 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")
101 changes: 101 additions & 0 deletions boot/cascadia/linux_boot.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// 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;
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 & 3) ||
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;
return LOAD_ADDRESS + c->dtb_offset;
}

/* Called only after validation and MMU/cache teardown, using physical RAM. */
void cascadia_relocate_initrd(u32 source, u32 target, u32 size)
{
volatile const u32 *from = (volatile const u32 *)source;
volatile u32 *to = (volatile u32 *)target;
volatile const u8 *tail_from;
volatile u8 *tail_to;

while (size >= sizeof(u32)) {
*to++ = *from++;
size -= sizeof(u32);
}
tail_from = (volatile const u8 *)from;
tail_to = (volatile u8 *)to;
while (size--)
*tail_to++ = *tail_from++;
}
Loading
Loading