call_end

    • Pl chevron_right

      Alberto Ruiz: Booting with Rust: Chapter 3

      news.movim.eu / PlanetGnome • 1 day ago • 5 minutes

    In Chapter 1 I gave the context for this project and in Chapter 2 I showed the bare minimum: an ELF that Open Firmware loads, a firmware service call, and an infinite loop.

    That was July 2024. Since then, the project has gone from that infinite loop to a bootloader that actually boots Linux kernels. This post covers the journey.

    The filesystem problem

    The Boot Loader Specification expects BLS snippets in a FAT filesystem under loaders/entries/ . So the bootloader needs to parse partition tables, mount FAT, traverse directories, and read files. All #![no_std] , all big-endian PowerPC.

    I tried writing my own minimal FAT32 implementation, then integrating simple-fatfs and fatfs . None worked well in a freestanding big-endian environment.

    Hadris

    The breakthrough was hadris , a no_std Rust crate supporting FAT12/16/32 and ISO9660. It needed some work to get going on PowerPC though. I submitted fixes upstream for:

    • thiserror pulling in std : default features were not disabled, preventing no_std builds.
    • Endianness bug : the FAT table code read cluster entries as native-endian u32 . On x86 that’s invisible; on big-endian PowerPC it produced garbage cluster chains.
    • Performance : every cluster lookup hit the firmware’s block I/O separately. I implemented a 4MiB readahead cache for the FAT table, made the window size parametric at build time, and improved read_to_vec() to coalesce contiguous fragments into a single I/O. This made kernel loading practical.

    All patches were merged upstream.

    Disk I/O

    Hadris expects Read + Seek traits. I wrote a PROMDisk adapter that forwards to OF’s read and seek client calls, and a Partition wrapper that restricts I/O to a byte range. The filesystem code has no idea it’s talking to Open Firmware.

    Partition tables: GPT, MBR, and CHRP

    PowerVM with modern disks uses GPT (via the gpt-parser crate): a PReP partition for the bootloader and an ESP for kernels and BLS entries.

    Installation media uses MBR. I wrote a small mbr-parser subcrate using explicit-endian types so little-endian LBA fields decode correctly on big-endian hosts. It recognizes FAT32, FAT16, EFI ESP, and CHRP (type 0x96 ) partitions.

    The CHRP type is what CD/DVD boot uses on PowerPC. For ISO9660 I integrated hadris-iso with the same Read + Seek pattern.

    Boot strategy? Try GPT first, fall back to MBR, then try raw ISO9660 on the whole device (CD-ROM). This covers disk, USB, and optical media.

    The firmware allocator wall

    This cost me a lot of time.

    Open Firmware provides claim and release for memory allocation. My initial approach was to implement Rust’s GlobalAlloc by calling claim for every allocation. This worked fine until I started doing real work: parsing partitions, mounting filesystems, building vectors, sorting strings. The allocation count went through the roof and the firmware started crashing.

    It turns out SLOF has a limited number of tracked allocations. Once you exhaust that internal table, claim either fails or silently corrupts state. There is no documented limit; you discover it when things break.

    The fix was to claim a single large region at startup (1/4 of physical RAM, clamped to 16-512 MB) and implement a free-list allocator on top of it with block splitting and coalescing. Getting this right was painful: the allocator handles arbitrary alignment, coalesces adjacent free blocks, and does all this without itself allocating. Early versions had coalescing bugs that caused crashes which were extremely hard to debug – no debugger, no backtrace, just writing strings to the OF console on a 32-bit big-endian target.

    And the kernel boots!

    March 7, 2026. The commit message says it all: “And the kernel boots!”

    The sequence:

    1. BLS discovery : walk loaders/entries/*.conf , parse into BLSEntry structs, filter by architecture ( ppc64le ), sort by version using rpmvercmp .

    2. ELF loading : parse the kernel ELF, iterate PT_LOAD segments, claim a contiguous region, copy segments to their virtual address offsets, zero BSS.

    3. Initrd : claim memory, load the initramfs.

    4. Bootargs : set /chosen/bootargs via setprop .

    5. Jump : inline assembly trampoline – r3=initrd address, r4=initrd size, r5=OF client interface, branch to kernel:

    core::arch::asm!(
        "mr 7, 3",   // save of_client
        "mr 0, 4",   // r0 = kernel_entry
        "mr 3, 5",   // r3 = initrd_addr
        "mr 4, 6",   // r4 = initrd_size
        "mr 5, 7",   // r5 = of_client
        "mtctr 0",
        "bctr",
        in("r3") of_client,
        in("r4") kernel_entry,
        in("r5") initrd_addr as usize,
        in("r6") initrd_size as usize,
        options(nostack, noreturn)
    )
    

    One gotcha: do NOT close stdout/stdin before jumping. On some firmware, closing them corrupts /chosen and the kernel hits a machine check. We also skip calling exit or release – the kernel gets its memory map from the device tree and avoids claimed regions naturally.

    The boot menu

    I implemented a GRUB-style interactive menu:

    • Countdown : boots the default after 5 seconds unless interrupted.
    • Arrow/PgUp/PgDn/Home/End navigation .
    • ESC : type an entry number directly.
    • e : edit the kernel command line with cursor navigation and word jumping (Ctrl+arrows).

    This runs on the OF console with ANSI escape sequences. Terminal size comes from OF’s Forth interpret service ( #columns / #lines ), with serial forced to 80×24 because SLOF reports nonsensical values.

    Secure boot (initial, untested)

    IBM POWER has its own secure boot: the ibm,secure-boot device tree property (0=disabled, 1=audit, 2=enforce, 3=enforce+OS). The Linux kernel uses an appended signature format – PKCS#7 signed data appended to the kernel file, same format GRUB2 uses on IEEE 1275.

    I wrote an appended-sig crate that parses the appended signature layout, extracts an RSA key from a DER X.509 certificate (compiled in via include_bytes! ), and verifies the signature (SHA-256/SHA-512) using the RustCrypto crates, all no_std .

    The unit tests pass, including an end-to-end sign-and-verify test. But I have not tested this on real firmware yet. It needs a PowerVM LPAR with secure boot enforced and properly signed kernels, which QEMU/SLOF cannot emulate. High on my list.

    The ieee1275-rs crate

    The crate has grown well beyond Chapter 2. It now provides: claim / release , the custom heap allocator, device tree access ( finddevice , getprop , instance-to-package ), block I/O, console I/O with read_stdin , a Forth interpret interface, milliseconds for timing, and a GlobalAlloc implementation so Vec and String just work.

    Published on crates.io at github.com/rust-osdev/ieee1275-rs .

    What’s next

    I would like to test the Secure Boot feature on an end to end setup but I have not gotten around to request access to a PowerVM PAR. Beyond that I want to refine the menu. Another idea would be to perhaps support the equivalent of the Unified Kernel Image using ELF. Who knows, if anybody finds this interesting let me know!

    The source is at the powerpc-bootloader repository . Contributions welcome, especially from anyone with POWER hardware access.