LinkedIn Sourceforge

Vincent's Blog

Pleasure in the job puts perfection in the work (Aristote)

Void Linux is very dangerous for FreeBSD on my laptop

Posted on 2026-09-18 22:47:00 from Vincent in VoidLinux ZFS

Give a shoutout to aa aa on unsplash.com This whole thing started because of Stefano and his blog. His guide to installing Void Linux on an encrypted ZFS root with hibernation support was the thing that got me curious enough to actually try it — and, as these things go, curiosity turned into an afternoon spent repartitioning a laptop I use every day. My setup below is simpler than his (no LUKS-encrypted swap, no hibernation), because I just wanted ZFS-on-root dual-booted alongside my existing FreeBSD/GhostBSD/OpenBSD rEFInd menu, but the bones of the approach — and the credit for pointing me at zfsbootmenu in the first place — are his.


Replacing a FreeBSD Partition with a ZFS-Rooted Void Linux, Dual-Booted via rEFInd

I run GhostBSD and FreeBSD boxes, so ZFS is already part of my vocabulary — zpool, zfs create, ashift=12, none of that is new to me. What is different when bringing ZFS-on-root to a Linux distribution like Void Linux is the boot chain: instead of the FreeBSD boot loader or bectl, I ended up relying on zfsbootmenu, dracut, and (in this setup) rEFInd chaining into it. This post walks through the real migration I did — replacing an existing FreeBSD partition with a ZFS-rooted Void Linux install — and explains why each step exists, not just what I typed.

My laptop is a Lenovo T14s with Ryzen pro7. This machine is not FreeBSD friendly because no driver for the wifi card (which is solded), no suspend, no hibernate. Depsite OpenBSD runs perfectly, I was interested to have ZFS based system with a better hardware support.

Context for this walkthrough:
- The old FreeBSD partition I replaced: /dev/nvme0n1p5
- The existing EFI System Partition (ESP), already used by rEFInd: /dev/nvme0n1p1
- Goal: dual-boot Void Linux (on ZFS) alongside OpenBSD and GhostBSD, which rEFInd already boots

1. Boot from a USB installer

Create a USB disk and boot from it.

