Skip to content
FreeBSDDeep Dive Published Updated 7 min readViews unavailable

FreeBSD makefs: Build and Validate Filesystem Images from Staged Trees

Create FreeBSD filesystem images with makefs, control ownership and timestamps, validate formats, and avoid confusing an image with a bootable disk.

makefs(8) creates a filesystem image file from a directory tree or an mtree(5) manifest. It is useful for installer media, test fixtures, embedded images, and reproducible build pipelines because it can construct a filesystem without mounting a target disk. It does not automatically create a partition table, install boot code, prove firmware compatibility, or guarantee that an image is safe to write to physical media.

Keep the build stages separate: prepare a staged tree, define metadata and capacity requirements, create an image file, inspect the resulting filesystem, then package or attach the image using a separate tool. Never point makefs at a physical device path unless the exact manual and intended operation explicitly require it; the normal output is a named image file.

Prepare a controlled staging tree

Use a dedicated staging directory rather than building directly from a live root filesystem. Confirm that the tree contains only the intended files, has the expected symlinks, and does not contain host-specific secrets or temporary state:

find /var/tmp/image-root -xdev -type f -print | sort | head -100
du -sk /var/tmp/image-root
mtree -c -p /var/tmp/image-root > /var/tmp/image-root.mtree

This creates a manifest for review; it is not automatically the same as a makefs manifest argument. mtree has several spec and comparison modes, and makefs -F has different semantics from using an mtree manifest as the input tree. Read the makefs(8) and mtree(8) manuals before combining them. A path-only inventory helps detect unexpected inclusions but does not validate file contents or permissions.

Ensure numeric ownership can be resolved consistently. makefs -N userdb-dir can use text master password and group databases instead of the build host’s name-service lookups. This is valuable when image owners do not exist on the builder. Keep the user/group database files controlled and avoid embedding the actual host’s password database in an image build unless that is explicitly intended.

Choose the filesystem and size deliberately

The -t option selects the image filesystem type. The documented formats include FFS, CD9660, msdos, and ZFS, with format-specific options. For a small UFS2 laboratory image, a command can look like:

makefs -t ffs -s 268435456 \
  -o version=2,label=lab-root,softupdates=1 \
  /var/tmp/lab-root.ufs /var/tmp/image-root

The -s size is a fixed image size, so the build fails if the filesystem cannot hold the input within its constraints. -M and -m set minimum and maximum image sizes in relevant formats; -b and -f can request minimum free blocks or files. Do not select a size based only on the current tree’s du output: filesystem metadata, inode density, directory count, and future free-space requirements also matter.

For a FAT image, choose -t msdos and review the format-specific options in the manual. For ISO 9660, use -t cd9660 and select only documented extensions and boot-image options that the deployment target supports. These are distinct filesystems, not interchangeable wrappers around one generic image.

Control timestamps and reproducibility

The -T option supplies a timestamp for filesystem files and directories created by makefs, allowing repeatable builds when inputs and other parameters are also stable. It accepts a pathname or an integer interpreted as seconds since the epoch. It does not normalize the contents of files, ordering in external build scripts, ownership resolution, or every metadata field.

Record the exact source tree revision, staging manifest, makefs version, command line, environment, user/group database, and output hash. Build twice in a clean directory with the same inputs and compare hashes. If they differ, inspect timestamps, file ordering, generated identifiers, metadata, and tool version rather than asserting reproducibility from a fixed -T alone.

For ZFS images, the FreeBSD manual describes stable pool GUID behavior across identical makefs invocations and warns that importing such a pool may conflict with another generated pool unless the GUID is changed. Use zpool reguid according to the documented workflow before importing an image that shares a GUID with another active pool. A successful image build is not evidence that the image is ready to import into a production host.

Use mtree intentionally

An mtree manifest can describe paths and metadata, and makefs can also use a spec file to augment entries that exist in the source tree. These options are easy to confuse. A manifest used as the primary input is the final positional operand. -F selects a spec file for an underlying directory tree; it is not the argument that tells makefs to read the manifest as the only source.

