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.
218 lines
12 KiB
Markdown
218 lines
12 KiB
Markdown
# 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.
|