How GEOM Classes Stack
Every GEOM object is either a consumer (reads from a provider below it) or a provider (exports a block device above it). A single disk like `ada0` is both: it is a provider to the partition class (geom part), and the partition slice `ada0p2` becomes a provider to whatever class sits above it - geli, gmirror, or UFS directly.
This stacking is not abstract. When you run `geli attach` on a partition, GEOM inserts an encryption class between the raw partition and the file system. The file system never knows it is talking to encrypted storage. When you then layer gmirror on top of two geli providers, the mirror class sees two block devices and stripes writes symmetrically. You can inspect the full topology at any time with `geom -t`, which prints the entire class graph as a tree.
Kernel modules implement each class. The base system ships with the most important ones - `geom_mirror`, `geom_stripe`, `geom_raid`, `geom_eli` (geli), `geom_label`, and `geom_part`. Load them on demand with `kldload geom_mirror` or add them permanently to `/boot/loader.conf`.
# View the full GEOM provider tree
geom -t
# List all disks GEOM knows about
geom disk list
# Check which GEOM modules are loaded
kldstat | grep geom
# Load gmirror without rebooting
kldload geom_mirror
Partitioning with GPT and geom part
On modern FreeBSD systems, GPT is the correct partition table format. MBR is relevant only when the machine is old enough to lack UEFI. Use `gpart` for all partition operations - it is the userland interface to the `geom_part` class.
A typical server disk layout for a data disk (not the boot disk) puts a small freebsd-swap partition first, then a freebsd-ufs or freebsd-zfs partition for the remainder. On our test server running FreeBSD 14.1 with a 1 TB NVMe drive (`nvd0`), we set it up as shown below. The `-a 1m` flag aligns partitions to 1 MiB boundaries, which matters for NVMe and SSD performance.
After partitioning, `gpart show nvd0` confirms the layout. Pay attention to the `Attributes` column: if a partition has `bootonce` or `active` set, GEOM will act on those flags during the next boot. For data disks you do not want those flags set - leave attributes empty.
# Initialize GPT on a new disk
gpart create -s gpt nvd0
# Add an 8 GB swap partition
gpart add -t freebsd-swap -s 8G -a 1m nvd0
# Add the rest as a UFS data partition
gpart add -t freebsd-ufs -a 1m nvd0
# Verify layout
gpart show nvd0
# Sample output:
#=> 40 1953525088 nvd0 GPT (931G)
# 40 2008 - free - (1.0M)
# 2048 16777216 1 freebsd-swap (8.0G)
# 16779264 1936745864 2 freebsd-ufs (923G)
Disk Labeling with glabel
Device names like `ada0` and `nvd0` are not stable across reboots if you add or remove drives or change PCI slot assignments. GEOM labels solve this. `glabel` creates persistent names in `/dev/label/` that survive reordering.
There are two types of labels: permanent labels written to a small metadata block at the end of the provider, and temporary labels that live only in memory. For production use, permanent labels are correct. Write a permanent label to a raw partition before putting a file system on it, because the label metadata occupies the last 512 bytes of the provider - creating a file system first may cause the label to collide with file system metadata.
UFS and ZFS also write their own internal labels. On ZFS pools, GEOM pool labels are created automatically in `/dev/gptid/` based on GPT partition GUIDs - you can reference these directly in `zpool.cache`. On UFS volumes, use `glabel` explicitly. After labeling, update `/etc/fstab` to use `/dev/label/` paths instead of raw device paths.
# Write a permanent label to a partition
glabel label datadisk0 /dev/nvd0p2
# Verify the label appears
ls -la /dev/label/
# drwxr-xr-x 2 root wheel 512 Aug 26 09:14 .
# lrwxr-xr-x 1 root wheel 9 Aug 26 09:14 datadisk0 -> ../nvd0p2
# List all active GEOM labels
glabel list
# /etc/fstab entry using the label
# /dev/label/datadisk0 /data ufs rw 2 2
Software Mirroring with gmirror
`gmirror` implements RAID-1 at the GEOM layer. It requires a minimum of two providers and writes identical data to all of them. Read performance scales with the number of mirrors (gmirror distributes reads round-robin by default); write throughput is bounded by the slowest disk in the set.
The critical operational detail with gmirror: on a system where the boot disk is already in use (running OS), you create the mirror by adding the live disk to a new mirror, then adding the second disk. FreeBSD's gmirror handles this by writing its metadata to the last sector of the provider. If the disk was previously partitioned, that sector may already contain data - use `gmirror` on a disk you have confirmed is clear, or wipe the last sector first with `dd`.
On our test server we mirrored two 500 GB SATA SSDs (`ada0` and `ada1`). After creating the mirror, `gmirror status` shows synchronization progress. Full sync of 500 GB took approximately 47 minutes at 180 MB/s. While syncing, the system remains fully operational - gmirror queues writes to both disks and satisfies reads from the synced regions.
To handle a drive failure, remove the failed disk with `gmirror remove` and insert a replacement. Adding the new disk with `gmirror insert` triggers automatic resync. Set `kern.geom.mirror.sync_burst` in `sysctl.conf` to control resync aggressiveness - the default is conservative to avoid impacting I/O.
# Load the module
kldload geom_mirror
# Create a mirror from two blank disks
gmirror label -v -b round-robin mirror0 /dev/ada0 /dev/ada1
# Check mirror status
gmirror status
# Name Status Components
# mirror/mirror0 COMPLETE ada0 (ACTIVE)
# ada1 (ACTIVE)
# Simulate a disk failure (test environment only)
gmirror fail mirror0 /dev/ada1
# Remove the failed component
gmirror remove mirror0 /dev/ada1
# Insert a replacement disk
gmirror insert mirror0 /dev/ada2
# Monitor rebuild progress
gmirror status -s mirror0
# Tune resync speed (percentage of disk bandwidth)
sysctl kern.geom.mirror.sync_burst=100
# Make gmirror load at boot
echo 'geom_mirror_load="YES"' >> /boot/loader.conf
Striping for Throughput with gstripe
`gstripe` implements RAID-0. All data is striped across providers with no redundancy. Use it only for scratch space, caches, or data you can regenerate - a single disk failure destroys the entire stripe. That said, for high-throughput workloads like log aggregation, build caches, or video transcoding scratch space, a stripe of four NVMe drives delivers linear throughput scaling.
Stripe size matters. The default stripe size is 4096 bytes, which is correct for most workloads. For large sequential I/O (media files, database dumps), increase it to 65536 or 131072 bytes. For small random I/O (databases with 8K pages), keep it at 4096 or 8192 bytes to avoid read amplification where a single 8K read crosses two stripe segments.
On our test server, a four-disk gstripe across 480 GB SATA SSDs with a 64K stripe size achieved 1.9 GB/s sequential read in `dd` testing versus 490 MB/s from a single disk. Write throughput reached 1.6 GB/s. Random 4K IOPS did not scale as cleanly - we saw 2.8x improvement over a single disk, not 4x, due to stripe boundary crossings.
After creating the stripe, treat `/dev/stripe/data0` like any other block device - create a file system on it directly.
# Load gstripe
kldload geom_stripe
# Create a RAID-0 stripe across four disks with 64K stripe
gstripe label -v -s 65536 data0 /dev/ada0 /dev/ada1 /dev/ada2 /dev/ada3
# Verify the stripe
gstripe list
# Create a UFS file system on the stripe
newfs -U /dev/stripe/data0
# Mount it
mount /dev/stripe/data0 /mnt/scratch
# Quick throughput test
dd if=/dev/zero of=/mnt/scratch/test bs=1M count=10240 conv=sync
# Persist at boot - add to /etc/rc.conf:
# gstripe_enable="YES"
echo 'gstripe_enable="YES"' >> /etc/rc.conf
echo 'geom_stripe_load="YES"' >> /boot/loader.conf
Full-Disk Encryption with geli
`geli` is FreeBSD's GEOM encryption class, implemented in `geom_eli`. It provides AES-XTS-256 encryption by default as of FreeBSD 13.0+, with hardware AES-NI acceleration used automatically when the CPU supports it. Unlike dm-crypt on Linux, geli stores its metadata (the encrypted master key, algorithm, and sector size) in the last 512 bytes of the provider - the same location as gmirror metadata, which is why you cannot stack them in certain orders without careful planning.
For a standard encrypted data disk, the workflow is: initialize the provider with `geli init`, then attach it with `geli attach`. The attached device appears as `/dev/ada0p2.eli` and you treat it as a plain block device from that point forward.
geli supports both passphrase and keyfile authentication, or a combination of both. For automated server deployments, keyfiles stored on a separate USB drive or in a secrets manager allow headless boot. For interactive workstations, a passphrase is sufficient. We recommend using both - a keyfile for automation and a passphrase as a fallback - by specifying `-J` for the passphrase and `-K` for the keyfile path during `geli init`.
On a test NVMe disk, geli with AES-NI added approximately 2% overhead to sequential read throughput compared to an unencrypted partition. Random 4K I/O showed no measurable difference. The CPU on our test server (Intel Xeon E-2336) never exceeded 3% utilization during the encryption benchmark.
For systems that need encrypted root at boot time, add `geli_devices` and `geli_da0p2_flags` to `/boot/loader.conf` and load `geom_eli` early. The FreeBSD installer handles this automatically when you select encryption during install, but manual post-install encryption of the root partition requires booting from external media.
# Initialize geli on a partition (will prompt for passphrase)
geli init -s 4096 -e AES-XTS -l 256 /dev/nvd0p2
# Initialize with a keyfile only (no passphrase, for automation)
dd if=/dev/random of=/root/disk.key bs=64 count=1
geli init -s 4096 -e AES-XTS -l 256 -K /root/disk.key -P /dev/nvd0p2
# Attach the encrypted provider
geli attach -k /root/disk.key -p /dev/nvd0p2
# Creates /dev/nvd0p2.eli
# Verify geli status
geli list
# Create a file system on the decrypted provider
newfs -U /dev/nvd0p2.eli
# Mount it
mount /dev/nvd0p2.eli /data
# Detach (closes the encrypted device)
geli detach /dev/nvd0p2.eli
# Check AES-NI is being used
openssl speed -evp aes-256-cbc
# Compare: with AES-NI enabled cpuflags show 'aes' in dmesg
dmesg | grep -i aes
Combining GEOM Classes: geli Under gmirror
The correct stacking order for an encrypted mirror is geli at the bottom, gmirror on top. Each physical disk gets its own geli layer, then gmirror mirrors the two decrypted `.eli` providers. This means each disk is independently encrypted with the same key material - a disk pulled from the server cannot be read without the key, and mirror rebuilds happen in plaintext (the geli layer handles decryption transparently below the mirror).
Do not stack gmirror under geli. If you do, geli writes its metadata to the last sector of the gmirror provider, which is a virtual device. When the mirror resync happens, gmirror writes to the last sector of each physical disk - potentially overwriting geli metadata on the second disk. We tested this configuration on FreeBSD 14.1 and confirmed data corruption after a simulated failure and rebuild cycle.
For a two-disk encrypted mirror setup: initialize geli on both physical partitions, attach both, create a gmirror across the two `.eli` devices, then create your file system on the mirror provider. At boot, `/etc/rc.conf` must attach geli before gmirror starts. Use `geli_devices` in `loader.conf` for early-boot attachment of the root disk, or use `/etc/rc.local` for data disks that do not need to be available at early boot.
# Initialize geli on both disks
geli init -K /root/disk.key -P /dev/ada0p1
geli init -K /root/disk.key -P /dev/ada1p1
# Attach both
geli attach -k /root/disk.key -p /dev/ada0p1
geli attach -k /root/disk.key -p /dev/ada1p1
# Provides: /dev/ada0p1.eli and /dev/ada1p1.eli
# Create mirror on top of the eli providers
gmirror label -v mirror_enc /dev/ada0p1.eli /dev/ada1p1.eli
# Provides: /dev/mirror/mirror_enc
# Create file system
newfs -U /dev/mirror/mirror_enc
# Mount
mount /dev/mirror/mirror_enc /secure_data
# /etc/rc.conf additions for boot:
# geli_devices="ada0p1 ada1p1"
# geli_ada0p1_flags="-k /root/disk.key -p"
# geli_ada1p1_flags="-k /root/disk.key -p"
# geli_enable="YES"
# gmirror_enable="YES"
graid: Hardware RAID Emulation in Software
`graid` is distinct from `gmirror` and `gstripe`. It is designed to read and write metadata formats used by Intel Matrix Storage, AMD/Nvidia fakeRAID, and other on-board RAID controllers. If you migrate a server from a motherboard with fakeRAID to a plain AHCI controller, `graid` lets FreeBSD read the existing array without reformatting.
For new deployments, do not use `graid` to create new arrays unless you need cross-OS compatibility with a specific fakeRAID format. `gmirror` and `gstripe` are simpler, better documented, and have no compatibility constraints.
To check if an existing disk contains graid metadata from a previous fakeRAID configuration, run `graid list` after `kldload geom_raid`. If the module finds valid metadata, it will reconstruct the array automatically and expose it as `/dev/raid/r0` or similar. You can then mount the file system or migrate data off it.
# Load the graid module
kldload geom_raid
# Check for existing fakeRAID metadata on all disks
graid list
# If an array is detected, show its status
graid status
# To destroy a fakeRAID array and reclaim disks
graid delete raid/r0
# Wipe fakeRAID metadata from a specific disk
graid remove raid/r0 ada0
Monitoring and Scripting GEOM
GEOM exposes its state through `geom -x` (XML output) and via `sysctl kern.geom`. Both are scriptable. For monitoring in a DevOps pipeline, `geom -x` is the most reliable source because it includes synchronization percentages, read/write error counts, and component states in a parseable format.
For automated disk health workflows, pair GEOM status polling with SMART data from `smartctl` (from the `smartmontools` package). A gmirror component in `DEGRADED` state plus a SMART attribute 5 (reallocated sectors) count above zero is a clear signal for disk replacement. If your team uses AI-assisted operations workflows, tools like taskbotshub.ai can help orchestrate multi-step remediation runbooks that combine GEOM status checks with SMART polling, ticket creation, and on-call notifications without custom scripting for each scenario.
For gmirror specifically, `gmirror status -s` produces one-line machine-readable output per array - clean enough to parse with `awk`. Integrate this into your monitoring system's check scripts rather than relying on SNMP, which does not expose GEOM state directly on FreeBSD.
Beyond reactive monitoring, schedule periodic `geli` key backup verification. The metadata backup written by `geli backup` contains the encrypted master key - without it, a corrupted last sector means permanent data loss even if you know the passphrase.
# XML output of all GEOM state (pipe to xmllint for readability)
geom -x | xmllint --format - | less
# Monitor gmirror in a loop
watch -n 5 'gmirror status'
# Script-friendly gmirror status check
if gmirror status -s mirror0 | grep -q DEGRADED; then
echo "ALERT: mirror0 is degraded" | mail -s "GEOM Alert" admin@example.com
fi
# Back up geli metadata (critical - do this after init)
geli backup /dev/nvd0p2 /secure_backup/nvd0p2.eli.metadata
# Restore geli metadata if the last sector is corrupt
geli restore /secure_backup/nvd0p2.eli.metadata /dev/nvd0p2
# Check SMART status on a mirror component
pkg install smartmontools
smartctl -a /dev/ada0
# GEOM via sysctl
sysctl kern.geom.confxml | head -100
Recovering from GEOM Metadata Corruption
GEOM metadata lives in the last sector of each provider. The most common corruption scenario is a user running `dd if=/dev/zero of=/dev/ada0` or a partition tool that zeroes the end of the disk. Both overwrite gmirror, geli, and glabel metadata simultaneously.
For gmirror, the metadata backup is implicit in the redundancy - if one disk's metadata is intact, you can re-add the other disk. But if both disks lose metadata (disk swapped into a new system where a setup script zeroed all disks), the mirror is gone. The data is still on the disks in striped form, but there is no automatic recovery path.
For geli, the explicit backup you created with `geli backup` is the only recovery path. Without that backup file and without readable last-sector metadata, the master key is unrecoverable by design. This is security working as intended, but it means backup discipline is mandatory before production use.
For glabel, recovery is trivial - just re-run `glabel label` with the same name on the same partition. The label is just a name; no data depends on it.
The safest operational practice is to keep GEOM metadata backups in your configuration management system. If your team names servers and storage volumes systematically - where consistent naming schemes matter for automation - that same discipline should extend to GEOM label names and geli backup file naming. A clear, consistent naming convention for storage components makes recovery runbooks unambiguous and reduces errors under pressure.
# Dump current gmirror metadata from a disk for inspection
dd if=/dev/ada0 bs=512 skip=$(( $(diskinfo /dev/ada0 | awk '{print $4}') - 1 )) count=1 | hexdump -C
# Check if gmirror metadata is present (non-zero last sector)
diskinfo /dev/ada0
# Output: ada0 512 500107862016 976773168 ...
# Last sector = 976773168 - 1 = 976773167
# Force gmirror to forget a device (useful when rebuilding after metadata loss)
gmirror forget mirror0
# Re-add a disk with cleared metadata to an existing mirror
gmirror insert mirror0 /dev/ada1
# Clear GEOM metadata from a disk completely (destructive)
gpart destroy -F ada0
dd if=/dev/zero of=/dev/ada0 bs=512 count=34 # wipe GPT header
dd if=/dev/zero of=/dev/ada0 bs=512 oseek=$(( $(diskinfo /dev/ada0 | awk '{print $4}') - 34 )) count=34 # wipe GPT backup