FreeBSD kmodloader — porting plan

A FreeBSD-only daemon that ensures every hardware-class kernel module on the system autoloads at boot when its hardware is detected. Default FreeBSD does not do this aggressively (especially for GPU drivers and the third-party drm-kmod / nvidia stacks); this project closes that gap. Companion to freebsd-launchd and freebsd-configd.

Status: Phase 1 shipped v1

1. Goal & non-goals

1.1 Goal

When hardware is present at boot, the kernel module that drives it loads without manual configuration. The user installs the system, plugs in their hardware, the system works. Zero kld_list= edits, zero kldload invocations from the user.

1.2 Non-goals (this iteration)

2. Architecture

Three independent match paths run sequentially in KMDaemon.runOneShot. Each emits a list of kld names. Lists are concatenated, deduplicated, and the GPU set is pulled to the front so the framebuffer is owned by a DRM driver before peripheral driver attaches happen. Then kldload -n each.

+----------------------------+ +-------------------+ | pciconf -l | | devmatch | | (display-class scan) | | (linker.hints) | +-------------+--------------+ +----------+--------+ | | vendor map | kld names | v v [i915kms] [if_em, umass, ...] [amdgpu/radeonkms] [nvidia-drm] | | +-------------+---------------+ | v [GPU klds first] | v NSMutableOrderedSet (dedupe) | v /sbin/kldload -n <name> (per kld) | v kernel newbus probe + driver attach

2.1 Why three paths instead of one

FreeBSD's hardware drivers split cleanly into two categories by how match information is published:

CategoryMatch data lives in...Reachable via
In-tree drivers with PNP_INFOlinker.hints (kldxref output)devmatch(8)
drm-kmod GPU drivers + nvidia binary blobCompiled-in C structs; not in linker.hintsPCI scan + 3-vendor map

Trying to unify both under one mechanism — the original plan's "personality plists with IOPCIMatch" — required hand-curating the device-ID lists for category 2. That doesn't scale: amdgpu's supported list grows each AMD generation, and the personality plists were always going to lag real hardware. The two-path split lets each category use the data source that already has the answer.

Hypervisor guest additions (vboxguest, vboxvfs) are intentionally NOT in scope. They're service-related kmods, not hardware drivers; the right home is the matching launchd plist (org.freebsd.vboxservice.plist) loading them before launching its userspace daemon, the same way FreeBSD's rc.d/vboxservice works. Phase 1g briefly shipped a third "hypervisor scan" path; it was reaching outside kmodloader's lane and got removed.

3. The three match paths in detail

3.1 Path 1 — devmatch + linker.hints

Bare devmatch (no flag) walks the live device tree and emits one kld basename per line for every enabled-but-unattached device that has a registered driver in linker.hints. linker.hints is built by kldxref(8) from each driver's PNP_INFO() macro at install time — pkg's post-install script regenerates it whenever a kld package lands. So at boot the index already covers everything that ships from base + ports.

$ devmatch
pchtherm.ko
rtsx.ko
if_iwlwifi.ko
if_iwm.ko
ichsmb.ko
acpi_wmi.ko

Coverage on a representative laptop (Skylake-era Lenovo): NICs (if_em), WiFi (if_iwlwifi / if_iwm), thermal (pchtherm), SD reader (rtsx), SMBus (ichsmb), ACPI WMI. Most of the system except the GPU.

NOT devmatch -a. The -a flag prints devname: kld for every enabled device including already-attached ones — wrong format, wrong filter. Bare devmatch is the canonical "what needs loading" query.

3.2 Path 2 — GPU PCI scan

drm-kmod's i915kms.ko, amdgpu.ko, radeonkms.ko, and the nvidia binary kmod do not declare PNP_INFO. kldxref -d /boot/modules/linker.hints | grep i915 is empty. strings /boot/modules/i915kms.ko | grep pnp_info is empty. devmatch will never see them.

Solution: walk pciconf -l for class 0x0300xx (display controllers; covers VGA, XGA, 3D), extract (vendor, device) per device, and route per a small in-code map:

