From 26e7d91776fbe1fbaf51c27697539360a3328ee7 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Sun, 18 Oct 2026 09:18:14 +0300 Subject: [PATCH 1/2] install: the split NOR image is a uImage + squashfs OpenIPC moves the split NOR layout's firmware.bin from a FIT to the legacy uImage followed by the squashfs at the next 64 KiB block, and the kernel now finds the layout's partitions and root in its own device tree, whatever the bootloader passes. Any U-Boot that bootm's a uImage at 0x50000 boots it, the factory XM U-Boot included, which has no FIT. - split_image_end() reads the uImage header (data size at bytes 12..15) instead of a FIT's totalsize; the shape checks are otherwise the same. - The warning on a plan without the uboot stage goes: the U-Boot already on the flash boots the image whichever it is. - README and the firmware table say uImage. Tests: the split fixture is a uImage; a FIT now fails the shape check. --- README.md | 9 +++++---- src/defib/firmware.py | 8 ++++---- src/defib/install/firmware.py | 30 ++++++++++++++++-------------- src/defib/install/orchestrator.py | 10 ++-------- tests/test_install_nor_split.py | 18 ++++++++++-------- 5 files changed, 37 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index a5594df..286291d 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ ships a split NOR package, `openipc.-nor--split.tgz`, and |--------|------|----------| | `0x000000` | 256K | boot: `u-boot--nor.bin` | | `0x040000` | 64K | env | -| `0x050000` | rest of chip | firmware: `firmware.bin.` (a hashed FIT, then the squashfs at the next 64 KiB); the kernel divides it into `kernel`, `rootfs` and `rootfs_data` at boot | +| `0x050000` | rest of chip | firmware: `firmware.bin.` (the uImage, then the squashfs at the next 64 KiB); the kernel divides it into `kernel`, `rootfs` and `rootfs_data` at boot, from its own device tree | ```bash defib install -c gk7205v300 \ @@ -126,9 +126,10 @@ defib install -c gk7205v300 \ The `rootfs` stage writes `firmware.bin` at `0x50000` and erases the rest of the chip, so the first boot starts with an empty overlay; the `kernel` and -`rootfs-data` stages have nothing of their own to do. Only that U-Boot boots the -image: a plan without the `uboot` stage warns that the one already on the flash -must be it. See the [OpenIPC wiki](https://github.com/OpenIPC/wiki/blob/master/en/nor-split-layout.md) +`rootfs-data` stages have nothing of their own to do. Any U-Boot that loads the +kernel from `0x50000` boots the image -- the kernel ignores whatever `mtdparts` +and `root=` it passes -- so a plan without the `uboot` stage needs no more than +that. See the [OpenIPC wiki](https://github.com/OpenIPC/wiki/blob/master/en/nor-split-layout.md) for the layout itself. Targets that must bootstrap through a stock U-Boot use an explicit U-Boot diff --git a/src/defib/firmware.py b/src/defib/firmware.py index 06880da..93831f1 100644 --- a/src/defib/firmware.py +++ b/src/defib/firmware.py @@ -56,11 +56,11 @@ }) # SoCs whose OpenIPC firmware also ships the split NOR layout, -# openipc.-nor--split.tgz: one firmware.bin (a hashed FIT, then +# openipc.-nor--split.tgz: one firmware.bin (the uImage, then # the squashfs at the next 64 KiB) written at 0x50000, which the kernel divides -# into kernel, rootfs and rootfs_data at boot. Their OpenIPC/u-boot NOR build -# boots it, and also scans the fixed slots, so it owns mtdparts and bootcmd on -# either layout. +# into kernel, rootfs and rootfs_data at boot from its own device tree. Any +# U-Boot that loads the kernel from 0x50000 boots it; their OpenIPC/u-boot NOR +# build also scans the flash and owns mtdparts and bootcmd on either layout. NOR_SPLIT_SOCS: frozenset[str] = frozenset({ "gk7202v300", "gk7205v200", "gk7205v300", "gk7605v100", }) diff --git a/src/defib/install/firmware.py b/src/defib/install/firmware.py index 45975fb..2253bd2 100644 --- a/src/defib/install/firmware.py +++ b/src/defib/install/firmware.py @@ -19,8 +19,9 @@ class FirmwareBundle: ``kernel`` is ``b""``. For a split NOR package (``openipc.-nor--split.tgz``) the - one image is ``firmware.bin.``: it is carried as the rootfs, the - kernel is empty, and ``split`` is set. + one image is ``firmware.bin.`` -- the uImage, then the squashfs at + the next 64 KiB block: it is carried as the rootfs, the kernel is empty, + and ``split`` is set. """ kernel_name: str @@ -31,25 +32,26 @@ class FirmwareBundle: ERASE_BLOCK = 0x10000 -FDT_MAGIC = b"\xd0\x0d\xfe\xed" +UIMAGE_MAGIC = b"\x27\x05\x19\x56" +UIMAGE_HEADER = 64 SQUASHFS_MAGIC = b"hsqs" def split_image_end(image: bytes) -> int: """Where rootfs_data starts, relative to the image, for a split firmware.bin. - The FIT's ``totalsize`` (header bytes 4..7, big-endian) rounded up to a - 64 KiB block is where the squashfs starts; its ``bytes_used`` (superblock - offset 40, little-endian) rounded up the same way is where it ends. The - kernel's mtdsplit, U-Boot and sysupgrade all draw the same lines. - Raises ValueError for anything else. + The uImage header (64 bytes) plus its data size (bytes 12..15, big-endian) + rounded up to a 64 KiB block is where the squashfs starts; its + ``bytes_used`` (superblock offset 40, little-endian) rounded up the same way + is where it ends. The kernel's mtdsplit, U-Boot and sysupgrade all draw the + same lines. Raises ValueError for anything else. """ - if image[:4] != FDT_MAGIC: - raise ValueError("firmware.bin does not start with a FIT image") - fit = int.from_bytes(image[4:8], "big") - squash = (fit + ERASE_BLOCK - 1) // ERASE_BLOCK * ERASE_BLOCK - if not fit or squash + 48 > len(image) or image[squash:squash + 4] != SQUASHFS_MAGIC: - raise ValueError("firmware.bin has no squashfs at the 64 KiB block after its FIT") + if image[:4] != UIMAGE_MAGIC: + raise ValueError("firmware.bin does not start with a uImage") + kernel = UIMAGE_HEADER + int.from_bytes(image[12:16], "big") + squash = (kernel + ERASE_BLOCK - 1) // ERASE_BLOCK * ERASE_BLOCK + if squash + 48 > len(image) or image[squash:squash + 4] != SQUASHFS_MAGIC: + raise ValueError("firmware.bin has no squashfs at the 64 KiB block after its uImage") used = int.from_bytes(image[squash + 40:squash + 48], "little") end = squash + (used + ERASE_BLOCK - 1) // ERASE_BLOCK * ERASE_BLOCK if not used or len(image) < squash + used: diff --git a/src/defib/install/orchestrator.py b/src/defib/install/orchestrator.py index 37a00af..b5ffcaf 100644 --- a/src/defib/install/orchestrator.py +++ b/src/defib/install/orchestrator.py @@ -266,12 +266,6 @@ def warn(message: str) -> None: "where rootfs_data starts depends on the image written there", exit_code=2, ) - if "rootfs" in stage_set and "uboot" not in stage_set: - warn( - "firmware.bin boots only on an OpenIPC U-Boot that knows the split " - "layout; the uboot stage is not in this plan, so the U-Boot " - "already on the flash must be one." - ) needs_tftp = bool(stage_set & {"uboot", "rootfs"}) kernel_name = firmware.kernel_name @@ -297,7 +291,7 @@ def warn(message: str) -> None: elif split: console.print( f" Image: [cyan]{rootfs_name}[/cyan] ({len(rootfs_data)} bytes; " - "split NOR layout, FIT + squashfs at 0x50000)" + "split NOR layout, uImage + squashfs at 0x50000)" ) else: console.print(f" Kernel: [cyan]{kernel_name}[/cyan] ({len(kernel_data)} bytes)") @@ -1363,7 +1357,7 @@ async def tftp_and_flash( elif "kernel" in stage_set and split: if output == "human": console.print( - "\n [bold]kernel[/bold]: the FIT at the start of " + "\n [bold]kernel[/bold]: the uImage at the start of " "firmware.bin (written by the rootfs stage). Nothing to do." ) elif "kernel" in stage_set: diff --git a/tests/test_install_nor_split.py b/tests/test_install_nor_split.py index c59e85d..03cb0d9 100644 --- a/tests/test_install_nor_split.py +++ b/tests/test_install_nor_split.py @@ -1,6 +1,6 @@ """Split NOR install for the gk7205v200 family (OpenIPC/u-boot openipc-hisilicon). -openipc.-nor--split.tgz carries one firmware.bin: a hashed FIT, +openipc.-nor--split.tgz carries one firmware.bin: the uImage, then the squashfs at the next 64 KiB block. It is written at 0x50000 and the rest of the chip is erased; the kernel divides it into kernel, rootfs and rootfs_data at boot. That U-Boot works out the NOR layout itself, so the env @@ -24,13 +24,15 @@ CHIP_8M = 8 * 1024 * 1024 -def _split_image(fit_size: int = 0x18000, squashfs_used: int = 0x2F000) -> bytes: - """A FIT whose totalsize rounds up to 0x20000, then a squashfs there.""" - fit = b"\xd0\x0d\xfe\xed" + fit_size.to_bytes(4, "big") - fit = fit.ljust((fit_size + BLOCK - 1) // BLOCK * BLOCK, b"\xab") +def _split_image(kernel_size: int = 0x18000, squashfs_used: int = 0x2F000) -> bytes: + """A uImage whose header and data round up to 0x20000, then a squashfs there.""" + data = kernel_size - 64 + kernel = b"\x27\x05\x19\x56" + b"\x00" * 8 + data.to_bytes(4, "big") + kernel = (kernel + b"\x00" * 16 + b"Linux-4.9.37-gk7205v200").ljust(64, b"\x00") + kernel = kernel.ljust((kernel_size + BLOCK - 1) // BLOCK * BLOCK, b"\xab") squashfs = b"hsqs" + b"\x00" * 36 + squashfs_used.to_bytes(8, "little") squashfs = squashfs.ljust((squashfs_used + BLOCK - 1) // BLOCK * BLOCK, b"\xcd") - return fit + squashfs + return kernel + squashfs def _split_package(path: Path, board: str = "gk7205v200", image: bytes | None = None, @@ -162,7 +164,7 @@ def test_split_package_for_another_board(tmp_path): @pytest.mark.parametrize("image, reason", [ - (b"\x27\x05\x19\x56" + b"\x00" * 0x1FFFC, "does not start with a FIT"), + (b"\xd0\x0d\xfe\xed" + b"\x00" * 0x1FFFC, "does not start with a uImage"), (_split_image()[:0x20000] + b"\x00" * 0x30000, "no squashfs"), (_split_image() + b"\x00" * BLOCK, "runs past the end of its squashfs"), (_split_image()[:0x20000 + 0x2E000], "truncated"), @@ -223,7 +225,7 @@ async def test_split_kernel_and_rootfs_data_stages_write_nothing(monkeypatch, tm @pytest.mark.asyncio async def test_split_image_too_big_for_the_chip(monkeypatch, tmp_path): - image = _split_image(fit_size=0x18000, squashfs_used=0x7B0000 - 0x20000) # ends at the chip end + image = _split_image(kernel_size=0x18000, squashfs_used=0x7B0000 - 0x20000) # ends at the chip end firmware = _split_package(tmp_path / "fw.tgz", image=image) with pytest.raises(typer.Exit): await _install(monkeypatch, tmp_path, firmware) From 9cf4ff1f0f95bd80900915cc0956580c21747866 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:48:04 +0300 Subject: [PATCH 2/2] install: a split image whose uImage holds no kernel is refused A uImage header declaring zero data bytes rounds up to the first 64 KiB block, so a squashfs there passed the shape check and the install would have flashed an image with no kernel. --- src/defib/install/firmware.py | 5 ++++- tests/test_install_nor_split.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/defib/install/firmware.py b/src/defib/install/firmware.py index 2253bd2..50eac3b 100644 --- a/src/defib/install/firmware.py +++ b/src/defib/install/firmware.py @@ -48,7 +48,10 @@ def split_image_end(image: bytes) -> int: """ if image[:4] != UIMAGE_MAGIC: raise ValueError("firmware.bin does not start with a uImage") - kernel = UIMAGE_HEADER + int.from_bytes(image[12:16], "big") + data = int.from_bytes(image[12:16], "big") + if not data: + raise ValueError("firmware.bin's uImage holds no kernel") + kernel = UIMAGE_HEADER + data squash = (kernel + ERASE_BLOCK - 1) // ERASE_BLOCK * ERASE_BLOCK if squash + 48 > len(image) or image[squash:squash + 4] != SQUASHFS_MAGIC: raise ValueError("firmware.bin has no squashfs at the 64 KiB block after its uImage") diff --git a/tests/test_install_nor_split.py b/tests/test_install_nor_split.py index 03cb0d9..b060d97 100644 --- a/tests/test_install_nor_split.py +++ b/tests/test_install_nor_split.py @@ -168,7 +168,9 @@ def test_split_package_for_another_board(tmp_path): (_split_image()[:0x20000] + b"\x00" * 0x30000, "no squashfs"), (_split_image() + b"\x00" * BLOCK, "runs past the end of its squashfs"), (_split_image()[:0x20000 + 0x2E000], "truncated"), -], ids=["uimage", "no-squashfs", "trailing-bytes", "truncated"]) + (_split_image(kernel_size=64)[:0x40] + b"\x00" * (BLOCK - 0x40) + _split_image()[0x20000:], + "holds no kernel"), +], ids=["uimage", "no-squashfs", "trailing-bytes", "truncated", "empty-kernel"]) def test_split_package_shape_is_checked(tmp_path, image, reason): with pytest.raises(ValueError, match=reason): load_firmware_bundle(_split_package(tmp_path / "fw.tgz", image=image))