← Back · gates kextd porting / device-matching convergence (#177)

FreeBSD .ko → Apple .kext conversion

Convert 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.

2026-06-03. From a 3-agent code-grounded scope: Apple .kext/Info.plist/IOKitPersonalities + firmware (XNU/kext_tools), FreeBSD .ko metadata + firmware(9) (freebsd-src@releng/15.0, ports), and a prior-art/feasibility pass (ravynOS/Darling/PureDarwin + the NextBSD tree).

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.

Contents

  1. The achievable model (format, not kernel)
  2. Bundle layout + Info.plist
  3. MODULE_PNP_INFO → IOKitPersonalities
  4. Firmware
  5. The conversion workflow
  6. Caveats (boot, deps, codesign)
  7. Effort & sequencing
  8. Eliminating loader.conf (built-ins vs. kexts)
  9. kext userland tools (port from kext_tools)

1. The achievable model (format, not kernel)

Two layers, sharply separated by the scoping:

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).

2. Bundle layout + Info.plist

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):

KeySource
CFBundleIdentifiersynthesized reverse-DNS, e.g. org.nextbsd.driver.if_em (from the module name in MDT_MODULE)
CFBundleExecutablethe .ko filename
CFBundleVersionfrom MODULE_VERSION (a single int → synthesized x.y.zheuristic)
CFBundlePackageTypeKEXT
OSBundleLibrariesfrom MODULE_DEPEND (name→bundle-id via a small table; version range collapses to one min — decorative, see §6)
OSBundleRequiredonly for the (few) drivers that may load early; most omit it (see §6)
IOKitPersonalitiesfrom 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.

3. MODULE_PNP_INFOIOKitPersonalities

Each 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 personalityQuality
PCI vendor+deviceIOProviderClass=IOPCIDevice, IOPCIPrimaryMatch="0x{device}{vendor}" (device<<16 | vendor)mechanical
PCI subvendor+subdeviceIOPCISecondaryMatch (subsystem id)mechanical
PCI class/subclass (gated by M16:mask)IOPCIClassMatch (with mask)mostly
USB vendor/product (bus uhub)idVendor/idProduct (decimal), provider IOUSBHostDevice/Interfacemechanical
USB class/subclass/proto, T:mode=host/devicebDeviceClass… / provider class selectionmostly
ACPI Z:_HID/Z:_CID (strings)IONameMatch against the ID stringmechanical
PCI revid; M16 mask bit semanticsno clean Apple keygap

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.

4. Firmware

FreeBSD ships driver firmware two ways today, and Apple has one way — so the converter must bridge both:

FreeBSDWhat 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):

5. The conversion workflow — a NextBSD kmutil

No 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:

  1. Build the .ko as today.
  2. Extract metadata (module name, MODULE_VERSION, MODULE_DEPEND, every MDT_PNP_INFO table) by parsing the .ko (port kldxref’s scan) or its linker.hints.
  3. Classify: real driver (has PNP) vs firmware stub (Model A) vs plain payload (Model B).
  4. Resolve dependencies: reuse 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.
  5. Generate Info.plist (§2) with IOKitPersonalities (§3); lay out Foo.kext/Contents/{Info.plist,MacOS/Foo,Resources/…}.
  6. Install bundles into the image overlay at /System/Library/Extensions (firmware into the relevant kext’s Resources/).
  7. Point the loader at the Extensions tree (interim: kld via kern.module_path/full-path; later: the ported kextd reads bundle personalities).
  8. Optionally prelink a selected set into a boot collection (reuse 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)

6. Caveats

7. Effort & sequencing

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):

  1. busyState/waitQuiet (#176) + EVFILT_MACHPORT (#168) — format-agnostic kernel capabilities; proceed independently (in flight).
  2. This .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.
  3. kextd porting (device-matching convergence #177) — the ported kextd loads the .kext bundles this conversion produces, driven by match-notifications + waitQuiet.
  4. kext userland tools (§9) — port 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).
  5. 3rd-party kexts — same 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).

8. Eliminating 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 todayDisposition
ahci/nvme/virtio*/mfi_load="YES"already in GENERICdrop (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_delayloader-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:

  1. Boot-critical → baked into NEXTBSD (FFS + disk already are; add mach, ROOTDEVNAME, INIT_PATH).
  2. Real drivers → .kext, loaded on-demand by the kextd/devmatch-replacement (§1–7).
  3. Ship only what’s loaded/needed — trim the unused .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.

9. kext userland tools (port from 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 toolSourceNextBSD port = front-end over…
kextstatkext_tools (APSL 2.0)kldstat(2) — list loaded
kextloadkext_toolskldload(2) of the inner .ko + register IOKitPersonalities
kextunloadkext_toolskldunload(2)
kextutil / kextfindkext_toolsbundle validation / search over /System/Library/Extensions
kextlibskext_toolsscan 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.
kextsymboltoolkext_toolsgenerate/merge kext symbol files (debug/link aid); paired with kextlibs for the dependency pass.
kextd (daemon)kext_toolson-demand autoload, kld backend — #177
kextcachekext_toolscollection builder — ties to #180 boot collection
kmutilnot open sourcepresent 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 enginekextd 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).