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.
kmodloader/ at the top of the launchd repo, alongside src/ (launchd) and configd/.kldload or kld_list= edits. GPUs auto-engage DRM/KMS. NICs come up. WiFi cards register. USB classes attach. VM guest additions load when in a VM.PNP_INFO macros (NICs, USB classes, ACPI HID, most storage). Zero curation; FreeBSD's kldxref already built the index.PNP_INFO). Routes by vendor: Intel → i915kms, NVIDIA → nvidia-drm, AMD → per-device split via a frozen radeonkms ID list vs. amdgpu default.linker.hints); we just have to use it. The narrow gap (drm-kmod) is closed with a three-vendor map, not 700+ device IDs.org.freebsd.vboxservice.plist (Phase 2+) which kldloads its own kmods before launching VBoxService — matching FreeBSD's rc.d/vboxservice pattern. open-vm-tools ships zero kmods; the kernel side (vmx) is in GENERIC. Phase 1g's hypervisor scan (briefly shipped) was reaching outside kmodloader's lane and got removed.RunAtLoad=true, KeepAlive=false. Phase 2 (planned) keeps the daemon alive on a DISPATCH_SOURCE_TYPE_READ source over /dev/devctl for hot-plug.kextd + IOKitPersonalities; the implementation that shipped is closer in spirit to FreeBSD's own devmatch. Clean-room ObjC throughout.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.
devmatch, pciconf, sysctl); we don't modify the kernel.kldload. We wrap it via NSTask.devd(8). devd handles non-kmod-load actions (running scripts on attach, configuring network on link-up). kmodloader is narrower: only kmod loading. devd's role — if we want it — is separate (and currently absent; we replicate its essential parts case-by-case in launchd-driven daemons)..ko files and load on demand.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.
FreeBSD's hardware drivers split cleanly into two categories by how match information is published:
| Category | Match data lives in... | Reachable via |
|---|---|---|
In-tree drivers with PNP_INFO | linker.hints (kldxref output) | devmatch(8) |
| drm-kmod GPU drivers + nvidia binary blob | Compiled-in C structs; not in linker.hints | PCI 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.
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.
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.
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.
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.
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.
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:
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.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.
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:
overlays/System/Library/LaunchDaemons/org.freebsd.kmodloader.plistoverlays/boot/loader.conf — sets hw.nvidiadrm.modeset="1"pkglist.txt — lists drm-latest-kmod, nvidia-drm-latest-kmod, wifi-firmware-*-kmod, virtualbox-ose-additions-nox11, open-vm-tools-nox11build.sh — chroot-side, runs gmake -C kmodloader install after launchd + configd buildsbuild.sh handles kmodloaderSame 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.
| Decision | Choice |
|---|---|
| Implementation language | Objective-C with GNUstep Foundation. Same substrate as launchd + configd. |
| Event loop (Phase 1) | None. One-shot run, exit. Phase 2 adds libdispatch. |
| Match data sources | Two: linker.hints via devmatch, pciconf -l for GPU PCI scan. (Removed: kern.vm_guest hypervisor scan — out of scope, see §3.3.) |
| Kmod load mechanism | NSTask wrapping /sbin/kldload -n <module>. The -n flag = "do not error if already loaded". |
| devmatch invocation | Bare /sbin/devmatch (no flags). NOT devmatch -a — that flag's output format is wrong for our use. |
| GPU vendor map | Three-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 modeset | Required 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 time | build.sh sets LICENSES_ACCEPTED=NVIDIA in the runtime-pkgs pkg install env. nvidia-drm-latest-kmod ships under restricted-distribution license. |
| Coexistence with devd | devd 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. |
<?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.
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.
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.
| Source | License | How we handle it |
|---|---|---|
| This repo's code (daemon + Makefile + scripts) | BSD-2-Clause | SPDX header in every file. |
RadeonPCIIDs.h generated table | Data — not copyrightable | Comment cites drm-kmod's drm_pciids.h as source. |
| GNUstep Foundation, libdispatch, libobjc2 (linked, not in tree) | LGPL/MIT/Apache as applicable | Listed in NOTICE; same calculus as launchd + configd. |
kmodloader/ created at the top of freebsd-launchd; placeholder Makefile.NOTICE updated.org.freebsd.kmodloader.plist in overlays.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.
KMRegistry / KMMatch / KMDevice / KMBusEnumerate classes.devmatch; parse one-kld-per-line output; kldload each.PNP_INFO).pciconf -l walk for class 0x0300xx, three-vendor map.nvidia-drm-latest-kmod to pkglist; added LICENSES_ACCEPTED=NVIDIA to build.sh's pkg env; added hw.nvidiadrm.modeset="1" to loader.conf./usr/sbin/devmatch but the actual binary is /sbin/devmatch — daemon had been silent no-op since shipped.radeon_PCI_IDS as RadeonPCIIDs.h (699 frozen device IDs).radeonkms; everything else → amdgpu.sysctl kern.vm_guest and kldloaded VBox / VMware additions.vboxvideo doesn't exist on FreeBSD; vmware_drv is an Xorg driver; open-vm-tools ships zero kmods.virtualbox-ose-additions-nox11, open-vm-tools-nox11) so the binaries are present for users and for the future Phase 2+ launchd plists.org.freebsd.kmodloader.plist to KeepAlive=true.KMDevctlSource: open /dev/devctl, DISPATCH_SOURCE_TYPE_READ, parse newline-delimited messages of the form !system=... type=ATTACH ... (event format documented in devd(8) source).type=ATTACH: re-run the appropriate match path. PCI device → check vendor, possibly run GPU map. devmatch path: re-run devmatch (it'll only emit klds for newly-unattached devices).type=DETACH: log only. Phase 1 doesn't unload anything; same default for Phase 2.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.
kldload via dispatch concurrent queues (multiple devices' klds load in parallel).kldstat -v shell-out: maintain the loaded-set in-process across the three paths so we don't re-fork for it.MODULE_PNP_INFO declarations from amdgpu's and radeonkms's compiled-in supported tables. If accepted upstream, devmatch handles GPUs automatically and Path 2 collapses to "i915kms only" or disappears entirely./dev/devctl are kernel-supported; both daemons see the same events.
RadeonPCIIDs.h; everything not in the set defaults to amdgpu. Phase 1f delivered.
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.
/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.
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).
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.
devmatch(8): man 8 devmatch — primary match-data tool.devctl(4): man 4 devctl — Phase 2 event source.kldxref(8) + MODULE_PNP_INFO(): man 8 kldxref, kernel docs in sys/sys/module.h.devd(8): man 8 devd — not used directly; reference for hot-plug semantics.include/drm/drm_pciids.h is the source for RadeonPCIIDs.h.fwget(8): man 8 fwget — auto-detects firmware packages needed for current hardware. Useful diagnostic during bench testing.