- (NSArray<NSString *> *)gpuKldsForDevices:(NSArray<NSDictionary *> *)devices
{
    NSMutableOrderedSet<NSString *> *r = [NSMutableOrderedSet orderedSet];
    for (NSDictionary *dev in devices) {
        NSString *vendor = dev[@"vendor"];
        NSString *device = dev[@"device"];

        if ([vendor isEqualToString:@"0x8086"]) {
            [r addObject:@"i915kms"];
        }
        else if ([vendor isEqualToString:@"0x1002"]) {
            // Per-device split: radeonkms list is frozen upstream.
            uint16_t devID = (uint16_t)strtoul(device.UTF8String, NULL, 16);
            BOOL isRadeon = NO;
            for (size_t i = 0; i < kRadeonKMSDeviceIDsCount; i++) {
                if (kRadeonKMSDeviceIDs[i] == devID) { isRadeon = YES; break; }
            }
            [r addObject:isRadeon ? @"radeonkms" : @"amdgpu"];
        }
        else if ([vendor isEqualToString:@"0x10de"]) {
            [r addObject:@"nvidia-drm"];   // pulls nvidia-modeset + nvidia
        }
    }
    return r.array;
}

Three vendor IDs. 0x8086 (Intel), 0x1002 (AMD/ATI), 0x10de (NVIDIA). PCI-SIG assignments from the 1980s and 90s; will not change.

3.2.1 The AMD split: radeonkms vs. amdgpu

The only category-2 entry that needs more than vendor info. radeonkms covers TeraScale through HD 7000 + early Sea Islands; amdgpu covers GCN 1.1+ through current RDNA*. Linux's radeon driver is in maintenance-only mode upstream and stopped gaining new device IDs years ago — drm-kmod inherits the same frozen list.

So we vendor it once: 699 unique 16-bit device IDs from drm-kmod's include/drm/drm_pciids.h (the radeon_PCI_IDS macro) baked into kmodloader/src/RadeonPCIIDs.h as a static const uint16_t[]. Any AMD device whose ID matches the frozen set goes to radeonkms; everything else (Polaris onward) defaults to amdgpu. amdgpu is the open default: hardware AMD ships next year picks up amdgpu without us changing the table.

Update procedure (rare): re-fetch drm_pciids.h, re-extract the 0x1002 entries, regenerate the header. The set has not changed in years.

3.2.2 NVIDIA chain via MODULE_DEPEND

kldload nvidia-drm pulls nvidia-modeset which pulls nvidia via MODULE_DEPEND declarations in the kmods. We only have to name the leaf. Requires hw.nvidiadrm.modeset="1" in /boot/loader.conf so nvidia-drm.ko engages DRM/KMS at init — a kenv tunable, must be set kenv-time.

3.2.3 Multi-GPU + non-GPU systems

Multi-GPU laptops (Intel iGPU + Nvidia dGPU is common) are handled naturally: the scan finds both vendors, both kld sets get loaded. Bare-metal-with-no-GPU systems (some VMs) return an empty device list — the GPU pass loads nothing.

3.3 (Removed) hypervisor scan

