← Back · ties together #67 hwregd settle, #176 busyState/waitQuiet, and #168 (EVFILT_MACHPORT / configd fold)

Eliminating standalone hwregd — Apple-shaped device matching & autoload

How 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 (kextdkernelmanagerd) 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.

2026-06-03. Synthesized from a 3-agent scope: Apple’s XNU/IOKit + kext_tools model, a NextBSD hwregd/configd/launchd/mach.ko audit, and an EVFILT_MACHPORT feasibility study. Citations are to XNU/Apple sources and nextbsd-redux/nextbsd / freebsd-src@releng/15.0.

Guiding principle The end state is to port Apple’s (open-source) services to replace FreeBSD’s devd + devmatchconfigd (+ 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 kldloadnot 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.)

Contents

  1. What Apple actually does
  2. What NextBSD does today (and what hwregd really owns)
  3. FreeBSD → Apple → NextBSD: component map
  4. The building blocks (and which exist)
  5. Convergence options
  6. Recommendation
  7. Phased roadmap
  8. Caveats

1. What Apple actually does

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.

2. What NextBSD does today (and what 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):

  1. Autoload: drains /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).
  2. The hardware registry tree: builds an in-memory newbus/IORegistry tree from hw.bus.* + PCI enrichment (its own tree, not configd’s SCDynamicStore).
  3. A Mach serving surface (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.

3. FreeBSD → Apple → NextBSD: component map

How FreeBSD’s devd/devmatch stack maps to Apple’s components, what each does, and where it lands under this plan.

FreeBSDWhat it doesApple equivalent & how Apple handles itNextBSD target (this plan)
devdUserland 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 (IOServiceAddMatchingNotificationIONotificationPort); configd/clients subscribe.mach.ko emits match-notifications over Mach (Phase 2); configd-hosted observers subscribe. The devd socket+rules model goes away.
devmatchMatches 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 (kextdkernelmanagerd) 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/devctlKernel 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 / kldstatLoad / 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).
kldxrefBuilds 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.

4. The building blocks (and which exist)

BlockApple equivalentNextBSD status
In-kernel matchingIOService 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 + fdskqueue + 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 MachIOServiceAddMatchingNotification❌ synthesized in userland by hwregd today; no kernel-native delivery
Event-driven loader under launchdkextd Mach-service-activated🟡 launchd has Mach on-demand activation; HardwareMatch dispatch unimplemented

5. Convergence options

OptionWhat it isApple-fidelityCost / 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.HighNeeds 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 + loaderPush autoload into the kernel itself.Not even Apple (Apple loads in userland)Reject — more kernel surface than warranted; diverges from Apple.

6. Recommendation

Converge to the Apple shape as a blend of A + B over shared kernel building blocks, not a single big rewrite:

  1. Keep matching in-kernel (newbus already does) and add the two kernel signals Apple has and we lack: busyState/waitQuiet (#176) and kernel-native match-notifications over Mach (a mach.ko surface emitting publish/matched/terminated, replacing hwregd’s userland synthesis).
  2. Add 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.
  3. Fold hwregd’s matcher + registry + serving into configd (Option A) using 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.

7. Phased roadmap

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.

  1. Phase 0 — busyState/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.
  2. Phase 1 — 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.
  3. Phase 2 — kernel-native match notifications over Mach. mach.ko emits publish/matched/terminated for newbus attach/detach/nomatch over a Mach notification port (the IOServiceAddMatchingNotification analog), so userland stops synthesizing them from /dev/devctl.
  4. Phase 3 — retire standalone hwregd. Fold its matcher + registry + serving into configd (Option A, on 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.

8. Caveats

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.