A research-grade attempt to fix the kernel-vs-userspace namespace split via gunion (block-level RAM overlay) + reboot -r reroot. The simpler symlink trick (cdroot /boot/firmware → /sysroot/boot/firmware) verified working on real hardware, which addresses the immediate firmware-loading problem without an architectural rewrite. This experiment is preserved as a reference for the alternative architecture; pursue if the symlink approach starts hitting limitations or if a future feature requires kernel and userspace to genuinely share a root.
cdroot/boot/firmware → /sysroot/boot/firmware) was implemented and verified working on real hardware in freebsd-launchd commit 51e1b80 (Lenovo iwlwifi-8260: net.wlan.devices populates, wlan0 created, DMC firmware loads). Backported to both freebsd-livecd-unionfs and freebsd-livecd-gunion. This document is preserved as the alternative-architecture reference; build-out is no longer urgent.reboot -r would actually move the kernel's root to the gunion-backed UFS, eliminating the namespace split. The symlink trick achieves the same end goal (kernel can resolve /boot/firmware/foo) without changing the boot architecture — just by exploiting that kernel namei follows symlinks across mount points./boot/firmware/. Result: when iwlwifi/i915kms etc. ask for firmware via kernel-context vn_open, the file is invisible. Wifi doesn't come up; GPU acceleration is degraded.reboot -r after early init.sh setup, so the kernel actually re-mounts root from the gunion-backed UFS. Now the kernel and userspace see the same root. Kernel-context firmware loading works.kern_reroot() calls vfs_unmountall() before re-mounting root. cd9660 has the rootfs.uzip vnode held busy by mdconfig — unmount fails. Either the system gets stuck half-rerooted or, worse, panics in vfs_mountroot() due to inconsistent root-vnode state.loader.conf mfsBSD-style, or copy rootfs.uzip into a malloc-md in init.sh and detach the vnode-md before reroot.Build a livecd variant where the kernel's root mount IS the writable rootfs (via gunion + reroot), so kernel-context filesystem operations (firmware loading, kld auto-search, etc.) see the same view as userspace. Specifically: iwlwifi/i915kms/amdgpu firmware loads correctly without needing a 1GB cdroot bloat or a `/boot/firmware → /sysroot/boot/firmware` symlink workaround.
freebsd-livecd-gunion in favor of it; until then both coexist.kern_reroot(), mdconfig(8), gunion(8), loader(8) features. If any of those need bug fixes to make this work, file upstream tickets, don't carry patches.All three of our existing livecd projects share the same architectural shape, and it has the same kernel-firmware-loading bug.
| Project | Overlay | Handoff | Firmware bug? |
|---|---|---|---|
| freebsd-launchd | unionfs | chroot via Option D shebang on /init.sh | Yes |
| freebsd-livecd-unionfs | unionfs | chroot via init_chroot kenv | Yes |
| freebsd-livecd-gunion | gunion | chroot via init_chroot kenv | Yes |
The bug: kernel-context vn_open in sys/kern/subr_firmware.c's try_binary_file() resolves /boot/firmware/<name> against the kernel's root namespace, which on these livecds is the cd9660 mount. Userspace processes (post-chroot) see /boot/firmware/ in the unionfs/gunion view, which has the pkg-installed firmware files. The kernel's view is the cd9660 root, which contains only the bootloader-stage firmware (a small subset). Result: iwlwifi-8000C-36.ucode exists for userspace but not for kernel; wifi doesn't come up.
Verified empirically on a Lenovo laptop running freebsd-launchd's continuous-release ISO: file present (2.4MB) via ls /boot/firmware/iwlwifi-8000C-36.ucode; iwlwifi attaches at the bus level but reports "could not load binary firmware /boot/firmware/iwlwifi-8000C-36.ucode either" followed by "File size way too small!" — the kernel's read returned zero bytes despite the file being right there from userspace.
The fix is conceptually simple: make the kernel's root mount the same thing userspace sees. chroot can't do this (chroot only changes the userspace process's view, kernel root stays put). kern_reroot() can — it actually unmounts the kernel's root and re-mounts a new one. After reroot, kernel namei resolves paths against the new root.
Three architectural improvements over the chroot variants:
The naive version of this — set up gunion + reroot without preloading the rootfs.uzip — fails because:
1. /init.sh runs in cd9660 root
2. mdconfig -a -t vnode -f /rootfs.uzip ← holds /rootfs.uzip vnode busy
/dev/md0 is now backed by the FILE on cd9660
3. gunion create /dev/md0.uzip ← /dev/gunion0
4. reboot -r with vfs.root.mountfrom=ufs:/dev/gunion0
5. kern_reroot() → vfs_unmountall()
6. unmount cd9660 → EBUSY (md is holding /rootfs.uzip vnode)
7. cd9660 stays mounted; vfs_mountroot() runs anyway with old root still half-mounted
8. Inconsistent rootvnode state → panic in vfs_mountroot() OR
half-rerooted system in unstable state
The exact failure mode (panic vs hang vs error) depends on FreeBSD's vfs_mountroot implementation details I haven't traced exactly. Either way it's bad enough that "don't try this naively" is the correct stance.
Add to cdroot's /boot/loader.conf:
mfs_load="YES"
mfs_name="/rootfs.uzip"
mfs_type="mfs_root"
The FreeBSD loader reads /rootfs.uzip from the cd9660 boot media at loader time, before kernel boot. Loads the entire file into RAM. Kernel sees md0 already configured at boot — backed by RAM, not by a vnode on cd9660.
After loader hands off to kernel: cd9660 mounts as initial root (for kernel binary access), but nothing on cd9660 is held busy by a vnode-backed mdconfig (because md0 came from the loader, not from a runtime mdconfig -t vnode -f /rootfs.uzip).
cd9660 is the rescue environment. It already contains /rescue/sh, /sbin/gunion, /sbin/mdconfig, /sbin/reboot, /sbin/kenv — everything /init.sh needs to run gunion-create and reroot. We do NOT build or load a separate "rescue.img" memdisk. The cd9660 IS the rescue env.
Flow:
# /init.sh on cd9660 (Option D shebang as PID 1):
# /dev/md0.uzip already exists, RAM-backed (loader preloaded rootfs.uzip)
# Layer gunion on top for writable upper
gunion create /dev/md0.uzip # /dev/gunion0
# Hand kernel the new root and trigger reroot
kenv vfs.root.mountfrom="ufs:/dev/gunion0"
exec /sbin/reboot -r # process is replaced; kernel reroots
After reboot -r: kern_reroot() calls vfs_unmountall() which unmounts cd9660 cleanly (no busy vnodes). vfs_mountroot() mounts /dev/gunion0 as the new kernel root. /sbin/init runs from the gunion-backed UFS. From this point the kernel root and userspace root are the same view; firmware loading works natively.
Pros: standard FreeBSD pattern (mfsBSD has used it for decades). Loader-handled, simple init.sh, no separate rescue.img to build or maintain.
Cons: loader has to slurp ~400MB into RAM at boot — slower boot by 5-15 seconds depending on read speed (USB stick, optical, network). Loader has size limits in some BIOSes/UEFI firmwares; needs verification on the target hardware.
Same end state, done in init.sh instead of loader:
# Inside /init.sh, before any other md/gunion setup:
# 1. Create a malloc-backed memdisk sized for the rootfs
mdconfig -a -t malloc -s 400m -u 1 # /dev/md1, RAM-backed, empty
# 2. Copy rootfs.uzip into it
dd if=/rootfs.uzip of=/dev/md1 bs=1m
# 3. Detach the vnode-backed md (if we created one)
mdconfig -d -u 0 2>/dev/null # no-op if we never created md0
# 4. /dev/md1.uzip is now RAM-backed; cd9660 is no longer needed
gunion create /dev/md1.uzip # /dev/gunion0
# 5. Set reroot target and trigger
kenv vfs.root.mountfrom="ufs:/dev/gunion0"
reboot -r
Pros: doesn't depend on loader behavior. Easier to debug (it's a shell script, can be poked at interactively).
Cons: dd-copy of ~400MB takes 1-3 seconds on RAM. cd9660 stays mounted for the duration of the copy. Init.sh becomes more complex and order-sensitive.
Try Approach A first — it's the canonical pattern, less code, and if the loader supports the file size we get a clean boot. Fall back to B if loader-preload fails on real hardware.
The mfsBSD-canonical alternative: build a tiny rescue.img (~30MB UFS with just init + tools) and have the loader preload it alongside rootfs.uzip. Kernel boots from rescue.img (RAM-backed memdisk); rescue's init.sh sets up gunion over the second RAM-backed memdisk; reroots to gunion-backed UFS. cd9660 may not even be mounted at runtime.
# cdroot/boot/loader.conf:
mfs_load="YES"
mfs_name="/rescue.img"
mfs_type="mfs_root"
# Second memdisk (rootfs payload) loaded as a separate kernel preload:
rootfs_load="YES"
rootfs_name="/rootfs.uzip"
rootfs_type="md_image"
# /init.sh inside rescue.img:
gunion create /dev/md1.uzip # md1 is the rootfs payload
kenv vfs.root.mountfrom="ufs:/dev/gunion0"
exec /sbin/reboot -r
| Scenario | Why miniroot helps | Applies to our livecd? |
|---|---|---|
| PXE / netboot | kernel + rescue.img are small files PXE-servable in seconds. The big payload can come later over NFS / HTTP / wherever, decoupled from the boot path. | No — we boot from cd9660 / USB optical media. |
| Eject-boot-media-mid-install | Once the rescue is in RAM, the user can yank the USB stick / eject the disc; install continues from a target disk. Useful for "USB stick → install to internal NVMe, eject USB." | No — our livecd assumes media stays attached. We don't currently have an installer. |
| Multiple payload images | Rescue can pick at boot time which rootfs to load (minimal / full / desktop / server). One disc, several flavors. | No — we ship a single rootfs.uzip. Could become useful if we ever ship "freebsd-launchd minimal" + "freebsd-launchd full Plasma" on the same image. |
| Diskless workstations | Rescue is the fixed in-RAM boot env; payload comes from a network server. Common in fleet deployments. | No — single-machine livecd, no fleet scenario. |
| Offline rescue when rootfs is broken | If the big rootfs fails to load (corrupt download, hardware failure mid-install), the rescue is guaranteed-working in RAM and the user gets a shell. | Partial — our cd9660 already serves this role. If /rootfs.uzip is corrupt, the cd9660 is still mounted with all its tools and the user is in a working shell. |
| Smaller loader I/O footprint | Loader handles only ~30MB rescue, not 400MB+ rootfs. Faster initial boot on slow media. | Marginal — Approach A's loader-preload of the full rootfs costs 5-15s extra. Acceptable tradeoff for build simplicity. |
None of the scenarios where miniroot earns its complexity apply to our livecd today:
/rescue/sh + tools are reachable any time the cd9660 is mounted, which is "until reroot." Anything that fails before reroot lands the user in cd9660's rescue shell.Cost of adopting it anyway: an entire second build pipeline (rescue.img generation, separate Makefile target, separate set of binaries to keep version-aligned with the rootfs), debug surface (two stages of init), and the mfsBSD-canonical pattern's quirks (loader configuration of multiple memdisks, kernel preload type for the secondary md, etc.). All for "the same outcome we already get from cd9660."
Verdict: keep this pattern documented as the right choice for mfsBSD-shape use cases (PXE, ejectable media, multi-payload, diskless rescue). If freebsd-launchd ever grows into one of those use cases — most plausibly an installer that wants to eject the USB stick after install completes — this is the right architecture to revisit. For the current livecd, Approach A (cd9660 as rescue + loader-preloaded rootfs) is correct and simpler.
| Overlay | Layer | Can be reroot target? | RAM cost | Notes |
|---|---|---|---|---|
| unionfs | vnode-stacking | No | low (writes only) | Can't be a single-spec root mount; can't be mounted over /. Per unionfs(8): "meaningless to mount unionfs over a root mount point." |
| gunion | GEOM block-device | Yes | low-medium | Creates a writable virtual block device backed by RO underlying + RAM upper. UFS on top mounts cleanly as root via standard vfs.root.mountfrom=ufs:/dev/gunion0. |
| tmpfs root | tmpfs (RAM) | Yes | HIGH (entire rootfs in RAM uncompressed, ~1-2GB) | Works but burns RAM. mfsBSD does this for tiny rootfs; ours is too big. |
| RO UFS + selective tmpfs subdirs | UFS + tmpfs | Yes (UFS as root) | low (only writable subdirs in RAM) | The mfsBSD/Path-A pattern. Simpler than gunion. Trade: only writable where we explicitly mount tmpfs. |
gunion gives us the cleanest "writable root" semantic — no thinking about which subdirs are writable, no skeleton-population dance, no per-iface tmpfs-mount choreography. Everything is writable; writes go to RAM via gunion's upper layer; reads come from the RO uzip via gunion's lower.
Trade-off vs. RO UFS + tmpfs subdirs: gunion has a known performance overhead (block-level overlay isn't free) and write coalescing/syncing semantics that have caused issues historically. We moved off gunion to unionfs in freebsd-livecd-unionfs partly because of these.
For this experimental architecture, gunion is the right pick because it's the only writable-everywhere overlay that can be a reroot target. The performance trade-off is worth re-evaluating now that we have the rootfs entirely in RAM (less I/O contention than gunion-over-cd9660-uzip).
github.com/pkgdemon/freebsd-livecd-gunion-reroot repo. Fork from freebsd-livecd-gunion as a starting point so we inherit the working build.sh + cdroot setup + boot-test infrastructure.build.sh: copy rootfs.uzip from $WORK/cdroot/rootfs.uzip to also be readable by loader (it should already be — loader reads from the cd9660 root). Verify path.boot/loader.conf to add:
mfs_load="YES"
mfs_name="/rootfs.uzip"
mfs_type="mfs_root"
/dev/md0 at boot (memdisk-backed). Confirm /dev/md0.uzip auto-created by geom_uzip. Confirm cd9660 has no busy vnodes./init.sh:
#!/rescue/sh
# /dev/md0.uzip already exists (loader preloaded)
# Layer gunion on top for writable upper
gunion create /dev/md0.uzip # /dev/gunion0
gunion commit /dev/gunion0 ON # ensure writable, RAM upper
# Hand kernel the new root and reroot
kenv vfs.root.mountfrom="ufs:/dev/gunion0"
exec /sbin/reboot -r
/sbin/init runs from gunion-backed UFS, normal boot continues.kldstat, mount, ifconfig all behave as expected.kldstat | grep iwlwifi shows loaded; dmesg | grep iwlwifi shows successful firmware load (no "File size way too small!"); ifconfig | grep wlan shows wlan0; sysctl net.wlan.devices non-empty.dmesg | grep DMC).mount shows /dev/gunion0 as /, not cd9660.Comparison done before reroot was built. The symlink trick succeeded on real hardware:
| Axis | Symlink (shipped) | gunion + reroot (this plan) |
|---|---|---|
| Code complexity | 1 line in build.sh | Phase 0-4 work (~3-5 days) |
| RAM floor | ~200MB (unchanged) | ~600-800MB |
| Boot time | unchanged | +10-30s loader preload |
| Risk of breaking boot path | essentially zero | real (half-rerooted state, panic on busy cd9660) |
| Solves firmware loading | yes (verified) | yes (would have, if built) |
| Solves future kernel-context vfs ops | only paths under /boot/firmware | everything (kernel root = userspace root) |
The symlink wins on every axis except "future-proofing for unrelated kernel-context vfs operations" — which we don't currently need, and which hasn't surfaced as a real problem after the firmware case was fixed. Keep this experiment on the shelf in case that future need materializes.
reboot -r and the new init's first log line is a black-box failure. Mitigation: aggressive serial-console logging, test on real hardware before declaring success in CI.
rootfs.uzip at ~400MB exceeds the limit, loader fails to preload and we fall back to in-init.sh memdisk copy (Approach B). Need to test on multiple firmware vendors.
kern_reroot() partially succeeds but vfs_mountroot() fails, the system can land in a state where neither the old nor new root is fully mounted. Behavior is undefined; might panic. Mitigation: extensive boot testing; have a recovery serial console available.
| Axis | freebsd-launchd (today) | freebsd-livecd-unionfs | freebsd-livecd-gunion | freebsd-livecd-gunion-reroot (this) |
|---|---|---|---|---|
| Overlay | unionfs | unionfs | gunion | gunion |
| Handoff | chroot (Option D) | chroot (init_chroot) | chroot (init_chroot) | reboot -r |
| Kernel root after boot | cd9660 | cd9660 | cd9660 | gunion-backed UFS |
| Kernel firmware loading | broken (workaround: symlink) | broken | broken | works natively |
| Rootfs.uzip location at runtime | vnode on cd9660 | vnode on cd9660 | vnode on cd9660 | RAM (loader-preloaded) |
| RAM floor | ~200MB | ~200MB | ~200MB | ~600-800MB |
| Writable surface | everywhere (unionfs) | everywhere (unionfs) | everywhere (gunion) | everywhere (gunion) |
| Boot time | fast | fast | fast | +10-30s (loader preload) |
| cd9660 stays mounted post-boot | yes (kernel root) | yes | yes | no (unmounted by reroot) |
| Architectural risk | known-good | known-good | known-good | experimental |
vfs.root.mountfrom=ufs:/dev/gunion0 work as expected? gunion presents as a normal block device with a UFS filesystem. Should mount via standard root-mount machinery. Verify in Phase 2.
reboot -r from a shell-as-PID-1 (Option D pattern) work? Our existing init.sh patterns set things up and exec the real init. reboot -r is a syscall (reboot(2) with RB_REROOT), should be callable from any process. But "shell as PID 1 calling reboot -r" is unusual; needs verification.
reboot -r will be killed mid-execution. The kernel re-mounts root and execs init_path in the new root. The shell script's "after reboot -r" code never runs. Need to make sure reboot -r is the LAST thing in init.sh.
reboot -r the same way. But verifying is post-Phase-5 work, not in scope for this plan.
kern_reroot: sys/kern/kern_shutdown.c in the FreeBSD source tree.vfs_mountroot: sys/kern/vfs_mountroot.c.gunion(8): man 8 gunion.mdconfig(8): man 8 mdconfig.reboot(8) (specifically the -r flag): man 8 reboot.