Phase 1g briefly shipped a third match path that read kern.vm_guest and kldloaded VirtualBox / VMware guest additions kmods. Removed shortly after — for two reasons:

  1. Wrong scope. Guest additions are services, not hardware drivers. The Apple-shape pattern (and FreeBSD's existing rc.d/vboxservice pattern) is for the service's launchd job to kldload its supporting kmods before starting the userspace daemon. kmodloader's job is hardware autodetection.
  2. The kld names were also wrong. vboxvideo doesn't exist on FreeBSD — that's a Linux thing. vmware_drv is an Xorg driver, not a kld. vmxnet3 is in GENERIC already. Open-vm-tools ships zero kmods at all (everything is userspace daemons).

What we still ship in pkglist.txt: emulators/virtualbox-ose-additions-nox11 (provides vboxguest.ko, vboxvfs.ko, and userspace VBoxService) and emulators/open-vm-tools-nox11 (provides userspace vmtoolsd + helpers). The kmods sit on disk; the userspace daemons sit on disk. Nothing auto-runs them in Phase 1. Phase 2+ will introduce org.freebsd.vboxservice.plist and org.freebsd.vmtoolsd.plist which handle their own kmod loading and start the userspace daemon under launchd supervision.

4. Repository

4.1 Layout under freebsd-launchd/kmodloader/

freebsd-launchd/kmodloader/
├── Makefile                 gmake build, installs to /usr/libexec/kmodloader
└── src/
    ├── main.m               @autoreleasepool { [KMDaemon runOneShot]; }
    ├── KMDaemon.{h,m}       runOneShot orchestrator + the three match paths
    └── RadeonPCIIDs.h       generated frozen device list (radeon_PCI_IDS, 699 IDs)

No personalities/ directory. No KMRegistry / KMMatch / KMDevice / KMBusEnumerate classes. Phase 1d ripped them out: ~900 LOC removed, replaced by ~150 LOC of three-path orchestration.

Top-level (freebsd-launchd/) hosts:

4.2 How build.sh handles kmodloader

Same chroot session that builds launchd + configd. After make-configd.sh finishes, build.sh rsyncs kmodloader/ into chroot:/tmp/kmodloader/ and runs gmake -C /tmp/kmodloader install — the Makefile compiles the daemon and installs to /usr/libexec/kmodloader. No wrapper script.

5. Locked architectural decisions

DecisionChoice
Implementation languageObjective-C with GNUstep Foundation. Same substrate as launchd + configd.
Event loop (Phase 1)None. One-shot run, exit. Phase 2 adds libdispatch.
Match data sourcesTwo: linker.hints via devmatch, pciconf -l for GPU PCI scan. (Removed: kern.vm_guest hypervisor scan — out of scope, see §3.3.)
Kmod load mechanismNSTask wrapping /sbin/kldload -n <module>. The -n flag = "do not error if already loaded".
devmatch invocationBare /sbin/devmatch (no flags). NOT devmatch -a — that flag's output format is wrong for our use.
GPU vendor mapThree-vendor static map, no curated device-ID tables. The single per-device split (AMD radeonkms vs amdgpu) uses an upstream-frozen list vendored as RadeonPCIIDs.h.
nvidia-drm modesetRequired tunable in /boot/loader.conf: hw.nvidiadrm.modeset="1". Set kenv-time, before kernel start. Harmless on non-NVIDIA systems since the kmod only loads when the PCI scan finds 0x10de.
NVIDIA license at build timebuild.sh sets LICENSES_ACCEPTED=NVIDIA in the runtime-pkgs pkg install env. nvidia-drm-latest-kmod ships under restricted-distribution license.
Coexistence with devddevd is not currently installed. If added later: both daemons can read /dev/devctl; multiple readers are kernel-supported.
License (top-level)BSD-2-Clause. No Apple per-file headers — clean-room implementation.

6. launchd integration

6.1 The plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>             <string>org.freebsd.kmodloader</string>
    <key>ProgramArguments</key>  <array><string>/usr/libexec/kmodloader</string></array>
    <key>RunAtLoad</key>         <true/>
    <key>KeepAlive</key>         <false/>
</dict>
</plist>

One-shot at boot. KeepAlive=false — daemon exits when work is done; launchd does not respawn. Phase 2 will swap to a long-running shape with a DISPATCH_SOURCE_TYPE_READ source on /dev/devctl for hot-plug events.

6.2 Boot ordering

kmodloader runs early. Plist filenames sort alphabetically; org.freebsd.kmodloader sorts before org.freebsd.netconfigd, so any NIC kld kmodloader brings in is visible by the time netconfigd does its getifaddrs() walk. That's the dependency that matters today; finer ordering (a RequiresPhase key, or per-job dependencies) is a launchd-side feature for later.

7. Licensing

BSD-2-Clause across the board. No Apple per-file headers — this is clean-room code, not a port. The original plan referenced Apple's kextd + IOKit design; the implementation that shipped is closer to FreeBSD's own devmatch. Either way, no Apple source is vendored.

SourceLicenseHow we handle it
This repo's code (daemon + Makefile + scripts)BSD-2-ClauseSPDX header in every file.
RadeonPCIIDs.h generated tableData — not copyrightableComment cites drm-kmod's drm_pciids.h as source.
GNUstep Foundation, libdispatch, libobjc2 (linked, not in tree)LGPL/MIT/Apache as applicableListed in NOTICE; same calculus as launchd + configd.

8. Phased delivery

Phase 0 — repo scaffold DONE

Phase 1a-c — personality plist approach (later abandoned) SUPERSEDED

Initial implementation followed the original plan: Apple-shaped .kext bundles in /System/Library/Extensions/, an NSDictionary registry, probe-score matching against sysctl dev. walks. Shipped i915kms.kext, if_em.kext, amdgpu.kext, radeonkms.kext, if_iwlwifi.kext, if_rtw88.kext, if_rtw89.kext.

Discarded in Phase 1d: hand-curating IOPCIMatch lists doesn't scale to FreeBSD's hardware coverage, and devmatch already had the data via linker.hints. Replaced by the three-path scan.

Phase 1d — pivot to devmatch DONE

Phase 1e — GPU PCI scan DONE

Phase 1f — AMD per-device split DONE

Phase 1g — hypervisor scan (later removed) SUPERSEDED

Phase 2 — hot-plug via /dev/devctl PLANNED

Phase 3 — firmware loading from /boot/firmware/ RESOLVED

Bench testing surfaced a real problem: drm-kmod's GPU drivers and the iwlwifi LinuxKPI port load firmware via firmware_get() + try_binary_file() in sys/kern/subr_firmware.c. The function vn_opens /boot/firmware/<name>, but in our chroot setup the kernel context's namei resolves /boot/firmware/ against the cd9660 root (loader-only files) instead of the unionfs upper layer (where pkg-installed firmware lives). Result: i915kms loads but DMC firmware fails with "could not load binary firmware"; iwlwifi reports "File size way too small!" on a 2.4 MB blob.

Resolved by the symlink trick in freebsd-launchd commit 51e1b80: build.sh creates a symlink at cdroot/boot/firmware → /sysroot/boot/firmware. Kernel namei follows the symlink across the cd9660-to-unionfs mount-point boundary into the unionfs view, finds the file, firmware loads. Verified empirically on a Lenovo with iwlwifi-8260: net.wlan.devices populates, wlan0 is created, DRM DMC firmware loads.

The symlink fix is overlay-mechanism-agnostic — works equally on unionfs and gunion. Backported to freebsd-livecd-unionfs and freebsd-livecd-gunion.

For the architectural alternative (gunion + reboot -r reroot, which would eliminate the chroot/namei split entirely instead of working around it), see the freebsd-livecd-gunion-reroot experimental plan. Lower priority now that the symlink trick works.

Phase 4 — performance polish FUTURE

Phase 5 — upstream contributions FUTURE

9. Open questions

RESOLVED — Coexistence with devd. devd is not installed on the live ISO. We replicate its essential parts case-by-case in launchd-driven daemons (kmodloader for kld loading, netconfigd for link-up DHCP). When/if devd comes back, multiple readers on /dev/devctl are kernel-supported; both daemons see the same events.
RESOLVED — Personality plists vs. devmatch. Phase 1d picked devmatch + linker.hints. Personality plists were the original plan; they shipped in 1a-c then got ripped out. The narrow gap (drm-kmod, VM additions) is closed with three vendor-map functions, not 700+ device IDs.
RESOLVED — AMD radeonkms vs. amdgpu split without curating IDs. radeonkms's upstream device list is frozen (Linux's radeon driver is in maintenance-only mode). Vendor it once as RadeonPCIIDs.h; everything not in the set defaults to amdgpu. Phase 1f delivered.
RESOLVED — Hybrid graphics (Optimus, switchable). The PCI scan finds both Intel and NVIDIA vendors; both kld sets load. Each driver attaches to its own card. Display routing policy belongs in a desktop layer, not in kmodloader.
Q. Firmware loading in live-ISO setup. — RESOLVED. The kernel firmware loader's try_binary_file() reads from /boot/firmware/ in kernel-thread context, which doesn't follow the chroot. Fixed by symlinking /boot/firmware on the cd9660 layer to /sysroot/boot/firmware: kernel namei follows the symlink across the unionfs mount-point boundary into the right view. See Phase 3 above.
Q. Phase 2 hot-plug parsing. /dev/devctl events are textual, newline-delimited, with a single-character type prefix (+ attach, - detach, ? nomatch, ! notify). Parser is straightforward; the design question is whether to re-fork devmatch on every nomatch event (simple, slow) or maintain our own incremental match state (complex, fast). Lean toward the former until profile says otherwise.
Q. PCI bridges that load drivers for downstream devices. If a bridge driver hasn't loaded yet at our scan time, devices behind it aren't visible to pciconf -l or devmatch. We may need a second pass after Phase 1's first kldload round — Phase 2's hot-plug source covers this incidentally (the bridge attaching emits a NOMATCH for its children).
Q. nvidia-driver license interaction. nvidia-drm-latest-kmod is a binary-only blob under restricted-distribution license. We accept the license at pkg install time via LICENSES_ACCEPTED=NVIDIA in build.sh. Distributing the resulting ISO has its own license obligations — out of scope for this plan; tracked at the project level.

10. References