NextBSD · Driver delivery · kld / kext / IOKit

kld, kext, or IOKit? Scoping how NextBSD delivers drivers

A sidebar to the monorepo plan. Should NextBSD reconstruct every driver as a "kext"? Only firmware drivers? A mixture of kexts and klds? And does any of it need Apple's in-kernel IOKit runtime? Short answer: a deliberate mixture — and no IOKit C++ runtime, which is already your direction. This unifies ~14 existing plans into one policy and refines it where the evidence points.

Investigated 2026-07-17 by seven parallel source-grounded agents (prior-art recon over your own docs + ko2kext tooling, firmware-without-pkg, devmatch & wired-memory, multi-version coexistence, driver-form policy, one-build-system integration, IOKit-runtime verdict). Provisional / not settled — this is the sidebar you flagged; IOKit is your open topic. Planning only, no code written.

Draft · sidebar to the monorepo plan This is the IOKit/kext breakout called out as unsettled in the monorepo plan's §12. It builds on your extensive existing work (the ko→kext conversion plan, the kld-to-kext catalog/naming, the in-kernel matcher feasibility, the nvidia kext + userland plans, the graphics plan, the input spike) rather than restarting the question. Where it disagrees with an older draft, it says so.

Verdict

0 · What's already decided 1 · The IOKit verdict (A/B/C) 2 · Your four reasons, honestly 3 · The three-tier form policy 4 · Matching: devmatch + your matcher 5 · Firmware without pkg 6 · Multi-version coexistence 7 · One build system (make extensions) 8 · Open items & honest tensions 9 · Decisions for you

0What's already decided (don't relitigate)

The recon read all ~14 of your planning docs plus the live ko2kext.sh + gen-*-personalities.sh tooling and CI. You've made and shipped more of this than the "should we do IOKit" framing implied. This doc treats the following as settled and builds on them.

  1. A kext wraps a verbatim, unmodified FreeBSD .ko; ELF stays ELF, kld is the real link/load engine, and kextload/stat/unload are thin front-ends over kldload/….
  2. Apple's in-kernel IOKit C++ runtime (OSMetaClass/OSObject/IOService RTTI/KXLD) is OUT — ruled out consistently for three months as a 3–5 engineer-year diversion.
  3. Device→driver matching runs in-kernel via a flat-C IOCatalogue + matcher on newbus's device_nomatch path, reading IOPCIPrimaryMatch/IOProbeScore from bundle personalities — built and hardware-verified on a T420 (0x24f38086 → IntelWiFi).
  4. Personalities are auto-generated from each driver's own device-id tables (the six gen-*-personalities.sh), never hand-curated — the hand-curated experiment was explicitly ripped out as unscalable.
  5. Firmware is bundled inside the kext (Contents/Resources/firmware/, symlinks dereferenced) and found by firmware(9) — offline, no pkg. Already implemented.
  6. Naming: org.nextbsd.{driver,filesystems,crypto,net,security,kpi,compat,firmware,kext}.*; CFBundleExecutable = the real KMOD= name; nvidia = NVIDIAGraphics<NNN> per branch.
  7. Base-owned, never pkg — kexts ride the NextBSD-kernel-extensions package into /System/Library/Extensions; nvidia userland is staged from the vendor tarball with an LLVM-free libgbm closure guard (to stay off mesa→llvm19).
The one thing NOT to relitigate Whether to implement Apple's in-kernel IOKit C++ runtime. Settled NO across every doc, and the shipped design is unambiguous. Any "IOKit" in NextBSD is the flat-C matcher bolted onto newbus — already built — not the object runtime. §1 confirms this holds under fresh analysis.

1The IOKit verdict — A, B, or C

The vocabulary is the trap: in NextBSD, "kext" = delivery/matching/versioning wrapper over a kld. That is separable from the IOKit runtime. You can keep the entire kext model and never link a line of libkern C++.

