vhaudiquet 32e4c2e5da stub: render failure diagnostics as bit-bands on the panel
Round 6: pa==0 accepted but parse still fails on device (5 blinks).
All local reproductions pass, so the remaining unknowns are the actual
runtime values. Since the scanout address is known without any parse
(DRAM address 0 — Round 5), fail_dump() now renders diagnostic words as
32 white/black bit-cells per row directly into the scanout buffer after
the 5-blink execution signal.

Marker 0xC0DE0001: find_coreboot_reg failed — dumps x0 + raw DTB header.
Marker 0xC0DE0002|rc: find_framebuffer failed — dumps LBIO addr/size,
raw table magic/header_bytes/entries, and raw words at +24..+68.

Also fix a build break: u32 array = {0} emitted memset in freestanding.

host_test and qemu_test pass; payload e619166a... flashed and verified.
2026-08-29 23:46:21 +02:00

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.
S
Description
No description provided
Readme
2.9 MiB
Languages
C 54.5%
Shell 19.6%
Python 15.4%
Assembly 9.4%
Linker Script 1.1%