← Back · companion to Eliminating standalone hwregd (convergence plan) & hardware registry / IOKit plan
We can now auto-load a driver kext when its hardware appears — the open question is where the matching decision lives. This doc lays out the single architectural fork that everything else hangs off, with enough FreeBSD/Apple ground truth to decide together. The crux, stated plainly: in Apple’s design the kernel matches drivers to devices and merely asks a userland daemon to load the code; a userland matcher (which is what our current hwregd + libIOKit is) is a deliberate divergence, not a faithful port.
The one decision Driver–device matching can live (B) in the kernel — the Apple-faithful answer — or (A/C) in userland — quicker to build, but a divergence from how macOS actually works. (“Quicker” = implementation effort only. At runtime the in-kernel matcher B is actually faster and earlier: it matches in-kernel at probe time with no IPC and binds drivers before userland is even up, where a userland matcher needs a per-device device→daemon round-trip and can’t bind anything until it’s running.) Everything else (dropping hwregd, dropping devd/devmatch, kextd as a loader, IOKitPersonalities on kexts) follows from this one choice. Pick this first.
Decided — 2026-06-05 Option B: in-kernel IOKit registry + matcher (Apple-faithful). The kernel owns the registry + IOCatalogue + matching; kextd is reduced to a passive loader that loads the bundle the kernel names. hwregd and devd/devmatch are retired as the kernel becomes the registry. Phase 0 (mandatory, cheap) is a kernel lock-safety proof-of-concept — prove a match can trigger a deferred kldload without deadlocking under the bus lock — before committing to the full subsystem. Shared prerequisites (ko2kext emits IOKitPersonalities from MODULE_PNP_INFO; matcher honors IOPCIPrimaryMatch) start in parallel since they’re needed regardless and carry no kernel risk.
Phase 0 PASSED — 2026-06-06 The throwaway PoC built + booted: real unmatched devices (drmn0/psm0/vga0) fired device_nomatch, the deferred taskqueue task ran and acquired the linker lock off the bus lock with no deadlock. The matcher’s “match → deferred load request” mechanism (K3) is viable. (nextbsd-kernel PR #14, closed unmerged; #212.)
The concern was: would “in-kernel IOKit” mean PNP/device IDs are compiled into the kernel, so a new device needs a kernel rebuild? No. Two things are easy to conflate:
device_t tree dynamically as buses enumerate; it is already exposed to userland via the hw.bus sysctl (the /sys-like tree our hwregd snapshots). No per-device code.8086:24f3) lives with the driver, as data: Apple puts it in the kext’s IOKitPersonalities (Info.plist); FreeBSD compiles it into the driver as MODULE_PNP_INFO tables. Adding a new device = editing a kext’s personality, never the kernel.“In-kernel IOKit” only moves the registry + matching engine into the kernel (like XNU’s IOCatalogue). The IDs still come from kext personalities loaded into it — the kernel hardcodes none. So in every option below, supporting new hardware is a kext change. The fork is purely where the registry and matcher run.
This is the finding that reframes the whole decision. In macOS/XNU, IOKit is in-kernel end to end:
IORegistryEntry / IOService) are created as a bus controller discovers hardware and publishes a nub (e.g. IOPCIDevice).IOKitPersonalities at boot/install.IOService: class match → passive (property) match → active probe() with probe-score arbitration. The highest score wins and is start()ed.The userland daemon — historically kextd, now kernelmanagerd — is a passive loader. Apple’s own kext_tools source: it “simply loads exactly what the kernel requests by bundle ID… a passive executor of kernel directives, not an intelligent matching engine.” It registers a Mach service, pushes personalities into gIOCatalogue (IOCatalogueSendData), and answers kernel load-requests (kKextRequestPredicateRequestLoad) by loading the named bundle’s executable. It does not match.
Reference load flow (1) daemon pushes all kext personalities into the in-kernel IOCatalogue at boot → (2) bus discovers a device, kernel creates a nub → (3) kernel matches the nub against IOCatalogue, picks the winner by probe score → (4) if that driver’s code isn’t resident, kernel requests it by bundle ID → (5) daemon loads the executable, tells the catalogue → (6) kernel start()s the driver. The kernel decides; userland fetches.
Consequence: an Apple-faithful NextBSD puts matching in the kernel and reduces kextd to a loader. A userland matcher is a real architecture — but it splits device-tree authority (kernel) from binding authority (userland), loses the kernel’s atomic match locking, and can’t bind critical-path devices until userland is up. It should be chosen with eyes open, documented as a divergence.
/sys)Concretely, the registry is an in-kernel, in-memory tree: one node per device or service, each holding a property dictionary (key/value pairs). It is built live as buses enumerate hardware (nothing on disk) and torn down on unplug. The closest mental model is the device model behind Linux’s /sys — the same kind of data (a tree of devices with properties), just reached through an API rather than a mounted filesystem.
| The in-kernel tree | How userland reads it | The match database | |
|---|---|---|---|
| Linux | driver core / kobject tree | /sys (sysfs filesystem) | modalias + driver tables; udev (userland) |
| Apple | IORegistry (IOService nodes) | IOKit APIs / ioreg(8) | IOCatalogue (kext personalities), in-kernel |
| FreeBSD | newbus device_t tree | hw.bus sysctl / devinfo(8) | driver MODULE_PNP_INFO + devmatch |
A node for your t420’s WiFi is just a path in the tree plus a property bag — pure data describing the hardware found (it carries no logic, and no IDs are hardcoded in the kernel):
IORegistry (in kernel)
└─ pci0
└─ IOPCIDevice@4:0:0 <- one registry node = one device
IOProviderClass = "IOPCIDevice"
vendor-id = 0x8086 (Intel)
device-id = 0x24f3 (Wireless-AC 8260)
class-code = 0x028000 (network controller)
IONameMatch = "pci8086,24f3"
(no driver bound yet)
Beside the registry sits the IOCatalogue — the union of every kext’s IOKitPersonalities. IntelWiFi.kext’s personality is also just data, and this is where device IDs live:
IntelWiFi.kext / Info.plist -> IOKitPersonalities -> "IntelWiFi"
IOProviderClass = "IOPCIDevice"
IOPCIPrimaryMatch = "0x24f38086" <- device 0x24f3, vendor 0x8086
IOClass = "if_iwlwifi"
IOProbeScore = 400
The matcher is the kernel code that, when the registry gains the 8260 node, walks the IOCatalogue, finds the personality whose IOPCIPrimaryMatch equals the node’s vendor-id/device-id, and — since that driver’s code isn’t resident — asks kextd to load IntelWiFi.kext. So an “in-kernel IOKit registry” is really three cooperating pieces:
/sys-like data).newbus already gives us #1 (the device_t tree + discovery) and a probe/attach engine close to #3. What Option B adds is a registry representation with property bags, an IOCatalogue holding externalized personalities (vs newbus’s compiled-in ID tables), and the matcher + the kernel→kextd load request. That delta is the build.
The good news: newbus is structurally close to IOKit, and already in-kernel. A faithful IOKit matcher is closer to a superset of newbus than a from-scratch subsystem.
| Apple IOKit (in-kernel) | FreeBSD newbus (in-kernel) |
|---|---|
| IORegistry (IOService plane) | device_t tree (sys/kern/subr_bus.c) |
nub (IOPCIDevice) | child device_t created by the parent bus driver |
probe() → probe score | DEVICE_PROBE → priority (BUS_PROBE_*) |
start() | DEVICE_ATTACH |
| IOCatalogue (external personality data) | per-driver MODULE_PNP_INFO ID tables (compiled into the driver) |
| IOKitPersonalities (Info.plist, data) | pci_device_id tables (C code, in the .ko) |
What’s already exposed to userland today: hw.bus.info (generation count) + hw.bus.devices.<gen>.<idx> (one struct u_device per node: name, desc, driver, pnpinfo, location, parent, state); the /dev/devctl event stream (+ attach / - detach / ? nomatch / ! notify); and /dev/pci PCIOCGETCONF for vendor/device IDs. The matching substrate already exists; the question is who consumes it.
The one structural difference that matters: newbus keeps match data compiled into each driver (linker-set ID tables, read at runtime via linker.hints by devmatch); IOKit keeps it as external data (personalities) in a kernel catalogue. A faithful port means giving NextBSD kexts IOKitPersonalities and an IOCatalogue-equivalent to hold them — rather than relying on the driver’s compiled-in PNP table.
What exists, and what it implies:
hwregd (src/hwregd/, ~1700 LOC) — a userland daemon that snapshots hw.bus into an in-memory registry, enriches PCI IDs from /dev/pci, watches /dev/devctl, and serves the registry over Mach-RPC (+ a watch API). This is the un-Apple piece — Apple has no userland registry daemon; the registry is the kernel.libIOKit (src/libIOKit/) — implements IOServiceMatching/IOServiceGetMatchingServices/IORegistryEntryCreateCFProperties/notifications, but as a client of hwregd. Matching keys are currently limited (IOProviderClass, IONameMatch); property matches like IOPCIPrimaryMatch are not yet honored.OSKextCopyPersonalitiesArray() is implemented (reads IOKitPersonalities from a kext’s Info.plist), but OSKextSendPersonalitiesToKernel() / RemovePersonalities() are deliberate no-op stubs — there is no in-kernel IOCatalogue to send to.IOKitPersonalities yet — kexts are personality-less today (the “personalities” half of #179 is unstarted). if_iwlwifi.ko does carry MODULE_PNP_INFO (its PCI IDs), so matching data can be generated from the driver.HardwareMatch job directive (parsed, not yet wired). Dropping hwregd must account for it.So today: nothing auto-loads (kextload is manual), and the matching infrastructure that does exist is the userland-matcher shape that diverges from Apple.
libIOKit in-process (no daemon)Move hwregd’s newbus reading (hw.bus snapshot + /dev/pci + /dev/devctl watch) into libIOKit, so IOServiceMatching/notifications work with no separate daemon. kextd links libIOKit, matches IOKitPersonalities itself, and kextloads. Drops the hwregd daemon + Mach-RPC.
Build an IORegistry/IOCatalogue-equivalent and a three-phase matcher in the kernel, hooked into newbus device_probe_and_attach(). Personalities are pushed in from userland (an IOCatalogue-style interface) or read from loaded kexts’ metadata. kextd becomes a passive loader answering kernel load-requests by bundle ID — exactly Apple’s shape.
kextd reads newbus directlyA single kextd daemon listens on /dev/devctl, reads hw.bus, matches personalities (or reuses the linker.hints/devmatch logic), and kextloads. Drops hwregd; retires the reusable libIOKit matching layer (other consumers get their own path).
| Aspect | A — libIOKit in-process | B — in-kernel IOKit | C — kextd-direct |
|---|---|---|---|
| Apple-faithful? | Divergent userland matches | Faithful kernel matches | Most divergent |
| Kernel changes | None | ~1000–1500 LOC new subsystem | None (opt. re-probe hook) |
| Userland code | ~800–1000 LOC (into libIOKit) | kextd loader ~ small; opt. catalogue IPC | ~1000–1200 LOC (kextd) |
| Drops hwregd | Yes (logic moves in-process) | Yes (kernel is the registry) | Yes |
| Lock/risk | Medium (in-proc concurrency) | High (kldload under bus locks — needs a deadlock-safety PoC; likely deferred-taskqueue load) | Low (userland) |
| Rough effort (build time) | ~2–3 wk | ~6–8 wk (+ mandatory lock PoC) | ~3–4 wk |
| Runtime / boot binding | in-process match; needs libIOKit running; not before userland | Fastest in-kernel at probe time, no IPC, binds before userland is up | slowest: per-device devctl→daemon round-trip; only after userland is up |
| libIOKit API survives | Yes | Yes (reads kernel registry) | No (retired) |
All three need the same two prerequisites regardless: kexts must carry IOKitPersonalities (generate them from MODULE_PNP_INFO in ko2kext — the #179 personalities work), and the matcher must honor real device-property keys (IOPCIPrimaryMatch et al., encoded device-high/vendor-low, e.g. 0x24f38086).
This is a “faithful but big” vs “pragmatic but divergent” call. Two coherent ways to sequence it:
Recommended Phase the faithful target; don’t skip it. (1) Land the shared prerequisites now — teach ko2kext to emit IOKitPersonalities from MODULE_PNP_INFO, and make the matcher honor IOPCIPrimaryMatch. (2) Stand up a minimal kextd as a launchd Mach-service loader (the half that’s identical in every option). (3) Do a kernel lock-safety proof-of-concept for Option B — can a device’s match trigger a deferred kldload without deadlocking under the bus lock? If yes, build the in-kernel matcher (the Apple-faithful end state) and retire hwregd as the registry moves into the kernel. The in-process/userland matcher (A) is the fallback only if B’s lock PoC proves intractable.
Why not just ship the userland matcher (A/C) and call it done? Because the stated north star is Apple-faithful, and the agents confirmed a userland matcher is a genuine architectural divergence, not a smaller version of the same thing. Shipping A/C as “the” design quietly bakes the divergence in; shipping A/C explicitly as an interim while B’s feasibility is proven keeps the faithful target alive. Either is defensible — but it should be a chosen trade, which is the point of this doc.
Note this doc’s sibling, the convergence plan, currently leans userland (port Apple’s kextd + a configd event plugin over FreeBSD kld). This doc’s new contribution is the explicit in-kernel option B with effort/risk, so the two can be weighed as one decision rather than defaulting to userland by omission.
Apple’s split — tiny in-kernel core (IOKit runtime + xnu) + a few boot kexts prelinked into the kernelcache + everything else loaded on match — maps onto NextBSD as the table below. First, the load-bearing point: configd / IPConfiguration don’t care how a driver loaded. They see em0 via the routing socket / getifaddrs whether its driver is compiled into the kernel or a loaded kext. So none of this is required for networking to function — it’s required only for the Apple-faithful shape.
| Layer | NextBSD today | Target (Apple-faithful) |
|---|---|---|
| Boot-critical: root storage (ahci / nvme / virtio), console | compiled into the kernel (#180) | stays in-kernel — NextBSD’s equivalent of Apple’s prelinked boot kext collection; chicken-and-egg (need the disk to read a kext) |
| IOKit/Mach + linuxkpi/wlan infrastructure | compiled in (COMPAT_MACH, COMPAT_LINUXKPI + device wlan) | stays in-kernel (the framework drivers bind against) |
WiFi (if_iwlwifi) | already a kext (IntelWiFi.kext) — not a built-in device | kext, auto-loaded by kextd |
NICs (em…), sound, GPU (drm/i915/amdgpu), USB peripherals | built into the kernel (GENERIC device lines) | become kexts — auto-loaded on match (#179) |
You cannot have both. A built-in device em claims the PCI NIC at boot, so a separate em.kext would hit “already attached.” Converting a driver to a kext therefore means removing its device line from the NEXTBSD kernel config and shipping it as a kext — an either/or, per driver.
Ordering — do not invert kextd auto-load must work before any built-in driver is removed. Pull device em out before matching+load works and the machine has no network (nothing loads it). So: (1) decide the matching location (§6) → (2) build kextd → (3) convert non-boot drivers to kexts one at a time, verifying each auto-loads and removing its device line only as the kext path is proven. Never remove a built-in before its kext loads.
Net trajectory: the kernel shrinks toward boot-critical + IOKit/Mach/linuxkpi infrastructure, while the driver fleet (NICs, sound, GPU, USB, WiFi) lives in /System/Library/Extensions as kexts that kextd binds on match — the Apple shape. WiFi already demonstrates it; em is the natural first built-in to convert once kextd exists.
IOKitPersonalities in Info.plist, not binary-extracted ones; and the build image has no kldxref/devmatch (proven), so parsing the .ko’s MODULE_PNP_INFO was a dead end. ko2kext -p now injects a personalities block, and gen-iwlwifi-personalities.sh derives the IOPCIPrimaryMatch id list straight from the iwlwifi driver’s own pci_device_id table in /usr/src — the shipped IntelWiFi.kext carries 85 Intel ids (incl. 0x24f38086, the 8260). Inert until K3.kextd (APSL provenance) per the convergence plan’s decision.device_nomatch → taskqueue → linker_file_foreach) acquired the kld lock off the bus lock and booted clean — deferred load from the match path is deadlock-free. The K3 mechanism is viable.IOKitPersonalities and pushes a flat match-record to the kernel; the kernel never parses XML. Detailed in §9.HOST_KEXTD_PORT, not /dev/devctl. The faithful channel is a host special port kextd checks in as (Apple’s actual mechanism), with the kernel sending a Mach {load, bundle-id} message — not devd’s broadcast device. A freebsd-src + Mach-layer agent review (2026-06-06) confirmed feasibility: the HOST_KEXTD_PORT slot is pre-defined, kernel-originated Mach send is already proven by ipc_notify, and newbus auto-re-probes the device once the kext loads. Detailed in §9 (K3b).With the forks resolved (§8), here is the concrete build for the in-kernel matcher on mechanism (a) — userland parses personalities and pushes them to the kernel; the kernel matches and requests; userland loads. The guiding constraint: no XML/CF parsing in the kernel, and reuse FreeBSD plumbing that already exists (newbus, /dev/devctl notify, the kldload re-probe) rather than inventing upcall infrastructure. This keeps the new kernel surface small — a flat catalogue store plus a matcher in the device_nomatch path — which is what makes it build- and boot-verifiable in CI.
End-to-end flow (mechanism a) (1) boot: kextd scans /System/Library/Extensions, parses each Info.plist’s IOKitPersonalities, and pushes a flat match-record per personality to the kernel IOCatalogue via an ioctl. (2) a bus enumerates a device with no built-in driver → newbus fires device_nomatch. (3) the PoC-proven hook defers to a taskqueue (off the bus lock) and the kernel matcher builds the device’s match key (0x<device><vendor> from pci_get_device/vendor), searches the catalogue, and picks the highest IOProbeScore. (4) the kernel emits a devctl notify naming the winning CFBundleIdentifier. (5) kextd reads /dev/devctl, finds that bundle, and kextloads it. (6) kldload registering the driver makes newbus re-probe the waiting device automatically — it attaches. The kernel decided; userland fetched.
newbus already is the in-kernel device tree (the device_t hierarchy, exposed via the hw.bus sysctl — §3). K1 is therefore not a parallel registry; it is a thin definition of how a nub yields its match keys. For a PCI device that is pci_get_vendor() / pci_get_device() (and subvendor/subdevice for secondary matches). No new tree, no new storage — the smallest phase. (A future cosmetic IORegistry-named view over newbus is optional and not on the critical path.)
A small kernel subsystem holding a list of match-records. Each record is a flat C struct, not an OSDictionary: { char bundle_id[]; uint32_t provider_class; int32_t probe_score; uint32_t *primary_match; size_t nmatch; } — i.e. exactly the data the matcher needs, decoded in userland. Ingestion is an ioctl on a new control device /dev/iocatalogue (the moral equivalent of Apple’s IOCatalogueSendData): kextd parses the bundle plist and submits one encoded record per personality. The kernel copies it in, validates sizes, links it. A read-only hw.iokit.catalogue sysctl dumps the store for debugging and gives hwregd/launchd HardwareMatch a backend during the transition (so nothing loses its data source mid-flight — §8 decision 2). The kernel parses no XML — the defining property of mechanism (a). CI-verifiable: boot, kextload a bundle, watch its personality appear in the sysctl — provable in QEMU without any Intel NIC.
device_nomatch path shippedThe matcher hangs off the lock-safe hook the Phase 0 PoC proved: device_nomatch → taskqueue. In the deferred task it builds the device’s 0x<device><vendor> key (high-16 device, low-16 vendor) from pci_get_device/vendor, scans the K2 store for a record whose IOPCIPrimaryMatch contains it, and selects the maximum IOProbeScore. An IOCATIOCLOOKUP ioctl exposes that same lookup so it can be verified deterministically. Done + t420-verified: on the real 8260, kextd -l 0x24f38086 → org.nextbsd.kext.intelwifi score 10000. The agent review confirmed the hook is safe — device_nomatch is invoked under Giant (a sleepable context, subr_bus.c:693), so reading PCI ivars and taskqueue_enqueue from the handler are both fine.
On a match the driver code isn’t resident, so the kernel must ask userland to load it by bundle id. The faithful channel is not /dev/devctl (that is devd’s broadcast event device, the very thing §8 retires, and the agent review confirmed it can’t target one daemon). It is the mechanism Apple actually used: a host special port that the kext daemon checks in as, to which the kernel sends a Mach message — XNU’s HOST_KEXTD_PORT. The freebsd-src + Mach-layer review found this is almost entirely already present:
HOST_KEXTD_PORT = 15 is pre-defined in sys/sys/mach/host_special_ports.h (alongside HOST_BOOTSTRAP_PORT); realhost.special[] stores it. The only gap is the userland-set whitelist at compat/mach/mach_traps.c:308, which currently allows only HOST_BOOTSTRAP_PORT — one line to also admit HOST_KEXTD_PORT.compat/mach/ipc/ipc_notify.c (port-deleted/destroyed notifications) already originates a Mach message from the kernel to a userland port via ipc_kmsg_get_from_kernel + ipc_mqueue_send. K3b composes the same pieces into one send_to_kextd(bundle_id) helper. The one constraint — a valid thread context (current_task) — is satisfied because the matcher runs in a taskqueue worker, not interrupt context.kld — NextBSD’s KXLD analog), the review confirmed driver_module_handler → devclass_driver_added → bus_generic_driver_added → device_probe_and_attach automatically re-probes the waiting DS_NOTPRESENT device (subr_bus.c) — no rescan needed (a DEV_RESCAN/BUS_RESCAN path exists as a fallback).Flow: kextd allocates a receive port, host_set_special_port(HOST_KEXTD_PORT, send_right), and blocks on mach_msg receive → kernel matcher (taskqueue) reads realhost.special[HOST_KEXTD_PORT] and sends {load, bundle-id} → kextd loads via OSKext→kld → newbus attaches the device. No devd, no /dev/devctl. This is what makes kextd a persistent daemon (replacing the U1 on-demand push) and is the piece that also fixes boot timing: push-triggers-match — when kextd IOCATIOCADDs a personality the kernel re-checks already-unmatched devices against it, so the order kextd starts in no longer races device enumeration.
Remaining unknown Everything above is grounded in source, but two things are only provable on the way in: (1) a self-contained kernel→userland Mach-send PoC (modeled on ipc_notify, fired from a taskqueue) should be run first — the Phase-0 equivalent for K3b — before wiring the matcher to it; (2) the real device_nomatch → send → load → attach of the 8260 is a t420 test. CI can build + boot both halves and exercise the Mach round-trip with a synthetic request, but not the physical bind.
The kextd daemon (name per §8 decision 4) with two passive jobs. (1) Push — enumerate /System/Library/Extensions, parse each Info.plist’s IOKitPersonalities via the OSKext engine (CF is fine in userland), encode + push each to /dev/iocatalogue. Done + t420-verified: /usr/libexec/kextd -v pushes the IntelWiFi match table and it appears in hw.iokit.catalogue on the real 8260. (2) Listen (K3b) — register the HOST_KEXTD_PORT receive right and block on mach_msg; on a kernel {load, bundle-id} message, load that bundle through the same OSKext path kextload uses (#199/#204). It performs no matching; it executes what the kernel names. The push half currently runs on demand (a boot-time RunAtLoad daemon wedged launchd’s boot dispatch — a CF/OSKext job that early is too heavy); the listener half (2) is what makes kextd a persistent daemon, and push-triggers-match (§K3b) removes the boot-ordering constraint that forced the deferral.
| Phase | New kernel code | CI can prove (build + QEMU boot) | Needs t420 (8260) |
|---|---|---|---|
| K1 registry | negligible (reuse newbus) | builds; hw.bus already works | — |
| K2 IOCatalogue done | flat store + /dev/iocatalogue ioctl + sysctl | done — kext loaded, personality in the sysctl (also on t420) | — |
| K3a matcher + lookup done | catalogue search in the deferred hook + IOCATIOCLOOKUP | done — lookup resolves 0x24f38086→IntelWiFi (also on t420) | — |
| K3b request channel | HOST_KEXTD_PORT whitelist (1 line) + a kernel send_to_kextd() modeled on ipc_notify + push-triggers-match | builds + boots; the Mach round-trip exercised with a synthetic request | the actual NIC bind |
| U1 kextd push done / listen | none (userland); listener = Mach receive loop | push populates the catalogue (done, t420); load-on-request via a synthetic message | the real auto-load of IntelWiFi.kext |
K2, K3a, and the U1 push are done and t420-verified. K3b’s Mach round-trip can be exercised with a synthetic request in QEMU, but the payoff — an unmatched 8260 auto-loading IntelWiFi.kext and attaching wlan0 — is provable only on the t420. Each phase lands and boots green on its own, with the hardware test gating only the final bind.
Sequencing K2 → U1 push → K3a matcher + lookup (all done) → K3b: first a kernel→user Mach-send PoC (modeled on ipc_notify, from a taskqueue), then wire the matcher’s request to HOST_KEXTD_PORT + add kextd’s receive loop + push-triggers-match → hardware test on the t420 (8260 auto-loads IntelWiFi.kext). Only after that bind is proven does driver→kext conversion (D1, §7) begin — per §7’s rule, never remove a built-in device line before its kext auto-loads. hwregd retires (C1) once hw.iokit.catalogue answers the queries it backed.
Apple / XNU: IOKit Fundamentals (Architectural Overview; Driver and Device Matching), iokit/IOKit/IOCatalogue.h, iokit/Kernel/IOService.cpp, iokit/Kernel/IORegistryEntry.cpp; kext_tools/kextd_main.c & kextd_request.c; kernelmanagerd(8)/kmutil(8) man pages; IOKitUser IOKitLib.h. PCI ID encoding (device-high/vendor-low, e.g. 0x24f38086) community-corroborated — verify against IOPCIFamily before final.
FreeBSD: sys/kern/subr_bus.c (device_handle_nomatch @693; driver_module_handler→devclass_driver_added→bus_generic_driver_added→device_probe_and_attach auto-reprobe of DS_NOTPRESENT children; devctl2 DEV_RESCAN/BUS_RESCAN), sys/sys/bus.h, sys/kern/kern_devctl.c (devctl is broadcast-only), sys/sys/module.h, sys/sys/linker.h; Architecture Handbook “Newbus.”
NextBSD Mach layer (kernel→kextd feasibility, agent review 2026-06-06): sys/sys/mach/host_special_ports.h (HOST_KEXTD_PORT = 15 pre-defined), sys/sys/mach/host.h (realhost.special[]), compat/mach/mach_traps.c (host_set_special_port trap; whitelist @308), compat/mach/ipc/ipc_notify.c (proven kernel-originated Mach send), compat/mach/ipc/ipc_kmsg.c (ipc_kmsg_get_from_kernel/copyin_from_kernel), compat/mach/ipc/ipc_mqueue.c (ipc_mqueue_send/deliver), compat/mach/kern/ipc_tt.c (HOST_BOOTSTRAP_PORT set/read template); userland src/libmach/mach_traps.c (host_set_special_port wrapper).
NextBSD: src/hwregd/, src/libIOKit/, src/kext_tools/kext.subproj/OSKext.c, src/launchd/src/core.c.
2026-06-06. Option B chosen; forks resolved. Phase 0 lock-safety PoC PASSED. P1 (ko2kext IOKitPersonalities) done — the shipped IntelWiFi.kext carries the Intel match table (85 ids), derived from the driver source. IOCatalogue ingestion = mechanism (a), kextd pushes (§9). Shipped + t420-verified: K2 IOCatalogue, U1 push (kextd), K3a matcher + IOCATIOCLOOKUP (8260→IntelWiFi resolves on the real card). Decided for the remaining K3b: the kernel→kextd load request goes over a Mach HOST_KEXTD_PORT (not /dev/devctl), grounded by a freebsd-src + Mach-layer agent review (§9 K3b; slot pre-defined, kernel Mach-send proven by ipc_notify, newbus auto-reprobe confirmed). Next: a kernel→user Mach-send PoC, then K3b + push-triggers-match → t420 (8260) auto-load → C1 retire hwregd/devd → D1 driver→kext (§7). Companion: hwregd convergence plan.