The manual notes that an mtree spec can override permissions and modification time for existing entries, and can create missing entries when required attributes are supplied. Missing regular files may be created as zero-length files. Duplicate paths normally cause an error unless a documented duplicate-path option changes the behavior. Treat every manifest as executable build input: review it for unintended path entries, symlinks, ownership, modes, and duplicate records.

Never enable “warnings instead of errors” for duplicate paths merely to get a build to pass. Duplicate path intent is ambiguous and can hide an accidental overlay or generated-file collision. Resolve the manifest or staging tree and require a clean build.

Validate the image before distributing it

Check the output’s type, size, checksum, and filesystem contents using format-appropriate read-only tools. For UFS, use FreeBSD’s filesystem inspection utilities on an attached vnode-backed memory disk in a disposable test environment. For an ISO, inspect directory layout and boot catalog separately. For a ZFS image, import only in an isolated environment and follow the GUID warning above. A host macOS machine cannot run FreeBSD’s makefs or reliably validate every FreeBSD-specific image behavior; the build should run in a matching FreeBSD CI runner or test VM.

The following is an example of attaching an already built image for a controlled read-only check on FreeBSD. It does not format or write a physical disk:

md=$(mdconfig -a -t vnode -o readonly -f /var/tmp/lab-root.ufs) || exit 1
device="/dev/$md"
check_dir=$(mktemp -d /var/tmp/image-check.XXXXXX) || { mdconfig -d -u "$md"; exit 1; }
if mount -o ro "$device" "$check_dir"; then
  find "$check_dir" -maxdepth 2 -print | sort | head -100
  umount "$check_dir" || { echo "unmount failed; inspect $check_dir and $md before cleanup" >&2; exit 1; }
  rmdir "$check_dir"
else
  rmdir "$check_dir"
  mdconfig -d -u "$md"
  exit 1
fi
mdconfig -d -u "$md"

Test the exact commands on a disposable FreeBSD VM before operational use; device naming and filesystem-specific mount behavior must be confirmed against the installed manuals. Always detach only the vnode-backed md device created by the command, and verify its name before cleanup. Do not copy the example with a guessed md unit or point it at a physical disk.

For a bootable artifact, use the correct partitioning and bootcode tools in a separate, reviewed stage. A filesystem image can mount correctly and still fail firmware boot because it lacks a partition map, EFI System Partition, loader, or architecture-specific boot block. Test the exact boot path in a VM or on disposable hardware. Keep the filesystem construction result and boot integration result as separate acceptance records.

Common build failures and safe recovery

An image-size error usually means the tree plus metadata exceeds the maximum, or the requested filesystem parameters cannot represent the contents. Increase capacity only after reviewing target limits and deployment media. A missing user or group can indicate host lookup leakage; use a controlled -N database or a consistent builder account map. A manifest conflict should be fixed at the source rather than suppressed.

If an image is unexpectedly large, inspect sparse-file settings, requested free blocks, size units, and filesystem overhead. -Z creates a sparse FFS image; it saves host storage where supported but the apparent logical image size remains significant and may consume that size when copied to a non-sparse destination. Make capacity and transfer tooling aware of sparse behavior.

If a mounted test image differs from the staging tree, inspect filesystem-specific name restrictions, links, modes, timestamps, and spec overrides. Do not publish an artifact until a clean extraction or read-only mount comparison passes. Preserve the build logs and hashes so a later operator can distinguish a source regression from a packaging or transfer problem.

Production acceptance

A reliable image pipeline has a versioned staging tree, a reviewed manifest, explicit filesystem type and size, deterministic metadata inputs where required, a recorded command and tool version, an artifact hash, and a format-specific validation. Bootable images require an additional boot-chain test. No single successful makefs exit status proves correctness at all these layers.

Related:

Sources:

Comments