← Back · ties together #67 hwregd settle, #176 busyState/waitQuiet, and #168 (EVFILT_MACHPORT / configd fold)
hwregd — Apple-shaped device matching & autoloadHow to converge NextBSD’s device-matching + driver-autoload off a standalone devctl-socket + timer daemon toward Apple’s event-driven model. The honest framing first: Apple does not have “no daemon” — it keeps a userland loader (kextd → kernelmanagerd) under launchd. What makes Apple “better than FreeBSD’s devd” isn’t the absence of a process; it’s that matching is in-kernel, match events + quiescence reach userland over Mach (not a socket + poll), and the loader is launchd-Mach-service-activated and waitQuiet-driven rather than a busy loop. So the goal here is to retire the standalone polling hwregd and re-home its work into that event-driven shape — not to chase zero daemons.
Guiding principle The end state is to port Apple’s (open-source) services to replace FreeBSD’s devd + devmatch — configd (+ a KernelEventMonitor-style plugin) for device-event observation/registry, and a loader derived from Apple’s open-source kextd (the kernelmanagerd predecessor) adapted to drive kldload — not to keep a NextBSD-invented hwregd. hwregd is transitional: its jobs get re-homed into those ported Apple services and it is then deleted. The FreeBSD load primitives (kldload/kldxref/kldstat) stay underneath for now — Apple’s loader calls them. Verified open-source status (Apple kext_tools): kextd (full daemon source) + kextload/kextstat/kextutil/kextcache are open; kernelmanagerd and kmutil are not published. So the loader is ported from open-source kextd, adapted to kldload. kmutil being closed is moot — its job (kext/prelinked collections) isn’t needed; linker.hints/kldxref is the analog. The Apple-named kext CLIs (kextload/kextstat/kextunload) are deferred — NextBSD has no kexts at this stage; the kld* CLIs stay.
Naming decision — 2026-06-03 The ported loader keeps the name kextd (not kernelmanagerd): it is the actual APSL project being ported (provenance-honest), role-accurate, and the lowest novel trademark exposure — applying the closed-source kernelmanagerd name to kextd-derived code would be the riskier and less accurate move. Revisit kernelmanagerd only if the daemon later grows that broader scope. (Trademark review across all Apple-named daemons still advised before release.)
IOService::registerService() → startMatching() → doServiceMatch() → probeCandidates() runs probe/score/start in kernel config threads. (XNU iokit/Kernel/IOService.cpp)_adjustBusy() tracks busyState and propagates to the provider up to the root; waitQuiet() / IOServiceWaitQuiet / IORegistryEntryGetBusyState block/read it. (NextBSD’s #176 work is the analog.)IOServiceAddMatchingNotification delivers kIOFirstPublishNotification/kIOMatchedNotification/kIOTerminatedNotification as Mach messages on an IONotificationPort — this is Apple’s devctl analog, but Mach-native. (IOKitLib; IONotificationPortGetMachPort)com.apple.kernelmanagerd (+ kernelmanager_helper); the older kextd was removed around Catalina/Big Sur when Apple moved to the Signed System Volume + Boot Kernel Collections (kmutil). The open-source kextd (in kext_tools) is still the best documented reference for the pattern — kernelmanagerd is closed-source. That pattern: a launchd job with a MachService bound to a host special port (HostSpecialPort 15), RunAtLoad + KeepAlive={Crashed} — not a poll loop. The kernel queues a load request and pings the daemon over a Mach host special port (pingIOKitDaemon → kextd_ping) only when a needed binary isn’t in-kernel; the daemon loads it and tells IOCatalogue, which re-drives in-kernel matching. (Modern third-party drivers also moved to userspace DriverKit dexts via driverkitd/sysextd, outside the kernel entirely.) (XNU OSKext.cpp, kext_tools; modern: kernelmanagerd(8))Takeaway: the “better than FreeBSD” deltas are (1) Mach match-notifications instead of a /dev/devctl socket, (2) kernel busyState/waitQuiet instead of a wall-clock settle, and (3) a launchd-Mach-activated loader instead of an always-running poll daemon.
hwregd really owns)hwregd is a standalone two-thread daemon that is doing three jobs at once — all of which must survive if the process goes away (src/hwregd/hwregd.c):
/dev/devctl, runs the linker.hints matcher (the dropped-devmatch replacement), and kldloads under devctl_freeze()/devctl_thaw(); the backlog→live flip is a 250 ms timer today (#67/#176 replace that with quiescence).hw.bus.* + PCI enrichment (its own tree, not configd’s SCDynamicStore).org.freebsd.hwregd, MIG subsystem 30000): raw SUBSCRIBE pub/sub + get_root/children/node/properties/lookup + watch/unwatch + load_driver. Consumers: libIOKit (its IOServiceAddMatchingNotification facade calls hwreg_watch) and DiskArbitration (raw subscribe). launchd HardwareMatch is a planned (iter-2) consumer.Constraints found in-tree: configd’s loop is a single mach_msg over a port set and cannot also watch a device-event fd without EVFILT_MACHPORT (the #168 blocker, documented verbatim in KernelEventMonitor). launchd has Mach on-demand activation (a job starts when its MachService is messaged) but the HardwareMatch device-event driver is unimplemented. mach.ko has the event-bell pset→fd bridge and now the busyState hook + mach_wait_quiet (#176), but no kernel-native match-notification surface — today the IOKit notifications are synthesized in userland by hwregd.
How FreeBSD’s devd/devmatch stack maps to Apple’s components, what each does, and where it lands under this plan.
| FreeBSD | What it does | Apple equivalent & how Apple handles it | NextBSD target (this plan) |
|---|---|---|---|
devd | Userland daemon on /dev/devctl; runs devd.conf rules on attach/detach/nomatch (kldload, run scripts, notify). | No single daemon. Match-lifecycle events are delivered as IOKit notifications over Mach (IOServiceAddMatchingNotification → IONotificationPort); configd/clients subscribe. | mach.ko emits match-notifications over Mach (Phase 2); configd-hosted observers subscribe. The devd socket+rules model goes away. |
devmatch | Matches unmatched devices against linker.hints PNP tables and kldloads the right module (called by devd on nomatch). | Matching is in-kernel (IOService::registerService → probe/score/start vs IOCatalogue personalities); the binary load is a launchd-activated loader daemon (kextd → kernelmanagerd) the kernel pings on demand. | The PNP-match + load relocates from hwregd into the ported kextd-derived loader (driving kldload), woken by Phase 2 notifications + waitQuiet. (.ko matching stays userland unless later pushed in-kernel.) |
/dev/devctl | Kernel character device emitting device-event lines (?/+/-/!). | Mach notification port — the kernel sends Mach messages; no char-device socket. | Replaced by mach.ko match-notifications over Mach (Phase 2). |
linker.hints(built by kldxref) | The PNP→module + dependency database the matcher/loader consult. | IOCatalogue — in-kernel kext personalities (IOKitPersonalities matching dicts from each kext’s Info.plist). | Kept — it’s the FreeBSD analog of IOCatalogue; kldxref stays as build/install plumbing. |
kldload / kldunload / kldstat | Load / unload / list kernel modules (syscalls + CLIs). | kextload/kextunload CLIs + the kextd/kernelmanagerd daemon load via OSKext; kextstat lists. | kld* retained as the load primitive (the loader daemon calls them). Apple-named kext* CLIs deferred (no kexts yet). |
kldxref | Builds linker.hints from each module’s MODULE_PNP_INFO/dependency metadata. | kmutil/kextcache build prelinked / kext collections; personalities load into IOCatalogue. | Kept as build/install plumbing; no collection builder needed (NextBSD has no collections). |
| (quiescence) none today — hwregd 250 ms timer + devctl_freeze/thaw | Decide “devices have settled” before/around loading. | IOService busyState (_adjustBusy, propagates to the root) + waitQuiet / IOServiceWaitQuiet. | #176: kernel device_match_start/end hook → mach.ko bus_busy + mach_wait_quiet (the busyState/waitQuiet analog). |
| newbus probe/attach (in-kernel match of built-in/loaded drivers) | Probe/score/attach drivers already present in the kernel. | IOService match engine — in-kernel (same role). | Unchanged — newbus stays the in-kernel matcher; we add the Mach notification + quiescence surfaces around it. |
Note: FreeBSD splits matching across kernel (newbus, for already-present drivers) and userland (devmatch, to decide which not-yet-loaded module to kldload). Apple does all matching in-kernel (IOCatalogue) and only the binary load in userland. NextBSD keeps the .ko PNP match in userland (the ported loader) for now, matching FreeBSD’s split rather than Apple’s — pushing it fully in-kernel is a larger, optional later step.
| Block | Apple equivalent | NextBSD status |
|---|---|---|
| In-kernel matching | IOService match engine | ✅ newbus already matches in-kernel (loading is userland on both) |
Quiescence (busyState/waitQuiet) | _adjustBusy/waitQuiet | 🟡 #176 in flight — kernel hook merged; mach.ko bus_busy + mach_wait_quiet + libIOKit APIs is the pending consumer PR |
| One event loop over Mach + fds | kqueue + EVFILT_MACHPORT | ❌ #168 — needs ~5-line event.h base patch (define EVFILT_MACHPORT + bump EVFILT_SYSCOUNT) + a mach.ko-registered filter (kqueue_add_filteropts, reusing the event-bell pset seam). mach.ko event-bell bridge is a working stopgap. |
| Match events over Mach | IOServiceAddMatchingNotification | ❌ synthesized in userland by hwregd today; no kernel-native delivery |
| Event-driven loader under launchd | kextd Mach-service-activated | 🟡 launchd has Mach on-demand activation; HardwareMatch dispatch unimplemented |
| Option | What it is | Apple-fidelity | Cost / verdict |
|---|---|---|---|
| A. Fold hwregd into configd (in-process plugin) | Move the matcher + registry + notification-serving into configd; EVFILT_MACHPORT lets its one loop watch its Mach service port + the device-event source. Apple hosts comparable plumbing inside configd. | High | Needs EVFILT_MACHPORT (#168). Removes the standalone process; preserves the serving surface. Lead route |
B. launchd-activated loader (ported kextd) | Replace hwregd’s long-lived loop with a launchd Mach-service-activated loader, woken by a kernel ping / match-notification; no resident poller. This is the piece that replaces FreeBSD’s devd + devmatch load path. | High (kextd’s role; = kernelmanagerd on modern macOS) | Needs kernel match-notifications-over-Mach + launchd HardwareMatch iter-2. Complements A (A hosts registry/serving; B does on-demand loads). NOTE: Apple matches in-kernel (IOCatalogue) so its kernelmanagerd only loads; for .ko the PNP match (linker.hints/devmatch) stays in userland, so our loader also carries that match step unless it’s later pushed into the kernel. |
| C. In-kernel matcher + loader | Push autoload into the kernel itself. | Not even Apple (Apple loads in userland) | Reject — more kernel surface than warranted; diverges from Apple. |
Converge to the Apple shape as a blend of A + B over shared kernel building blocks, not a single big rewrite:
waitQuiet (#176) and kernel-native match-notifications over Mach (a mach.ko surface emitting publish/matched/terminated, replacing hwregd’s userland synthesis).EVFILT_MACHPORT (#168) so a single kqueue loop can watch a Mach service port + device events — the prerequisite for hosting matching anywhere but a dedicated daemon.EVFILT_MACHPORT, and make the actual driver load the ported kextd loader, launchd-Mach-activated (Option B) instead of an in-loop kldload poll. The standalone hwregd process goes away; every responsibility it owns (autoload, registry, libIOKit + DiskArbitration serving) is preserved, now event-driven.Net result: device appears → kernel matches → busyState/quiesce + match-notification over Mach → configd-hosted logic (or a launchd-activated loader) reacts and loads on demand — no devctl-socket poll daemon, no wall-clock settle. That is “better than FreeBSD” and faithful to Apple.
Each phase follows the established pattern (kernel hook first as a nextbsd-kernel patch → consumer PR in nextbsd → boot-test gate), and each lands value on its own.
waitQuiet (#176). Kernel device_match_start/end hook is merged (nextbsd-kernel #8). The consumer PR (mach.ko bus_busy + mach_wait_quiet + hwregd quiescence-flip + libIOKit APIs) is staged. Land it as the foundation — every later phase’s loader uses waitQuiet.EVFILT_MACHPORT (#168). nextbsd-kernel: ~5-line sys/sys/event.h patch (define EVFILT_MACHPORT, bump EVFILT_SYSCOUNT). nextbsd: mach.ko registers the filter at load (kqueue_add_filteropts) reusing the pset-signal seam; notify-only semantics. Unblocks single-loop multiplexing in configd.IOServiceAddMatchingNotification analog), so userland stops synthesizing them from /dev/devctl.EVFILT_MACHPORT); make autoload a launchd-Mach-activated loader (Option B, driven by Phase 2 notifications + waitQuiet). Migrate the libIOKit + DiskArbitration consumers to the new host. Remove the hwregd process.kextd/kernelmanagerd. The win is event-driven + launchd-activated, not zero processes. Phase 3 may end with a launchd-activated loader job rather than literally nothing.org.freebsd.hwregd serving surface back libIOKit and DiskArbitration today — Phase 3 must re-home all of it (into configd) before the process can be deleted, or those consumers break.EVFILT_MACHPORT) are independent and can proceed in parallel. This is a multi-week effort; the value is incremental (each phase is shippable).nextbsd-kernel patch or a mach.ko-registered hook (the match hook, the ~5-line EVFILT_MACHPORT reservation, the notification emitter) — no in-kernel module loader (Option C), consistent with Apple keeping loads in userland.kextd daemon — adapting its load backend, not keeping the .kext machinery. Port the open-source APSL kextd itself (its launchd check-in, Mach-service receive loop, and kernel-request handling from kext_tools) and swap the load backend from kext-loading to kldload + linker.hints matching (the matching/load logic that lives in hwregd today). What we do not port is the .kext-specific machinery kextd assumes — IOCatalogue personalities, code-signing/SIP, prelinkedkernel/AuxKC, kmutil — those are replaced by FreeBSD’s newbus + linker.hints + kldload. (And we do not port kernelmanagerd — it’s closed-source; kextd is the open predecessor we port.) Apple’s “loader tells IOCatalogue → re-drive matching” maps to FreeBSD’s built-in kldload → BUS_DRIVER_ADDED re-probe. The .kext bundle path IS a NextBSD goal — converting its drivers to .kext bundles installed in /System/Library/Extensions (built from FreeBSD module source, not unmodified Apple binaries). So the kext machinery is deferred-and-planned (its own conversion effort), not rejected; this convergence’s active backend is kld.kextd around a small load-backend seam (“load the driver matching this device”) with a kld backend now. The intended end state is .kext bundles in /System/Library/Extensions (see the separate .ko→.kext conversion plan). That conversion is decoupled from the loader and may land sooner/independently: the bundle format + Info.plist IOKitPersonalities can be established first, with the interim kld loader reading the bundles, and kextd/IOCatalogue following. Realistic model = .kext bundles wrapping FreeBSD .ko (kld-loaded), not a full XNU OSKext reimplementation.kextload/kextunload/kextstat/kextfind) are thin shims, not ports. Their function already exists as FreeBSD kldload/kldunload/kldstat. Where Apple-name familiarity is wanted, provide thin wrappers over kld* + the libIOKit/hwregd registry (e.g. kextstat = an Apple-style formatter over kldstat; kextfind = an ioreg-style query). kextutil is mostly .kext/codesign validation (N/A); kextcache/kmutil build prelinked/AuxKC collections that don’t exist on FreeBSD. The closest analog is kldxref building linker.hints — which is build/install-time plumbing (run by build.sh after laying down modules), independent of hwregd and unaffected when it’s retired. Note the three distinct linker.hints touchpoints: (1) build = kldxref (stays, plumbing); (2) kernel dependency autoload = kern_linker (in-kernel, unaffected); (3) the device→module matcher (the devmatch role) = currently in hwregd, and one of the responsibilities Phase 3 relocates into the configd plugin / launchd loader (it moves, it doesn’t disappear). These belong with the Apple-userland-cmds effort (#72/#115), not the loader-daemon convergence.Convergence plan, 2026-06-03. Umbrella over #67 (settle-window), #176 (busyState/waitQuiet), and #168 (EVFILT_MACHPORT / configd fold). Built from a 3-agent scope of Apple XNU/IOKit + kext_tools, the NextBSD hwregd/configd/launchd/mach.ko tree, and FreeBSD releng/15.0 kqueue internals.