Repository navigation
Expand file tree
/
Copy pathnextbsd-apple-device-matching-plan.html
More file actions
161 lines (147 loc) · 29.6 KB
/
Copy pathnextbsd-apple-device-matching-plan.html
File metadata and controls
161 lines (147 loc) · 29.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Eliminating standalone hwregd — Apple-shaped device matching & autoload (convergence plan)</title>
<style>
:root { --fg:#1a1a1a; --fg-muted:#555; --bg:#fafaf7; --accent:#b8472a; --accent-soft:#f3e7df; --border:#d8d4c8; --code-bg:#f0ece2; --table-stripe:#f4efe5; --warn:#8a5a00; --good:#2d6f3b; --bad:#a23030; }
* { box-sizing:border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,"Helvetica Neue",Helvetica,sans-serif; color:var(--fg); background:var(--bg); line-height:1.55; margin:0; padding:0; }
.wrap { max-width:880px; margin:0 auto; padding:48px 32px 96px; }
h1 { font-size:2.05rem; line-height:1.2; margin:0 0 8px; letter-spacing:-0.01em; }
h2 { font-size:1.4rem; margin:54px 0 12px; padding-top:18px; border-top:2px solid var(--border); }
h3 { font-size:1.12rem; margin:30px 0 10px; color:var(--accent); }
p { margin:0 0 14px; }
ul,ol { margin:0 0 14px 22px; padding:0; } li { margin:0 0 6px; }
code { font-family:"SF Mono",Menlo,Consolas,monospace; font-size:0.9em; background:var(--code-bg); padding:1px 5px; border-radius:3px; }
pre { font-family:"SF Mono",Menlo,Consolas,monospace; font-size:0.84em; line-height:1.5; background:var(--code-bg); border:1px solid var(--border); border-radius:4px; padding:14px 16px; overflow-x:auto; margin:0 0 14px; }
pre code { background:none; padding:0; }
.lede { font-size:1rem; color:var(--fg-muted); margin:0 0 28px; }
.meta { font-size:0.85rem; color:var(--fg-muted); margin:0 0 24px; }
table { border-collapse:collapse; width:100%; margin:12px 0 22px; font-size:0.92rem; }
th,td { text-align:left; padding:9px 12px; border:1px solid var(--border); vertical-align:top; }
th { background:var(--accent-soft); font-weight:600; }
tr:nth-child(even) td { background:var(--table-stripe); }
.callout { border-left:3px solid var(--accent); background:var(--accent-soft); padding:14px 18px; margin:18px 0 22px; border-radius:0 4px 4px 0; }
.callout p:last-child { margin-bottom:0; }
.callout-warn { border-left-color:var(--warn); background:#fbf3df; }
.callout-good { border-left-color:var(--good); background:#e8f1e3; }
.pill { display:inline-block; font-size:0.78rem; font-weight:600; text-transform:uppercase; letter-spacing:0.04em; padding:2px 8px; border-radius:10px; margin-right:8px; }
.pill-good { background:#d6ead0; color:var(--good); } .pill-warn { background:#f4dfbf; color:var(--warn); } .pill-bad { background:#f0c8c8; color:var(--bad); } .pill-neutral { background:#ddd; color:#333; }
.toc { background:white; border:1px solid var(--border); border-radius:4px; padding:18px 24px 14px 36px; margin:0 0 36px; font-size:0.95rem; }
.toc h2 { margin:0 0 8px; padding-top:0; border-top:none; font-size:1rem; text-transform:uppercase; letter-spacing:0.04em; color:var(--fg-muted); margin-left:-14px; }
.toc ol { margin:0 0 0 6px; } .toc li { margin-bottom:4px; }
.toc a { color:var(--fg); text-decoration:none; } .toc a:hover { text-decoration:underline; }
.back { font-size:0.9rem; margin-bottom:18px; } .back a { color:var(--accent); text-decoration:none; }
.footnote { font-size:0.85rem; color:var(--fg-muted); border-top:1px solid var(--border); margin-top:48px; padding-top:16px; }
.cite { font-size:0.82em; color:var(--fg-muted); }
</style>
</head>
<body>
<div class="wrap">
<p class="back"><a href="index.html">← Back</a> · ties together <a href="freebsd-hwregd-busystate-waitquiet-options.html">#67 hwregd settle</a>, <a href="freebsd-hwregd-busystate-impl-plan.html">#176 busyState/waitQuiet</a>, and #168 (EVFILT_MACHPORT / configd fold)</p>
<h1>Eliminating standalone <code>hwregd</code> — Apple-shaped device matching & autoload</h1>
<p class="lede">How to converge NextBSD’s device-matching + driver-autoload off a standalone devctl-socket + timer daemon toward Apple’s event-driven model. <strong>The honest framing first:</strong> Apple does <em>not</em> have “no daemon” — it keeps a userland loader (<code>kextd</code> → <code>kernelmanagerd</code>) under launchd. What makes Apple “better than FreeBSD’s <code>devd</code>” isn’t the absence of a process; it’s that <strong>matching is in-kernel, match events + quiescence reach userland over Mach (not a socket + poll), and the loader is launchd-Mach-service-activated and <code>waitQuiet</code>-driven rather than a busy loop.</strong> So the goal here is to retire the standalone <em>polling</em> <code>hwregd</code> and re-home its work into that event-driven shape — not to chase zero daemons.</p>
<p class="meta">2026-06-03. Synthesized from a 3-agent scope: Apple’s XNU/IOKit + kext_tools model, a NextBSD <code>hwregd</code>/configd/launchd/mach.ko audit, and an <code>EVFILT_MACHPORT</code> feasibility study. Citations are to XNU/Apple sources and <code>nextbsd-redux/nextbsd</code> / <code>freebsd-src@releng/15.0</code>.</p>
<div class="callout callout-good">
<p><span class="pill pill-good">Guiding principle</span> The end state is to <strong>port Apple’s (open-source) services</strong> to replace FreeBSD’s <code>devd</code> + <code>devmatch</code> — <strong>configd</strong> (+ a <code>KernelEventMonitor</code>-style plugin) for device-event observation/registry, and a loader derived from Apple’s open-source <strong><code>kextd</code></strong> (the <code>kernelmanagerd</code> predecessor) adapted to drive <code>kldload</code> — <em>not</em> to keep a NextBSD-invented <code>hwregd</code>. <strong><code>hwregd</code> is transitional</strong>: its jobs get re-homed into those ported Apple services and it is then <strong>deleted</strong>. The FreeBSD load primitives (<code>kldload</code>/<code>kldxref</code>/<code>kldstat</code>) stay underneath for now — Apple’s loader calls them. <strong>Verified open-source status (Apple <code>kext_tools</code>):</strong> <code>kextd</code> (full daemon source) + <code>kextload</code>/<code>kextstat</code>/<code>kextutil</code>/<code>kextcache</code> are open; <code>kernelmanagerd</code> and <code>kmutil</code> are <em>not</em> published. So the loader is ported from open-source <strong><code>kextd</code></strong>, adapted to <code>kldload</code>. <code>kmutil</code> being closed is moot — its job (kext/prelinked collections) isn’t needed; <code>linker.hints</code>/<code>kldxref</code> is the analog. The Apple-named kext CLIs (<code>kextload</code>/<code>kextstat</code>/<code>kextunload</code>) are <strong>deferred</strong> — NextBSD has no kexts at this stage; the <code>kld*</code> CLIs stay.</p>
<p><span class="pill pill-neutral">Naming decision — 2026-06-03</span> The ported loader keeps the name <strong><code>kextd</code></strong> (not <code>kernelmanagerd</code>): it is the actual APSL project being ported (provenance-honest), role-accurate, and the lowest novel trademark exposure — applying the <em>closed-source</em> <code>kernelmanagerd</code> name to <code>kextd</code>-derived code would be the riskier and less accurate move. Revisit <code>kernelmanagerd</code> only if the daemon later grows that broader scope. (Trademark review across all Apple-named daemons still advised before release.)</p>
</div>
<div class="toc">
<h2>Contents</h2>
<ol>
<li><a href="#apple">What Apple actually does</a></li>
<li><a href="#now">What NextBSD does today (and what hwregd really owns)</a></li>
<li><a href="#map">FreeBSD → Apple → NextBSD: component map</a></li>
<li><a href="#blocks">The building blocks (and which exist)</a></li>
<li><a href="#options">Convergence options</a></li>
<li><a href="#rec">Recommendation</a></li>
<li><a href="#roadmap">Phased roadmap</a></li>
<li><a href="#risks">Caveats</a></li>
</ol>
</div>
<h2 id="apple">1. What Apple actually does</h2>
<ul>
<li><strong>Matching is entirely in-kernel.</strong> <code>IOService::registerService()</code> → <code>startMatching()</code> → <code>doServiceMatch()</code> → <code>probeCandidates()</code> runs probe/score/start in kernel config threads. <span class="cite">(XNU <code>iokit/Kernel/IOService.cpp</code>)</span></li>
<li><strong>Quiescence is a kernel primitive.</strong> <code>_adjustBusy()</code> tracks busyState and <em>propagates to the provider</em> up to the root; <code>waitQuiet()</code> / <code>IOServiceWaitQuiet</code> / <code>IORegistryEntryGetBusyState</code> block/read it. (NextBSD’s #176 work is the analog.)</li>
<li><strong>Match events reach userland over Mach, not a socket.</strong> <code>IOServiceAddMatchingNotification</code> delivers <code>kIOFirstPublishNotification</code>/<code>kIOMatchedNotification</code>/<code>kIOTerminatedNotification</code> as Mach messages on an <code>IONotificationPort</code> — this is Apple’s <code>devctl</code> analog, but Mach-native. <span class="cite">(IOKitLib; <code>IONotificationPortGetMachPort</code>)</span></li>
<li><strong>The loader daemon exists but is event-driven.</strong> On <em>current</em> macOS this is <code>com.apple.kernelmanagerd</code> (+ <code>kernelmanager_helper</code>); the older <code>kextd</code> was <strong>removed</strong> around Catalina/Big Sur when Apple moved to the Signed System Volume + Boot Kernel Collections (<code>kmutil</code>). The open-source <code>kextd</code> (in <code>kext_tools</code>) is still the best <em>documented reference for the pattern</em> — <code>kernelmanagerd</code> is closed-source. That pattern: a launchd job with a <code>MachService</code> bound to a host special port (<code>HostSpecialPort 15</code>), <code>RunAtLoad</code> + <code>KeepAlive={Crashed}</code> — <em>not</em> a poll loop. The kernel queues a load request and <strong>pings the daemon over a Mach host special port</strong> (<code>pingIOKitDaemon</code> → <code>kextd_ping</code>) only when a needed binary isn’t in-kernel; the daemon loads it and tells <code>IOCatalogue</code>, which re-drives in-kernel matching. (Modern third-party drivers also moved to userspace <strong>DriverKit dexts</strong> via <code>driverkitd</code>/<code>sysextd</code>, outside the kernel entirely.) <span class="cite">(XNU <code>OSKext.cpp</code>, <code>kext_tools</code>; modern: <code>kernelmanagerd(8)</code>)</span></li>
</ul>
<p><strong>Takeaway:</strong> the “better than FreeBSD” deltas are (1) Mach match-notifications instead of a <code>/dev/devctl</code> socket, (2) kernel busyState/<code>waitQuiet</code> instead of a wall-clock settle, and (3) a launchd-Mach-activated loader instead of an always-running poll daemon.</p>
<h2 id="now">2. What NextBSD does today (and what <code>hwregd</code> really owns)</h2>
<p><code>hwregd</code> is a standalone two-thread daemon that is doing <em>three</em> jobs at once — all of which must survive if the process goes away <span class="cite">(<code>src/hwregd/hwregd.c</code>)</span>:</p>
<ol>
<li><strong>Autoload:</strong> drains <code>/dev/devctl</code>, runs the linker.hints matcher (the dropped-<code>devmatch</code> replacement), and <code>kldload</code>s under <code>devctl_freeze()</code>/<code>devctl_thaw()</code>; the backlog→live flip is a 250 ms timer today (#67/#176 replace that with quiescence).</li>
<li><strong>The hardware registry tree:</strong> builds an in-memory newbus/IORegistry tree from <code>hw.bus.*</code> + PCI enrichment (its own tree, not configd’s SCDynamicStore).</li>
<li><strong>A Mach serving surface</strong> (<code>org.freebsd.hwregd</code>, MIG subsystem 30000): raw <code>SUBSCRIBE</code> pub/sub + <code>get_root/children/node/properties/lookup</code> + <code>watch/unwatch</code> + <code>load_driver</code>. Consumers: <strong>libIOKit</strong> (its <code>IOServiceAddMatchingNotification</code> facade calls <code>hwreg_watch</code>) and <strong>DiskArbitration</strong> (raw subscribe). launchd <code>HardwareMatch</code> is a planned (iter-2) consumer.</li>
</ol>
<p>Constraints found in-tree: <strong>configd’s loop is a single <code>mach_msg</code> over a port set</strong> and <em>cannot</em> also watch a device-event fd without <code>EVFILT_MACHPORT</code> (the #168 blocker, documented verbatim in <code>KernelEventMonitor</code>). <strong>launchd has Mach on-demand activation</strong> (a job starts when its <code>MachService</code> is messaged) but the <code>HardwareMatch</code> device-event driver is unimplemented. <strong>mach.ko</strong> has the event-bell pset→fd bridge and now the busyState hook + <code>mach_wait_quiet</code> (#176), but <em>no kernel-native match-notification surface</em> — today the IOKit notifications are synthesized in userland by <code>hwregd</code>.</p>
<h2 id="map">3. FreeBSD → Apple → NextBSD: component map</h2>
<p>How FreeBSD’s <code>devd</code>/<code>devmatch</code> stack maps to Apple’s components, what each does, and where it lands under this plan.</p>
<table>
<thead><tr><th>FreeBSD</th><th>What it does</th><th>Apple equivalent & how Apple handles it</th><th>NextBSD target (this plan)</th></tr></thead>
<tbody>
<tr><td><code>devd</code></td><td>Userland daemon on <code>/dev/devctl</code>; runs <code>devd.conf</code> rules on attach/detach/nomatch (kldload, run scripts, notify).</td><td><strong>No single daemon.</strong> Match-lifecycle events are delivered as <strong>IOKit notifications over Mach</strong> (<code>IOServiceAddMatchingNotification</code> → <code>IONotificationPort</code>); configd/clients subscribe.</td><td>mach.ko emits match-notifications over Mach (Phase 2); configd-hosted observers subscribe. The <code>devd</code> socket+rules model goes away.</td></tr>
<tr><td><code>devmatch</code></td><td>Matches unmatched devices against <code>linker.hints</code> PNP tables and <code>kldload</code>s the right module (called by devd on nomatch).</td><td>Matching is <strong>in-kernel</strong> (<code>IOService::registerService</code> → probe/score/start vs <strong>IOCatalogue</strong> personalities); the binary load is a <strong>launchd-activated loader daemon</strong> (<code>kextd</code> → <code>kernelmanagerd</code>) the kernel pings on demand.</td><td>The PNP-match + load relocates from hwregd into the ported <strong><code>kextd</code>-derived loader</strong> (driving <code>kldload</code>), woken by Phase 2 notifications + <code>waitQuiet</code>. (<code>.ko</code> matching stays userland unless later pushed in-kernel.)</td></tr>
<tr><td><code>/dev/devctl</code></td><td>Kernel character device emitting device-event lines (<code>?</code>/<code>+</code>/<code>-</code>/<code>!</code>).</td><td><strong>Mach notification port</strong> — the kernel sends Mach messages; no char-device socket.</td><td>Replaced by mach.ko match-notifications over Mach (Phase 2).</td></tr>
<tr><td><code>linker.hints</code><br><span class="cite">(built by <code>kldxref</code>)</span></td><td>The PNP→module + dependency database the matcher/loader consult.</td><td><strong>IOCatalogue</strong> — in-kernel kext <em>personalities</em> (<code>IOKitPersonalities</code> matching dicts from each kext’s Info.plist).</td><td>Kept — it’s the FreeBSD analog of IOCatalogue; <code>kldxref</code> stays as build/install plumbing.</td></tr>
<tr><td><code>kldload</code> / <code>kldunload</code> / <code>kldstat</code></td><td>Load / unload / list kernel modules (syscalls + CLIs).</td><td><code>kextload</code>/<code>kextunload</code> CLIs + the <code>kextd</code>/<code>kernelmanagerd</code> daemon load via <code>OSKext</code>; <code>kextstat</code> lists.</td><td><code>kld*</code> retained as the load primitive (the loader daemon calls them). Apple-named <code>kext*</code> CLIs deferred (no kexts yet).</td></tr>
<tr><td><code>kldxref</code></td><td>Builds <code>linker.hints</code> from each module’s <code>MODULE_PNP_INFO</code>/dependency metadata.</td><td><code>kmutil</code>/<code>kextcache</code> build prelinked / kext <strong>collections</strong>; personalities load into IOCatalogue.</td><td>Kept as build/install plumbing; no collection builder needed (NextBSD has no collections).</td></tr>
<tr><td><em>(quiescence)</em><br><span class="cite">none today — hwregd 250 ms timer + <code>devctl_freeze</code>/<code>thaw</code></span></td><td>Decide “devices have settled” before/around loading.</td><td><strong><code>IOService</code> busyState</strong> (<code>_adjustBusy</code>, propagates to the root) + <strong><code>waitQuiet</code></strong> / <code>IOServiceWaitQuiet</code>.</td><td>#176: kernel <code>device_match_start/end</code> hook → mach.ko <code>bus_busy</code> + <code>mach_wait_quiet</code> (the busyState/<code>waitQuiet</code> analog).</td></tr>
<tr><td><em>newbus probe/attach</em><br><span class="cite">(in-kernel match of built-in/loaded drivers)</span></td><td>Probe/score/attach drivers already present in the kernel.</td><td><code>IOService</code> match engine — in-kernel (same role).</td><td>Unchanged — newbus stays the in-kernel matcher; we add the Mach notification + quiescence surfaces around it.</td></tr>
</tbody>
</table>
<p class="cite">Note: FreeBSD splits matching across kernel (newbus, for already-present drivers) and userland (<code>devmatch</code>, to decide which <em>not-yet-loaded</em> module to <code>kldload</code>). Apple does all matching in-kernel (IOCatalogue) and only the <em>binary load</em> in userland. NextBSD keeps the <code>.ko</code> 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.</p>
<h2 id="blocks">4. The building blocks (and which exist)</h2>
<table>
<thead><tr><th>Block</th><th>Apple equivalent</th><th>NextBSD status</th></tr></thead>
<tbody>
<tr><td>In-kernel matching</td><td><code>IOService</code> match engine</td><td>✅ newbus already matches in-kernel (loading is userland on both)</td></tr>
<tr><td>Quiescence (busyState/<code>waitQuiet</code>)</td><td><code>_adjustBusy</code>/<code>waitQuiet</code></td><td>🟡 <strong>#176 in flight</strong> — kernel hook merged; mach.ko <code>bus_busy</code> + <code>mach_wait_quiet</code> + libIOKit APIs is the pending consumer PR</td></tr>
<tr><td>One event loop over Mach + fds</td><td>kqueue + <code>EVFILT_MACHPORT</code></td><td>❌ <strong>#168</strong> — needs ~5-line <code>event.h</code> base patch (define <code>EVFILT_MACHPORT</code> + bump <code>EVFILT_SYSCOUNT</code>) + a mach.ko-registered filter (<code>kqueue_add_filteropts</code>, reusing the event-bell pset seam). mach.ko event-bell bridge is a working stopgap.</td></tr>
<tr><td>Match events over Mach</td><td><code>IOServiceAddMatchingNotification</code></td><td>❌ synthesized in userland by hwregd today; no kernel-native delivery</td></tr>
<tr><td>Event-driven loader under launchd</td><td><code>kextd</code> Mach-service-activated</td><td>🟡 launchd has Mach on-demand activation; <code>HardwareMatch</code> dispatch unimplemented</td></tr>
</tbody>
</table>
<h2 id="options">5. Convergence options</h2>
<table>
<thead><tr><th>Option</th><th>What it is</th><th>Apple-fidelity</th><th>Cost / verdict</th></tr></thead>
<tbody>
<tr><td><strong>A. Fold hwregd into configd</strong> (in-process plugin)</td><td>Move the matcher + registry + notification-serving into configd; <code>EVFILT_MACHPORT</code> lets its one loop watch its Mach service port + the device-event source. Apple hosts comparable plumbing inside configd.</td><td><span class="pill pill-good">High</span></td><td>Needs <code>EVFILT_MACHPORT</code> (#168). Removes the standalone process; preserves the serving surface. <span class="pill pill-good">Lead route</span></td></tr>
<tr><td><strong>B. launchd-activated loader</strong> (ported <code>kextd</code>)</td><td>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 <strong>devd + devmatch</strong> load path.</td><td><span class="pill pill-good">High</span> (<code>kextd</code>’s role; = <code>kernelmanagerd</code> on modern macOS)</td><td>Needs kernel match-notifications-over-Mach + launchd <code>HardwareMatch</code> iter-2. Complements A (A hosts registry/serving; B does on-demand loads). NOTE: Apple matches <em>in-kernel</em> (IOCatalogue) so its <code>kernelmanagerd</code> only loads; for <code>.ko</code> 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.</td></tr>
<tr><td><strong>C. In-kernel matcher + loader</strong></td><td>Push autoload into the kernel itself.</td><td><span class="pill pill-bad">Not even Apple</span> (Apple loads in userland)</td><td><span class="pill pill-bad">Reject</span> — more kernel surface than warranted; diverges from Apple.</td></tr>
</tbody>
</table>
<h2 id="rec">6. Recommendation</h2>
<div class="callout callout-good">
<p>Converge to the Apple shape as a <strong>blend of A + B over shared kernel building blocks</strong>, not a single big rewrite:</p>
<ol>
<li><strong>Keep matching in-kernel</strong> (newbus already does) and add the two kernel signals Apple has and we lack: <strong>busyState/<code>waitQuiet</code></strong> (#176) and <strong>kernel-native match-notifications over Mach</strong> (a mach.ko surface emitting publish/matched/terminated, replacing hwregd’s userland synthesis).</li>
<li><strong>Add <code>EVFILT_MACHPORT</code></strong> (#168) so a single kqueue loop can watch a Mach service port + device events — the prerequisite for hosting matching anywhere but a dedicated daemon.</li>
<li><strong>Fold hwregd’s matcher + registry + serving into configd</strong> (Option A) using <code>EVFILT_MACHPORT</code>, and make the actual driver load the <strong>ported <code>kextd</code> loader</strong>, launchd-Mach-activated (Option B) instead of an in-loop <code>kldload</code> poll. The standalone <code>hwregd</code> <em>process</em> goes away; every responsibility it owns (autoload, registry, libIOKit + DiskArbitration serving) is preserved, now event-driven.</li>
</ol>
<p>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.</p>
</div>
<h2 id="roadmap">7. Phased roadmap</h2>
<p>Each phase follows the established pattern (kernel hook first as a <code>nextbsd-kernel</code> patch → consumer PR in <code>nextbsd</code> → boot-test gate), and each lands value on its own.</p>
<ol>
<li><strong>Phase 0 — busyState/<code>waitQuiet</code> (#176).</strong> Kernel <code>device_match_start/end</code> hook is <strong>merged</strong> (nextbsd-kernel #8). The consumer PR (mach.ko <code>bus_busy</code> + <code>mach_wait_quiet</code> + hwregd quiescence-flip + libIOKit APIs) is staged. <em>Land it as the foundation</em> — every later phase’s loader uses <code>waitQuiet</code>.</li>
<li><strong>Phase 1 — <code>EVFILT_MACHPORT</code> (#168).</strong> <code>nextbsd-kernel</code>: ~5-line <code>sys/sys/event.h</code> patch (define <code>EVFILT_MACHPORT</code>, bump <code>EVFILT_SYSCOUNT</code>). <code>nextbsd</code>: mach.ko registers the filter at load (<code>kqueue_add_filteropts</code>) reusing the pset-signal seam; notify-only semantics. Unblocks single-loop multiplexing in configd.</li>
<li><strong>Phase 2 — kernel-native match notifications over Mach.</strong> mach.ko emits publish/matched/terminated for newbus attach/detach/nomatch over a Mach notification port (the <code>IOServiceAddMatchingNotification</code> analog), so userland stops synthesizing them from <code>/dev/devctl</code>.</li>
<li><strong>Phase 3 — retire standalone hwregd.</strong> Fold its matcher + registry + serving into configd (Option A, on <code>EVFILT_MACHPORT</code>); make autoload a launchd-Mach-activated loader (Option B, driven by Phase 2 notifications + <code>waitQuiet</code>). Migrate the libIOKit + DiskArbitration consumers to the new host. Remove the <code>hwregd</code> process.</li>
</ol>
<h2 id="risks">8. Caveats</h2>
<ul>
<li><strong>“No daemon” is not the goal (or Apple).</strong> Apple keeps <code>kextd</code>/<code>kernelmanagerd</code>. The win is event-driven + launchd-activated, not zero processes. Phase 3 may end with a launchd-activated loader job rather than literally nothing.</li>
<li><strong>hwregd owns more than loading.</strong> Its registry tree, PCI enrichment, and the <code>org.freebsd.hwregd</code> serving surface back <strong>libIOKit</strong> and <strong>DiskArbitration</strong> today — Phase 3 must re-home all of it (into configd) before the process can be deleted, or those consumers break.</li>
<li><strong>Sequencing.</strong> Phases 1–3 each depend on the prior kernel hook; Phase 0 (busyState) and Phase 1 (<code>EVFILT_MACHPORT</code>) are independent and can proceed in parallel. This is a multi-week effort; the value is incremental (each phase is shippable).</li>
<li><strong>Kernel surface stays minimal.</strong> Every kernel piece is a small <code>nextbsd-kernel</code> patch or a mach.ko-registered hook (the match hook, the ~5-line <code>EVFILT_MACHPORT</code> reservation, the notification emitter) — no in-kernel module loader (Option C), consistent with Apple keeping loads in userland.</li>
<li><strong>We DO port the <code>kextd</code> daemon — adapting its load backend, not keeping the <code>.kext</code> machinery.</strong> Port the open-source APSL <code>kextd</code> itself (its launchd check-in, Mach-service receive loop, and kernel-request handling from <code>kext_tools</code>) and <strong>swap the load backend</strong> from kext-loading to <code>kldload</code> + <code>linker.hints</code> matching (the matching/load logic that lives in <code>hwregd</code> today). What we do <em>not</em> port is the <code>.kext</code>-specific machinery <code>kextd</code> assumes — <code>IOCatalogue</code> personalities, code-signing/SIP, prelinkedkernel/AuxKC, <code>kmutil</code> — those are <strong>replaced</strong> by FreeBSD’s newbus + <code>linker.hints</code> + <code>kldload</code>. (And we do <em>not</em> port <code>kernelmanagerd</code> — it’s closed-source; <code>kextd</code> is the open predecessor we port.) Apple’s “loader tells <code>IOCatalogue</code> → re-drive matching” maps to FreeBSD’s built-in <code>kldload</code> → <code>BUS_DRIVER_ADDED</code> re-probe. The <code>.kext</code> bundle path IS a NextBSD goal — converting its drivers to <code>.kext</code> bundles installed in <code>/System/Library/Extensions</code> (built from FreeBSD module <em>source</em>, not unmodified Apple binaries). So the kext machinery is <strong>deferred-and-planned</strong> (its own conversion effort), not rejected; this convergence’s <em>active</em> backend is <code>kld</code>.</li>
<li><strong>The load-backend seam is the migration path to kexts, not a hedge.</strong> Structure the ported <code>kextd</code> around a small <strong>load-backend seam</strong> (“load the driver matching this device”) with a <code>kld</code> backend now. The intended end state is <code>.kext</code> bundles in <code>/System/Library/Extensions</code> (see the separate <a href="freebsd-ko-to-kext-conversion-plan.html"><code>.ko</code>→<code>.kext</code> conversion plan</a>). That conversion is decoupled from the loader and may land <strong>sooner/independently</strong>: the bundle <em>format</em> + <code>Info.plist</code> <code>IOKitPersonalities</code> can be established first, with the interim <code>kld</code> loader reading the bundles, and kextd/IOCatalogue following. Realistic model = <code>.kext</code> bundles wrapping FreeBSD <code>.ko</code> (kld-loaded), not a full XNU <code>OSKext</code> reimplementation.</li>
<li><strong>The kext CLIs (<code>kextload</code>/<code>kextunload</code>/<code>kextstat</code>/<code>kextfind</code>) are thin shims, not ports.</strong> Their function already exists as FreeBSD <code>kldload</code>/<code>kldunload</code>/<code>kldstat</code>. Where Apple-name familiarity is wanted, provide thin wrappers over <code>kld*</code> + the libIOKit/hwregd registry (e.g. <code>kextstat</code> = an Apple-style formatter over <code>kldstat</code>; <code>kextfind</code> = an <code>ioreg</code>-style query). <code>kextutil</code> is mostly <code>.kext</code>/codesign validation (N/A); <code>kextcache</code>/<code>kmutil</code> build prelinked/AuxKC collections that don’t exist on FreeBSD. The closest analog is <code>kldxref</code> <em>building</em> <code>linker.hints</code> — which is <strong>build/install-time plumbing</strong> (run by <code>build.sh</code> after laying down modules), independent of hwregd and unaffected when it’s retired. Note the three distinct <code>linker.hints</code> touchpoints: (1) <em>build</em> = <code>kldxref</code> (stays, plumbing); (2) <em>kernel dependency autoload</em> = <code>kern_linker</code> (in-kernel, unaffected); (3) the <em>device→module matcher</em> (the <code>devmatch</code> role) = currently in hwregd, and one of the responsibilities Phase 3 <strong>relocates</strong> 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.</li>
</ul>
<p class="footnote">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 <code>releng/15.0</code> kqueue internals.</p>
</div>
</body>
</html>