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..50eac3b 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,29 @@ 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") + 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") 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..b060d97 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,11 +164,13 @@ 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"), -], 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)) @@ -223,7 +227,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)