OptionWhatAssessment
ANative kld + devmatch (and your thin flat-C matcher) + personality/selector veneer + firmware-in-.ko/bundle. No IOKit runtime.Recommended — and what you already built. Delivers all four goals. You keep the Apple-shaped bundle, /System/Library/Extensions, IOKitPersonalities, Apple-named CLIs, the in-kernel matcher, and the HOST_KEXTD_PORT load channel — all on unmodified kld.
BA + a minimal source-level IOKit-compat layer for one subsystem (HID or a graphics family), to reuse Apple family source.No taker. You use evdev/hid and drm-kmod, not Apple family source — so it would cost a libkern-C++/OSObject subsystem (the very wired memory goal 4 forbids) for a capability nothing consumes. Skip.
CFull in-kernel IOKit runtime — OSMetaClass, IOService, libkern C++, IORegistry object graph, real OSKext Mach-O linking.Never justified for these goals. 3–5 engineer-years, duplicates newbus (two probe/attach engines fighting over device_t), and regresses goals 1 and 4. Only a hard pivot to reusing Apple kernel driver source verbatim would justify it — that's ravynOS abandoning the FreeBSD kernel for XNU, a different project.

Why the graphics case refutes rather than proves the need: GPUs sound like where IOKit earns its keep, but FreeBSD already runs modern Intel/AMD/Radeon — full KMS, /dev/dri, the hardest driver class — through drm-kmod + LinuxKPI with zero IOKit, and your nextbsd-graphics-plan does exactly that: IOGraphics.kext is drm.ko renamed. For nvidia the KMS bridge is nvidia-drm bound to drmn via MODULE_DEPEND — a LinuxKPI/newbus story. The hardest remaining piece (Wayland) is a userland GBM backend port, orthogonal to any kernel driver model.

2Your four reasons, mapped honestly

You gave four motivations for the kext model. All are legitimate — here's the lightest mechanism for each, and where IOKit is load-bearing (spoiler: nowhere).