I couldn't repartition or overwrite a filesystem that the running OS had mounted, and I certainly couldn't destroy the FreeBSD partition I was currently booted from. A live USB (Void's live image) gave me a userland environment with zfs/zpool tooling available and nothing mounted on the target disk, so the rest of the procedure below was safe to run.

Since the base Void Linux installer image doesn't include ZFS support, I used hrmpf, a Void-based rescue system that ships with ZFS built in.


2. Create the pool on the freed partition

zpool create -f -o ashift=12 \
       -O acltype=posixacl \
       -O xattra=sa \
       -O dnodesize=auto \
       -O normalization=formD \
       -O mountpoint=none \
       -O canmount=off \
       vpool /dev/nvme0n1p5

This is the same zpool create I already knew from FreeBSD, with a few flags worth calling out for anyone used to a default FreeBSD pool:

  • -f forces creation, overriding the "partition looks like it belongs to another pool" safety check — needed because that partition previously held UFS/ZFS data from FreeBSD.
  • ashift=12 tells ZFS to align I/O to 4K sectors. NVMe drives report 512e or 4Kn depending on the model, and getting ashift wrong at creation time is permanent — there's no zpool command to fix it later, so I set it explicitly rather than trusting auto-detection.
  • acltype=posixacl enables POSIX ACLs, which is what Linux actually uses (FreeBSD understands NFSv4 ACLs natively; on Linux you want posixacl for getfacl/setfacl to behave).
  • xattra=sa (extended attributes stored as system attributes rather than as hidden directory entries) is a Linux-on-ZFS performance convention — it avoids the extra hidden-directory lookup overhead for xattrs, which SELinux/security-label-heavy Linux workloads rely on more heavily than BSD does.
  • dnodesize=auto lets ZFS grow dnodes to fit large xattrs efficiently instead of spilling into separate blocks — a natural pairing with xattra=sa.
  • normalization=formD normalizes filenames for consistent Unicode comparison — mostly a "good hygiene" default matching what most Linux-on-ZFS guides use.
  • mountpoint=none / canmount=off on the pool itself is the important one for the layout below: the pool is just a container. Nothing should ever mount at the pool root — only the datasets underneath it should be mountable. This mirrors the FreeBSD convention of never using the pool root as /.

3. Lay out datasets: a ROOT container, the actual root, and /home

zfs create -o mountpoint=none   vpool/ROOT
zfs create -o mountpoint=/      vpool/ROOT/void
zfs create -o mountpoint=/home  vpool/home

vpool/ROOT is a non-mounted parent dataset — its only job is to hold one or more "boot environment" style datasets underneath it (vpool/ROOT/void, and later maybe vpool/ROOT/void-backup or similar). This is deliberately the same pattern used by zfsbootmenu and by FreeBSD's bectl boot environments: keeping the actual root filesystem as a child of a container dataset means I can later clone vpool/ROOT/void into a snapshot-based boot environment, set a different one as the boot target, and roll back an entire OS upgrade by picking a different dataset at boot time — without touching /home, which lives in its own dataset precisely so it survives that kind of root-swap intact.


4. Tell the pool which dataset to boot, and how

zpool set bootfs=vpool/ROOT/void vpool

This sets the pool-level bootfs property, which zfsbootmenu (and FreeBSD's boot loader, for that matter) reads to know which dataset is the root filesystem to hand off to the kernel when nothing else is specified. Without it, a ZFS-aware boot menu has no default and I'd have to pick a dataset manually at every boot.

zfs set org.zfsbootmenu:commandline="rw init=/sbin/init quiet vconsole.keymap=be-latin1" vpool/ROOT/void

zfsbootmenu reads kernel command-line arguments straight out of this ZFS property, per boot-environment dataset. That's the point: each cloned root dataset can carry its own kernel command line. Here it says to mount root read-write, use /sbin/init as PID 1, suppress verbose boot messages, and in my specific case, load the Belgian keyboard layout early (vconsole.keymap) so console input works correctly before I even reach a login prompt — that last part is a convenience, not a requirement, which is why the notes call it optional.


5. Re-import the pool under /mnt and mount the new datasets

zpool export vpool
zpool import -N -R /mnt vpool
zfs mount vpool/ROOT/void
zfs mount vpool/home

Exporting and re-importing isn't strictly required right after creation, but it's cheap insurance — it confirms the pool is cleanly importable (the same check I'd want before trusting a pool at boot time) rather than assuming the in-memory state from zpool create is authoritative.

The interesting part is -N -R /mnt:
- -N imports the pool without automatically mounting anything, because canmount=off/mountpoint=none on the higher-level datasets means an automatic mount would try (and fail, or mount somewhere wrong) at the pool root.
- -R /mnt sets an altroot — every dataset's mountpoint is treated as relative to /mnt for the life of this import, without permanently rewriting the mountpoint property. This is exactly the same trick I'd use on FreeBSD to install into a pool from a live system: my real root filesystem was the live USB, and I didn't want the new pool's / competing with it.

With altroot set, explicitly mounting vpool/ROOT/void and vpool/home puts them at /mnt and /mnt/home respectively — ready to receive an installed system.


6. Mount the existing EFI System Partition

mkdir -p /mnt/boot/efi
mount /dev/nvme0n1p1 /mnt/boot/efi

The ESP (/dev/nvme0n1p1) already existed and already contained rEFInd — I wasn't creating a new EFI partition, I was reusing the one my system already boots from. Mounting it under /mnt/boot/efi means that once the base system is installed, the standard /boot/efi layout Linux tooling expects will correctly point at my real ESP, and anything I drop there (like the zfsbootmenu EFI binary later) lands where firmware can actually find it.


7. Install the base system into the mounted target

XBPS_ARCH=x86_64 xbps-install -S -R https://repo-default.voidlinux.org/current -r /mnt base-system zfs dracut

This is Void's equivalent of pkg -c /mnt install or a FreeBSD bsdinstall distribution extraction: -r /mnt tells xbps to treat /mnt as the install root, -S syncs repo metadata first, and -R <url> pins the repository explicitly (useful when the live environment's default repo config might not be set up yet). base-system is Void's minimal-but-complete base package set; zfs pulls in the ZFS userland tools and kernel module inside the new install (the live USB's tools don't help the installed system — it needs its own); dracut is Void's initramfs builder, which matters because getting from "kernel loaded" to "ZFS pool importable and root mounted" requires an initramfs that knows how to import a pool — that's set up in a later step.


8. Bind-mount the live environment into the new install and chroot

mount --make-rslave /dev
mount --rbind /dev /mnt/dev
mount --rbind /proc /mnt/proc
mount --rbind /sys /mnt/sys
cp /etc/resolv.conf /mnt/etc/resolv.conf
chroot /mnt /bin/bash

This is the Linux equivalent of what I'd do with mount -t devfs and friends before chroot-ing into a FreeBSD install target: the new root doesn't have a populated /dev, /proc, or /sys of its own yet, and tools like xbps-reconfigure or dracut need real device nodes and kernel interfaces to function correctly (dracut, for instance, inspects /sys and /proc to figure out what's needed in the initramfs).

  • mount --make-rslave /dev marks the live system's /dev mount as a slave, so that recursively bind-mounting it into /mnt/dev doesn't create a mount-propagation loop back out to the live environment.
  • --rbind recursively bind-mounts, carrying over any submounts (like /dev/pts).
  • Copying resolv.conf in is a one-line fix for the otherwise-common "chroot has no network resolution" problem — without it, DNS lookups inside the chroot silently fail even though the network itself works.

Once inside the chroot, /mnt is /, so from here on my commands act on the new install directly — this is the equivalent of finishing an install from within chroot /mnt on FreeBSD.

echo "<my host name>" > /etc/hostname

Sets the new system's hostname — nothing ZFS- or Linux-specific here, just something that has to happen before first boot or I'd get a generic/empty hostname.


9. Locale configuration

# /etc/locale.conf
LANG=en_US.UTF-8
LC_COLLATE=C
# /etc/default/libc-locales — uncomment:
en_US.UTF-8 UTF-8
xbps-reconfigure -f libc-locales

Void doesn't ship locales pre-generated (to keep the base install small), so unlike FreeBSD, where locale data is essentially always present, here I had to explicitly declare which locales are permitted (/etc/default/libc-locales), state which one the system actually uses (/etc/locale.conf), and then force glibc to regenerate its locale archive (xbps-reconfigure -f libc-locales). Skip that last step and the LANG setting points at a locale that doesn't exist yet, and most programs will silently fall back to C/POSIX.


10. Timezone

ln -sf /usr/share/zoneinfo/UTC /etc/localtime

Same idea as FreeBSD's tzsetup, done by hand: /etc/localtime is just a symlink into the zoneinfo database, so pointing it at the zone I actually want is the configuration step. I swapped UTC for the zone that's correct for me.


11. /etc/fstab

tmpfs       /tmp    tmpfs   defaults,nosuid,nodev   0       0
UUID=1F0C-2CC9 /boot/efi vfat   defaults                0       2

Two things worth noting for BSD folks:

  • ZFS datasets are deliberately not in fstab. ZFS tracks its own mountpoints as dataset properties (as I set with -o mountpoint=... earlier), and the initramfs/zfsbootmenu handle mounting root at boot time. Putting them in fstab would just create redundant, potentially conflicting mount instructions.
  • The ESP, however, is a plain vfat filesystem that ZFS knows nothing about, so it needs a normal fstab line like on any Linux or BSD system, referenced by UUID rather than device name so it keeps working even if NVMe device enumeration order ever changes. I got that UUID with:
blkid -s UUID -o value /dev/nvme0n1p1

12. Teach the initramfs how to boot from ZFS

echo 'add_dracutmodules+=" zfs "' > /etc/dracut.conf.d/zfs.conf
xbps-reconfigure -f linux$(uname -r | cut -d. -f 1,2)

This is the single most important non-ZFS-specific step in the whole procedure, and the one most likely to bite anyone who skips it: without the zfs dracut module included, the generated initramfs has no idea how to import vpool or mount vpool/ROOT/void, and the boot will drop to an emergency shell no matter how correctly the pool itself is configured. add_dracutmodules+=" zfs " tells dracut to bundle the ZFS kernel module and userland tools into the initramfs image; xbps-reconfigure -f linux<version> is what actually triggers dracut to rebuild that image for the currently-installed kernel package (the -f forces a rebuild even if xbps thinks nothing changed).


13. Install zfsbootmenu onto the ESP

mkdir -p /boot/efi/EFI/zbm
curl -L https://get.zfsbootmenu.org/efi -o /boot/efi/EFI/zbm/zfsbootmenu.EFI

zfsbootmenu ships as a self-contained EFI executable — there's no package to install inside the chroot; I just dropped the binary onto the ESP where UEFI firmware (or, here, rEFInd) can chainload it directly. At boot, this binary is what actually reads the bootfs and org.zfsbootmenu:commandline properties I set back in step 4, imports the pool, lets me pick a boot environment interactively (or just boots the default), and hands off to the kernel — it's functionally playing the same role FreeBSD's ZFS-aware boot loader plays for bectl boot environments.


14. Register the new OS with rEFInd

menuentry "VoidLinux" {
    icon \EFI\refind\icons\os_void.png
    volume "EFI"
    loader \EFI\zbm\zfsbootmenu.EFI
}

I added this block to /boot/efi/EFI/refind/REFIND.CONF. Since rEFInd was already my boot menu (as it was for FreeBSD), I wasn't replacing it — I was adding one more entry that points at the zfsbootmenu.EFI binary I'd just installed. This is what actually makes the setup dual-boot: rEFInd now shows "VoidLinux" as a menu option alongside whatever else it already finds, and picking it hands control to zfsbootmenu, which in turn hands control to the Void kernel.


15. Install NetworkManager before leaving the chroot

xbps-install NetworkManager

Void's base install intentionally leaves networking unconfigured (no DHCP-on-eth0 by default the way some distros do), and the chroot already has working DNS resolution thanks to the resolv.conf I copied in back in step 8, plus a route out through the live environment's network stack — so this was the simplest point to pull NetworkManager in, rather than having to solve "get online" from a completely bare freshly-booted system with no package manager access yet. I didn't configure a connection here — that's an interactive step (nmtui) I left for first boot — I just made sure the package (and the service definition that comes with it) was already in place before exiting.


16. Leave the chroot and clean up

exit
umount -R /mnt
zpool export vpool

exit leaves the chroot back into the live environment. umount -R /mnt recursively unmounts everything under /mnt — the ESP, /dev, /proc, /sys, and the ZFS datasets alike — cleanly, rather than leaving stale mounts. zpool export vpool releases the pool entirely, which matters because a pool that's still "imported" (even read-write, even unmounted) when the real boot process tries to import it again can cause import conflicts or force an unwanted re-import with a different altroot. Exporting guarantees the next import — the one zfsbootmenu performs at boot — starts from a clean, known state.

At this point I could reboot into the new install.


17. First boot: connect to the network, then update

nmtui

NetworkManager and its text UI, nmtui, were already installed from step 15, so this first boot was just about picking and connecting to a Wi-Fi network or bringing up a wired connection — the fastest path from "freshly booted, no active connection" to "online" without hand-editing config files.

xbps-install -Su

A full system update immediately after install. Void's live/install media is a snapshot from whenever it was built, so the freshly installed base system was very likely already behind the current repo — running this now, before layering on desktop packages, avoided installing outdated versions of things I was about to add.


18. Install the rest of my desktop/workstation packages

xbps-install -S acpi alacritty alsa-pipewire atril cups curl feh firefox font-firacode \
  git got hplip htop libreoffice lightdm maim meld neovim nerd-fonts noto-fonts-emoji \
  opendoas openntpd pekwm picom pipewire python3-psutil python3-xdg shellcheck unzip \
  wireplumber xclip xcowsay xinput xorg xorg-fonts xorg-input-drivers xorg-minimal \
  xorg-video-drivers zip zsh

This is the "make it a usable machine" step, no different in spirit from a FreeBSD pkg install list after a base install — just a single batch install of everything from the X11 stack (xorg*), a window manager (pekwm) and compositor (picom), display manager (lightdm), audio (pipewire, alsa-pipewire, wireplumber), a browser and office suite, and the usual toolbox (git, neovim, htop, zsh, etc.). opendoas is worth flagging for BSD users specifically: it's doas, the one I already knew from OpenBSD, packaged for Void, so I got the same minimal privilege-escalation tool instead of defaulting to sudo. openntpd is likewise the OpenBSD NTP daemon, ported and packaged — a familiar, small alternative to chronyd/ntpd on Linux.


Summary

The unfamiliar part of this migration, coming from FreeBSD, wasn't ZFS — it was the Linux boot chain around it: dracut builds an initramfs that must explicitly know about ZFS, zfsbootmenu reads ZFS dataset properties to decide what to boot instead of relying on a traditional bootloader config file, and rEFInd just chainloads into zfsbootmenu the same way it would chainload into any other OS's EFI loader. Once that chain was wired up correctly, the day-to-day ZFS administration — snapshots, boot environments via vpool/ROOT/* clones, zfs set, zpool status — worked exactly the way I already expected it to.

It's amazing how fast VoidLinux boots, wifi is very fast and suspend works perfectly. Compared to FreeBSD I had just before the first impression is very very good.

This is surely not the last blog I'm doing about VoidLinux. I miss the Boot Environment of FreeBSD, but I'll comeback on this later. We will see how VoidLinux evolve after several updates/upgrades.



👍 0, 👎 0
displayed: 41



What is the second letter of the word Python?