Files
krane-fb-stub/README.md
T
vhaudiquet 7654ca72d7 README: revise flash plan to eMMC first, USB as recovery
The live environment boots from the USB stick; the eMMC kernel
partition is not load-bearing, so it is the safer first target.
Document backup locations, flash/verify commands, and recovery.
Backup of /dev/mmcblk0p1 taken before any write.
2026-08-29 19:58:50 +02:00

12 KiB

krane-fb-stub

Minimal arm64 "hello framebuffer" payload for the Lenovo IdeaPad Duet Chromebook (MT8183, google,krane sku176). It validates the depthcharge → custom-payload pipeline before any U-Boot bring-up work: it locates the boot-splash framebuffer handed over by coreboot/depthcharge and paints solid colors at checkpoints.

Checkpoint convention

Parsed first (instantaneous), then drawn — see Deviations:

stage success signal failure signal
1 screen RED ~2s screen keeps depthcharge output — DTB parse failed
2 YELLOW ~2s (yellow implies DTB parsed)
3 GREEN ~2s stuck on yellow — LBIO/fb record bad
4 BLUE, held forever stuck on green — fb fill faulted

If the stub never runs at all (header rejected, load failed), depthcharge falls back to its own screen — no color ever appears.

Files

  • stub.S — 64-byte arm64 Image header + entry stub (own stack, no PLT).
  • main.c — FDT walker, coreboot-table walker, framebuffer painter.
  • linker.ld — flat layout, .bss asserted empty (image_size must cover all state because depthcharge does not zero-fill beyond the file).
  • build.sh — build + relocation/bss checks + image_size patch.
  • host_test.c — host-compiled test of both parsers against the live /sys/firmware/fdt and a synthetic LBIO table.
  • qemu_test.py — end-to-end smoke test under qemu-system-aarch64 (synthetic coreboot table + DTB; asserts red→yellow→green→blue timeline).
  • build-payload.sh — packs krane-fb-stub.bin + krane-sku176.dtb into a signed depthcharge image via mkdepthcharge.
  • krane-fb-stub.bin — raw arm64 Image (18 KB).
  • krane-fb-stub-payload.bin — packed, devkey-signed depthcharge image, verified with futility vbutil_kernel --verify.
  • krane-sku176.dtb — the exact krane sku176 DTB from the currently-booting FIT on this unit (extracted with dumpimage from the p1 FIT, config conf-27 mt8183-kukui-krane-sku176).

