From d65bf17cd3cc2bbbfac9a578b2b56d4105b2bcbe Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Fri, 24 Jul 2026 17:41:32 +0200 Subject: [PATCH] fix(session): mount the boot FAT at /boot/firmware on Bookworm Raspberry Pi OS Bookworm keeps the FAT boot partition at /boot/firmware, where the raspi-firmware post-update hook syncs the initramfs. Binding it at /boot in the chroot sent update-initramfs's output to the ext4 root, so the firmware booted a stock initramfs without cryptsetup -> (initramfs) emergency shell, no LUKS prompt. boot_bind_target() detects the layout and binds (and unmounts) the FAT at the right place; legacy single-FAT layouts still use /boot. Co-Authored-By: Claude Opus 4.8 --- lim/image/session.py | 18 ++++++++++++++++-- tests/unit/test_image_session.py | 14 ++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lim/image/session.py b/lim/image/session.py index 4136e83..3157285 100644 --- a/lim/image/session.py +++ b/lim/image/session.py @@ -88,10 +88,24 @@ class ImageSession: ui.info("The following mounts refering this setup exist:") runner.run(["findmnt", "-R", str(self.working_folder)], check=False) + def boot_bind_target(self) -> str: + """Where the FAT partition belongs inside the chroot. + + Raspberry Pi OS Bookworm keeps it at /boot/firmware (the raspi-firmware + post-update hook syncs the initramfs there); older layouts use /boot. + Binding it wrong sends update-initramfs's output to the ext4 root, so the + firmware boots a stock initramfs without cryptsetup -> initramfs shell. + """ + if (self.root_mount_path / "boot/firmware").is_dir(): + return f"{self.root_mount_path}/boot/firmware" + return f"{self.root_mount_path}/boot" + def mount_chroot_binds(self) -> None: ui.info("Mount chroot environments...") root = self.root_mount_path - runner.run(["mount", "--bind", str(self.boot_mount_path), f"{root}/boot"], sudo=True) + runner.run( + ["mount", "--bind", str(self.boot_mount_path), self.boot_bind_target()], sudo=True + ) runner.run(["mount", "--bind", "/dev", f"{root}/dev"], sudo=True) runner.run(["mount", "--bind", "/sys", f"{root}/sys"], sudo=True) runner.run(["mount", "--bind", "/proc", f"{root}/proc"], sudo=True) @@ -142,7 +156,7 @@ class ImageSession: self._umount(f"{root}/dev", lazy=True) self._umount(f"{root}/proc") self._umount(f"{root}/sys") - self._umount(f"{root}/boot") + self._umount(self.boot_bind_target()) self._umount(str(root)) if self.boot_mount_path is not None: self._umount(str(self.boot_mount_path)) diff --git a/tests/unit/test_image_session.py b/tests/unit/test_image_session.py index cf5b5a7..e4dab1c 100644 --- a/tests/unit/test_image_session.py +++ b/tests/unit/test_image_session.py @@ -60,3 +60,17 @@ def test_destructor_closes_luks_mapper(tmp_path, fake_runner): session.decrypt_root() session.destructor() assert len(fake_runner.find("luksClose", "linux-image-manager-uuid-1")) == 1 + + +def test_boot_bind_target_bookworm_uses_boot_firmware(tmp_path): + session = ImageSession(Device("sda")) + session.root_mount_path = tmp_path + (tmp_path / "boot/firmware").mkdir(parents=True) + assert session.boot_bind_target() == f"{tmp_path}/boot/firmware" + + +def test_boot_bind_target_legacy_uses_boot(tmp_path): + session = ImageSession(Device("sda")) + session.root_mount_path = tmp_path + (tmp_path / "boot").mkdir() + assert session.boot_bind_target() == f"{tmp_path}/boot"