FreeBSD Boot Stages: What Actually Runs Before Your Kernel
FreeBSD's boot process has three discrete stages before the kernel takes over, and understanding them matters when you need to rescue a broken system.
Stage 1 is the Master Boot Record or UEFI boot entry - 512 bytes or a small EFI application that does nothing except load stage 2. On BIOS systems, this is /boot/boot0 (the boot manager) or /boot/boot1. On UEFI systems, the EFI application lives at /boot/efi/EFI/freebsd/loader.efi.
Stage 2 is /boot/boot2 on BIOS systems - a small program that understands UFS enough to find and execute stage 3. On UEFI, the EFI loader handles this directly. Stage 2 on BIOS is limited to 7.5KB, which is why it can only read UFS and not ZFS.
Stage 3 is /boot/loader - the actual interactive boot loader written in Forth. This is where loader.conf is read, modules are loaded, and kernel parameters are set. On a ZFS-only system, the EFI loader or a special BIOS-compatible ZFS boot block handles getting to this stage. You can interrupt at stage 3 by pressing any key at the graphical boot menu countdown.
# Check which boot files are installed on a disk
ls -la /boot/boot* /boot/loader
# On a UEFI system, verify the EFI loader
ls -la /boot/efi/EFI/freebsd/
# Check if you're running UEFI or BIOS from a live system
sysctl machdep.bootmethod
Installing and Reinstalling Boot Blocks
Corrupted boot blocks are the most common reason sysadmins land in single-user mode. FreeBSD provides gpart and bootcode subcommands to reinstall them without a full reinstall.
For a BIOS MBR system with a single disk ada0, use gpart to write both the MBR and the partition boot code. The MBR goes to the disk, the partition boot code goes to the freebsd-boot partition. On a standard install, that partition is typically ada0p1 with type freebsd-boot.
For UEFI systems, the EFI partition is mounted at /boot/efi and the loader.efi binary is copied there. If you've somehow nuked the EFI entry from nvram, use efibootmgr to recreate it.
# BIOS MBR system - reinstall boot blocks
gpart bootcode -b /boot/pmbr -p /boot/gptboot -i 1 ada0
# For a UFS slice-based (MBR) layout (older style)
gpart bootcode -b /boot/mbr ada0
gpart set -a active -i 1 ada0
# UEFI system - reinstall the loader
mount -t msdosfs /dev/ada0p1 /boot/efi
cp /boot/loader.efi /boot/efi/EFI/freebsd/loader.efi
# Recreate the NVRAM boot entry if lost
efibootmgr --create --label "FreeBSD" --loader /boot/efi/EFI/freebsd/loader.efi
# Verify gpart layout before touching anything
gpart show ada0
loader.conf: The Directives That Matter
/boot/loader.conf is an sh-syntax key=value file read by the loader before the kernel starts. Changes here take effect on next boot without rebuilding anything. The file doesn't exist by default on a minimal install - you create it. /boot/defaults/loader.conf contains every available directive with defaults; treat it as read-only documentation.
The most operationally important directives fall into four categories: kernel selection, module loading, kernel tunables, and console configuration.
Kernel selection lets you boot an alternate kernel. If you're testing a custom kernel build named MYKERNEL, set kernel to point to it. On failure you can revert at the loader prompt interactively.
Module loading with load_module syntax is how you enable drivers that aren't compiled into GENERIC. ZFS, Infiniband, and hardware monitoring modules all go here. The loader processes these before the kernel initializes devices, which matters for root-on-ZFS configurations.
Kernel tunables set via loader.conf override the compiled defaults and can also be set at runtime with sysctl, but some tunables are read-only after boot - only loader.conf gets you early enough. The vm.kmem_size and kern.maxfiles tunables fall into this category on some configurations.
Console configuration matters most on headless servers and serial console setups. Setting comconsole_speed and boot_serial here means your console works before rc.conf is even parsed.
# /boot/loader.conf - production server example
# Kernel
kernel="kernel"
# ZFS
zfs_load="YES"
# Enable AHCI instead of legacy ATA
ahci_load="YES"
# NVMe support (not in GENERIC on older releases)
nvme_load="YES"
# Thermal sensors
coretemp_load="YES"
# Hardware monitoring
smbus_load="YES"
bmcr_load="YES"
# Kernel tunables set early
kern.maxfiles=500000
kern.maxfilesperproc=100000
net.inet.tcp.syncookies=1
vm.kmem_size_max="12G"
# Serial console for IPMI/iDRAC access
boot_serial="YES"
comconsole_speed="115200"
console="comconsole,vidconsole"
# Verbose boot - useful to leave on for production
# Comment out after confirming stability
#boot_verbose="YES"
ZFS Boot Environments and the Loader
Root-on-ZFS changes the boot story significantly. The loader needs to know which ZFS pool to import and which dataset to use as root. This is handled through the zfs_be_root and vfs.root.mountfrom loader variables, plus the bectl(8) tool for managing boot environments.
Boot environments are ZFS snapshots promoted to clones under a specific dataset hierarchy. By convention, FreeBSD places boot environments under zroot/ROOT. Each named boot environment (default, 14.1-RELEASE, pre-upgrade-snapshot) is a separate dataset that can be booted independently. The loader scans this hierarchy and presents them in the boot menu.
bectl is your primary tool for managing this from a running system. Before any major change - OS upgrade, pkg upgrade of the base system, kernel rebuild - create a boot environment. If the system fails to boot, select the previous environment from the loader menu without touching a single config file.
In our testing on a ZFS-on-root system with FreeBSD 14.1, switching boot environments at the loader took under 3 seconds and required no additional configuration. The pool import happens in the loader itself before the kernel even starts.
The loader variable vfs.root.mountfrom is set automatically when you select a boot environment, but you can override it manually at the loader prompt for emergency recovery. The format is zfs:poolname/ROOT/bename.
# List current boot environments
bectl list
# Create a BE before a risky change
bectl create pre-pkg-upgrade
# Activate a BE for next boot (does not reboot)
bectl activate pre-pkg-upgrade
# At the loader prompt (interactive recovery)
# List available ZFS boot environments
lsbe
# Boot a specific environment manually at loader prompt
set vfs.root.mountfrom="zfs:zroot/ROOT/pre-pkg-upgrade"
boot
# After booting, confirm active BE
bectl list
# Destroy a BE you no longer need
bectl destroy old-environment-name
# Check ZFS pool status from loader before booting
# (at the loader interactive prompt)
zpool status
The Loader Forth Environment: Interactive Commands
Pressing any key during the graphical countdown drops you into a menu. Pressing 3 or typing 'escape' at the right moment gives you the loader prompt, which is a Forth interpreter. This is not a shell - it's stack-based Forth, but you don't need to understand Forth to use the commands that matter.
The most useful loader commands in a recovery scenario are: boot, reboot, lsmod, load, unload, set, show, and ls. The show command dumps all current loader variables, which is essential for understanding what the kernel will see at startup.
If a kernel module is causing a panic, unload it at the loader prompt before booting. If you need to pass a specific tunable that you forgot in loader.conf, set it interactively. These changes are not persistent - they apply only to the current boot.
For diagnosing boot failures on a remote system where you have IPMI or serial console access, the loader is often the fastest path to getting back online without a reinstall. We've recovered systems with corrupted /etc/rc.conf by booting to single user, which you can force by setting boot_single at the loader prompt.
# At the loader> prompt
# Show all set variables
show
# List loaded modules
lsmod
# Load a module manually
load /boot/kernel/zfs.ko
# Unload a module that's causing a panic
unload splash_bmp
# Set a variable for this boot only
set kern.maxfiles=200000
# Force single-user mode
set boot_single=1
boot
# Boot verbose (equivalent to -v flag)
set boot_verbose=1
boot
# Boot to safe mode (equivalent to -s -v flags)
boot -s -v
# List files in /boot
ls /boot
# Load a different kernel
load /boot/kernel.old/kernel
boot
Kernel Module Preloading: Order and Pitfalls
Module load order in loader.conf matters when modules have dependencies. The loader resolves dependencies automatically for modules that declare them via MODULE_DEPEND(), but not all out-of-tree or older modules do this correctly.
The naming convention for module load directives is the module filename without .ko, followed by _load="YES". For a module at /boot/kernel/carp.ko, the directive is carp_load="YES". For modules in subdirectories, use the module name as declared in the module itself, not the path.
When loading kernel modules for filesystems that might be used as root (ZFS, UFS), the loader must load them before handing off to the kernel. This is automatic for root-on-ZFS when zfs_load="YES" is set. For encrypted disks using geli, the geom_eli module must load before the kernel tries to mount root.
A common pitfall is loading conflicting modules. The drm/DRM situation on FreeBSD is a good example: drm2 and newer drm-kmod packages provide different modules, and loading the wrong one can cause a hang at boot. Always check which package version you have installed before adding GPU-related modules to loader.conf.
To verify that a module loaded correctly after boot, check kldstat. The Size column shows the module's memory footprint, and the Name column shows the exact module name as the kernel registered it.
# List all loaded modules after boot
kldstat
# Verbose: show dependencies
kldstat -v
# Load a module at runtime to test before adding to loader.conf
kldload carp
kldstat | grep carp
# Unload at runtime
kldunload carp
# Find the correct module name for a loader.conf directive
# The name in kldstat = the string before .ko = the _load directive name
# Example: if kldstat shows 'carp.ko', use carp_load="YES"
# For GELI encrypted root disk (loader.conf)
# geom_eli_load="YES"
# geli_ada0p3_keyfile0_load="YES"
# geli_ada0p3_keyfile0_type="ada0p3:geli_keyfile0"
# geli_ada0p3_keyfile0_name="/boot/keys/ada0p3.key"
UEFI Secure Boot and loader.efi
FreeBSD 13.1+ shipped with Secure Boot support via a signed loader.efi. As of FreeBSD 14.x, the signed loader is included in the distribution and enrolled in Microsoft's third-party certificate database, which means it works out of the box on most hardware that ships with Secure Boot enabled.
Verifying your EFI setup is straightforward. The loader.efi file at /boot/efi/EFI/freebsd/loader.efi should match the version installed by the freebsd-update or the source build. After a freebsd-update run, the EFI partition needs manual synchronization - freebsd-update does not automatically copy the new loader to the EFI partition.
This is a genuine gotcha. We've seen systems where freebsd-update successfully updated the kernel and base system but left an older loader.efi on the EFI partition. The system continued to boot because the loader was backward-compatible, but it's a time bomb on major version upgrades.
If your firmware enforces Secure Boot and you're building a custom kernel or loader, you'll need your own signing key enrolled via the firmware's MOK (Machine Owner Key) interface, or disable Secure Boot. There is no middle path on locked-down enterprise hardware.
# After freebsd-update, sync the EFI loader
mount -t msdosfs /dev/ada0p1 /boot/efi
cp /boot/loader.efi /boot/efi/EFI/freebsd/loader.efi
umount /boot/efi
# Verify EFI loader version (check file timestamp vs /boot/loader)
ls -la /boot/loader /boot/efi/EFI/freebsd/loader.efi
# Check Secure Boot state
dmesg | grep -i secure
sysctl machdep.bootmethod
# List EFI boot entries
efibootmgr
# If FreeBSD entry is missing after hardware change
efibootmgr --create \
--label "FreeBSD" \
--loader /boot/efi/EFI/freebsd/loader.efi \
--activate
Tuning Boot Performance and Console Output
On production servers, boot time matters for failover scenarios. FreeBSD's boot is already faster than most Linux distributions with full systemd initialization, but there are specific loader.conf and rc.conf options that affect time-to-network.
The boot_mute variable silences the loader output, which shaves a small amount of time on slow serial consoles but removes useful diagnostic output. We recommend keeping it off on servers with IPMI access since you want that output in the IPMI logs.
The autoboot_delay variable controls how long the loader waits before booting automatically. The default is 10 seconds. On production servers that should never need interactive loader intervention, set this to 3 or even 1. On development machines or systems you're actively tuning, keep it at 10.
kern.boot.verbose sysctl shows you what happened during boot after the fact. Pair this with dmesg to diagnose slow initialization. On systems with many USB devices or SCSI bus scans, the hardware enumeration during boot can add several seconds. Setting hw.usb.no_boot_wait="1" in loader.conf skips the USB device settle time.
For DevOps shops automating server provisioning, tools like taskbotshub.ai can handle post-boot configuration verification - checking that expected modules loaded, tunables are set correctly, and boot environment names match your naming convention - without writing custom shell scripts for each check.
# loader.conf performance tuning
autoboot_delay="3"
# Skip USB settle time on headless servers with no USB boot devices
hw.usb.no_boot_wait="1"
hw.usb.no_boot_wait_if_usb_power="1"
# Reduce verbose output in loader (not recommended for production)
# boot_mute="YES"
# Check actual boot time breakdown after boot
dmesg | head -5
# First timestamp shows when kernel started
# Subtract from login time to see total boot duration
# Check what tunables were set by loader
sysctl -a | grep kern.boot
# Verify all expected modules are loaded
kldstat | awk 'NR>1 {print $NF}' | sort
Recovering from a Failed Boot: The Toolkit
When a system won't boot past the loader or fails during kernel initialization, you have three paths: the interactive loader prompt, single-user mode, and booting from external media.
From the interactive loader prompt, you can diagnose module conflicts, try alternate kernels, and force safe mode. FreeBSD maintains /boot/kernel.old from the previous kernel installation - this is your first fallback. Type 'unload' then 'load /boot/kernel.old/kernel' then 'boot' at the loader prompt.
Single-user mode drops you to a root shell with minimal services. From there you can mount filesystems read-write, edit loader.conf, fix /etc/rc.conf errors, or roll back a broken boot environment with bectl. Single user mode gives you /bin and /sbin from the system - no network, no additional services.
If the kernel itself is panicking before you get to single-user, use a FreeBSD USB installer for the same major version. The installer's 'Shell' option gives you a live environment with access to your disk. Mount your root partition and fix the issue. For ZFS root, the live environment can import your pool directly with 'zpool import -R /mnt zroot'.
Document your recovery procedures and keep them in your runbook. For teams managing many FreeBSD hosts, standardizing on ZFS boot environments and scripting pre-change BE creation is the single most effective reliability improvement available - it turns a potentially hour-long recovery into a two-minute reboot.
# From single-user mode - mount filesystems read-write
mount -u /
mount -a
# Fix loader.conf if it's causing boot failures
vie /boot/loader.conf
# Roll back a broken boot environment
bectl list
bectl activate working-be-name
reboot
# From live USB - recover ZFS root system
zpool import -R /mnt zroot
mount -t devfs devfs /mnt/dev
chroot /mnt /bin/sh
# Inside chroot, fix the issue
vim /boot/loader.conf
vim /etc/rc.conf
# If kernel.old exists, use it as fallback from loader prompt
# (Type this at loader> prompt)
# unload
# load /boot/kernel.old/kernel
# boot
# After recovery, rebuild kernel.old from current working kernel
cp -R /boot/kernel /boot/kernel.old