← Back · part of NextBSD Research · sub-plan of the Pi 500+ native-boot plan

How an installer should decide what kind of boot to install

NextBSD’s installer writes GPT with an EFI System Partition, unconditionally. That is unbootable on a Raspberry Pi, whose firmware reads config.txt off a FAT partition and enters the kernel directly — no UEFI, no BIOS, no loader of any kind. This page surveys every way of telling those cases apart, says what each signal actually proves, and picks one. Written after four attempts at the detection got it wrong in four different ways, then revised twice more when the research contradicted the result.

Why this page exists The first four attempts were each plausible and each broken: matching a device-tree compatible string (the tool prints hex unless asked for text, and then truncates at the first NUL); testing machdep.efi_map (does not exist on arm64); treating “has a device tree” as “firmware booted us” (loader.efi installs a DTB too); and mounting candidate filesystems to look for config.txt (intrusive, and it broke three ways). Picking from a survey costs an afternoon. Picking without one cost a day and would have shipped a bug that bricks every arm64 VM install.

1. The cases, and why there are more than two

An installer has to produce a disk the machine’s firmware can actually start. There are three distinct arrangements, not two, and a fourth hybrid that catches people out.

CaseWhat starts the OSWhat must be on disk
Legacy BIOS (x86)MBR bootstrap → gptboot → loaderGPT + a freebsd-boot partition holding pmbr/gptboot
UEFI (x86, arm64)firmware → BOOTAA64.EFI → loaderGPT + an ESP (type c12a7328-…)
Firmware direct (Pi)EEPROM bootloader reads config.txt, enters the kernel at EL2MBR + FAT32 type 0x0c holding config.txt, a DTB, and the kernel
UEFI on a Pi (EDK2)EEPROM loads RPI_EFI.fd as an ARM stub, then UEFIboth: MBR + FAT32 0x0c for the EEPROM, carrying an EFI firmware image

The trap The fourth row is why “is this a Pi?” and “does this machine have UEFI?” are both insufficient on their own. An EDK2 Pi answers yes to UEFI, but the EEPROM — not EDK2 — is still what reads the partition table, so it cannot use a GPT ESP. The pftf RPi3 documentation says so outright: “Do not try to use GPT for the partition scheme … as these are unsupported by the CPU-embedded bootloader.”

2. What NextBSD is doing is not what FreeBSD does

Verified in source FreeBSD’s own Raspberry Pi image does not use firmware-direct boot. release/arm64/RPI.conf installs sysutils/u-boot-rpi-arm64 and ships a config.txt whose kernel= line names u-boot.bin. The chain is:

Pi EEPROM → u-boot.bin → U-Boot's built-in EFI → BOOTAA64.EFI → loader → kernel

U-Boot manufactures a UEFI environment so the ordinary arm64 path works. That is why nothing in FreeBSD’s installer has a case for us: upstream never needs one. NextBSD’s route (a) — firmware enters kernel8.img directly, no loader at all — is a genuinely different design, and the installer gap is a direct consequence of choosing it.

The same pattern holds almost everywhere. Fedora, openSUSE and Debian all chainload U-Boot in front of the kernel and thereby dodge the question entirely. No project surveyed probes for “the firmware entered the kernel directly”; the ones that genuinely support it resolve it by board identity or by declaring it at build time.

3. Every signal, and what it actually proves

Each of these was checked against FreeBSD source at releng/15.1, and the ones marked measured were read off a Pi 500+ running NextBSD.

