-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfreebsd-mach-kmod-plan.html
More file actions
690 lines (599 loc) · 49.8 KB
/
Copy pathfreebsd-mach-kmod-plan.html
File metadata and controls
690 lines (599 loc) · 49.8 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
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mach.ko: an out-of-tree FreeBSD kernel module for Mach IPC, shipped from freebsd-launchd</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; }
html { -webkit-text-size-adjust: 100%; }
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.1rem;
line-height: 1.2;
margin: 0 0 8px;
letter-spacing: -0.01em;
}
h2 {
font-size: 1.45rem;
margin: 56px 0 12px;
padding-top: 18px;
border-top: 2px solid var(--border);
letter-spacing: -0.005em;
}
h3 {
font-size: 1.15rem;
margin: 32px 0 10px;
color: var(--accent);
}
h4 {
font-size: 1rem;
margin: 24px 0 6px;
color: var(--fg);
}
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.86em;
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;
border-radius: 0;
font-size: inherit;
}
.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.94rem;
}
th, td {
text-align: left;
padding: 10px 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; }
.footnote {
font-size: 0.85rem;
color: var(--fg-muted);
border-top: 1px solid var(--border);
margin-top: 48px;
padding-top: 16px;
}
.pros-cons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
margin: 14px 0 18px;
}
.pros, .cons {
border: 1px solid var(--border);
border-radius: 4px;
padding: 12px 16px;
background: white;
}
.pros { border-left: 3px solid var(--good); }
.cons { border-left: 3px solid var(--bad); }
.pros h4, .cons h4 { margin-top: 0; }
.verdict {
font-size: 1.1rem;
font-weight: 600;
margin: 8px 0 14px;
}
.verdict-go { color: var(--good); }
.verdict-warn { color: var(--warn); }
.verdict-no { color: var(--bad); }
@media (max-width: 600px) {
.pros-cons { grid-template-columns: 1fr; }
.wrap { padding: 24px 18px 64px; }
}
</style>
</head>
<body>
<div class="wrap">
<h1>mach.ko — an out-of-tree FreeBSD kernel module for Mach IPC, shipped from <code>freebsd-launchd</code></h1>
<p class="lede">Feasibility study for a small kernel module (<code>/boot/kernel/mach.ko</code>) that re-introduces just enough Mach IPC to let launchd run a real bootstrap server and let configd boot unmodified. Built and released as a versioned artifact of the <code>freebsd-launchd</code> repo, alongside the live ISO. Targets only stable in-major-version FreeBSD KPIs so the artifact rebuilds at major bumps (15.x → 16.x), not minor releases.</p>
<p class="meta">Companion to <a href="freebsd-launchd-service-ordering.html">freebsd-launchd-service-ordering.html</a>. Where that doc surveys three options and broadly favours not porting Mach, this doc takes the "port Mach" path seriously and asks what a minimal, well-scoped implementation would actually look like. Written 2026-05-10.</p>
<div class="toc">
<h2>Contents</h2>
<ol>
<li><a href="#tldr">TL;DR — verdict and shape</a></li>
<li><a href="#scope">Scope — the "minimum useful Mach" target</a></li>
<li><a href="#why-kernel">Why a kernel module, not a userspace shim</a></li>
<li><a href="#kbi">Kernel KPI surface and the "rebuild only on major bumps" guarantee</a></li>
<li><a href="#arch">Architecture — mach.ko, libmach, launchd bootstrap server</a></li>
<li><a href="#userland">Userland API — what <code>libmach.so</code> exposes</a></li>
<li><a href="#launchd-changes">Changes to the freebsd-launchd daemon itself</a></li>
<li><a href="#configd">What configd needs, and what it doesn't</a></li>
<li><a href="#build">Build pipeline — shipping mach.ko as a freebsd-launchd release artifact</a></li>
<li><a href="#milestones">Milestones — A through E</a></li>
<li><a href="#risks">Risks and unknowns</a></li>
<li><a href="#license">License posture</a></li>
<li><a href="#decision">Decision criteria — when to go, when to bail</a></li>
</ol>
</div>
<h2 id="tldr">1. TL;DR — verdict and shape</h2>
<p class="verdict verdict-warn">Feasible, scoped, ~6–9 months from zero to "configd boots." Not feasible as "implement Mach." Feasible as "implement the ~15-call Mach subset that configd and launchd's bootstrap server actually use, in kernel, because that subset has clean port-rights and queueing semantics that are awkward in userspace."</p>
<ul>
<li>The <strong>Mach surface area configd actually exercises is small</strong>: ~12–15 distinct calls (<code>mach_msg</code>, <code>mach_port_allocate/deallocate/mod_refs/insert_right/request_notification/construct/destruct</code>, <code>vm_allocate/deallocate</code>, <code>mach_task_self</code>, <code>bootstrap_check_in/look_up</code>). Two MIG subsystems, ~20 routines in <code>_config_subsystem</code>. <strong>No out-of-line memory entries</strong>, <strong>no exception ports</strong>, <strong>no processor sets</strong>, <strong>no host ports</strong> in configd's hot path. (Verified by direct grep of <code>configd/src/</code> at version 963.270.3.)</li>
<li>The <strong>FreeBSD kernel KPIs we'd touch are all in the documented stable subset</strong>: <code>malloc(9)</code>, <code>mtx(9)</code>, <code>sx(9)</code>, <code>condvar(9)</code>, <code>callout(9)</code>, <code>sysctl(9)</code>, <code>make_dev(9)</code>, <code>kqueue(9)</code> custom filter, <code>SYSCALL_MODULE</code>/<code>syscall_helper_register</code>, <code>file(9)</code>, <code>vm_map_*</code> for inline copy, <code>eventhandler(9)</code> for proc-exit cleanup. Survey of <code>sys/modules/</code> finds <strong>no <code>__FreeBSD_version</code> conditionals</strong> in comparable out-of-tree-style modules — the convention is to rebuild against each major.</li>
<li><strong>Shipping path is clean</strong>: <code>freebsd-launchd</code>'s existing <code>build.sh</code> already runs inside a FreeBSD VM via <code>vmactions/freebsd-vm@v1</code>, and already stages and builds the <code>kmodloader</code> userland tool. Adding a <code>mach-kmod/</code> source tree and a <code>make-mach-kmod.sh</code> wrapper that invokes <code>bsd.kmod.mk</code> is a one-day pipeline change. The kmod ships <em>both</em> baked into the live ISO at <code>/boot/kernel/mach.ko</code> <em>and</em> uploaded as a standalone GitHub release asset per (FreeBSD-major × arch) tuple.</li>
<li>The <strong>launchd daemon needs about three structural changes</strong>: (1) build <code>liblaunch.c</code> (preserved but unbuilt today), (2) parse <code>MachServices</code> in <code>core.m</code>, (3) implement bootstrap server inside launchd PID 1 (registration table + on-demand spawn). The AF_UNIX IPC stays as the launchctl control channel; Mach is layered alongside, not replacing it.</li>
<li><strong>Hard-no: Apple source.</strong> XNU's Mach implementation is APSL-2.0; even where licensing allows, the code is intricately tied to XNU's vm and proc subsystems and is not portable. We re-implement the <em>API</em> from documentation; we do not port the <em>code</em>.</li>
</ul>
<div class="callout callout-warn">
<p><strong>Companion doc context.</strong> The service-ordering doc evaluates "port Mach" as Option 2 and prices it at 6–24 months, ranking it lowest on effort/risk among the three options. This doc agrees with that range but argues the lower bound (6–9 months) is reachable <em>if</em> we accept a hard scope freeze: configd's API surface, nothing more. Go beyond that and the timeline blows up.</p>
</div>
<h2 id="scope">2. Scope — the "minimum useful Mach" target</h2>
<p>The risk in any "port Mach" project is scope creep into Mach-the-microkernel: tasks, threads, exception ports, processor sets, virtual memory regions, the host abstraction, processor pinning, etc. Almost none of that is what configd or launchd actually want. They want <strong>named, queued message passing with kernel-mediated capability transfer</strong>. We define scope by enumeration:</p>
<table>
<thead><tr><th>Mach concept</th><th>In scope?</th><th>Justification</th></tr></thead>
<tbody>
<tr><td><code>mach_port_t</code> (receive right + send right semantics)</td><td><span class="pill pill-good">Yes</span></td><td>Required by every <code>bootstrap_*</code> call and every configd RPC. Core abstraction.</td></tr>
<tr><td><code>mach_msg()</code> with SEND/RCV/TIMEOUT options</td><td><span class="pill pill-good">Yes</span></td><td>138 direct call sites in configd; the whole MIG transport.</td></tr>
<tr><td>Inline message copy (header + body up to N KB)</td><td><span class="pill pill-good">Yes</span></td><td>configd's XML payloads travel inline.</td></tr>
<tr><td>Send-right transfer in messages (<code>MACH_MSG_PORT_DESCRIPTOR</code>)</td><td><span class="pill pill-good">Yes</span></td><td>Bootstrap hands send rights to clients; clients send reply ports back. Non-negotiable.</td></tr>
<tr><td>No-senders notification (<code>MACH_NOTIFY_NO_SENDERS</code>)</td><td><span class="pill pill-good">Yes</span></td><td>11 call sites in configd; how sessions are reaped when clients exit.</td></tr>
<tr><td>Dead-name notification (<code>MACH_NOTIFY_DEAD_NAME</code>)</td><td><span class="pill pill-good">Yes</span></td><td>Used for client tracking; small additional surface once no-senders is in.</td></tr>
<tr><td>Bootstrap server protocol (lookup, check-in, register)</td><td><span class="pill pill-good">Yes</span></td><td>Implemented in launchd PID 1, not in mach.ko itself, but mach.ko has to support the underlying RPC.</td></tr>
<tr><td>Out-of-line memory descriptors (<code>MACH_MSG_OOL_DESCRIPTOR</code>)</td><td><span class="pill pill-warn">Stub</span></td><td>Zero direct uses in configd, but MIG can emit them for large XML. Stub returns <code>KERN_NOT_SUPPORTED</code> initially; messages exceeding inline limit get an explicit error rather than silent truncation.</td></tr>
<tr><td>Memory entries (<code>mach_make_memory_entry_64</code>)</td><td><span class="pill pill-bad">No</span></td><td>Zero call sites in configd. Skip.</td></tr>
<tr><td>Tasks, threads, exception ports, processor sets, host ports</td><td><span class="pill pill-bad">No</span></td><td>Not used by configd or launchd-bootstrap. The "port" is just the IPC name; we don't implement the rest of the microkernel.</td></tr>
<tr><td>XPC layer (<code>xpc_connection_*</code>, <code>xpc_dictionary_*</code>)</td><td><span class="pill pill-bad">No (separate)</span></td><td>344 calls in configd's satellite daemons (IPMonitorControl, dnsinfo, network_information_server) but zero in the configd <em>core</em> bootstrap path. Layer XPC on later as <code>libxpc.so</code> over Mach; not part of mach.ko.</td></tr>
<tr><td>Audit tokens / peer credentials</td><td><span class="pill pill-warn">Yes, simplified</span></td><td>Carry pid + uid + gid in the kernel-attached message metadata. Configd uses these for authorization.</td></tr>
</tbody>
</table>
<p>The whole module fits in roughly: one cdev (<code>/dev/mach</code>) for control + a small handful of new syscalls registered via <code>SYSCALL_MODULE</code> for the hot <code>mach_msg</code> path. Estimated implementation budget: 4–6 KLoC of kernel code plus 2 KLoC of userland (<code>libmach.so</code> + bootstrap glue inside launchd).</p>
<h2 id="why-kernel">3. Why a kernel module, not a userspace shim</h2>
<p>The service-ordering doc's Option 1 is "rewrite configd to use AF_UNIX, drop Mach." That works and is cheaper. The argument for going to the kernel anyway:</p>
<div class="pros-cons">
<div class="pros">
<h4>What the kernel buys you</h4>
<ul>
<li><strong>Capability transfer with kernel-enforced rights.</strong> Send-right transfer over a Unix socket via <code>SCM_RIGHTS</code> exists, but it transfers file descriptors with full POSIX semantics. Mach's send/receive distinction (you can hold a send right without being able to receive on the port; multiple processes can share a send right) is not expressible in <code>SCM_RIGHTS</code>. Doing it in userspace means a trusted broker process mediates every right transfer — that's a Mach implementation in userland, with extra IPC.</li>
<li><strong>Atomic, kernel-queued message delivery.</strong> "Send X, receive Y, hand Z to a third party" can complete without the recipient or third party being scheduled. A userland broker would need to wake up to forward.</li>
<li><strong>One namespace, kernel-rooted.</strong> No "where did the broker socket go?" race at boot. mach.ko's namespace is available the moment the module loads — before launchd PID 1 is even <code>exec()</code>'d.</li>
<li><strong>configd-source compatibility, no patches.</strong> Apple's configd source compiles against <code><mach/mach.h></code> and links against a <code>libmach.so</code> with the standard symbols. A kernel-backed implementation is API-identical; a userland broker is not (broker-RPC stubs leak in everywhere).</li>
<li><strong>Future Apple-source ports come along free.</strong> Once mach.ko + libmach is in place, anything else from Darwin that wants Mach (notifyd, distnoted, asl, parts of CoreFoundation, anything that uses <code>NSMachPort</code>) can be ported without further IPC plumbing.</li>
</ul>
</div>
<div class="cons">
<h4>What you pay</h4>
<ul>
<li><strong>Kernel code is kernel code.</strong> Bugs panic the box. Memory has to be accounted. Lock orders matter. We need a real test harness inside a VM, not unit tests.</li>
<li><strong>KBI maintenance per major.</strong> Even with a stable-only KPI diet, every FreeBSD major needs a fresh build and a smoke test in CI.</li>
<li><strong>Userland alternative is genuinely smaller.</strong> A trusted-broker libmach over a single Unix socket to a <code>machd</code> daemon is doable in 2–3 KLoC. It's slower and the right semantics are slightly off, but configd would not notice.</li>
<li><strong>Audit surface.</strong> A new kernel-resident name registry that any process can publish to and look up against is a new attack surface. Has to be designed with default-deny ACLs, not "anyone can register any name."</li>
</ul>
</div>
</div>
<p>The recommendation in this doc: <strong>start kernel-resident</strong>, because the service-ordering doc's Section 4.4 makes the case that bootstrap-mediated activation is the only ordering primitive that scales. Anything that has to be "running before launchd" or "available before any process exists" wants to be in the kernel.</p>
<h2 id="kbi">4. Kernel KPI surface and the "rebuild only on major bumps" guarantee</h2>
<p>FreeBSD's KBI policy: stable within a major release, may break across majors. <code>__FreeBSD_version</code> in <code>sys/sys/param.h</code> is currently <code>1600018</code> (major 16, the encoding is <code>MMmmRXX</code>). Out-of-tree modules are expected to be rebuilt for each major. We commit to that; we want to be able to <em>not</em> commit to anything tighter.</p>
<p>The exhaustive list of KPIs <code>mach.ko</code> would touch, classified:</p>
<table>
<thead><tr><th>KPI</th><th>Used for</th><th>KBI stability</th></tr></thead>
<tbody>
<tr><td><code>malloc(9)</code>, <code>free(9)</code>, <code>MALLOC_DEFINE</code></td><td>Port object, message queue node, name table allocation</td><td><span class="pill pill-good">Stable</span></td></tr>
<tr><td><code>mtx(9)</code>, <code>sx(9)</code>, <code>rmlock(9)</code></td><td>Per-port queue lock; namespace rwlock; refcount mtx</td><td><span class="pill pill-good">Stable</span></td></tr>
<tr><td><code>condvar(9)</code></td><td>Blocking <code>mach_msg(MACH_RCV)</code> wait</td><td><span class="pill pill-good">Stable</span></td></tr>
<tr><td><code>callout(9)</code></td><td><code>MACH_RCV_TIMEOUT</code>, <code>MACH_SEND_TIMEOUT</code></td><td><span class="pill pill-good">Stable</span></td></tr>
<tr><td><code>sysctl(9)</code></td><td><code>kern.mach.*</code> introspection (port count, message bytes queued, namespace size)</td><td><span class="pill pill-good">Stable</span></td></tr>
<tr><td><code>make_dev_s(9)</code>, cdevsw, <code>d_ioctl</code></td><td><code>/dev/mach</code> control device for namespace setup, debug, audit</td><td><span class="pill pill-good">Stable</span> (modern <code>make_dev_s</code> API since 11.x)</td></tr>
<tr><td><code>SYSCALL_MODULE</code> / <code>syscall_helper_register</code></td><td>Hot path: <code>mach_msg</code>, <code>mach_port_allocate</code>, etc. as proper syscalls so MIG stubs see the right ABI</td><td><span class="pill pill-good">Stable</span> (linux compat shim has used this pattern since 7.x)</td></tr>
<tr><td><code>kqueue(9)</code> custom filter</td><td><code>EVFILT_MACHPORT</code> equivalent so libdispatch can wait on Mach receives alongside fd events</td><td><span class="pill pill-good">Stable</span> (filter registration API documented in <code>kqueue(9)</code>)</td></tr>
<tr><td><code>file(9)</code>, <code>fget</code>, <code>fput</code>, <code>finstall</code></td><td>Port-as-fd handle so kqueue and <code>poll</code> work; file descriptor inheritance for <code>posix_spawn</code> hand-down</td><td><span class="pill pill-good">Stable</span></td></tr>
<tr><td><code>copyin</code>, <code>copyout</code>, <code>fueword</code>, <code>suword</code></td><td>Userland message buffer transfer</td><td><span class="pill pill-good">Stable</span></td></tr>
<tr><td><code>vm_map_lookup</code>, <code>vm_map_protect</code> (read-only paths)</td><td>Validating user buffers; bounded inline message copy. We do <em>not</em> implement OOL memory entries.</td><td><span class="pill pill-good">Stable</span> (read-only KPIs)</td></tr>
<tr><td><code>proc(9)</code>: <code>curproc</code>, <code>p_pid</code>, <code>p_ucred</code></td><td>Tagging messages with sender pid/uid/gid</td><td><span class="pill pill-good">Stable</span></td></tr>
<tr><td><code>eventhandler(9)</code> — <code>process_exit</code></td><td>Reaping ports owned by a dying task; firing no-senders / dead-name notifications</td><td><span class="pill pill-good">Stable</span></td></tr>
<tr><td><code>uma(9)</code> zone allocator</td><td>Optional optimization for hot port-object alloc; can be deferred</td><td><span class="pill pill-good">Stable</span></td></tr>
</tbody>
</table>
<div class="callout callout-good">
<p><strong>The bet:</strong> every entry above is documented in <code>share/man/man9/</code> on FreeBSD-CURRENT and has been present and source-compatible since 13.x. We use <strong>none</strong> of: vnode internals, scheduler internals, network stack internals, USB stack, sound stack, GEOM internals, ZFS, jails (beyond <code>cred</code> read), capsicum hooks, NUMA topology APIs. A grep of <code>sys/modules/cuse</code>, <code>sys/modules/sysvipc</code>, <code>sys/modules/linux</code> finds <strong>zero <code>__FreeBSD_version</code> conditionals</strong> — that's the bar we hold ourselves to.</p>
</div>
<p>If we ever need a KPI that turns out to be major-version-sensitive, the policy is: <strong>add a thin per-major shim file under <code>mach-kmod/compat/</code> and select it via the Makefile's <code>SRCS</code>, not via <code>#if</code> inside the main code</strong>. This keeps the main module readable and pushes all version churn into one file per major.</p>
<h2 id="arch">5. Architecture — mach.ko, libmach, launchd bootstrap server</h2>
<p>Three components, each with a distinct responsibility:</p>
<pre><code>+----------------------------------------------------------+
| Userland |
| |
| +----------+ +---------------+ +-----------------+ |
| | scutil | | netconfigd | | other clients | |
| | client | | (configd) | | | |
| +----+-----+ +-------+-------+ +--------+--------+ |
| | | | |
| | bootstrap_ | bootstrap_ | |
| | look_up() | check_in() | |
| v v v |
| +-----------------------------------------------------+ |
| | libmach.so (libdispatch-compatible) | |
| | mach_msg, mach_port_*, vm_alloc, bootstrap_* | |
| +-----------------------------------------------------+ |
| | | |
| syscall | | UNIX socket |
| (mach_*) | | to PID 1 (boot) |
| v v |
+-----------------------|----------------|-----------------+
| Kernel | | |
| v | |
| +-----------------------------------------------------+ |
| | mach.ko (port namespace, message queues, rights, | |
| | notifications, /dev/mach, EVFILT_MACH) | |
| +-----------------------------------------------------+ |
| | |
+----------------------------------------|-----------------+
|
v
+----------------------+
| launchd PID 1 |
| - bootstrap server |
| - MachServices regs |
| - on-demand spawn |
| (AF_UNIX still |
| serves launchctl) |
+----------------------+
</code></pre>
<h3>5.1 mach.ko responsibilities</h3>
<ul>
<li>Maintain a <strong>process-attached port name space</strong> (per-task, like Mach). Names are 32-bit ints, allocated densely; lookup is O(1) via per-task hash.</li>
<li>Maintain a <strong>kernel-global port object table</strong>. Each port has: a receive-right holder (one task), a refcounted set of send-right holders, a bounded message queue, and a notification subscription list.</li>
<li>Implement <strong><code>mach_msg</code></strong>: blocking send/receive with timeout, port-descriptor copy semantics, sender credential attachment, queue-limit enforcement.</li>
<li>Implement <strong>port lifecycle</strong>: <code>allocate</code>, <code>deallocate</code>, <code>insert_right</code>, <code>mod_refs</code>, <code>request_notification</code>, <code>construct</code>/<code>destruct</code>.</li>
<li>Fire <strong>no-senders</strong> and <strong>dead-name</strong> notifications via <code>eventhandler(9)</code> on process exit and on refcount drop.</li>
<li>Expose <strong><code>/dev/mach</code></strong> for: namespace introspection (debug only), bootstrap-port handoff at task spawn, and any RPC the bootstrap server needs that doesn't fit cleanly in a syscall.</li>
<li>Expose a <strong><code>EVFILT_MACHPORT</code> kqueue filter</strong> so libdispatch's <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code> can multiplex Mach receives with regular fd events.</li>
</ul>
<h3>5.2 libmach.so responsibilities</h3>
<ul>
<li>Provide the standard <code><mach/mach.h></code> API surface so configd and Apple-source consumers compile and link unmodified.</li>
<li>Wrap the <code>mach_msg</code>/<code>mach_port_*</code> syscalls (the hot path).</li>
<li>Implement the <code>bootstrap_*</code> family as RPC over a well-known port (the "bootstrap port"), which every task inherits at spawn time.</li>
<li>Provide <code>mach_task_self()</code> as a process-local read of the inherited self-port.</li>
</ul>
<h3>5.3 launchd PID 1 responsibilities</h3>
<ul>
<li>At startup, after mach.ko has loaded, allocate a receive port and register itself as the bootstrap server in mach.ko's name registry (a single well-known capability the kernel hands out to every newly spawned task).</li>
<li>Parse <code>MachServices</code> dict in plists. For each entry, allocate a kernel port and store the mapping <code>"name" → (port, owning Job)</code>.</li>
<li>On <code>bootstrap_look_up("name")</code>: if the owning Job is not running, <code>posix_spawn</code> it (handing it the receive right via <code>posix_spawn_file_actions</code>-like Mach extension); return a send right to the caller.</li>
<li>On <code>bootstrap_check_in("name")</code>: hand the receive right to the calling task (which is the just-spawned job).</li>
<li>Continue serving the existing AF_UNIX <code>launchctl</code> protocol unchanged. AF_UNIX is the control channel; Mach is the activation channel. They coexist.</li>
</ul>
<h2 id="userland">6. Userland API — what <code>libmach.so</code> exposes</h2>
<p>The day-1 symbol export list. Configd's call-site grep gives us the closure; this list is the closure plus a small standard envelope.</p>
<table>
<thead><tr><th>Symbol</th><th>Implemented as</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>mach_msg</code></td><td>syscall</td><td>Hot path. SEND, RCV, SEND|RCV (round-trip), TIMEOUT options. Honors <code>MACH_MSG_PORT_DESCRIPTOR</code> in the body.</td></tr>
<tr><td><code>mach_port_allocate</code></td><td>syscall</td><td>RECEIVE only on day 1. PORT_SET deferred (configd doesn't use sets).</td></tr>
<tr><td><code>mach_port_deallocate</code></td><td>syscall</td><td>Drop one reference.</td></tr>
<tr><td><code>mach_port_mod_refs</code></td><td>syscall</td><td>Adjust refcount on a right. Used heavily by configd for session bookkeeping.</td></tr>
<tr><td><code>mach_port_insert_right</code></td><td>syscall</td><td>Attach a known port to a name in the calling task.</td></tr>
<tr><td><code>mach_port_construct</code> / <code>mach_port_destruct</code></td><td>syscall</td><td>Modern grouped allocate+configure. Used by newer configd code paths.</td></tr>
<tr><td><code>mach_port_request_notification</code></td><td>syscall</td><td>NO_SENDERS and DEAD_NAME only on day 1. PORT_DESTROYED, SEND_POSSIBLE deferred.</td></tr>
<tr><td><code>mach_task_self</code></td><td>cached value</td><td>Set by libmach init from the task-self capability the kernel hands out at exec.</td></tr>
<tr><td><code>vm_allocate</code> / <code>vm_deallocate</code></td><td>wraps <code>mmap</code>/<code>munmap</code></td><td>configd uses these for inline buffer setup; not for OOL transfer.</td></tr>
<tr><td><code>bootstrap_check_in</code></td><td>RPC to launchd PID 1</td><td>Returns receive right.</td></tr>
<tr><td><code>bootstrap_look_up</code> / <code>bootstrap_look_up2</code></td><td>RPC to launchd PID 1</td><td>Returns send right; triggers on-demand spawn.</td></tr>
<tr><td><code>bootstrap_register</code></td><td>RPC to launchd PID 1</td><td>Used by daemons that want to register a name not in their plist (rare, but configd uses it).</td></tr>
<tr><td><code>mig_*</code> support routines</td><td>thin wrappers</td><td>Allocate/deallocate, error reply, dispatch table walking. ~300 lines.</td></tr>
</tbody>
</table>
<p>Symbols explicitly <em>not</em> implemented in v1, returning <code>KERN_NOT_SUPPORTED</code>:</p>
<ul>
<li><code>mach_make_memory_entry_64</code> — OOL memory entries; no caller in configd</li>
<li><code>thread_*</code>, <code>task_*</code> beyond <code>task_self</code></li>
<li><code>host_*</code>, <code>processor_*</code>, <code>processor_set_*</code></li>
<li><code>exception_raise*</code></li>
<li>Anything in <code><mach/mach_voucher*.h></code></li>
</ul>
<h2 id="launchd-changes">7. Changes to the freebsd-launchd daemon itself</h2>
<p>From the IPC inventory: today's launchd has 6 plist keys, an AF_UNIX framed protocol, and a <code>liblaunch.c</code> that's checked in but excluded from <code>src/Makefile</code>. The Mach work adds, narrowly:</p>
<h3>7.1 Build <code>liblaunch.c</code></h3>
<p>Apple's <code>src/liblaunch/liblaunch.c</code> already contains the <code>launch_data_t</code> serializers, the <code>launch_mach_checkin_service()</code> path (currently calls <code>bootstrap_check_in</code>, line ~1010), and the Mach port marshalling (<code>launch_data_set_machport</code>, <code>launch_data_new_machport</code>). Adding it to <code>src/Makefile</code>'s <code>SRCS</code> is a one-line change once <code>libmach.so</code> exists.</p>
<p>Two missing headers from the Apple-private set: <code>bootstrap.h</code>, <code>vproc.h</code>, <code>vproc_priv.h</code>. We write these from scratch, declaring just the symbols liblaunch.c references. The MIG stubs (<code>vproc_mig_set_security_session</code>, etc.) become thin RPC wrappers calling launchd over its existing AF_UNIX socket — they don't all have to be Mach.</p>
<h3>7.2 Parse <code>MachServices</code> in <code>core.m</code></h3>
<p>Add to the recognized-keys table in <code>core.m:258</code>:</p>
<pre><code>// In jobmgr_load_plist_file(), after the existing key parsing:
NSDictionary *machServices = [plist objectForKey:@"MachServices"];
if (machServices) {
j->mach_services = [machServices retain];
j->run_at_load = false; // MachServices implies launch-on-demand
}</code></pre>
<p>And on <code>jobmgr_insert(j)</code>, walk <code>j->mach_services</code>, allocate a kernel port via <code>mach_port_allocate</code>, register the (name, port, j) tuple in launchd's bootstrap table.</p>
<h3>7.3 Bootstrap server loop</h3>
<p>A new file <code>src/src/bootstrap.c</code>, ~600 lines:</p>
<ul>
<li>One Mach port (the bootstrap port) on which launchd receives <code>bootstrap_look_up</code> / <code>bootstrap_check_in</code> / <code>bootstrap_register</code> requests.</li>
<li>Each request handler uses libdispatch (<code>DISPATCH_SOURCE_TYPE_MACH_RECV</code> on the bootstrap port).</li>
<li>Lookup handler: find the registered name; if its job isn't running, call <code>job_spawn(j)</code>; reply with a freshly-inserted send right to the kernel port.</li>
<li>Check-in handler: verify the calling pid matches the spawned job (using the kernel-attached audit token); transfer the receive right.</li>
</ul>
<h3>7.4 Hand the bootstrap port to spawned jobs</h3>
<p>Tiny mach.ko-supported extension to <code>posix_spawn</code>: an ioctl on <code>/dev/mach</code> that says "the next task this process spawns inherits port X as its bootstrap port." launchd does this immediately before each <code>posix_spawn</code> for a job. Cleaner than a Mach-specific syscall.</p>
<h2 id="configd">8. What configd needs, and what it doesn't</h2>
<p>From the configd Mach-surface inventory:</p>
<table>
<thead><tr><th>Demand</th><th>Met by</th></tr></thead>
<tbody>
<tr><td>Register <code>com.apple.SystemConfiguration.configd</code> at startup</td><td><code>bootstrap_check_in</code> → launchd bootstrap server → mach.ko hands receive right</td></tr>
<tr><td>~20 MIG routines in <code>_config_subsystem</code></td><td>Apple's MIG-generated <code>configMIGServer.c</code> compiles unchanged once libmach exists; dispatcher calls handlers in <code>configd.tproj/_*.c</code></td></tr>
<tr><td>Per-client session ports</td><td><code>mach_port_allocate</code> + <code>mach_port_insert_right</code> in mach.ko; one port per session</td></tr>
<tr><td>NO_SENDERS notification on session port for client-died cleanup</td><td><code>mach_port_request_notification(MACH_NOTIFY_NO_SENDERS)</code> — mach.ko fires via <code>eventhandler(9)</code> proc_exit</td></tr>
<tr><td>Inline XML payload transfer (typical 200 B – 8 KB)</td><td>Inline <code>mach_msg</code> body; mach.ko bounded queue, configurable upper bound (default 64 KB matching XNU)</td></tr>
<tr><td>Plugin process notifications via PF_SYSTEM</td><td><strong>Unrelated to mach.ko.</strong> KernelEventMonitor uses BSD route sockets and PF_SYSTEM; FreeBSD has <code>PF_ROUTE</code> and an event loop. Separate porting work, but not in this plan's scope.</td></tr>
<tr><td>scutil command-line tool</td><td>Talks to configd via <code>SCDynamicStore</code> → <code>bootstrap_look_up</code> → MIG over Mach. Works once libmach + bootstrap server are running.</td></tr>
<tr><td>XPC clients (IPMonitorControl, dnsinfo, network_information_server)</td><td><strong>Out of scope for v1.</strong> XPC daemons need <code>libxpc.so</code> over Mach, which is a separate implementation effort. Without XPC, the core configd boots; satellite features are degraded.</td></tr>
</tbody>
</table>
<p>The verdict here is the load-bearing claim of the whole plan: <strong>core configd's hard dependency on Mach is small and well-bounded</strong>. Most of the 690 grep hits collapse to repeated use of the same ~12 calls. The 344 XPC hits are concentrated in code that can be left out of v1.</p>
<h2 id="build">9. Build pipeline — shipping mach.ko as a freebsd-launchd release artifact</h2>
<p>Per the user's framing: mach.ko should be built and released by the freebsd-launchd repo, not as a separate downstream project. Today the repo's CI already builds inside a FreeBSD VM via <code>vmactions/freebsd-vm@v1</code> and publishes a continuous release per push.</p>
<h3>9.1 Source layout addition</h3>
<pre><code>freebsd-launchd/
├── boot/
├── build.sh <-- modify to invoke make-mach-kmod.sh
├── configd/
├── kmodloader/
├── mach-kmod/ <-- NEW
│ ├── Makefile (KMOD=mach, SRCS=...)
│ ├── compat/
│ │ ├── freebsd15.c (per-major shim, if needed)
│ │ └── freebsd16.c
│ ├── mach_dev.c (/dev/mach cdevsw)
│ ├── mach_msg.c (mach_msg syscall)
│ ├── mach_port.c (port objects, refs, notifications)
│ ├── mach_namespace.c (per-task name table)
│ ├── mach_kqueue.c (EVFILT_MACHPORT filter)
│ ├── mach_module.c (DECLARE_MODULE, MODULE_VERSION)
│ └── tests/ (kyua tests run inside the build VM)
├── make-launchd.sh
├── make-mach-kmod.sh <-- NEW
├── make-configd.sh
└── overlays/</code></pre>
<h3>9.2 Build script integration</h3>
<p><code>make-mach-kmod.sh</code> is a wrapper of the same shape as <code>make-launchd.sh</code>: stage <code>mach-kmod/</code> into the chroot, run <code>make</code> with FreeBSD's <code>bsd.kmod.mk</code>, install the resulting <code>mach.ko</code> to <code>${DESTDIR}/boot/kernel/mach.ko</code>. <code>build.sh</code> calls it after kmodloader and before launchd, so the live ISO contains <code>/boot/kernel/mach.ko</code> ready to be preloaded.</p>
<p>Live ISO <code>loader.conf</code> gains:</p>
<pre><code># /boot/loader.conf inside the live ISO
mach_load="YES" # load mach.ko before init starts
mach_modules="" # reserved for future submodules</code></pre>
<h3>9.3 Standalone artifact upload</h3>
<p>The CI job that today uploads <code>FreeBSD-15.0-amd64-launchd-YYYYMMDD.iso</code> as the continuous release also uploads, in parallel:</p>
<pre><code>mach.ko-FreeBSD-15.4-amd64.tar.gz
mach.ko-FreeBSD-16.0-amd64.tar.gz
mach.ko-FreeBSD-16.0-aarch64.tar.gz # if/when arm64 build is added</code></pre>
<p>Each tarball contains: <code>mach.ko</code>, a <code>README</code> with the exact <code>__FreeBSD_version</code> built against, the SHA256 of the build-tree kernel, and a tiny <code>install.sh</code> that drops the file into <code>/boot/kernel/</code> and updates <code>/boot/loader.conf</code>. Users on a stock FreeBSD install can fetch the tarball, install, reboot, and have a kernel that's ready to host launchd + configd from the freebsd-launchd userland.</p>
<h3>9.4 Per-major build matrix</h3>
<table>
<thead><tr><th>FreeBSD major</th><th>Build matrix entry</th><th>Notes</th></tr></thead>
<tbody>
<tr><td>15.x (last stable before bump)</td><td><code>vmactions/freebsd-vm@v1</code> with <code>release: 15.x-RELEASE</code></td><td>Built once per major-stable release; rebuilt only if KPIs we use shifted (rare; caught by CI smoke test).</td></tr>
<tr><td>16.x (current target)</td><td>Same, <code>release: 16.0-RELEASE</code></td><td>Primary target; baked into the live ISO.</td></tr>
<tr><td>CURRENT (-CURRENT, KBI churn possible)</td><td>Optional experimental matrix entry; allowed to break</td><td>Useful early-warning; not promised to users.</td></tr>
</tbody>
</table>
<p>The artifact naming embeds the exact <code>__FreeBSD_version</code> the module was built against. <strong>If a user's running kernel reports a different <code>__FreeBSD_version</code>, kldload refuses</strong> — FreeBSD's normal KBI safety net. Users see a clear error instead of a panic; we publish a fresh artifact for the new minor only if a real KBI bump occurred (which our KPI choice should make rare).</p>
<h2 id="milestones">10. Milestones — A through E</h2>
<p>Estimates assume one engineer at ~70% allocation. Multiply for vacation, calendar reality, and the fact that kernel work always surprises.</p>
<h3>Phase A — mach.ko skeleton boots and survives <code>kldload</code> (3 weeks)</h3>
<ul>
<li>Module skeleton with <code>DECLARE_MODULE</code>, <code>MODULE_VERSION</code>, sysctl tree, <code>/dev/mach</code> cdev with stub <code>d_ioctl</code>.</li>
<li>Per-task name space allocator (no actual ports yet, just name table mechanics).</li>
<li>CI builds the kmod inside the FreeBSD-15.x VM, kldloads it, runs a kyua smoke test, kldunloads, asserts no leaked memory.</li>
<li><strong>Exit criterion:</strong> green CI on every push.</li>
</ul>
<h3>Phase B — ports, messages, rights (8 weeks)</h3>
<ul>
<li><code>mach_port_allocate</code> / <code>deallocate</code> / <code>mod_refs</code> / <code>insert_right</code> as syscalls.</li>
<li><code>mach_msg</code> SEND, RCV, SEND|RCV with timeouts. Inline body up to 64 KB.</li>
<li>Send-right transfer via <code>MACH_MSG_PORT_DESCRIPTOR</code>.</li>
<li>Userland test program: two processes, port handoff via fork+exec, round-trip message.</li>
<li><strong>Exit criterion:</strong> a 200-line test program does <code>port_allocate</code> → child <code>mach_msg(SEND)</code> → parent <code>mach_msg(RCV)</code> → reply round-trip, repeated 1M times, no leaks (sysctl-reported port count returns to baseline).</li>
</ul>
<h3>Phase C — notifications, kqueue, libmach.so (5 weeks)</h3>
<ul>
<li><code>request_notification(NO_SENDERS)</code> + <code>(DEAD_NAME)</code>; eventhandler hook on <code>process_exit</code>.</li>
<li><code>EVFILT_MACHPORT</code> kqueue filter so libdispatch can drive a Mach receive on its dispatch_source.</li>
<li>Userland <code>libmach.so</code>: the symbol set in Section 6, plus <code><mach/mach.h></code> headers extracted/derived from XNU public headers (BSD-licensed parts).</li>
<li><strong>Exit criterion:</strong> a libdispatch test using <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code> works end-to-end.</li>
</ul>
<h3>Phase D — bootstrap server in launchd (4 weeks)</h3>
<ul>
<li><code>bootstrap.c</code> in <code>freebsd-launchd/src/src/</code>: name registration table, lookup/check-in/register handlers.</li>
<li><code>core.m</code>: parse <code>MachServices</code>, allocate ports at job-load time, fire on-demand spawn from lookup handler.</li>
<li>Build <code>liblaunch.c</code>; write minimal <code>bootstrap.h</code> / <code>vproc.h</code>.</li>
<li>End-to-end demo: a one-line "hello" daemon with a <code>MachServices</code> plist; <code>scutil</code>-style client looks up the name, sends a message, daemon was launched on demand.</li>
<li><strong>Exit criterion:</strong> the example daemon launches on first lookup, not at boot, and replies correctly.</li>
</ul>
<h3>Phase E — configd boots (4 weeks)</h3>
<ul>
<li>Build configd against libmach. Resolve link errors one at a time; expect ~20–40.</li>
<li>Write <code>MachServices</code> plist for <code>org.freebsd.netconfigd</code>.</li>
<li>Run <code>scutil</code> against it; expect at least <code>list</code>, <code>get</code>, <code>set</code> to work.</li>
<li>Stub the FreeBSD-specific bits configd needs that aren't Mach (PF_ROUTE for KernelEventMonitor; that's a separate, parallel work item).</li>
<li><strong>Exit criterion:</strong> netconfigd registers with launchd on boot via Mach activation; <code>scutil --get HostName</code> returns the right answer.</li>
</ul>
<p>Total: <strong>24 weeks (~5.5 months) elapsed at 70% allocation</strong>. Add 6–8 weeks of "configd actually works for real use cases including KernelEventMonitor PF_ROUTE port" before this is shippable to end users. <strong>Total realistic budget: 7–9 months.</strong></p>
<h2 id="risks">11. Risks and unknowns</h2>
<table>
<thead><tr><th>Risk</th><th>Likelihood</th><th>Impact</th><th>Mitigation</th></tr></thead>
<tbody>
<tr>
<td>Some "rare" Mach call turns out to be on configd's hot path after all (e.g., MIG emits OOL descriptors for messages over inline limit)</td>
<td>Medium</td><td>High</td>
<td>Phase B exit gate includes "compile MIG-generated <code>configMIGServer.c</code> and link." Any unsatisfied symbols surface immediately, not at Phase E.</td>
</tr>
<tr>
<td>libdispatch on FreeBSD relies on Mach-specific dispatch_source semantics we underestimate</td>
<td>Medium</td><td>High</td>
<td>Phase C explicitly tests <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code> end-to-end; if it doesn't work we redesign the kqueue filter before Phase D, not after.</td>
</tr>
<tr>
<td>FreeBSD KBI shifts in a minor release because we hit a less-stable corner of a "stable" KPI</td>
<td>Low</td><td>Medium</td>
<td>CI matrix includes the latest minor of each supported major. Failures show up as red builds, not as user-reported panics. Per-major <code>compat/</code> shim keeps churn isolated.</td>
</tr>
<tr>
<td>Send-right semantics are subtly wrong vs Apple, configd works but later imports (e.g., notifyd) don't</td>
<td>Medium</td><td>Medium</td>
<td>Build a portable test suite from XNU's <code>tests/mach_*.c</code> early. Run it in CI from Phase B onward.</td>
</tr>
<tr>
<td>Security model: any process can register any bootstrap name — trivial spoofing</td>
<td>High if not addressed</td><td>High</td>
<td>Bootstrap server rejects <code>bootstrap_register</code> from non-root unless the name is in a per-user namespace (mirrors Apple's user vs system bootstrap). MachServices registration is launchd-only.</td>
</tr>
<tr>
<td>Performance: kernel-resident port queues add a syscall per message vs Apple's optimized fastpath</td>
<td>Low for v1</td><td>Low</td>
<td>configd is not a high-throughput service. Defer fastpath work to a later phase. Initial benchmark target: 50k round-trips/sec single-threaded, comfortably above configd's working load.</td>
</tr>
<tr>
<td>Apple-source MIG stub generation requires the <code>mig</code> tool, which is Apple-only</td>
<td>Confirmed</td><td>Medium</td>
<td>Use the open-source MIG re-implementation from Darling, or pre-generate stubs once and check them into the repo. Phase A includes a one-time decision on which.</td>
</tr>
<tr>
<td>Scope creep: someone wants exception ports, processor sets, host ports for an unrelated import</td>
<td>Likely</td><td>High</td>
<td>Hard rule, written into <code>mach-kmod/README.md</code>: any new symbol added to <code>libmach.so</code> requires a justification in the form "<named caller> uses it for <named purpose>." No speculative implementations.</td>
</tr>
<tr>
<td>The XPC layer (which configd's satellite daemons need) turns out to be much larger than Mach itself</td>
<td>Confirmed by inspection</td><td>Medium</td>
<td>v1 ships without XPC. Satellite daemons (IPMonitorControl, dnsinfo, network_information_server) are explicitly out of scope until libxpc is a separate workstream. Document this loudly.</td>
</tr>
</tbody>
</table>
<h2 id="license">12. License posture</h2>
<p>We re-implement the Mach API from public documentation. We do not import APSL-2.0 code from XNU. The <code><mach/*.h></code> headers in XNU are largely BSD-licensed (CMU/Mach origin) and can be used with attribution; the implementation files (<code>osfmk/ipc/*</code>) are APSL and we leave them alone.</p>
<p>This puts mach.ko's license at <strong>BSD-2-Clause</strong> matching the rest of <code>freebsd-launchd</code>. The headers we ship in <code>mach-kmod/include/mach/</code> retain CMU notices where applicable. Apple's <code>liblaunch.c</code> and the configd source remain under their original Apache-2.0 / APSL headers per file, exactly the existing repo policy (per <code>NOTICE</code>).</p>
<h2 id="decision">13. Decision criteria — when to go, when to bail</h2>
<div class="callout callout-good">
<p><strong>Go signal:</strong> Phases A–C complete on schedule (16 weeks), and the libdispatch <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code> test in Phase C passes without significant kqueue redesign. Phases D–E are then mostly userland glue.</p>
</div>
<div class="callout callout-warn">
<p><strong>Pause-and-reassess signal:</strong> Phase B reveals a Mach semantic we hadn't budgeted (e.g., port sets are needed by MIG in a way we missed; OOL is on configd's hot path; send-once rights are required by some bootstrap path). Add 4 weeks; if the new estimate exceeds 10 months total, switch to the service-ordering doc's Option 1 (rewrite configd to AF_UNIX) for the netconfigd milestone and keep mach.ko as a longer-horizon project.</p>
</div>
<div class="callout">
<p><strong>Bail signal:</strong> Either (a) the FreeBSD KPIs we depend on turn out to require <code>__FreeBSD_version</code> conditionals every minor release (would be very surprising given the survey, but if so the per-major artifact promise is dead), or (b) a kernel panic in <code>mach.ko</code> proves to require deep VM-subsystem knowledge to fix. The contingency is the same: ship Option 1, treat configd as a one-off, defer mach.ko.</p>
</div>
<p>Even in the bail case, the work is not wasted: <code>libmach.so</code> with all entry points stubbed to return <code>KERN_NOT_SUPPORTED</code> still lets us compile-test Apple-source ports, and the <code>MachServices</code> parsing in <code>core.m</code> + the bootstrap server in launchd still solves the service-ordering problem (Sections 4.4 and 10 of the companion doc) by giving us a name registry, even if the actual port handed out is backed by an AF_UNIX socket rather than a kernel port.</p>
<h2 id="closing">Closing summary</h2>
<p>An out-of-tree <code>mach.ko</code> shipped from <code>freebsd-launchd</code> is feasible if and only if we hold ruthlessly to the "configd's call set, nothing more" scope. The kernel-side surface fits in ~5 KLoC of stable-KPI code; the userland glue is another ~2 KLoC; the launchd changes are bounded to building <code>liblaunch.c</code>, parsing <code>MachServices</code>, and adding a bootstrap server loop.</p>
<p>The shipping story matches the existing repo: a <code>mach-kmod/</code> source tree, a <code>make-mach-kmod.sh</code> wrapper, integration into <code>build.sh</code>, and a per-FreeBSD-major release artifact uploaded alongside the live ISO. The KBI bet — rebuild only on major bumps — holds because the KPIs we touch are documented stable and because comparable out-of-tree-style modules in the FreeBSD tree carry no <code>__FreeBSD_version</code> conditionals.</p>
<p>The realistic budget is 7–9 months. The realistic risk is scope creep into Mach-the-microkernel; the discipline is documented in Section 11.</p>
<p class="footnote">Research basis: direct inspection of <code>/Users/jmaloney/Documents/launchd/freebsd-launchd</code> (launchd port, configd 963.270.3 import, build pipeline) and <code>/Users/jmaloney/Documents/launchd/freebsd-src</code> (FreeBSD kernel module patterns, KPI documentation, <code>__FreeBSD_version</code> at 1600018). Companion to <a href="freebsd-launchd-service-ordering.html">freebsd-launchd-service-ordering.html</a> and <a href="freebsd-launchd-plan.html">freebsd-launchd-plan.html</a>.</p>
</div>
</body>
</html>