Verification performed (per the prompt's "verify against source" list)

1. coreboot table layout — CHECKED, matches prompt

Cloned coreboot main; read src/commonlib/include/commonlib/coreboot_tables.h:

  • physical_address is lb_uint64_t = __aligned(4) uint64_t (LB_ENTRY_ALIGN == 4). On little-endian AArch64 this is a plain 8-byte LE u64 at offset 8 of struct lb_framebuffer; the "hi/lo split" is only an alignment trick, not separate halves in memory.
  • Full struct is 40 bytes: tag@0, size@4, physical_address@8, x_resolution@16, y_resolution@20, bytes_per_line@24, bits_per_pixel@28, red 29/30, green 31/32, blue 33/34, reserved 35/36, orientation@37, flags@38, pad@39. Verified by compiling a sizeof/offsetof probe against the real header on the host (sizeof == 40).
  • lb_header matches the prompt: signature[4] "LBIO", then header_bytes, header_checksum, table_bytes, table_checksum, table_entries (LE u32 each). Records start after header_bytes.
  • LB_TAG_FRAMEBUFFER = 0x12 confirmed.
  • Cross-check on this live unit: parsing the live /sys/firmware/fdt (which contains depthcharge's injected /firmware/coreboot node) yields LBIO at 0xffed9000, size 0x380 — matches the prompt.

2. arm64 Image header — CHECKED against Linux booting.rst

  • 64-byte header, magic 0x644d5241 ("ARM\x64") at 0x38, LE fields.
  • code0 must branch past the header; entry is at the image start with x0=DTB phys addr, x1..x3=0, MMU off, DAIF masked.
  • image_size must be exact (non-zero), little-endian.
  • Cross-checked against depthcharge's consumer: src/arch/arm/boot64.c reads the same header (magic check, image_size, text_offset as load_offset for KASLR slot math).

3. depthcharge behavior — CHECKED against chromiumos depthcharge source

  • src/arch/arm/boot64.c: image is decompressed/relocated to a random 2 MiB-aligned slot; the Image header magic is verified; entry is the first byte of the (relocated) image; x0 = flat DTB, x1..x3 = 0 (boot64_asm.S zeroes x1-x3, disables MMU, br x25).
  • => the stub must be position-independent (built -fPIC, checked for zero dynamic relocations) and carry its own stack (done, 16 KiB inside the image).
  • Prompt assumption contradicted by source: a FIT config without an FDT subimage is skipped (fit.c: "config %s has no FDT, skipping"), and with no match depthcharge gives up. So the packed FIT does include a DTB — the real krane-sku176 DTB taken from the working FIT — while depthcharge still injects its own /firmware/coreboot node at boot via src/boot/coreboot.c install_coreboot_data (it adds #address-cells, #size-cells, ranges, compatible, and the reg pairs to /firmware/coreboot). Our DTB is only a config-selection key; the runtime DTB the stub receives at x0 is depthcharge's fixed-up tree.
  • depthcharge injects /firmware/coreboot reg = (table addr, CBMEM addr) in src/boot/coreboot.c; the krane root DTS uses #address-cells=2/#size-cells=2, so the first reg pair decodes to LBIO @0xffed9000 size 0x380 — matching the prompt.
  • Existing kernel p1 body confirmed to be a FIT (d00dfeed at body offset 0), body load address 0x100000, so FIT (--format fit, the arm default in mkdepthcharge) is the right container.

4. mkdepthcharge input format — CHECKED, answers the prompt's question

  • mkdepthcharge builds the FIT itself (mkimage -f auto -T kernel -O linux -C none -d <vmlinuz> -b <dtbs>, then patches the kernel subimage type to kernel_noload), then signs with vbutil_kernel.
  • A raw arm64 Image binary can be passed directly as -d/VMLINUZ — no manual uImage wrapping. mkdepthcharge auto-detects arm64 from the ARM\x64 magic at file offset 0x38 and defaults to FIT format for arm.
  • Packed command (see build-payload.sh): mkdepthcharge -A arm64 -o <out> -n <desc> -d krane-fb-stub.bin -b krane-sku176.dtb
  • The packed image verifies with the devkeys and carries one config (conf-1, description "krane-sku176") with kernel_noload + flat_dt subimages, compression none — the same shape as the working kernel.

Deviations from the prompt (all deliberate, explained)

  1. Red-on-entry is physically impossible. The framebuffer address is only discoverable by parsing the DTB (depthcharge injects it) and the coreboot table. Parsing is sub-millisecond, so the sequence is: parse → red → yellow → green → blue. A pre-parse failure leaves depthcharge's dev-mode screen visible, which is a strictly better "stuck at stage N" indicator than a stuck color (a stuck color would hide which parse failed; an unpainted screen + depthcharge's own on-screen error identifies the stage).
  2. A DTB is included in the FIT (contrary to "no DTB"): this unit's depthcharge requires an fdt subimage in the chosen config. We embed the genuine krane sku176 DTB; depthcharge's fixup adds the coreboot node at runtime, exactly as the prompt's hardware facts describe.
  3. Colors are derived from the mask_pos/mask_size fields of the framebuffer record rather than hard-coded xRGB — works for any format the panel reports (krane reports 32bpp xRGB: red@16/8, green@8/8, blue@0/8).

Testing done (host)

  • host_test (host C): FDT parser against the LIVE /sys/firmware/fdt (finds LBIO @0xffed9000 size 0x380 — exact match to the prompt's manually-confirmed values), synthetic coreboot table layout test, color composition for all four checkpoints, and full-screen fill bounds (first/last pixel written, no overflow past yres*bpl).
  • qemu end-to-end (qemu_test.py, needs qemu-system-aarch64 + dtc): real stub binary + real krane DTB + synthetic coreboot table under qemu-system-aarch64 -M virt. Guest memory sampled via the QEMU monitor: first pixel every 0.5s must transition exactly red → yellow → green → blue with blue held forever, and the final full-screen snapshot must be uniform blue. All checks pass.
./build.sh && python3 qemu_test.py   # run the end-to-end test
gcc -O2 -o host_test host_test.c && ./host_test   # host parser tests
  • Packed payload verified with futility vbutil_kernel --verify (body verification succeeded, devkeys).

Firmware state confirmed on this unit

  • dev_boot_usb = 1 — read directly from the device's VBNV flash region (RW_NVRAM @ 0x80000 in the 8 MB SPI, latest 16-byte vbnv blob at 0x81bb0; CRC8-vbnv valid; DEV_FLAGS_OFFSET bit0 set, signed-only off). crossystem could not run because distro flashrom lacks the host programmer; flashrom -p internal works and the blob was decoded from the read-back image.
  • Boot chain evidence: p1 body is a FIT with 27 configs including mt8183-kukui-krane-sku176; depthcharge FIT config matching is by compatible from the config nodes, and our packed FIT's conf-1 carries that description (mt8183-kukui-krane-sku176) as its fdt subimage, so this unit's depthcharge (with kukui compat strings) selects it.

Build & pack

./build.sh                     # -> krane-fb-stub.bin (+ relocation/bss checks)
sh build-payload.sh            # -> krane-fb-stub-payload.bin (signed, verifiable)

Flash plan (revised: eMMC first, USB is the recovery path)

Rationale: the working system on this unit boots from the USB stick (/dev/sda1, pmOS, ChromeOS-kernel GUID FE3A2A5D-…), chrooting into the Ubuntu rootfs on eMMC. The eMMC kernel partition (/dev/mmcblk0p1) is therefore NOT load-bearing for the live environment, while the stick's kernel partition IS. Flashing the stub to eMMC keeps recovery independent of the flashed target — the inverse of the original prompt's ordering, so the original "USB stick first" safety rule is satisfied in spirit by backing up both kernel partitions first:

Backups taken before any write (verified sha256):

  • /dev/mmcblk0p1 (pmOS kernel on eMMC, 32 MiB) → ~/mmcblk0p1-pmos-backup.img (on eMMC) — copy this to the USB stick too so recovery never depends on eMMC.
  • USB stick sda1 is NOT touched — it keeps booting the system.

Flash steps (executed by the operator, not automated here):

dd if=krane-fb-stub-payload.bin of=/dev/mmcblk0p1 bs=4M conv=fsync status=none
cmp krane-fb-stub-payload.bin /dev/mmcblk0p1   # verify what landed

Then reboot, pick the internal-boot option in the depthcharge dev-mode menu, and watch for red → yellow → green → blue. Recovery if it fails: power-cycle, boot USB (unchanged), and dd if=mmcblk0p1-pmos-backup.img of=/dev/mmcblk0p1.

Key-verification parity: the packed payload uses the same devkeys as the currently-booting p1 kernel (data key sha1 d6170aa4… matches) and the same kernel version 1, so the TPM stored-version check that admits the existing kernel admits ours equally.

Known limitations / notes

  • The stub never returns; it spins on WFE holding blue (or halts unpainted on a parse failure).
  • No timer driver: delays use the architected generic timer registers (CNTFRQ/CNTPCT) — mandatory in the boot protocol anyway.
  • No cache maintenance is done for the framebuffer writes: we enter with the D-cache off, and writes to the (DMA'd) display controller read physical memory; on this platform the splash buffer is already coherent.
  • The 0x380/0xffed9000 constants from the prompt are used ONLY in the host tests as expected values, never in the stub logic.