Skip to content

feat(grub): install EFI GRUB without loop devices or chroot - #415

Draft
lengau wants to merge 1 commit into
work/IMAGECRAFT-176/imgfsfrom
work/IMAGECRAFT-176/grub-efi
Draft

feat(grub): install EFI GRUB without loop devices or chroot#415
lengau wants to merge 1 commit into
work/IMAGECRAFT-176/imgfsfrom
work/IMAGECRAFT-176/grub-efi

Conversation

@lengau

@lengau lengau commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Part 5 of a stack that makes GRUB installation work without root privileges.
This is the core of the change.

The problem

Installing GRUB used to mean: attach a loop device to the image, mount the
partitions, bind-mount /dev, /proc and /sys, chroot in, and run the
distribution's grub-install. All of that needs root and privileged access to
the host's loop devices, which is why imagecraft could not build images in an
unprivileged container.

The approach

Build the bootloader on the host instead of inside the image:

  1. grub-mkimage assembles a GRUB EFI binary with the modules we need, with a
    prefix pointing at the ESP.
  2. The binary and its modules are copied into the ESP with mtools
    (via imgfs from the previous PR) -- no mount.
  3. Installed kernels and initrds are enumerated straight out of the rootfs with
    debugfs, and a grub.cfg is generated for them.
  4. Where the distribution ships signed shim/GRUB binaries, those are used
    instead so the image boots with Secure Boot enabled.

Nothing is mounted, no loop device is attached, and no chroot happens.

Scope

Only the EFI/GPT path is converted here. BIOS still uses the old chroot
implementation, and setup_grub dispatches to whichever is appropriate, so the
tree is working and testable at this commit. BIOS is converted in the next PR.

Testing

tests/spread/boot/classic becomes boot/classic-efi (to make room for the MBR
counterpart), and boot/classic-efi-secureboot is added. Both build an image
and boot it under QEMU, asserting the guest reaches userspace.

Replacing update-grub

update-grub rendered /etc/default/grub into the config. Since it is no
longer run, the generated config honours GRUB_CMDLINE_LINUX,
GRUB_CMDLINE_LINUX_DEFAULT and GRUB_TIMEOUT directly, and orders menu
entries newest kernel first so default=0 boots the newest one.

@lengau
lengau force-pushed the work/IMAGECRAFT-176/grub-efi branch 2 times, most recently from 7b70ea3 to 9d9f078 Compare August 13, 2026 12:15
@lengau
lengau force-pushed the work/IMAGECRAFT-176/grub-efi branch from 9d9f078 to a70c387 Compare August 13, 2026 17:03
@lengau
lengau force-pushed the work/IMAGECRAFT-176/grub-efi branch from a70c387 to 16dc20b Compare August 13, 2026 19:43
@lengau
lengau force-pushed the work/IMAGECRAFT-176/grub-efi branch 2 times, most recently from f5ef26d to 5498d96 Compare August 13, 2026 23:18
@lengau
lengau force-pushed the work/IMAGECRAFT-176/grub-efi branch from 5498d96 to d6e07d5 Compare August 14, 2026 02:49
@lengau
lengau force-pushed the work/IMAGECRAFT-176/grub-efi branch from d6e07d5 to 608616f Compare August 20, 2026 16:45
Copilot AI lite review requested due to automatic review settings August 20, 2026 16:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reworks imagecraft’s EFI/GPT GRUB installation to operate directly on raw disk images (using host-side grub-mkimage, mtools, and debugfs) rather than relying on privileged loop-device mounts and a chroot, enabling unprivileged/container builds. It also updates boot validation to exercise the new EFI path (including Secure Boot).

Changes:

  • Implement EFI GRUB installation in imagecraft.pack.grubutil via direct disk image manipulation (no mount/loop/chroot), with optional deployment of signed shim/GRUB when present.
  • Add/expand unit + end-to-end integration tests that build small real disk images and verify GRUB artifacts/config placement without privileged operations.
  • Update spread boot tasks/workflow and snap packaging to support the new tooling (grub-mkimage, EFI boot tests, Secure Boot test).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
imagecraft/pack/grubutil.py Core EFI/GPT GRUB install path rewritten to operate on raw images with debugfs/mtools/grub-mkimage; BIOS/MBR path retained via chroot for now.
tests/unit/pack/test_grubutil.py New/expanded unit + integration coverage for EFI GRUB installation and config generation, including signed shim/GRUB handling and separate /boot cases.
tests/spread/boot/classic-efi/task.yaml Spread boot test adjusted for EFI boot under QEMU with serial capture via script.
tests/spread/boot/classic-efi/imagecraft.yaml Test image tweaked to ensure reliable serial console output and GRUB defaults compatible with the new config generation.
tests/spread/boot/classic-efi-secureboot/task.yaml New Secure Boot-enabled QEMU boot validation using OVMF secboot firmware + Microsoft CA vars template.
tests/spread/boot/classic-efi-secureboot/imagecraft.yaml New Secure Boot test image definition (installs signed GRUB + shim packages, configures GRUB defaults).
snap/snapcraft.yaml Add grub-common and stage grub-mkimage into the snap’s tool path.
.github/workflows/spread-test.yaml Update ARM classic boot spread job to run the new classic-efi spread test.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/unit/pack/test_grubutil.py Outdated
)


def test_setup_grub_efi_unsigned(new_dir, fake_kernel_files):
Comment thread tests/unit/pack/test_grubutil.py Outdated
Comment on lines +832 to +834
@pytest.mark.slow
@pytest.mark.usefixtures("new_dir")
def _build_efi_disk(tmp_path: Path, root_content: Path, esp_content: Path):
Replace the chroot-over-loop-device GRUB installation for EFI systems
with direct manipulation of the disk image:

- The root (and optional separate boot) ext4 partitions are exposed
  through the unprivileged FUSE mounts from imagecraft.utils.mount.
- GRUB's EFI binary is built by running the image's own grub-mkimage
  inside a chroot of the mounted rootfs (via chroot(1)), guaranteeing
  the builder matches the modules it consumes — no host/image version
  skew and no module dumping.
- ESP files are written in place with mtools' image@@offset addressing;
  fusefat is deliberately not used here as it proves unreliable when
  stacked over fusefile.
- grub.cfg is hand-generated (kernel enumeration newest-first,
  /etc/default/grub cmdline handling, UEFI firmware entry) because
  grub-mkconfig/grub-probe require a real block device.

This removes the need for loop devices when building EFI images, which
is what allows imagecraft to run inside unprivileged LXD containers.

The BIOS path still uses the chroot implementation and is converted in
a follow-up, so setup_grub dispatches to the new EFI implementation for
GPT volumes and to the retained chroot implementation for MBR volumes.

The spread test boot/classic is renamed to boot/classic-efi to make
room for an MBR counterpart, and boot/classic-efi-secureboot is added
to cover installing the distribution's signed shim and GRUB binaries.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lengau
lengau force-pushed the work/IMAGECRAFT-176/grub-efi branch from fc58ff0 to ae8088a Compare August 25, 2026 21:15
@lengau lengau closed this Aug 25, 2026
@lengau lengau reopened this Aug 25, 2026
@lengau
lengau force-pushed the work/IMAGECRAFT-176/grub-efi branch from 7b20ed5 to ae8088a Compare August 27, 2026 13:44
@lengau
lengau deleted the branch work/IMAGECRAFT-176/imgfs August 27, 2026 13:48
@lengau lengau closed this Aug 27, 2026
@lengau lengau reopened this Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants