DEV Community

Cover image for Building Bootable Images in Userspace: Avoiding the Loop Device Trap
Vincent Yang
Vincent Yang

Posted on • Originally published at blog.vyang.org

Building Bootable Images in Userspace: Avoiding the Loop Device Trap

Building Disk Images Without Root (And Without Regret)

If you have built enough disk images, you eventually hit the same Rite Of Passage:

It is 1:27 AM, you are tired, and you want run a fast "one-liner" with sudo dd or sudo mkfs.fat.
You falsely typed one character.
You did not wipe the test image.
You wipe a host partition.

That story is so common it might as well be onboarding material.

The trap is not triggered because low-level tools are bad. But because the trap is running low-level tools with maximum privilege on the host, in a workflow with high typo probability and weak guardrails.

This guide is about building bootable disk images in User Space while keeping Host Safety intact. And not get your host disk ruined.

Failure Mode: The Root Workflow Drift

Most people start with this pattern because it looks "standard":

# 1) Create a blank disk image
dd if=/dev/zero of=disk.img bs=1M count=512

# 2) Partition it
parted -s disk.img mklabel msdos mkpart primary fat32 1MiB 100%

# 3) Bind with a loop device
loop_dev=$(sudo losetup -fP --show disk.img)

# 4) Make filesystem + mount + copy
sudo mkfs.fat -F 32 "${loop_dev}p1"
mkdir -p ./mnt
sudo mount "${loop_dev}p1" ./mnt
sudo cp -r ./payload/. ./mnt/
sudo umount ./mnt
sudo losetup -d "${loop_dev}"
Enter fullscreen mode Exit fullscreen mode

Nothing here is unusual. That is exactly why it is dangerous, because what you think is not dangerous could be very dangerous. This sequence normalizes a habit where every critical operation uses sudo, so one typo has host-level blast radius.

The Hierarchy of Hazard

Use a Symptom Vs Catastrophe model when evaluating root-heavy image workflows.

Tier 1: Annoying Symptom

  • Wrong partition offset in dd.
  • Files copied into the wrong path.
  • Disk not booting due to broken VBR.

These are fixable and mostly local to the image artifact.

Tier 2: Costly Symptom

  • Loop device leaks because losetup -d was skipped.
  • Confusing /dev/loopN state after repeated runs.
  • CI/CD scripts becoming non-deterministic.

Now your workflow degrades over time and debugging cost increases.

Tier 3: Catastrophe

  • mkfs points to a host partition instead of the image.
  • dd writes to the wrong block device.
  • grub-install targets host storage after device resolution fails.

At this tier, you are no longer "debugging an image." You are performing incident response on your own workstation.

What Usually Breaks First

Loopback Device Lifecycle

What Breaks

Loop devices accumulate when scripts exit early or cleanup paths are missed. Eventually your flow depends on stale /dev/loopN state and implicit assumptions about which loop index maps to which file.

Why it Matters

Your build stops being deterministic. On a good day it "works on my machine". On a bad day it formats the wrong target because loop_dev is not what your script thinks it is.

GRUB Target Resolution

What Breaks

grub-install is called with an invalid or unexpected target device, then falls back to behavior you did not intend, GRUB will fallback to your host disk if the disk you specified to GRUB does not exist.

Why it Matters

Bootloader writes are destructive by design. A mis-targeted install can modify host boot configuration and convert a test run into a host recovery task.

Three User Space Strategies

The goal is simple: no mount(2), no host loop device dependency for routine file operations, and minimal root privilege.

Option 1: Libguestfs (guestfish)

guestfish builds and edits disk images inside an appliance VM. That isolation is the key safety property.

guestfish << 'EOF'
sparse disk.img 512M
run
part-init /dev/sda mbr
part-add /dev/sda p 2048 -1
mkfs vfat /dev/sda1
mount /dev/sda1 /
copy-in payload/. /
sync
quit
EOF
Enter fullscreen mode Exit fullscreen mode

Pros

  • Strong Host Safety boundary because operations happen in a guest appliance.
  • Scriptable for CI with stable command primitives.
  • Good default for "just make me a bootable image" pipelines.

