-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfreebsd-kmodloader-plan.html
More file actions
485 lines (412 loc) · 38.1 KB
/
Copy pathfreebsd-kmodloader-plan.html
File metadata and controls
485 lines (412 loc) · 38.1 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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FreeBSD kmodloader — porting plan</title>
<style>
:root {
--bg: #fbfbf8;
--fg: #1a1a1a;
--muted: #555;
--accent: #b03000;
--accent2: #0a4d68;
--ok: #1f7a1f;
--warn: #b06800;
--bad: #b00020;
--code-bg: #f0ece4;
--rule: #d6cfc0;
--card: #fff;
}
html { -webkit-text-size-adjust: 100%; }
body { margin: 0 auto; max-width: 980px; padding: 2.5rem 1.5rem 6rem;
font: 16px/1.55 -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
color: var(--fg); background: var(--bg); }
h1 { font-size: 2rem; line-height: 1.2; margin: 0 0 .25rem; }
h2 { font-size: 1.4rem; margin: 2.5rem 0 .75rem; padding-bottom: .25rem; border-bottom: 2px solid var(--rule); }
h3 { font-size: 1.15rem; margin: 1.75rem 0 .5rem; color: var(--accent2); }
h4 { margin: 1.25rem 0 .35rem; }
.subtitle { color: var(--muted); font-size: 1.05rem; margin: 0 0 2rem; }
code, pre, kbd { font-family: "SF Mono", Menlo, Consolas, monospace; }
code { background: var(--code-bg); padding: 1px 5px; border-radius: 3px; font-size: .9em; }
pre { background: var(--code-bg); padding: .85rem 1rem; border-radius: 6px;
overflow-x: auto; font-size: .82rem; line-height: 1.45;
border-left: 3px solid var(--accent2); }
pre code { background: none; padding: 0; }
pre.shell { border-left-color: var(--ok); }
pre.objc { border-left-color: var(--accent); }
pre.warn-pre { border-left-color: var(--warn); background: #fff5e6; }
a { color: var(--accent2); }
a:hover { color: var(--accent); }
.tldr { background: var(--card); border: 1px solid var(--rule); border-left: 4px solid var(--accent2);
padding: 1rem 1.25rem; border-radius: 6px; margin-bottom: 2rem; }
.tldr h3 { margin-top: 0; color: var(--accent2); }
.pill { display: inline-block; font-size: .72rem; padding: 1px 8px; border-radius: 999px;
background: #eee; color: #333; margin-left: .35rem; vertical-align: middle;
font-weight: 600; letter-spacing: .02em; }
.pill.ok { background: #d8efd8; color: var(--ok); }
.pill.warn { background: #f6e4cb; color: var(--warn); }
.pill.bad { background: #f5d0d6; color: var(--bad); }
.pill.info { background: #d6e6f3; color: var(--accent2); }
.grid { display: grid; gap: 1rem; grid-template-columns: 1fr 1fr; margin: 1rem 0; }
@media (max-width: 700px) { .grid { grid-template-columns: 1fr; } }
.card { background: var(--card); border: 1px solid var(--rule); border-radius: 6px;
padding: 1rem 1.1rem; }
.card h4 { margin-top: 0; }
.phase { background: var(--card); border: 1px solid var(--rule); border-radius: 6px;
padding: 1.2rem 1.4rem; margin: 1rem 0; }
.phase h3 { margin-top: 0; }
.phase.done { border-left: 4px solid var(--ok); }
.phase.todo { border-left: 4px solid var(--accent2); }
table { border-collapse: collapse; width: 100%; margin: 1rem 0; font-size: .92rem; }
th, td { text-align: left; padding: .5rem .65rem; border-bottom: 1px solid var(--rule); vertical-align: top; }
th { background: #eee5d6; }
tr:nth-child(even) td { background: #faf6ed; }
blockquote { margin: 1rem 0; padding: .5rem 1rem; border-left: 3px solid var(--accent);
background: #fff8f3; color: #333; }
.footnote { font-size: .85rem; color: var(--muted); }
ol li, ul li { margin: .25rem 0; }
.nav { position: sticky; top: 0; background: var(--bg); margin: -2.5rem -1.5rem 2rem;
padding: .75rem 1.5rem; border-bottom: 1px solid var(--rule);
font-size: .88rem; z-index: 10; }
.nav a { margin-right: .9rem; text-decoration: none; }
.filename { color: var(--accent); font-family: "SF Mono", Menlo, Consolas, monospace; font-size: .9em; }
.verdict { font-weight: 600; }
.verdict.go { color: var(--ok); }
.verdict.maybe { color: var(--warn); }
.verdict.no { color: var(--bad); }
hr { border: 0; border-top: 1px dashed var(--rule); margin: 2rem 0; }
.resolved { background: #ecf7ec; border-left: 4px solid var(--ok); padding: .8rem 1rem; margin: 1rem 0; border-radius: 0 6px 6px 0; }
.resolved strong { color: var(--ok); }
.risk { background: #fff5e6; border-left: 4px solid var(--warn); padding: .8rem 1rem; margin: 1rem 0; border-radius: 0 6px 6px 0; }
.risk strong { color: var(--warn); }
.ascii-diagram { font-family: "SF Mono", Menlo, Consolas, monospace; font-size: .82rem; line-height: 1.3; white-space: pre; background: var(--code-bg); padding: 1rem; border-radius: 6px; overflow-x: auto; }
.step-num { display: inline-block; width: 1.6em; height: 1.6em; line-height: 1.6em; text-align: center; background: var(--accent); color: white; border-radius: 50%; font-weight: 700; font-size: .85em; margin-right: .35em; }
.open-q { background: #fff8d6; border: 1px solid #e5d76b; padding: .8rem 1rem; margin: 1rem 0; border-radius: 6px; font-size: .92rem; }
.open-q strong { color: #7a5e00; }
</style>
</head>
<body>
<nav class="nav">
<a href="#tldr">Status</a>
<a href="#goal">Goal</a>
<a href="#arch">Architecture</a>
<a href="#paths">Three paths</a>
<a href="#repo">Repo</a>
<a href="#decisions">Decisions</a>
<a href="#integration">launchd integration</a>
<a href="#license">License</a>
<a href="#phases">Phases</a>
<a href="#open">Open questions</a>
</nav>
<h1>FreeBSD kmodloader — porting plan <span class="pill info">freebsd-launchd (AF_UNIX) effort</span> <span class="pill pill-warn">Superseded by hwregd for the Mach track</span></h1>
<p class="subtitle">A FreeBSD-only daemon that ensures every hardware-class kernel module on the system autoloads at boot when its hardware is detected. Default FreeBSD does not do this aggressively (especially for GPU drivers and the third-party drm-kmod / nvidia stacks); this project closes that gap. Companion to <a href="freebsd-launchd-plan.html">freebsd-launchd</a> and <a href="nextbsd-configd-plan.html">nextbsd-configd</a>.</p>
<section class="tldr">
<h3>Revision 2026-05-23 — superseded by hwregd in the Mach track</h3>
<p>This plan lives under the <strong>sibling <code>freebsd-launchd</code> (AF_UNIX) effort</strong> as an Objective-C / GNUstep-based daemon (clean-room, modelled loosely on Apple's <code>kextd</code>). On the <code>freebsd-launchd-mach</code> (v2) track the same scope — reading <code>/dev/devctl</code>, calling <code>kldload</code> on <code>?nomatch</code>, surfacing hardware events — has been absorbed into <strong><code>hwregd</code></strong>'s Phase 0 (see the <a href="freebsd-hardware-registry-iokit-plan.html">hardware registry + IOKit userland plan</a>). hwregd is MIG-served, in C, and emits Mach notifications consumable by every other Apple-shape daemon in that repo (configd, IPConfiguration, the upcoming DiskArbitration). The kmodloader plan below remains relevant <em>only</em> for the AF_UNIX repo and is kept here for historical context. Do not start fresh work against this plan for the Mach repo.</p>
</section>
<section id="tldr" class="tldr">
<h3>Status: Phase 1 shipped <span class="pill ok">v1</span> (AF_UNIX repo)</h3>
<ul>
<li><strong>Repo:</strong> <a href="https://github.com/pkgdemon/freebsd-launchd">github.com/pkgdemon/freebsd-launchd</a> — <strong>monorepo</strong>. kmodloader source lives under <code>kmodloader/</code> at the top of the launchd repo, alongside <code>src/</code> (launchd) and <code>configd/</code>.</li>
<li><strong>Mission:</strong> on this OS, hardware that has a FreeBSD driver works without manual <code>kldload</code> or <code>kld_list=</code> edits. GPUs auto-engage DRM/KMS. NICs come up. WiFi cards register. USB classes attach. VM guest additions load when in a VM.</li>
<li><strong>Architecture:</strong> two parallel match paths feed one kldload loop:
<ol>
<li><strong>devmatch(8) + linker.hints</strong> — covers anything in-tree with <code>PNP_INFO</code> macros (NICs, USB classes, ACPI HID, most storage). Zero curation; FreeBSD's <code>kldxref</code> already built the index.</li>
<li><strong>PCI scan for display class 0x0300xx</strong> — covers drm-kmod GPU drivers (no <code>PNP_INFO</code>). Routes by vendor: Intel → <code>i915kms</code>, NVIDIA → <code>nvidia-drm</code>, AMD → per-device split via a frozen radeonkms ID list vs. amdgpu default.</li>
</ol>
</li>
<li><strong>What we do NOT do anymore:</strong> hand-curated IOPCIMatch personality plists. Phase 1a-c shipped them; Phase 1d ripped them out in favor of devmatch + a small targeted GPU PCI scan. Reason: hand-curation doesn't scale to FreeBSD's hardware coverage. devmatch already has the data (<code>linker.hints</code>); we just have to use it. The narrow gap (drm-kmod) is closed with a three-vendor map, not 700+ device IDs.</li>
<li><strong>What's NOT in kmodloader's scope:</strong> hypervisor guest additions (vboxguest, vboxvfs). Those are <em>service</em>-related kmods, not hardware drivers. The right home is the eventual <code>org.freebsd.vboxservice.plist</code> (Phase 2+) which kldloads its own kmods before launching VBoxService — matching FreeBSD's <code>rc.d/vboxservice</code> pattern. open-vm-tools ships zero kmods; the kernel side (vmx) is in GENERIC. Phase 1g's hypervisor scan (briefly shipped) was reaching outside kmodloader's lane and got removed.</li>
<li><strong>Boot timing:</strong> Phase 1 is one-shot at boot — <code>RunAtLoad=true</code>, <code>KeepAlive=false</code>. Phase 2 (planned) keeps the daemon alive on a <code>DISPATCH_SOURCE_TYPE_READ</code> source over <code>/dev/devctl</code> for hot-plug.</li>
<li><strong>No Apple source vendored.</strong> The original plan was modeled on Apple's <code>kextd</code> + <code>IOKitPersonalities</code>; the implementation that shipped is closer in spirit to FreeBSD's own <code>devmatch</code>. Clean-room ObjC throughout.</li>
<li><strong>Licensing:</strong> BSD-2-Clause. No Apple per-file headers — clean-room code, not a port.</li>
</ul>
</section>
<h2 id="goal">1. Goal & non-goals</h2>
<h3>1.1 Goal</h3>
<p>When hardware is present at boot, the kernel module that drives it loads <strong>without manual configuration</strong>. The user installs the system, plugs in their hardware, the system works. <strong>Zero <code>kld_list=</code> edits, zero <code>kldload</code> invocations from the user.</strong></p>
<h3>1.2 Non-goals (this iteration)</h3>
<ul>
<li><strong>No kernel patches.</strong> We use FreeBSD's existing tools (<code>devmatch</code>, <code>pciconf</code>, <code>sysctl</code>); we don't modify the kernel.</li>
<li><strong>No reimplementation of <code>kldload</code>.</strong> We wrap it via <code>NSTask</code>.</li>
<li><strong>No driver porting.</strong> Drivers come from FreeBSD base, drm-kmod, nvidia-driver, virtualbox-additions, etc.</li>
<li><strong>No replacement for <code>devd(8)</code>.</strong> devd handles non-kmod-load actions (running scripts on attach, configuring network on link-up). kmodloader is narrower: only kmod loading. devd's role — if we want it — is separate (and currently absent; we replicate its essential parts case-by-case in launchd-driven daemons).</li>
<li><strong>No Mach IPC, no IOKit, no XPC.</strong> Same project-wide rule.</li>
<li><strong>No prelinked kernel image.</strong> We keep kmods as separate <code>.ko</code> files and load on demand.</li>
</ul>
<h2 id="arch">2. Architecture</h2>
<p>Three independent match paths run sequentially in <code>KMDaemon.runOneShot</code>. Each emits a list of kld names. Lists are concatenated, deduplicated, and the GPU set is pulled to the front so the framebuffer is owned by a DRM driver before peripheral driver attaches happen. Then <code>kldload -n</code> each.</p>
<div class="ascii-diagram"> +----------------------------+ +-------------------+
| pciconf -l | | devmatch |
| (display-class scan) | | (linker.hints) |
+-------------+--------------+ +----------+--------+
| |
vendor map | kld names |
v v
[i915kms] [if_em, umass, ...]
[amdgpu/radeonkms]
[nvidia-drm]
| |
+-------------+---------------+
|
v
[GPU klds first]
|
v
NSMutableOrderedSet (dedupe)
|
v
/sbin/kldload -n <name> (per kld)
|
v
kernel newbus probe + driver attach</div>
<h3>2.1 Why three paths instead of one</h3>
<p>FreeBSD's hardware drivers split cleanly into two categories by how match information is published:</p>
<table>
<thead><tr><th>Category</th><th>Match data lives in...</th><th>Reachable via</th></tr></thead>
<tbody>
<tr><td>In-tree drivers with <code>PNP_INFO</code></td><td><code>linker.hints</code> (kldxref output)</td><td><code>devmatch(8)</code></td></tr>
<tr><td>drm-kmod GPU drivers + nvidia binary blob</td><td>Compiled-in C structs; not in <code>linker.hints</code></td><td>PCI scan + 3-vendor map</td></tr>
</tbody>
</table>
<p>Trying to unify both under one mechanism — the original plan's "personality plists with IOPCIMatch" — required hand-curating the device-ID lists for category 2. That doesn't scale: amdgpu's supported list grows each AMD generation, and the personality plists were always going to lag real hardware. The two-path split lets each category use the data source that already has the answer.</p>
<p><strong>Hypervisor guest additions (vboxguest, vboxvfs) are intentionally NOT in scope.</strong> They're service-related kmods, not hardware drivers; the right home is the matching launchd plist (<code>org.freebsd.vboxservice.plist</code>) loading them before launching its userspace daemon, the same way FreeBSD's <code>rc.d/vboxservice</code> works. Phase 1g briefly shipped a third "hypervisor scan" path; it was reaching outside kmodloader's lane and got removed.</p>
<h2 id="paths">3. The three match paths in detail</h2>
<h3 id="path-devmatch">3.1 Path 1 — devmatch + linker.hints</h3>
<p>Bare <code>devmatch</code> (no flag) walks the live device tree and emits one kld basename per line for every enabled-but-unattached device that has a registered driver in <code>linker.hints</code>. <code>linker.hints</code> is built by <code>kldxref(8)</code> from each driver's <code>PNP_INFO()</code> macro at install time — pkg's post-install script regenerates it whenever a kld package lands. So at boot the index already covers everything that ships from base + ports.</p>
<pre class="shell"><code>$ devmatch
pchtherm.ko
rtsx.ko
if_iwlwifi.ko
if_iwm.ko
ichsmb.ko
acpi_wmi.ko</code></pre>
<p><strong>Coverage on a representative laptop</strong> (Skylake-era Lenovo): NICs (<code>if_em</code>), WiFi (<code>if_iwlwifi</code> / <code>if_iwm</code>), thermal (<code>pchtherm</code>), SD reader (<code>rtsx</code>), SMBus (<code>ichsmb</code>), ACPI WMI. Most of the system except the GPU.</p>
<p><strong>NOT </strong> <code>devmatch -a</code>. The <code>-a</code> flag prints <code>devname: kld</code> for every enabled device including already-attached ones — wrong format, wrong filter. Bare <code>devmatch</code> is the canonical "what needs loading" query.</p>
<h3 id="path-gpu">3.2 Path 2 — GPU PCI scan</h3>
<p>drm-kmod's <code>i915kms.ko</code>, <code>amdgpu.ko</code>, <code>radeonkms.ko</code>, and the nvidia binary kmod do not declare <code>PNP_INFO</code>. <code>kldxref -d /boot/modules/linker.hints | grep i915</code> is empty. <code>strings /boot/modules/i915kms.ko | grep pnp_info</code> is empty. devmatch will never see them.</p>
<p>Solution: walk <code>pciconf -l</code> for class <code>0x0300xx</code> (display controllers; covers VGA, XGA, 3D), extract <code>(vendor, device)</code> per device, and route per a small in-code map:</p>
<pre class="objc"><code>- (NSArray<NSString *> *)gpuKldsForDevices:(NSArray<NSDictionary *> *)devices
{
NSMutableOrderedSet<NSString *> *r = [NSMutableOrderedSet orderedSet];
for (NSDictionary *dev in devices) {
NSString *vendor = dev[@"vendor"];
NSString *device = dev[@"device"];
if ([vendor isEqualToString:@"0x8086"]) {
[r addObject:@"i915kms"];
}
else if ([vendor isEqualToString:@"0x1002"]) {
// Per-device split: radeonkms list is frozen upstream.
uint16_t devID = (uint16_t)strtoul(device.UTF8String, NULL, 16);
BOOL isRadeon = NO;
for (size_t i = 0; i < kRadeonKMSDeviceIDsCount; i++) {
if (kRadeonKMSDeviceIDs[i] == devID) { isRadeon = YES; break; }
}
[r addObject:isRadeon ? @"radeonkms" : @"amdgpu"];
}
else if ([vendor isEqualToString:@"0x10de"]) {
[r addObject:@"nvidia-drm"]; // pulls nvidia-modeset + nvidia
}
}
return r.array;
}</code></pre>
<p><strong>Three vendor IDs.</strong> <code>0x8086</code> (Intel), <code>0x1002</code> (AMD/ATI), <code>0x10de</code> (NVIDIA). PCI-SIG assignments from the 1980s and 90s; will not change.</p>
<h4>3.2.1 The AMD split: radeonkms vs. amdgpu</h4>
<p>The only category-2 entry that needs more than vendor info. radeonkms covers TeraScale through HD 7000 + early Sea Islands; amdgpu covers GCN 1.1+ through current RDNA*. Linux's radeon driver is in maintenance-only mode upstream and stopped gaining new device IDs years ago — drm-kmod inherits the same frozen list.</p>
<p>So we vendor it once: 699 unique 16-bit device IDs from drm-kmod's <code>include/drm/drm_pciids.h</code> (the <code>radeon_PCI_IDS</code> macro) baked into <code>kmodloader/src/RadeonPCIIDs.h</code> as a <code>static const uint16_t[]</code>. Any AMD device whose ID matches the frozen set goes to <code>radeonkms</code>; everything else (Polaris onward) defaults to <code>amdgpu</code>. <strong>amdgpu is the open default</strong>: hardware AMD ships next year picks up amdgpu without us changing the table.</p>
<p>Update procedure (rare): re-fetch <code>drm_pciids.h</code>, re-extract the <code>0x1002</code> entries, regenerate the header. The set has not changed in years.</p>
<h4>3.2.2 NVIDIA chain via MODULE_DEPEND</h4>
<p><code>kldload nvidia-drm</code> pulls <code>nvidia-modeset</code> which pulls <code>nvidia</code> via <code>MODULE_DEPEND</code> declarations in the kmods. We only have to name the leaf. Requires <code>hw.nvidiadrm.modeset="1"</code> in <code>/boot/loader.conf</code> so <code>nvidia-drm.ko</code> engages DRM/KMS at init — a kenv tunable, must be set kenv-time.</p>
<h4>3.2.3 Multi-GPU + non-GPU systems</h4>
<p>Multi-GPU laptops (Intel iGPU + Nvidia dGPU is common) are handled naturally: the scan finds both vendors, both kld sets get loaded. Bare-metal-with-no-GPU systems (some VMs) return an empty device list — the GPU pass loads nothing.</p>
<h3 id="path-vm">3.3 (Removed) hypervisor scan</h3>
<p>Phase 1g briefly shipped a third match path that read <code>kern.vm_guest</code> and kldloaded VirtualBox / VMware guest additions kmods. Removed shortly after — for two reasons:</p>
<ol>
<li><strong>Wrong scope.</strong> Guest additions are services, not hardware drivers. The Apple-shape pattern (and FreeBSD's existing <code>rc.d/vboxservice</code> pattern) is for the service's launchd job to kldload its supporting kmods before starting the userspace daemon. kmodloader's job is hardware autodetection.</li>
<li><strong>The kld names were also wrong.</strong> <code>vboxvideo</code> doesn't exist on FreeBSD — that's a Linux thing. <code>vmware_drv</code> is an Xorg driver, not a kld. <code>vmxnet3</code> is in GENERIC already. Open-vm-tools ships zero kmods at all (everything is userspace daemons).</li>
</ol>
<p>What we still ship in <code>pkglist.txt</code>: <code>emulators/virtualbox-ose-additions-nox11</code> (provides <code>vboxguest.ko</code>, <code>vboxvfs.ko</code>, and userspace <code>VBoxService</code>) and <code>emulators/open-vm-tools-nox11</code> (provides userspace <code>vmtoolsd</code> + helpers). The kmods sit on disk; the userspace daemons sit on disk. Nothing auto-runs them in Phase 1. Phase 2+ will introduce <code>org.freebsd.vboxservice.plist</code> and <code>org.freebsd.vmtoolsd.plist</code> which handle their own kmod loading and start the userspace daemon under launchd supervision.</p>
<h2 id="repo">4. Repository</h2>
<h3>4.1 Layout under <code>freebsd-launchd/kmodloader/</code></h3>
<pre><code>freebsd-launchd/kmodloader/
├── Makefile gmake build, installs to /usr/libexec/kmodloader
└── src/
├── main.m @autoreleasepool { [KMDaemon runOneShot]; }
├── KMDaemon.{h,m} runOneShot orchestrator + the three match paths
└── RadeonPCIIDs.h generated frozen device list (radeon_PCI_IDS, 699 IDs)
</code></pre>
<p>No <code>personalities/</code> directory. No <code>KMRegistry</code> / <code>KMMatch</code> / <code>KMDevice</code> / <code>KMBusEnumerate</code> classes. Phase 1d ripped them out: ~900 LOC removed, replaced by ~150 LOC of three-path orchestration.</p>
<p>Top-level (<code>freebsd-launchd/</code>) hosts:</p>
<ul>
<li><code>overlays/System/Library/LaunchDaemons/org.freebsd.kmodloader.plist</code></li>
<li><code>overlays/boot/loader.conf</code> — sets <code>hw.nvidiadrm.modeset="1"</code></li>
<li><code>pkglist.txt</code> — lists drm-latest-kmod, nvidia-drm-latest-kmod, wifi-firmware-*-kmod, virtualbox-ose-additions-nox11, open-vm-tools-nox11</li>
<li><code>build.sh</code> — chroot-side, runs <code>gmake -C kmodloader install</code> after launchd + configd builds</li>
</ul>
<h3>4.2 How <code>build.sh</code> handles kmodloader</h3>
<p>Same chroot session that builds launchd + configd. After <code>make-configd.sh</code> finishes, <code>build.sh</code> rsyncs <code>kmodloader/</code> into <code>chroot:/tmp/kmodloader/</code> and runs <code>gmake -C /tmp/kmodloader install</code> — the Makefile compiles the daemon and installs to <code>/usr/libexec/kmodloader</code>. No wrapper script.</p>
<h2 id="decisions">5. Locked architectural decisions</h2>
<table>
<thead>
<tr><th>Decision</th><th>Choice</th></tr>
</thead>
<tbody>
<tr><td>Implementation language</td><td>Objective-C with GNUstep Foundation. Same substrate as launchd + configd.</td></tr>
<tr><td>Event loop (Phase 1)</td><td>None. One-shot run, exit. Phase 2 adds libdispatch.</td></tr>
<tr><td>Match data sources</td><td>Two: <code>linker.hints</code> via devmatch, <code>pciconf -l</code> for GPU PCI scan. (Removed: <code>kern.vm_guest</code> hypervisor scan — out of scope, see §3.3.)</td></tr>
<tr><td>Kmod load mechanism</td><td><code>NSTask</code> wrapping <code>/sbin/kldload -n <module></code>. The <code>-n</code> flag = "do not error if already loaded".</td></tr>
<tr><td>devmatch invocation</td><td>Bare <code>/sbin/devmatch</code> (no flags). NOT <code>devmatch -a</code> — that flag's output format is wrong for our use.</td></tr>
<tr><td>GPU vendor map</td><td>Three-vendor static map, no curated device-ID tables. The single per-device split (AMD radeonkms vs amdgpu) uses an upstream-frozen list vendored as <code>RadeonPCIIDs.h</code>.</td></tr>
<tr><td>nvidia-drm modeset</td><td>Required tunable in <code>/boot/loader.conf</code>: <code>hw.nvidiadrm.modeset="1"</code>. Set kenv-time, before kernel start. Harmless on non-NVIDIA systems since the kmod only loads when the PCI scan finds <code>0x10de</code>.</td></tr>
<tr><td>NVIDIA license at build time</td><td>build.sh sets <code>LICENSES_ACCEPTED=NVIDIA</code> in the runtime-pkgs <code>pkg install</code> env. nvidia-drm-latest-kmod ships under restricted-distribution license.</td></tr>
<tr><td>Coexistence with devd</td><td>devd is not currently installed. If added later: both daemons can read <code>/dev/devctl</code>; multiple readers are kernel-supported.</td></tr>
<tr><td>License (top-level)</td><td>BSD-2-Clause. No Apple per-file headers — clean-room implementation.</td></tr>
</tbody>
</table>
<h2 id="integration">6. launchd integration</h2>
<h3>6.1 The plist</h3>
<pre><code><?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key> <string>org.freebsd.kmodloader</string>
<key>ProgramArguments</key> <array><string>/usr/libexec/kmodloader</string></array>
<key>RunAtLoad</key> <true/>
<key>KeepAlive</key> <false/>
</dict>
</plist></code></pre>
<p>One-shot at boot. <code>KeepAlive=false</code> — daemon exits when work is done; launchd does not respawn. Phase 2 will swap to a long-running shape with a <code>DISPATCH_SOURCE_TYPE_READ</code> source on <code>/dev/devctl</code> for hot-plug events.</p>
<h3>6.2 Boot ordering</h3>
<p>kmodloader runs early. Plist filenames sort alphabetically; <code>org.freebsd.kmodloader</code> sorts before <code>org.freebsd.netconfigd</code>, so any NIC kld kmodloader brings in is visible by the time netconfigd does its <code>getifaddrs()</code> walk. That's the dependency that matters today; finer ordering (a <code>RequiresPhase</code> key, or per-job dependencies) is a launchd-side feature for later.</p>
<h2 id="license">7. Licensing</h2>
<p>BSD-2-Clause across the board. No Apple per-file headers — this is clean-room code, not a port. The original plan referenced Apple's <code>kextd</code> + IOKit design; the implementation that shipped is closer to FreeBSD's own <code>devmatch</code>. Either way, no Apple source is vendored.</p>
<table>
<thead><tr><th>Source</th><th>License</th><th>How we handle it</th></tr></thead>
<tbody>
<tr><td>This repo's code (daemon + Makefile + scripts)</td><td>BSD-2-Clause</td><td>SPDX header in every file.</td></tr>
<tr><td><code>RadeonPCIIDs.h</code> generated table</td><td>Data — not copyrightable</td><td>Comment cites drm-kmod's drm_pciids.h as source.</td></tr>
<tr><td>GNUstep Foundation, libdispatch, libobjc2 (linked, not in tree)</td><td>LGPL/MIT/Apache as applicable</td><td>Listed in NOTICE; same calculus as launchd + configd.</td></tr>
</tbody>
</table>
<h2 id="phases">8. Phased delivery</h2>
<div class="phase done">
<h3>Phase 0 — repo scaffold <span class="pill ok">DONE</span></h3>
<ul>
<li><code>kmodloader/</code> created at the top of <code>freebsd-launchd</code>; placeholder Makefile.</li>
<li><code>NOTICE</code> updated.</li>
<li>Stub <code>org.freebsd.kmodloader.plist</code> in overlays.</li>
</ul>
</div>
<div class="phase done">
<h3>Phase 1a-c — personality plist approach (later abandoned) <span class="pill warn">SUPERSEDED</span></h3>
<p>Initial implementation followed the original plan: Apple-shaped <code>.kext</code> bundles in <code>/System/Library/Extensions/</code>, an <code>NSDictionary</code> registry, probe-score matching against <code>sysctl dev.</code> walks. Shipped <code>i915kms.kext</code>, <code>if_em.kext</code>, <code>amdgpu.kext</code>, <code>radeonkms.kext</code>, <code>if_iwlwifi.kext</code>, <code>if_rtw88.kext</code>, <code>if_rtw89.kext</code>.</p>
<p>Discarded in Phase 1d: hand-curating IOPCIMatch lists doesn't scale to FreeBSD's hardware coverage, and devmatch already had the data via <code>linker.hints</code>. Replaced by the three-path scan.</p>
</div>
<div class="phase done">
<h3>Phase 1d — pivot to devmatch <span class="pill ok">DONE</span></h3>
<ul>
<li>Deleted personality bundles + <code>KMRegistry</code> / <code>KMMatch</code> / <code>KMDevice</code> / <code>KMBusEnumerate</code> classes.</li>
<li>Replaced with shell-out to bare <code>devmatch</code>; parse one-kld-per-line output; <code>kldload</code> each.</li>
<li>Net change: ~900 LOC removed.</li>
</ul>
</div>
<div class="phase done">
<h3>Phase 1e — GPU PCI scan <span class="pill ok">DONE</span></h3>
<ul>
<li>Bench-tested on a Skylake laptop: devmatch covered NIC, WiFi, thermal, SD reader. Did not cover the Intel HD 520 GPU (drm-kmod has no <code>PNP_INFO</code>).</li>
<li>Added second match path: <code>pciconf -l</code> walk for class <code>0x0300xx</code>, three-vendor map.</li>
<li>Re-added <code>nvidia-drm-latest-kmod</code> to pkglist; added <code>LICENSES_ACCEPTED=NVIDIA</code> to build.sh's pkg env; added <code>hw.nvidiadrm.modeset="1"</code> to loader.conf.</li>
<li>Also fixed a long-shipped bug: kmodloader hardcoded <code>/usr/sbin/devmatch</code> but the actual binary is <code>/sbin/devmatch</code> — daemon had been silent no-op since shipped.</li>
</ul>
</div>
<div class="phase done">
<h3>Phase 1f — AMD per-device split <span class="pill ok">DONE</span></h3>
<ul>
<li>Replaced "load both amdgpu + radeonkms when vendor is 0x1002" with per-device routing.</li>
<li>Vendored drm-kmod's <code>radeon_PCI_IDS</code> as <code>RadeonPCIIDs.h</code> (699 frozen device IDs).</li>
<li>Devices in the frozen set → <code>radeonkms</code>; everything else → <code>amdgpu</code>.</li>
<li>amdgpu becomes the open default: future AMD silicon picks up amdgpu without code changes.</li>
</ul>
</div>
<div class="phase done">
<h3>Phase 1g — hypervisor scan (later removed) <span class="pill warn">SUPERSEDED</span></h3>
<ul>
<li>Briefly shipped a third match path that read <code>sysctl kern.vm_guest</code> and kldloaded VBox / VMware additions.</li>
<li>Removed in a follow-up commit on user pushback. Two reasons:
<ul>
<li>Wrong scope — guest additions are services, not hardware drivers. Their launchd plists (Phase 2+) own kmod loading.</li>
<li>kld names were wrong: <code>vboxvideo</code> doesn't exist on FreeBSD; <code>vmware_drv</code> is an Xorg driver; open-vm-tools ships zero kmods.</li>
</ul>
</li>
<li>The pkglist additions stayed (<code>virtualbox-ose-additions-nox11</code>, <code>open-vm-tools-nox11</code>) so the binaries are present for users and for the future Phase 2+ launchd plists.</li>
</ul>
</div>
<div class="phase todo">
<h3>Phase 2 — hot-plug via /dev/devctl <span class="pill info">PLANNED</span></h3>
<ul>
<li>Switch <code>org.freebsd.kmodloader.plist</code> to <code>KeepAlive=true</code>.</li>
<li><code>KMDevctlSource</code>: open <code>/dev/devctl</code>, <code>DISPATCH_SOURCE_TYPE_READ</code>, parse newline-delimited messages of the form <code>!system=... type=ATTACH ...</code> (event format documented in <code>devd(8)</code> source).</li>
<li>On <code>type=ATTACH</code>: re-run the appropriate match path. PCI device → check vendor, possibly run GPU map. devmatch path: re-run devmatch (it'll only emit klds for newly-unattached devices).</li>
<li>On <code>type=DETACH</code>: log only. Phase 1 doesn't unload anything; same default for Phase 2.</li>
<li>Boot test extension: in CI, attach a USB device via qemu monitor mid-run; watch matched-and-loaded log line.</li>
</ul>
</div>
<div class="phase todo">
<h3>Phase 3 — firmware loading from /boot/firmware/ <span class="pill ok">RESOLVED</span></h3>
<p>Bench testing surfaced a real problem: drm-kmod's GPU drivers and the iwlwifi LinuxKPI port load firmware via <code>firmware_get()</code> + <code>try_binary_file()</code> in <code>sys/kern/subr_firmware.c</code>. The function <code>vn_open</code>s <code>/boot/firmware/<name></code>, but in our chroot setup the kernel context's namei resolves <code>/boot/firmware/</code> against the cd9660 root (loader-only files) instead of the unionfs upper layer (where pkg-installed firmware lives). Result: i915kms loads but DMC firmware fails with "could not load binary firmware"; iwlwifi reports "File size way too small!" on a 2.4 MB blob.</p>
<p><strong>Resolved by the symlink trick</strong> in <a href="https://github.com/pkgdemon/freebsd-launchd/commit/51e1b80">freebsd-launchd commit 51e1b80</a>: <code>build.sh</code> creates a symlink at <code>cdroot/boot/firmware → /sysroot/boot/firmware</code>. Kernel namei follows the symlink across the cd9660-to-unionfs mount-point boundary into the unionfs view, finds the file, firmware loads. Verified empirically on a Lenovo with iwlwifi-8260: <code>net.wlan.devices</code> populates, <code>wlan0</code> is created, DRM DMC firmware loads.</p>
<p>The symlink fix is overlay-mechanism-agnostic — works equally on unionfs and gunion. Backported to <a href="https://github.com/pkgdemon/freebsd-livecd-unionfs">freebsd-livecd-unionfs</a> and <a href="https://github.com/pkgdemon/freebsd-livecd-gunion">freebsd-livecd-gunion</a>.</p>
<p>For the architectural alternative (gunion + <code>reboot -r</code> reroot, which would eliminate the chroot/namei split entirely instead of working around it), see the <a href="freebsd-livecd-gunion-reroot-plan.html">freebsd-livecd-gunion-reroot experimental plan</a>. Lower priority now that the symlink trick works.</p>
</div>
<div class="phase todo">
<h3>Phase 4 — performance polish <span class="pill info">FUTURE</span></h3>
<ul>
<li>Concurrent <code>kldload</code> via dispatch concurrent queues (multiple devices' klds load in parallel).</li>
<li>Skip the <code>kldstat -v</code> shell-out: maintain the loaded-set in-process across the three paths so we don't re-fork for it.</li>
</ul>
</div>
<div class="phase todo">
<h3>Phase 5 — upstream contributions <span class="pill info">FUTURE</span></h3>
<ul>
<li>File a drm-kmod PR adding <code>MODULE_PNP_INFO</code> declarations from amdgpu's and radeonkms's compiled-in supported tables. If accepted upstream, devmatch handles GPUs automatically and Path 2 collapses to "i915kms only" or disappears entirely.</li>
<li>Same for nvidia-drm-kmod (adds NVIDIA's binary's compiled-in supported list).</li>
</ul>
</div>
<h2 id="open">9. Open questions</h2>
<div class="resolved">
<strong>RESOLVED — Coexistence with devd.</strong> devd is not installed on the live ISO. We replicate its essential parts case-by-case in launchd-driven daemons (kmodloader for kld loading, netconfigd for link-up DHCP). When/if devd comes back, multiple readers on <code>/dev/devctl</code> are kernel-supported; both daemons see the same events.
</div>
<div class="resolved">
<strong>RESOLVED — Personality plists vs. devmatch.</strong> Phase 1d picked devmatch + linker.hints. Personality plists were the original plan; they shipped in 1a-c then got ripped out. The narrow gap (drm-kmod, VM additions) is closed with three vendor-map functions, not 700+ device IDs.
</div>
<div class="resolved">
<strong>RESOLVED — AMD radeonkms vs. amdgpu split without curating IDs.</strong> radeonkms's upstream device list is frozen (Linux's radeon driver is in maintenance-only mode). Vendor it once as <code>RadeonPCIIDs.h</code>; everything not in the set defaults to amdgpu. Phase 1f delivered.
</div>
<div class="resolved">
<strong>RESOLVED — Hybrid graphics (Optimus, switchable).</strong> The PCI scan finds both Intel and NVIDIA vendors; both kld sets load. Each driver attaches to its own card. Display routing policy belongs in a desktop layer, not in kmodloader.
</div>
<div class="open-q">
<strong>Q. Firmware loading in live-ISO setup. — RESOLVED.</strong> The kernel firmware loader's <code>try_binary_file()</code> reads from <code>/boot/firmware/</code> in kernel-thread context, which doesn't follow the chroot. Fixed by symlinking <code>/boot/firmware</code> on the cd9660 layer to <code>/sysroot/boot/firmware</code>: kernel namei follows the symlink across the unionfs mount-point boundary into the right view. See Phase 3 above.
</div>
<div class="open-q">
<strong>Q. Phase 2 hot-plug parsing.</strong> <code>/dev/devctl</code> events are textual, newline-delimited, with a single-character type prefix (<code>+</code> attach, <code>-</code> detach, <code>?</code> nomatch, <code>!</code> notify). Parser is straightforward; the design question is whether to re-fork devmatch on every nomatch event (simple, slow) or maintain our own incremental match state (complex, fast). Lean toward the former until profile says otherwise.
</div>
<div class="open-q">
<strong>Q. PCI bridges that load drivers for downstream devices.</strong> If a bridge driver hasn't loaded yet at our scan time, devices behind it aren't visible to <code>pciconf -l</code> or <code>devmatch</code>. We may need a second pass after Phase 1's first kldload round — Phase 2's hot-plug source covers this incidentally (the bridge attaching emits a NOMATCH for its children).
</div>
<div class="open-q">
<strong>Q. nvidia-driver license interaction.</strong> nvidia-drm-latest-kmod is a binary-only blob under restricted-distribution license. We accept the license at pkg install time via <code>LICENSES_ACCEPTED=NVIDIA</code> in build.sh. Distributing the resulting ISO has its own license obligations — out of scope for this plan; tracked at the project level.
</div>
<h2 id="refs">10. References</h2>
<ul>
<li>FreeBSD <code>devmatch(8)</code>: <code>man 8 devmatch</code> — primary match-data tool.</li>
<li>FreeBSD <code>devctl(4)</code>: <code>man 4 devctl</code> — Phase 2 event source.</li>
<li>FreeBSD <code>kldxref(8)</code> + <code>MODULE_PNP_INFO()</code>: <code>man 8 kldxref</code>, kernel docs in <code>sys/sys/module.h</code>.</li>
<li>FreeBSD <code>devd(8)</code>: <code>man 8 devd</code> — not used directly; reference for hot-plug semantics.</li>
<li>drm-kmod source: <a href="https://github.com/freebsd/drm-kmod">github.com/freebsd/drm-kmod</a>. <code>include/drm/drm_pciids.h</code> is the source for <code>RadeonPCIIDs.h</code>.</li>
<li>FreeBSD release engineering's <code>fwget(8)</code>: <code>man 8 fwget</code> — auto-detects firmware packages needed for current hardware. Useful diagnostic during bench testing.</li>
<li>Companion plans: <a href="freebsd-launchd-plan.html">freebsd-launchd</a>, <a href="nextbsd-configd-plan.html">nextbsd-configd</a>.</li>
</ul>
</body>
</html>