{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreicnv4fgqr2z4kcqgskyfnzwzoyuzyhzwcieyt6ngkui2kzmejzoym",
    "uri": "at://did:plc:bqma3dxvtfkv542aaek7xf6c/app.bsky.feed.post/3mhcr2wy7nhd2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreifqaymphlhaou7ymuxlsdcq46k6c4k27dbs2lrqq2rxp5ioaxmdoy"
    },
    "mimeType": "image/jpeg",
    "size": 8554
  },
  "path": "/2026/03/18/booting-with-rust-chapter-3/",
  "publishedAt": "2026-03-18T05:30:59.879Z",
  "site": "https://siliconislandblog.wordpress.com",
  "tags": [
    "Chapter 1",
    "Chapter 2",
    "Boot Loader Specification",
    "hadris",
    "github.com/rust-osdev/ieee1275-rs",
    "powerpc-bootloader repository"
  ],
  "textContent": "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.\n\nThat 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.\n\n## The filesystem problem\n\nThe 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.\n\nI tried writing my own minimal FAT32 implementation, then integrating `simple-fatfs` and `fatfs`. None worked well in a freestanding big-endian environment.\n\n### Hadris\n\nThe 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:\n\n  * **`thiserror` pulling in `std`**: default features were not disabled, preventing `no_std` builds.\n  * **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.\n  * **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.\n\n\n\nAll patches were merged upstream.\n\n## Disk I/O\n\nHadris 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.\n\n## Partition tables: GPT, MBR, and CHRP\n\nPowerVM with modern disks uses GPT (via the `gpt-parser` crate): a PReP partition for the bootloader and an ESP for kernels and BLS entries.\n\nInstallation 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.\n\nThe CHRP type is what CD/DVD boot uses on PowerPC. For ISO9660 I integrated `hadris-iso` with the same `Read + Seek` pattern.\n\nBoot 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.\n\n## The firmware allocator wall\n\nThis cost me a lot of time.\n\nOpen 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.\n\nIt 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.\n\nThe 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.\n\n## And the kernel boots!\n\nMarch 7, 2026. The commit message says it all: “And the kernel boots!”\n\nThe sequence:\n\n  1. **BLS discovery** : walk `loaders/entries/*.conf`, parse into `BLSEntry` structs, filter by architecture (`ppc64le`), sort by version using `rpmvercmp`.\n\n  2. **ELF loading** : parse the kernel ELF, iterate `PT_LOAD` segments, `claim` a contiguous region, copy segments to their virtual address offsets, zero BSS.\n\n  3. **Initrd** : `claim` memory, load the initramfs.\n\n  4. **Bootargs** : set `/chosen/bootargs` via `setprop`.\n\n  5. **Jump** : inline assembly trampoline – r3=initrd address, r4=initrd size, r5=OF client interface, branch to kernel:\n\n\n\n\n\n    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) )\n\nOne 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.\n\n## The boot menu\n\nI implemented a GRUB-style interactive menu:\n\n  * **Countdown** : boots the default after 5 seconds unless interrupted.\n  * **Arrow/PgUp/PgDn/Home/End navigation**.\n  * **ESC** : type an entry number directly.\n  * **`e`** : edit the kernel command line with cursor navigation and word jumping (Ctrl+arrows).\n\n\n\nThis 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.\n\n## Secure boot (initial, untested)\n\nIBM 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.\n\nI 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`.\n\nThe 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.\n\n## The ieee1275-rs crate\n\nThe 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.\n\nPublished on crates.io at github.com/rust-osdev/ieee1275-rs.\n\n## What’s next\n\nI 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!\n\nThe source is at the powerpc-bootloader repository. Contributions welcome, especially from anyone with POWER hardware access.",
  "title": "Booting with Rust: Chapter 3",
  "updatedAt": "2026-03-18T04:52:25.000Z"
}