Cons

  • Slower on weak hosts because all I/O is VM-mediated.
  • Kernel/appliance setup can be brittle on some distros.
  • FAT behavior can become MysteryMeat in edge sizes.

On that FAT point: mkfs vfat may auto-select FAT12/FAT16/FAT32 based on partition geometry. You can end up with a FAT16-style VBR while the partition type marker says 0x0c (FAT32 LBA). The image is technically "built," but tooling assumptions diverge and boot behavior becomes inconsistent, if you have your own OS, it may be a problem.

When to use this

Use when you want maximum safety and can tolerate lower build speed, it is also good when you want maximum customizability.

Option 2: DiskCombining (Assemble Image Segments)

This method creates partition payloads separately, then injects them into a pre-partitioned disk image. It favors explicit geometry control and reproducibility.

disk_size_mb=512
part_size_mb=511
part_start_lba=2048

dd if=/dev/zero of=disk.img bs=1M count="${disk_size_mb}"

# Create partition table non-interactively
parted -s disk.img mklabel msdos
parted -s disk.img mkpart primary fat32 1MiB 100%

# Build partition payload independently
dd if=/dev/zero of=part.img bs=1M count="${part_size_mb}"
mkfs.fat -F 32 part.img

# Populate partition payload using user-space FAT tools
mmd -i part.img ::/boot
mcopy -i part.img kernel.bin ::/boot/kernel.bin

# Inject partition payload at the partition start
dd if=part.img of=disk.img bs=512 seek="${part_start_lba}" conv=notrunc

# Set partition type to W95 FAT32 (LBA)
printf '\x0c' | dd of=disk.img bs=1 seek=450 conv=notrunc
Enter fullscreen mode Exit fullscreen mode

Pros

  • Maximum control over geometry, offsets, and byte-level layout.
  • Fast and deterministic once script variables are correct.
  • Works well for advanced boot experiments.

Cons

  • Higher cognitive load; offset math mistakes are easy.
  • MBR-first workflows are simpler here than GPT-first workflows.
  • You own all correctness checks.

When to use this

Use when you need precise layout control and are comfortable validating disk geometry manually.

Option 3: mtools-First FAT Workflow

This is the most underrated path for FAT boot media. Use mtools to manipulate filesystems entirely in UserSpace.

# Create FAT image file directly
dd if=/dev/zero of=part.img bs=1M count=64
mkfs.fat -F 32 part.img

# No mount syscall required
mmd -i part.img ::/EFI
mmd -i part.img ::/EFI/BOOT
mcopy -i part.img BOOTX64.EFI ::/EFI/BOOT/BOOTX64.EFI
Enter fullscreen mode Exit fullscreen mode

Pros

  • mcopy and mmd operate without mount, so no root requirement for file operations.
  • Cleaner CI scripting because there is no loop device lifecycle to manage.
  • Excellent for FAT payload staging before final disk assembly.

Cons

  • Primarily useful for FAT workflows; not a universal filesystem toolkit.
  • Less familiar to developers used to mount + cp habits.
  • You still need a partitioning plan for full-disk images.

When to use this

Use when your target is FAT-based boot media and you want the safest day-to-day developer loop.

Practical Verdict

  • If you optimize for Safety First: choose guestfish.
  • If you optimize for Layout Control: choose Disk Combining.
  • If you optimize for daily velocity on FAT images: choose mtools + assembly.

My default in real projects is hybrid:

  1. Use mtools for payload edits.
  2. Use Disk Combining for reproducible final image assembly.
  3. Keep guestfish as the "safe fallback" when environment-specific tools become noisy.

This keeps the core loop in UserSpace while preserving deep control when needed.

Conclusion

Root privileges are not a badge of low-level skill. They are a liability multiplier.

If your disk workflow requires frequent sudo, you are one typo away from turning a build task into a recovery task. Shift routine image operations to UserSpace, isolate risky steps, and reserve privilege for the rare places where it is truly necessary.

Experiments & Resources

I spend roughly 6 hours a week outside my main work researching these systems performance bottlenecks and refining my build toolchain. For those building for unpackage, I’ve packaged my advanced, rootless disk-building scripts—along with the full source for this experiment into the latest code bundle on my Support page. It’s the fastest way to move from this theory to a repeatable production pipeline.

Top comments (0)