SignalProvesVerdict
machdep.bootmethodBIOS / UEFI / PVH. What bsdinstall itself uses.x86 only Declared in sys/x86/x86/cpu_machdep.c. Absent on arm64.
machdep.efi_mapThe EFI memory map.amd64 only One declaration in the tree, sys/amd64/amd64/machdep.c:1724. This is the bug that would have bricked arm64 VM installs.
/dev/efiEFI runtime services present and workingefidev(4) declines to attach otherwise.Good Arch-neutral. Absence does not prove BIOS (RT can be broken or disabled).
kenv efi-versionloader.efi ran. Set nowhere else (stand/efi/libefi/env.c).Good Arch-neutral, cheap.
hw.fdt.compatibleThe root node’s compatible, NULs already joined into spaces by the kernel.Best board ID World-readable; gives the whole property. Measured raspberrypi,500 brcm,bcm2712
ofwdump -P compatible /Same, via /dev/openfirm.Avoid Needs root (mode 0600); prints raw hex without -S, and -S stops at the first NUL — so brcm,bcm2712 is invisible. -R does return the whole property.
/chosen/bootloader in the FDTThe Raspberry Pi firmware booted this system. It adds the whole subtree at handover — boot-mode, partition, pm_rsts, tryboot, version — and it is absent from the DTB on disk.Positive evidence Measured Present in the blob NextBSD received (bootloader, rpi-boardrev-ext, os_prefix, tryboot, boot-mode). Better in kind than inferring a firmware boot from the absence of UEFI — though reading it needs the DTB blob, since ofwdump does not enumerate the subnode.
“an FDT exists”Nothing about the boot method.Misleading loader.efi installs the DTB from the EFI configuration table — “Using DTB provided by EFI at %p”. Every UEFI arm64 VM has /dev/openfirm.
debug.dump_modinfoWhich metadata the loader passed. No MODINFOMD_ENVP ⇒ no FreeBSD loader ran.Strongest arm64 evidence Arch-neutral, but requires parsing text.
machdep.acpi_rootOn arm64, non-zero only if a loader passed acpi.rsdp — there is no memory-scan fallback.Corroborating Measured 0 on the Pi.
hw.efi.*, machdep.efi_rt_handle_faultsThat the kernel was compiled with options EFIRT.Not evidence Present on BIOS amd64 and DTB arm64 alike.
kenv -lReturns ENOENT Needs PRESERVE_EARLY_KENV, which no GENERIC sets.
gpart show typesPartition scheme and type names, straight from disk metadata.No mount needed Measured da0s1 fat32lba, da0s2 freebsd
/dev/msdosfs/<label>A FAT volume with that label exists.Conditional Needs GEOM_LABEL and a non-empty label; unlabelled volumes produce no node.

4. What is actually required, versus merely conventional

A surprising amount of Pi boot lore turns out to be convention. Separating the two matters, because every genuine requirement is a constraint on the installer and every convention is a free choice.

ClaimStatus
MBR, not GPTDepends on generation and media Pi 1/2/3 from SD: MBR only, and that is silicon — the mask ROM cannot be updated. Pi 2B v1.2/3B/3B+ over USB mass storage: GPT works. Pi 4 and 5: GPT since firmware 2020-09-14 (“Add support GPT and Hybrid MBR partition tables”), with GPT FAT16 added 2021-10-04 and 4K-native-sector support on 2712. Raspberry Pi’s own rpi-image-gen ships image/gpt/ and image/mbr/ side by side. So MBR is the only scheme that works everywhere — a decision, not a constraint.
FAT partition type 0x0c (fat32lba)Folklore The documented rule is about the filesystem, not the type byte. autoboot.adoc: “Bootable partitions must be formatted as FAT12, FAT16 or FAT32 and contain a start.elf file (or config.txt file on Raspberry Pi 5) in order to be classed as be bootable by the bootloader.” NOOBS — Raspberry Pi’s own installer — shipped its bootable partition as 0x0E; pftf’s Pi 4 port recommends 0xef. 0x0c is the conventional choice and a perfectly good one, but it is not the requirement.
Some type bytes are actively dangerousYes 0x07 exFAT as the first partition hangs the BCM2711 B0 mask ROM — a brief ACT flash, then nothing at all: no UART, no HDMI. This is the classic “brand-new 64 GB card will not boot” report. Also: 0x0F extended containers were unbootable before firmware 2024-05, and MBR slot 4 was broken before 2025-05.
The boot partition must be marked activeConvention only Every Raspberry Pi OS image decoded, 2012–2026, has status byte 0x00. FreeBSD’s arm.subr sets it anyway; rpi-imager writes 0x80. It evidently does not matter.
FAT32 specificallyNot strictly Images were FAT16 until 2017-01-11, and FreeBSD’s RPI image still formats FAT16 while labelling the partition 0x0c. The firmware tolerates the mismatch.
config.txt must exist on a Pi 5Required boot.adoc:135: “Raspberry Pi 5 requires a config.txt file to be present to indicate that the partition is bootable.” On earlier models that role is played by start.elf. So the file is not merely configuration — on this generation it is the marker that makes a partition a boot candidate at all. An installer that wrote a boot partition without one would produce a disk the firmware skips silently.
A partition table at allRequired The EEPROM looks for a FAT partition; it does not read a bare filesystem.

