Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ ships a split NOR package, `openipc.<board>-nor-<variant>-split.tgz`, and
|--------|------|----------|
| `0x000000` | 256K | boot: `u-boot-<chip>-nor.bin` |
| `0x040000` | 64K | env |
| `0x050000` | rest of chip | firmware: `firmware.bin.<board>` (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.<board>` (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 \
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions src/defib/firmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@
})

# SoCs whose OpenIPC firmware also ships the split NOR layout,
# openipc.<board>-nor-<variant>-split.tgz: one firmware.bin (a hashed FIT, then
# openipc.<board>-nor-<variant>-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",
})
Expand Down
33 changes: 19 additions & 14 deletions src/defib/install/firmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ class FirmwareBundle:
``kernel`` is ``b""``.

For a split NOR package (``openipc.<board>-nor-<variant>-split.tgz``) the
one image is ``firmware.bin.<board>``: it is carried as the rootfs, the
kernel is empty, and ``split`` is set.
one image is ``firmware.bin.<board>`` -- 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
Expand All @@ -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:
Expand Down
10 changes: 2 additions & 8 deletions src/defib/install/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)")
Expand Down Expand Up @@ -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:
Expand Down
22 changes: 13 additions & 9 deletions tests/test_install_nor_split.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Split NOR install for the gk7205v200 family (OpenIPC/u-boot openipc-hisilicon).

openipc.<board>-nor-<variant>-split.tgz carries one firmware.bin: a hashed FIT,
openipc.<board>-nor-<variant>-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
Expand All @@ -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,
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down
Loading