GoalLightest mechanismIOKit runtime needed?
1. Offline firmware, no fwget/pkgfirmware(9): blob in the image (raw /boot/firmware/ on FreeBSD 15, or .incbin'd .ko), resolved by name at attach.No Apple's OSKextRequestResource is worse here — async, daemon-served, not on the early-boot path.
2. HW→driver+firmware matchingdevmatch + MODULE_PNP_INFO, or your flat-C matcher reading auto-derived personalities. Same match key (0xDDDDVVVV).No Familiar vocabulary, not a better engine. Your own history proves it: the literal IOKit/IOPCIMatch port was ripped out for devmatch because plists "don't scale."
3. nvidia multi-version coexistenceVersioned bundles inert on disk + a selector that autoloads exactly one by matched GPU.No On-disk coexistence is packaging. The hard limit (one nvidia-named module resident) is a linker-namespace wall IOKit doesn't escape; your design makes OSBundleLibraries decorative and keeps MODULE_DEPEND authoritative.
4. On-demand load, cut wired memorykld autoload on match; boot-critical drivers stay compiled-in/preloaded.No — runtime is a NET COST An in-kernel IOKit permanently wires the class registry, per-node OSDictionary, the full IOCatalogue, and IOWorkLoop threads — resident regardless of load. That directly contradicts goal 4 (your K2 dodged it on purpose: "flat C struct, not an OSDictionary").
Honest correction on goal 4 The wired-memory payoff is real but modest — tens of MB, not hundreds. Measured: on FreeBSD 14.3 a stripped 15 MB kernel showed the same 120 MB wired as GENERIC, because the vm_page array (~42 MB, scales with installed RAM) + UMA/buffer overhead dominate; driver text is a minority. Input+ethernet together are only a few MB. The genuinely large drivers (nvidia ~30–40 MB, amdgpu+fw ~15–30 MB, zfs ~7 MB) are already loadable in stock FreeBSD — you get those savings without any new tiering. And idle drivers can't be unloaded (attached device → EBUSY; devmatch never unloads), so the lever is "never load the unused driver," not "unload after idle." Meaningful on 256 MB–1 GB embedded targets; marginal on a 16 GB desktop. Lead the case with version-coexistence, the IOKit-shaped registry, and modularity/attack-surface instead.

3The three-tier form policy

Your questions — everything a kext? only firmware? a mixture? drop redundant klds? — answered. The answer is a mixture with an explicit rule, and it sharpens your existing 343-vs-131 split.

All three forms run the same unmodified .ko through the same kld linker. The choice is about packaging cost, so pay a cost only when the driver has the matching need:

P1 boot-essential (root FS / boot console / boot bus)? ── YES ─▶ COMPILED-IN (or loader-preloaded) │ NO P2 has-firmware? OR P3 needs-multi-version? ── YES ─▶ KEXT (personality + firmware + version select) │ NO everything else ─────────▶ PLAIN KLD (devmatch autoload; NO personality)
ClassFormDeciding property
Root/boot storage (ahci, nvme, virtio_blk, cam), root FS (ufs), core buses (pci/acpi/cpu), console (vt), USB core+HCscompiled-in (root HBA + root FS may be loader-preloaded)P1 — needed before mountroot / devmatch
Ethernet (common: em/igb, re, bge, virtio_net)plain kldno firmware, no versioning → devmatch autoloads. The cleanest "not a kext" class.
Input / HID (hidbus, hkbd/ukbd, hms/ums, touch); small in-base HID leavesplain kld (tiny leaves already compiled-in per your input spike)hotplug, no firmware
Audio, bluetooth (unless fw), pseudo-devices, linuxkpi shimplain kldnone of P1–P3 → default tier
Wi-Fi (iwlwifi, rtw88/89, ath, mt76)kextP2 — essentially every part needs a firmware blob
Firmware ethernet (ice+DDP, cxgbe, mxge)kextP2 — firmware
GPU/DRM (amdgpu, i915, radeonkms)kextP2 firmware + P3 (drm 6.6/6.12 coexistence)
nvidia (NVIDIAGraphics<NNN> per branch)kextP3 multi-version (primary) + P2 (GSP fw on modern) — the canonical versioning case

The four sub-questions, answered

The biggest refinement to your existing plan Your catalog splits 474 modules into 343 kext candidates vs 131 compiled-in. The analysis says only ~50–60 of the 343 should be actual kexts (the firmware ∨ multi-version subset ≈ ~35 Wi-Fi + ~13 firmware NIC/storage + GPU + nvidia). The other ~250 should stay plain klds on native devmatch — no personality generation, no bundle. That confines every piece of bespoke kext machinery to the drivers that actually need it, and shrinks your open D1 (naming) decision to the ~50–60 real kexts instead of 343.

4Matching: devmatch + your in-kernel matcher are complementary

A genuine tension the research surfaced — handled by scoping, not by tearing anything out.

Native devmatch (reads MODULE_PNP_INFOlinker.hints, autoloads the matching .ko on a newbus NOMATCH event) already does device→driver matching for the whole plain-kld tier — including firmware drivers like iwlwifi/i915, which ship PNP tables too. It's free, authoritative, and handles PCI/USB/ACPI subtleties. So a personality generated for a plain kld would be a lossier copy fighting the real matcher.

Your in-kernel flat-C matcher (built, T420-verified) isn't redundant with devmatch — it serves what devmatch doesn't: feeding the kext autoload channel (HOST_KEXTD_PORT) and populating the IOKit-shaped registry that Gershwin/loginwindow read (the libIOKit facade, HardwareMatch). The clean division:

Net: keep your matcher — but point it at the ~50–60 kexts, and let devmatch carry the long tail. That's less personality-generation and less parallel-matcher surface than the current 343-wide plan, with no loss of capability.

5Firmware without pkg

The offline/no-pkg goal is already met by firmware(9); the kext bundling is curation on top.

The runtime firmware path is 100% local: a driver calls firmware_get("iwlwifi-cc-a0-77.ucode"); the kernel autoloads a firmware .ko that firmware_register()s the blob, or (new in FreeBSD 15.0) reads a raw file from /boot/firmware/. fwget/pkg are only an install-time delivery vehicle — never in the load path. So the no-pkg/offline goal is met simply by baking the blobs into the image.

6Multi-version coexistence — the honest split

The case that made you want a kext model. It works — for the coexistence you can actually have.

CaseFeasible?Mechanism
Sequential-per-boot — install 470/535/595 side-by-side, one loaded, auto-selected by the detected GPUYes — straightforward, no pkg, no IOKitVersioned .ko paths (your NVIDIAGraphics<NNN>.kext) + a PCI-ID selector reading supported-gpus.json legacybranchkldload the chosen branch; version-locked userland via the sibling NVIDIA<NNN>.bundle + symlink activation (your nvidia-activate.sh).
Simultaneous — two branches resident at once for two different-generation GPUsNo — impossible on any OSAll branches compile to the module name nvidia → the kernel linker permits one resident (EEXIST); one global /dev/nvidiactl + userland ABI. IOKit/DKMS/nvidia's own installer can't do it either. Real answers are external (one covering branch, or per-GPU VM passthrough). A documented wall, not a defect of your model.

Because pkg keys one version per origin and collides on shared paths, packages fundamentally can't deliver even the sequential case — which is exactly why your base-owned versioned-bundle model is the right call here. Just document the simultaneous-two-GPU limit in the selector (pick the newest branch covering the primary GPU; warn on an un-coverable secondary).

7One build system — make extensions

"Does buildkernel make klds? Do we need a make extensions target?" Yes, and yes — cleanly.

make buildkernel KERNCONF=NEXTBSD already builds the klds. Its all target = kernel-all + modules-all (per sys/conf/kern.post.mk), recursing into sys/modules; installkernel stages kernel + .kos into /boot/kernel. Tier-2 klds are already a native, meta-mode-incremental output. Nothing to add.

Kexts = built .ko + generated personalities + bundled firmware + OSBundleLibraries — exactly what ko2kext.sh + gen-*-personalities.sh already do as a post-build step. Formalize that into a new NextBSD-owned target:

# nextbsd/extensions/Makefile — invoked after buildkernel, keyed off the .ko objdir
extensions: .PHONY
.for k in ${KEXTS}                       # IntelWiFi i915 amdgpu NVIDIAGraphics595 ...
	@${MAKE} -f ${SRCTOP}/nextbsd/share/mk/bsd.kext.mk extension \
	    KEXT_NAME=${${k}_NAME} KEXT_ID=${${k}_ID} KO=${KODIR}/${${k}_KO} \
	    KEXT_PERSGEN=${SRCTOP}/nextbsd/tools/gen-${${k}_GEN}-personalities.sh \
	    KEXT_FW="${${k}_FW}" KEXT_DEPS="${${k}_DEPS}"
.endfor
installextensions: .PHONY
	cp -R ${KEXTDIR}/*.kext ${DESTDIR}/System/Library/Extensions/

8Open items & honest tensions

What the recon flags as genuinely unresolved (fair game), plus the tensions this analysis raises.

9Decisions for you

What I'd lock (with your nod)
  1. Option A confirmed — keep the full kext delivery/matching/versioning model; no IOKit C++ runtime (you'd already scoped C out — this just makes it explicit as the answer to the sidebar).
  2. Adopt the three-tier policy + the kext ⇐ firmware ∨ multi-version rule, and add the form column to the catalog as single source of truth with generated build-excludes (one form per device).
  3. Refine 343 → ~50–60 real kexts; let devmatch carry the ~250 plain klds; point your in-kernel matcher + personalities at the kext tier only.
  4. Formalize make extensions into the monorepo, ko2kext moved in, zero upstream edits.
Still genuinely open — worth a sidebar of their own

Every load-bearing claim traces to a source-cited research pass (FreeBSD firmware(9)/devmatch(8)/build(7) + real freebsd-src file:line, Apple IOKit/OSKext + Darling's userspace-only IOKit, nvidia/DKMS multi-version, and your own 14 planning docs + ko2kext tooling read directly). Provisional sidebar to the monorepo plan — IOKit remains your explicitly open topic. No code written, no repos changed in producing this.