Corrected after publication This section first listed “MBR, not GPT” as a hard requirement, on the strength of Raspberry Pi OS shipping MBR for fourteen years and the pftf UEFI ports saying GPT was unsupported. Both facts are real; the conclusion drawn from them was not. The firmware gained GPT support in 2020, and the pftf statement describes the Pi 3. Left visible rather than quietly edited, because mistaking a convention for a constraint is precisely the failure this page exists to prevent — and it happened here, in the section written to prevent it.

5. The options

A. Match the board’s compatible string

Test: hw.fdt.compatible contains raspberrypi,.
For: direct, cheap, world-readable, and the whole property is available.
Against: answers “which board”, not “which boot method”. Fails on an EDK2 Pi, which is a Pi but wants the EFI payload. Needs a list that grows with the hardware.

B. Test for UEFI, everything else is firmware boot

Test: /dev/efi or kenv efi-version.
For: arch-neutral; correctly sends an EDK2 Pi down the UEFI path; no board list.
Against: “not UEFI” is not the same as “Pi firmware” — a U-Boot board without EFI payload support would be misread. And it still gets the EDK2 Pi’s partitioning wrong, since that needs MBR regardless.

C. Look for config.txt on the boot medium

Test: mount candidate FAT partitions, look for config.txt plus the kernel it names.
For: proof rather than inference — that file is the boot method, and finding it also locates the files to copy.
Against: Mounts media the operator did not nominate. Intrusive, and in practice it produced three separate bugs: probing the install target, leaking mounts, and failing silently on stale state.

D. Read the partition table only

Test: gpart reports an MBR disk with a fat32lba slice.
For: no mounting; structural rather than by convention, so it recognises a stock Raspberry Pi OS card or a hand-made stick.
Against: describes a layout, not a capability. A machine could have such a disk attached and still be a PC.

E. Reproduce the medium’s own boot setup

Not a detection method — a way of acting on one. Rather than generating a config.txt from a template, copy the boot partition that started this machine. It demonstrably works on this hardware, which no amount of detection can promise, and it needs no per-board knowledge at all — a Pi 4, a Pi 5 or a board nobody has thought of yet all work unchanged.

6. The decision

The gap that reordered this Test UEFI first and a real case breaks. Stock FreeBSD’s Pi image boots through a UEFI layer — U-Boot’s, not EDK2’s — so on that system /dev/efi exists and a UEFI-first rule answers “generic” and writes GPT+ESP. That disk will not boot, because the EEPROM is still what reads the partition table and it wants MBR+FAT. The same is true of an EDK2 Pi. A UEFI layer on a Pi does not change who reads the partition table.

The fix is not a different test but a different question. Two decisions were being conflated:

DecisionDetermined byBecause
Partition scheme and boot filesystemthe boardwhatever reads the partition table first is fixed in silicon or EEPROM. On a Pi that is always the GPU firmware, whatever runs afterwards.
What goes on the boot partitionthe boot methodkernel8.img + config.txt for firmware-direct; u-boot.bin or RPI_EFI.fd plus an EFI tree where a UEFI layer is in play.

Chosen Board decides the layout. Boot method decides the payload. Always reproduce the medium’s boot setup rather than generating one.

. # 1. LAYOUT -- who reads the partition table?
  #    A Pi is a Pi whether or not something UEFI-shaped runs later.
  case "$(sysctl -n hw.fdt.compatible 2>/dev/null)" in
  *raspberrypi,*|*brcm,bcm2*)  LAYOUT=mbr-fat ;;
  *)                           LAYOUT=gpt     ;;
  esac

  # 2. PAYLOAD -- did a UEFI loader run?
  #    NOT machdep.efi_map: that does not exist on arm64.
  if [ -e /dev/efi ] || [ -n "$(kenv -q efi-version)" ]; then
          PAYLOAD=efi          # loader.efi into an EFI/BOOT tree
  else
          PAYLOAD=firmware     # config.txt + DTB + kernel8.img
  fi

For NextBSD today that yields mbr-fat + firmware on the Pi and gpt + efi everywhere else — the same two outcomes as before. The difference is that the two remaining combinations are now expressible instead of silently wrong.

A refinement not yet taken. The payload test infers a firmware boot from the absence of UEFI, which is weaker than it could be. /chosen/bootloader is positive evidence — the Pi firmware adds that subtree at handover and it is not in the DTB on disk — and it is measurably present in the blob NextBSD receives. It is not used yet only because reading it means parsing hw.fdt.dtb rather than asking a sysctl; ofwdump does not enumerate the subnode. Worth doing if the absence-based test ever proves fragile.

