← Back · gates kextd porting / device-matching convergence (#177)
.ko → Apple .kext conversionConvert NextBSD’s kernel modules from FreeBSD .ko to Apple .kext bundles installed in /System/Library/Extensions — the Apple-shaped format, established before the kextd loader port so kextd has a bundle format to load. The honest scope: this is a format/packaging conversion (a .kext bundle wrapping the unmodified ELF .ko, still kld-loaded), not a port of XNU’s in-kernel kext machinery. Nobody has done this on a FreeBSD kernel before — ravynOS runs plain .ko, or switched to real XNU — so it’s NextBSD-original.
Verdict Achievable in weeks as a format conversion; not a kernel project. A .kext bundle wraps the unmodified FreeBSD .ko; kld loads it (it accepts a full path); the bundle’s Info.plist IOKitPersonalities feed the userspace matcher NextBSD already has (libIOKit/IOKitMatching.c → hwregd). The XNU path (real OSKext Mach-O linking, in-kernel IOCatalogue, AuxKC, codesign) is a 3–5 engineer-year diversion that collides with newbus — out of scope. We get the Apple shape on top of the unchanged FreeBSD KLD mechanism.
Two layers, sharply separated by the scoping:
.kext bundle directory + Info.plist (with IOKitPersonalities derived from the module’s PNP tables), installed in /System/Library/Extensions; the unmodified ELF .ko is the bundle’s “executable”; kld remains the linker (kldload <bundle>/Contents/MacOS/<name>, or extend kern.module_path into the Extensions tree). Matching is the userspace analog of IOCatalogue that already exists (libIOKit facade → hwregd registry).OSKext Mach-O kext linking, kmod_info, in-kernel IOCatalogue driving IOService::registerService, AuxKC/Boot-KC, codesign/SIP. None exist on FreeBSD; FreeBSD matching is newbus. This is the path ravynOS abandoned the FreeBSD kernel to get (its darwin branch builds real XNU).So the conversion buys the Apple-canonical format (bundles, personalities, /System/Library/Extensions, Apple-named CLIs) while the kernel still treats each as a plain FreeBSD KLD. Only the matching dictionary is derived from a module; the driver code stays an ELF .ko (it does not become a Mach-O kext).
NetworkDriver.kext/
└── Contents/
├── Info.plist # CFBundle* + OSBundle* + IOKitPersonalities
├── MacOS/
│ └── NetworkDriver # the unmodified FreeBSD .ko (renamed/symlinked)
└── Resources/ # firmware blobs (see §4), if any
Info.plist keys we generate per module (Apple KEXT docs / OSKextLib.h):
| Key | Source |
|---|---|
CFBundleIdentifier | synthesized reverse-DNS, e.g. org.nextbsd.driver.if_em (from the module name in MDT_MODULE) |
CFBundleExecutable | the .ko filename |
CFBundleVersion | from MODULE_VERSION (a single int → synthesized x.y.z — heuristic) |
CFBundlePackageType | KEXT |
OSBundleLibraries | from MODULE_DEPEND (name→bundle-id via a small table; version range collapses to one min — decorative, see §6) |
OSBundleRequired | only for the (few) drivers that may load early; most omit it (see §6) |
IOKitPersonalities | from MODULE_PNP_INFO (§3) |
Install target is /System/Library/Extensions per NextBSD’s Apple-shaped layout (on real macOS that volume is sealed/SIP-protected; on NextBSD’s own image it is writable). Third-party kexts would conventionally live in /Library/Extensions.
MODULE_PNP_INFO → IOKitPersonalitiesEach module embeds its match tables as a modmetadata_set ELF linker set of struct mod_metadata (MDT_MODULE/VERSION/DEPEND/PNP_INFO) (sys/sys/module.h) — the same data kldxref reads. A converter either re-implements that ELF scan (port kldxref’s helpers) or parses the linker.hints it already emits. Each MDT_PNP_INFO is a descriptor string ("M16:mask;U16:vendor;U16:device;…") + a binary table; one personality dict (or one OR-token) is emitted per table row.
| FreeBSD PNP (bus) | Apple personality | Quality |
|---|---|---|
PCI vendor+device | IOProviderClass=IOPCIDevice, IOPCIPrimaryMatch="0x{device}{vendor}" (device<<16 | vendor) | mechanical |
PCI subvendor+subdevice | IOPCISecondaryMatch (subsystem id) | mechanical |
PCI class/subclass (gated by M16:mask) | IOPCIClassMatch (with mask) | mostly |
USB vendor/product (bus uhub) | idVendor/idProduct (decimal), provider IOUSBHostDevice/Interface | mechanical |
USB class/subclass/proto, T:mode=host/device | bDeviceClass… / provider class selection | mostly |
ACPI Z:_HID/Z:_CID (strings) | IONameMatch against the ID string | mechanical |
PCI revid; M16 mask bit semantics | no clean Apple key | gap |
Worked example: if_em registers IFLIB_PNP_INFO(pci, em, em_vendor_info_array) with rows like PVID(0x8086, 0x100E, …) → one personality with IOPCIPrimaryMatch=0x100E8086. (sys/net/iflib.h, sys/dev/e1000/if_em.c) Heuristics/gaps: the single-int MODULE_VERSION, the MODULE_DEPEND name→bundle-id table, revid, BCD ranges, and ACPI _CID secondary IDs.
FreeBSD ships driver firmware two ways today, and Apple has one way — so the converter must bridge both:
| FreeBSD | What it is | → kext |
|---|---|---|
Model A — firmware as .ko (e.g. gpu-firmware-amd-kmod, USES=kmod) | kmod.mk .incbins the blob into a module whose MOD_LOAD calls firmware_register(name, data, len, ver); no PNP table. | a data/firmware kext: blob verbatim in Contents/Resources/, CFBundleVersion from the register version, no IOKitPersonalities. Detect by: MODULE_DEPEND(.., firmware,..) + no PNP + a single big _binary_* rodata blob. |
Model B — raw blobs in /boot/firmware (modern wifi-firmware-*-kmod, NO_BUILD) | not a module at all — just copies linux-firmware files into /boot/firmware. | flat firmware payload: bundle the blobs as Resources/ of the consuming/firmware kext (keyed by filename). Nothing to “convert” — it’s packaging. |
Apple model: firmware lives as files in a kext’s Contents/Resources/; the in-kernel driver requests it by name via OSKextRequestResource() — asynchronous, served by the userspace loader daemon (kextd/kernelmanagerd), so not available on the early-boot path. (XNU OSKextLib.h)
For NextBSD now: the underlying FreeBSD firmware(9) path still works (the .ko//boot/firmware mechanisms are unchanged); the kext is packaging. Re-targeting driver firmware_get() to an OSKextRequestResource-style daemon fetch is a later, kextd-era step. Either way, firmware-dependent drivers are post-boot/on-demand, consistent with the boot caveat below.
Combining drm-kmod + all GPU firmware into one bundle — “is it a linker thing? do I convert paths?” Neither, if we keep firmware(9):
firmware_get("amdgpu/<asic>_*.bin"). The driver is never statically bound to a blob; the “intelligence to know which firmware” is already in the driver — we add nothing.Resources/ under the firmware namespace (Resources/amdgpu/…) and register each via firmware_register(name,…) (a small registration step, or the existing gpu-firmware .ko logic, bundled). The driver’s firmware_get(name) resolves by name — driver unchanged, no remap. The registration namespace is the firmware-name namespace.OSKextRequestResource, which serves flat Resources/ filenames (no subdirectories) — then amdgpu/vega20_ce.bin must be flattened (amdgpu_vega20_ce.bin) and the fetch shimmed to remap. That’s a kextd-era choice; defer it. For now, firmware(9) by name = zero path conversion.drm.kext carrying the driver(s) + all-vendor firmware works but is large (hundreds of MB). Splitting per-vendor (amd/intel/radeon) uses the identical mechanism if size matters.kmutilNo Apple tool ever converted a foreign module into a kext — macOS modules were always kexts, and the one format change (NeXT loadable kernel servers → OS X IOKit) was a manual rewrite. So the wrap/convert step is genuinely novel; the closest precedent is kextlibs for the dependency half (§9). The right packaging is to build a single NextBSD orchestrator — reasonably named kmutil (clean-room reimplementation composing the open-source kext_tools pieces, not a port of the closed kmutil) — that subsumes the converter, dep-resolution, and collection-building under one Apple-shaped command. A build/CI step layered after bsd.kmod.mk produces each .ko — natural home is the nextbsd-kernel-modules build:
.ko as today.MODULE_VERSION, MODULE_DEPEND, every MDT_PNP_INFO table) by parsing the .ko (port kldxref’s scan) or its linker.hints.kextlibs’ symbol→OSBundleLibraries logic, but matched against our own kernel’s exported symbols (no Apple KDK needed). Note OSBundleLibraries is decorative here (§6) — MODULE_DEPEND/linker.hints stays authoritative — so this is for fidelity, not load order.Info.plist (§2) with IOKitPersonalities (§3); lay out Foo.kext/Contents/{Info.plist,MacOS/Foo,Resources/…}./System/Library/Extensions (firmware into the relevant kext’s Resources/).kld via kern.module_path/full-path; later: the ported kextd reads bundle personalities).kextcache logic) — ties to §8/#180.The conversion is a deterministic codegen step; it touches no kernel code. It can ship in nextbsd-kernel-modules independently of the kextd/hwregd work. The orchestrator unifies this converter (§5), the kext userland (§9), and the boot collection (§8) behind one kmutil:
nextbsd kmutil ├─ convert .ko → Foo.kext bundle ← novel (no Apple precedent) ├─ plist MODULE_PNP_INFO → IOKitPersonalities ← novel │ MODULE_DEPEND → OSBundleLibraries ← reuse kextlibs vs OUR kernel symbols ├─ collection prelink kexts → boot collection ← reuse kextcache (§8 / #180) └─ load/inspect ← kld backend (shared with #177 kextd)
kextd/kernelmanagerd runs; the daemon only does post-boot on-demand loads. NextBSD already has the faithful analog of that boot tier: drivers built into the NEXTBSD kernel (GENERIC) + .ko preloaded by the FreeBSD loader from /boot/kernel (loader.conf). So boot-critical drivers ride that tier (mirroring Apple’s bootloader/collection tier), and .kext bundles + the kextd-derived daemon are the post-boot, on-demand tier (mirroring Apple’s daemon tier). OSBundleRequired is load-bearing on Apple (it selects Boot-KC membership) but informational on NextBSD until a bundle-aware boot-collection / loader-preload mechanism exists (a possible future step: teach the loader to preload the .ko out of a .kext bundle, i.e. a real boot collection). launchd can’t do this tier — it’s PID 1, started only after root mounts, which already needs the storage driver; so boot-critical loading is inherently a bootloader / kernel-built-in job on Apple and FreeBSD, never a userland daemon. Shrinking loader.conf means building drivers into NEXTBSD or a loader-loaded collection, not moving the work to launchd.OSBundleLibraries is decorative. Real load ordering stays MODULE_DEPEND + linker.hints resolved by kern_linker; the Info.plist dep list is metadata only. Keep the FreeBSD side authoritative (don’t let the two diverge).kext_tools (§9) where that machinery doesn’t exist yet. Apple’s modern kextd/kextutil gate on signatures; we drop the check entirely and never add it back.probe/score/start (that’s newbus, and the XNU rework we’re not doing).Format conversion is a weeks-scale codegen + packaging change with no kernel work; the kernel-deep XNU path is explicitly out of scope. Sequencing relative to the convergence (#177):
waitQuiet (#176) + EVFILT_MACHPORT (#168) — format-agnostic kernel capabilities; proceed independently (in flight)..ko→.kext conversion — establishes the bundle format + personalities + /System/Library/Extensions, with the interim kld loader. Lands before kextd porting (kextd must load this format). Can start now; no dependency on the busyState/EVFILT phases.kextd loads the .kext bundles this conversion produces, driven by match-notifications + waitQuiet.kextstat/kextload/kextunload/kextutil as front-ends over kld; rides in this epic (they consume the bundles, so they don’t gate the converter, but they ship together so the system is genuinely Apple-shaped).nextbsd-kernel-modules repo (not a separate repo): convert drm-kmod and other pkg kmods with the same workflow. Option: one combined drm bundle carrying the driver(s) + all GPU firmware, resolved at runtime by the driver’s existing ASIC→name firmware_get (see §4).loader.conf (built-ins vs. kexts)Goal: no hand-maintained module list at boot. Grounded in what NextBSD actually loads today — kldstat shows only kernel + mach.ko, and overlays/boot/loader.conf.d/freebsd-launchd-mach.conf confirms why.
Boot-critical is already a non-issue. FFS and the disk drivers (ahci nvme virtio* mfi) are in GENERIC — built into the kernel image. The fragment’s own comment admits the *_load lines are “harmless if the running kernel already has any of them built in.” The bootloader hands control to the kernel, the kernel mounts root with its built-in FFS, then launchd (PID 1) starts. No daemon — launchd or kextd — can load the driver its own root filesystem depends on; that tier is inherently kernel/bootloader, exactly as on Apple.
loader.conf line today | Disposition |
|---|---|
ahci/nvme/virtio*/mfi_load="YES" | already in GENERIC → drop (built-in) |
mach_load="YES" | options MACH — compile mach into NEXTBSD (it’s boot-tier: launchd needs Mach before any on-demand load, and kextd is launched by launchd) |
vfs.root.mountfrom="ufs:/dev/ufs/ROOTFS" | options ROOTDEVNAME="ufs:/dev/ufs/ROOTFS" |
init_path="/sbin/launchd" | options INIT_PATH="/sbin/launchd" |
beastie_disable, autoboot_delay | loader-UI defaults — the only possible residue (a 2-line stub or accept defaults) |
hw.nvidiadrm.modeset="1" | a property on the on-demand nvidia kext |
“Don’t copy what we don’t need” is the same cleanup. The image carries the full FreeBSD /boot/kernel/*.ko set (hundreds of modules) yet loads only two. xz.ko is the canonical example — it serves geom_uzip/compressed images, but the boot path is plain UFS (“no cd9660, no uzip”), so it is never used → don’t ship it. The end model:
NEXTBSD (FFS + disk already are; add mach, ROOTDEVNAME, INIT_PATH)..kext, loaded on-demand by the kextd/devmatch-replacement (§1–7)..ko set → smaller image and nothing left to list.Repo split: the lever is nextbsd-kernel/config/NEXTBSD (today just a GENERIC passthrough — this is where the options land). The one real refactor is mach-into-kernel: mach builds out-of-tree as mach_kmod in nextbsd; options MACH means adding its sources + a files/config entry via the kernel patch series (never touching freebsd-src). nextbsd drops the *_load lines as they migrate; nextbsd-kernel-modules converts drivers to kexts and stops shipping dead .ko; nextbsd-freebsd-compat owns any residual boot-config/userland glue. Scoped under its own ticket; sequence it alongside the built-ins work, independent of the kextd port.
kext_tools)If we’re converting .ko→.kext, we port Apple’s tools too — not as polish but so the system is genuinely Apple-shaped. The whole family except kmutil is open source: kext_tools (APSL 2.0, on opensource.apple.com / apple-oss-distributions/kext_tools) ships the CLI + daemon source, and the bundle/load engine OSKext lives in IOKitUser (kext.subproj/OSKext.c, also APSL) — the same libIOKit lineage already being ported for device matching. Because the backend is kld, each tool is a thin front-end:
| Apple tool | Source | NextBSD port = front-end over… |
|---|---|---|
kextstat | kext_tools (APSL 2.0) | kldstat(2) — list loaded |
kextload | kext_tools | kldload(2) of the inner .ko + register IOKitPersonalities |
kextunload | kext_tools | kldunload(2) |
kextutil / kextfind | kext_tools | bundle validation / search over /System/Library/Extensions |
kextlibs | kext_tools | scan a built kext’s undefined symbols → compute OSBundleLibraries. Reusable in the converter (§5 step 4), matched against our kernel’s exported symbols — no Apple KDK needed. |
kextsymboltool | kext_tools | generate/merge kext symbol files (debug/link aid); paired with kextlibs for the dependency pass. |
kextd (daemon) | kext_tools | on-demand autoload, kld backend — #177 |
kextcache | kext_tools | collection builder — ties to #180 boot collection |
kmutil | not open source | present a kmutil-shaped front later if wanted; build from kext_tools |
Pick an older kext_tools tag on purpose. Apple publishes kext_tools per macOS release, and older revisions are substantially simpler — pre-SIP, pre-codesign-enforcement, pre-kext-collections, no kmutil. That is exactly the shape that maps onto a kld-backed loader, so an older kext_tools + matching IOKitUser is the better porting base than current main.
What we do not drop: kldload/kldstat/kldxref remain the engine — kextd calls kldload, kextstat reads kldstat, kldxref still builds linker.hints (its PNP role is supplemented by IOKitPersonalities, not replaced). The kext* tools become the front; kld stays the back. Fully removing kld would mean reimplementing XNU’s in-kernel OSKext loader — out of scope (§1).
Conversion plan, 2026-06-03. Gates the kextd port in the device-matching convergence (#177). Sourced from XNU OSKextLib.h + Apple KEXT/IOKit docs, freebsd-src@releng/15.0 (sys/sys/module.h, usr.sbin/kldxref, sys/sys/firmware.h, sys/conf/kmod.mk), the firmware ports, and a ravynOS/Darling/PureDarwin prior-art pass (none package .ko as .kext).