Why the board test can come first without an EDK2 exception. Because the layout it selects is correct for an EDK2 Pi too. That was the case the earlier ordering could not represent: it is a Pi and it is UEFI, and both facts have consequences that do not conflict once they are asked separately.

Why not mount to detect (option C). Mounting during detection is intrusive and was the direct cause of three bugs — probing the install target, leaking mounts, and failing silently on stale state. The information it yields is needed only when copying, at which point mounting one identified device read-only is proportionate.

Why E regardless. Every generated config.txt encodes an assumption about the board. A copied one encodes what actually booted. It is also the only approach that handles a Pi 3 — whose boot partition needs bootcode.bin and start.elf that a Pi 5 image has never heard of — without a per-generation file manifest.

Why MBR, given that GPT would work on a Pi 4 or 5. Reach. MBR is the only scheme a Pi 3 can boot from SD, it is what Raspberry Pi OS has shipped for fourteen years, and it costs nothing here. GPT would buy A/B partitioning, which nothing needs yet.

7. What is still unresolved

Resolved by the research UEFI on a Pi 5 is not a case worth building for. There is no pftf/RPi5, and edk2-platforms contains only RPi3/ and RPi4/ — searching it for BCM2712 returns nothing. The only port, worproject/rpi5-uefi, was archived in February 2025; RP1 has no ACPI description, so Ethernet, GPIO and fan control do not work under it, and EEPROM access is broken so UEFI variables cannot persist. Raspberry Pi have declined to help twice, most recently “that would require a sizeable support commitment”. Nobody has reported booting it on a Pi 500 at all. The layout/payload split above handles it correctly if it ever appears; nothing needs to be built for it now.

Open Root label collision. The generic path labels the installed root NEXTBSD so a leftover install medium cannot hijack the boot, and overrides the kernel’s baked-in default through loader.conf. On a Pi there is no loader to do the overriding, so the target must be labelled ROOTFS — the same label the install medium carries. The current mitigation is “do not leave the stick plugged in”, which is weak. The real fix is a board kernel whose ROOTDEVNAME names something distinct.

Do not rely on it efibootmgr at install time. U-Boot cannot set UEFI variables at runtime at all — “we don’t allow SetVariableRT, since the OS doesn’t know how to write that file” — and pftf’s Pi 4 firmware stores variables inside RPI_EFI.fd and flushes them only during boot services, so entries written from a running OS silently vanish on reboot. EBBR makes SetVariable optional after ExitBootServices(). The installer already writes the removable-media fallback path \EFI\BOOT\BOOTAA64.EFI and treats the NVRAM entry as best-effort, which is the correct contract; this is a note not to tighten that later.

Reasoned Beyond the Pi, the layout question does not generalise. Allwinner wants U-Boot at raw sector 16 or 256, Rockchip at sector 64, Amlogic at sector 1 — none of them read a partition table at that stage, and the offsets collide with a primary GPT. FreeBSD, Fedora and Armbian have each independently converged on a per-SoC-family install hook, which is strong evidence no filesystem-only model exists. If NextBSD ever targets a non-Pi board, budget for that shape rather than extending this one.

Not verified The FAT cluster and reserved-sector constraints for the Broadcom firmware specifically. Documented for TI’s ROM, absent for this one. mkfs defaults have always worked here, so this is a latent unknown rather than a live problem.

8. Measured on the hardware

A Pi 500+ running NextBSD from USB, 2026-08-24. Everything in the decision above is consistent with these readings.

/dev/efi                  absent
kenv efi-version          unset
machdep.acpi_root         0
dev.efirtc.0              absent
hw.fdt.compatible         raspberrypi,500 brcm,bcm2712
hw.fdt.model              Raspberry Pi 500 Rev 1.0
ofwdump -S -P compatible  raspberrypi,500          <- truncated at the first NUL
kern.disks                da0 nda0

gpart show -p da0
  =>     1  30871551    da0  MBR  (15G)
         1    204800  da0s1  fat32lba  (100M)
    204801   4500096  da0s2  freebsd   (2.1G)

mount
  /dev/ufs/ROOTFS on / (ufs, local, noatime, soft-updates)
  devfs on /dev (devfs)          <- the boot partition is NOT mounted at runtime

2026-08-24. Detection logic verified against freebsd/freebsd-src at releng/15.1; Raspberry Pi behaviour against the raspberrypi/firmware, pi-gen and pftf repositories; everything in §8 read off the board itself. Implemented in nextbsd-userland#60, tracking nextbsd#420.