Repository navigation
Expand file tree
/
Copy pathfreebsd-disk-arbitration-plan.html
More file actions
497 lines (422 loc) · 42.8 KB
/
Copy pathfreebsd-disk-arbitration-plan.html
File metadata and controls
497 lines (422 loc) · 42.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
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FreeBSD DiskArbitration — 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.plist { border-left-color: var(--accent); }
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.info { background: #d6e6f3; color: var(--accent2); }
.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; }
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; }
.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; }
.verdict { font-weight: 600; }
.verdict.go { color: var(--ok); }
.verdict.maybe { color: var(--warn); }
.verdict.no { color: var(--bad); }
.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; }
.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; }
.resolved { background: #ecf7ec; border-left: 4px solid var(--ok); padding: .8rem 1rem; margin: 1rem 0; border-radius: 0 6px 6px 0; }
</style>
</head>
<body>
<nav class="nav">
<a href="#tldr">Status</a>
<a href="#goal">Goal</a>
<a href="#repo">Repo</a>
<a href="#arch">Architecture</a>
<a href="#paths">Install paths</a>
<a href="#decisions">Decisions</a>
<a href="#files">File-by-file</a>
<a href="#bsd-wins">FreeBSD wins</a>
<a href="#usecases">Use cases</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 DiskArbitration — porting plan <span class="pill info">freebsd-launchd-mach (v2) effort</span></h1>
<p class="subtitle">A port of Apple's DiskArbitration framework + <code>diskarbitrationd</code> to FreeBSD: disk attach/detach events, mount/unmount/eject coordination, mount-policy approval prompts, and the same <code>DiskArbitration.framework</code> C API that Apple-derived applications already use. Replaces ad-hoc <code>kqueue(EVFILT_FS)</code> polling with a clean event API. Companion to <a href="freebsd-launchd-plan.html">launchd</a>, <a href="nextbsd-configd-plan.html">configd</a>, <a href="freebsd-hardware-registry-iokit-plan.html">hwregd</a>, <a href="freebsd-kmodloader-plan.html">kmodloader</a>, <a href="freebsd-asl-plan.html">asl</a>, <a href="freebsd-notifyd-plan.html">notifyd</a>, <a href="freebsd-mdnsresponder-plan.html">mDNSResponder</a>, <a href="nextbsd-ipconfiguration-plan.html">IPConfiguration</a>.</p>
<div class="resolved">
<strong>Revision 2026-05-23.</strong> This plan originally targeted the sibling <a href="https://github.com/pkgdemon/freebsd-launchd"><code>freebsd-launchd</code></a> (AF_UNIX / GNUstep Distributed Objects) repo where <code>libgeom</code> replaces IOKit and DO replaces Mach IPC. <strong>Refactored 2026-05-23 to target <a href="https://github.com/pkgdemon/freebsd-launchd-mach"><code>freebsd-launchd-mach</code></a> (v2)</strong>: this repo has <strong><code>hwregd</code></strong> — a MIG-served IORegistry-shape daemon at <code>org.freebsd.hwregd</code> that aggregates <code>devctl(4)</code> events + GEOM data — so DiskArbitration consumes <code>hwregd</code>'s storage-device-class notifications instead of walking GEOM directly for hot-plug. Mach IPC is <strong>retained</strong> for the daemon's <code>DiskArbitration.framework</code> client API (MIG IDL <code>da.defs</code>, Mach service <code>com.apple.DiskArbitration</code>, <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code> event loop). <code>libgeom</code> is used only for partition / UUID / label / FS-type <strong>enrichment</strong>, in-process, not as the event source.
</div>
<section id="tldr" class="tldr">
<h3>Status: planning <span class="pill info">v0</span> — deferred</h3>
<ul>
<li><strong>Repo:</strong> <a href="https://github.com/pkgdemon/freebsd-launchd-mach">github.com/pkgdemon/freebsd-launchd-mach</a> (v2 / Mach-IPC track) — <strong>monorepo</strong>. DiskArbitration source under <code>diskarb/</code>.</li>
<li><strong>Mission:</strong> give Apple-derived apps (Workspace's File Viewer, disk utilities, backup tools) the disk-event API they expect. Replaces a polling-based grab-bag of <code>kqueue(EVFILT_FS)</code> + <code>geom(8)</code> walks with an Apple-shaped event-driven model: register a callback once, receive events for the lifetime of your process.</li>
<li><strong>Source:</strong> Apple <code>DiskArbitration-79.3</code> (latest tag at <code>apple-oss-distributions/DiskArbitration</code>, APSL 2.0). 71 source files, ~34.5k LOC. 10 files have <code><mach/></code> includes — the daemon-client IPC plus IOKit's I/O Registry traversal.</li>
<li><strong>The hard part:</strong> Apple's DiskArbitration is <strong>IOKit-tied at its core.</strong> The daemon discovers disks by walking the IOKit registry's <code>IOMedia</code> objects; matches mount policies based on IOKit metadata (<code>kIOMediaContentKey</code>, etc.); subscribes to IOKit notifications for hot-plug. <strong>This repo has <code>hwregd</code></strong> — a MIG-served IORegistry-shape daemon at <code>org.freebsd.hwregd</code> already aggregating <code>devctl(4)</code> + GEOM data with a 10-routine RPC surface and a watch/notify channel. The port replaces IOKit calls with <strong>MIG RPC to <code>hwregd</code></strong> for storage-device-class events; <code>libgeom</code> is retained in-process only for partition-table / UUID / label / FS-type enrichment that <code>hwregd</code> doesn't yet expose. This is a real porting effort, but the hot-plug event source is now a clean MIG subscription, not a hand-rolled devctl reader.</li>
<li><strong>Mach IPC retained.</strong> Daemon-client channel stays on Mach (this is the Mach-IPC track). MIG IDL <code>da.defs</code> (matches the <code>hwreg.defs</code> / <code>ipconfig.defs</code> shape) defines a ~5–10 routine surface: session create/release, register disk-appeared / disappeared / mount-approval callbacks, claim, eject, mount, unmount. Service name <code>com.apple.DiskArbitration</code> (Apple-canonical). <code>DiskArbitration.framework</code> client-side calls keep <code>mach_msg</code>.</li>
<li><strong>Event loop:</strong> libdispatch sources. <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code> on the <code>com.apple.DiskArbitration</code> service port (client RPC); separate MIG subscription to <code>hwregd</code>'s storage-device-class notify channel for hot-plug; <code>DISPATCH_SOURCE_TYPE_VNODE</code> on <code>/etc/fstab</code> for mount-policy reload; one timer source per pending mount-approval-prompt.</li>
<li><strong>Why this is deferred:</strong> Workspace's File Viewer is a Phase 8+ concern and the simpler event needs (live ISO file-system mount/unmount feedback) can be served by direct <code>kqueue</code> in a few hundred LOC. DiskArbitration's value compounds with gershwin's full desktop UX maturity, not before. <strong>Phase 8+ work, sequenced after mDNSResponder.</strong></li>
<li><strong>Licensing:</strong> APSL 2.0 (same as configd / asl). Per-file headers preserved on Apple-derived files; top-level repo stays BSD-2-Clause.</li>
</ul>
</section>
<h2 id="goal">1. Goal & non-goals</h2>
<h3>1.1 Goal</h3>
<p>Provide a working <code>diskarbitrationd</code> + <code>libDiskArbitration</code> on FreeBSD so apps calling the standard DiskArbitration C API (<code>DARegisterDiskAppearedCallback</code>, <code>DARegisterDiskMountApprovalCallback</code>, <code>DADiskUnmount</code>, <code>DADiskEject</code>, <code>DADiskClaim</code>, etc.) work without source modification. Replace the IOKit-based disk-discovery layer with <strong>MIG RPC to <code>hwregd</code></strong> (which already aggregates devctl + GEOM into an IORegistry-shape registry); use <code>libgeom</code> in-process only for enrichment metadata that <code>hwregd</code> doesn't expose; preserve the policy framework that lets apps register mount approval / disapproval callbacks (e.g., disk-encryption tools that want a chance to unlock a disk before it's mounted); serve the framework over the Apple-canonical Mach service <code>com.apple.DiskArbitration</code>.</p>
<h3>1.2 Non-goals (this iteration)</h3>
<ul>
<li><strong>No IOKit port.</strong> Replace IOKit lookups / notifications with MIG RPC to this repo's <code>hwregd</code>. Don't try to port IOKit-without-Mach — that's a configd-scale project on its own, and <code>hwregd</code> already shipped the IORegistry-shape surface DiskArbitration needs.</li>
<li><strong>No HFS+ / APFS support.</strong> FreeBSD doesn't natively mount these filesystems (and shouldn't). DiskArbitration's filesystem-type detection skips them.</li>
<li><strong>No FileVault / encrypted-volume integration.</strong> macOS-specific encryption metadata; FreeBSD has GELI / GBDE / ZFS-native encryption with their own tooling.</li>
<li><strong>No Spotlight integration.</strong> Apple's DiskArbitration triggers Spotlight indexing on mount; we don't ship Spotlight.</li>
<li><strong>No SetupAssistant hooks.</strong> Apple-specific OOBE flow.</li>
</ul>
<h2 id="repo">2. Repository</h2>
<p>Monorepo. Source under <code>diskarb/</code> in <code>freebsd-launchd-mach</code>:</p>
<pre><code>freebsd-launchd-mach/
├── src/ launchd
├── configd/ Apple configd (MIG-served)
├── hwregd/ IORegistry-shape MIG daemon (devctl + GEOM)
├── IPConfiguration/ Apple IPConfiguration (MIG-served)
├── kmodloader/ clean-room kmodloader
├── asl/ Apple syslog
├── notifyd/ Apple Libnotify
├── mdns/ Apple mDNSResponder
├── diskarb/ Apple DiskArbitration (this plan)
│ ├── scripts/import-source.sh
│ ├── Makefile
│ ├── compat/ FreeBSD-specific shims (hwregd MIG client + libgeom enrichment)
│ ├── mig/ da.defs MIG IDL + generated stubs
│ └── src/ forked Apple DiskArbitration-79.3
│ ├── DiskArbitration/ framework (libDiskArbitration.so)
│ ├── DiskArbitrationAgent/ per-user mount-approval prompt agent
│ ├── diskarbitrationd/ the daemon
│ ├── autodiskmount/ legacy automount shim (drop or keep small)
│ ├── datest/ test harness
│ └── Modules/ IOKit hooks (mostly dropped + replaced by hwregd MIG calls)
└── make-diskarb.sh STANDALONE — builds + installs
</code></pre>
<h2 id="arch">3. Architecture</h2>
<div class="ascii-diagram"> +------------------+ +------------------------+
| /etc/fstab | | hwregd |
| /etc/auto_* | | (org.freebsd.hwregd) |
+--------+---------+ | storage device class |
| | ATTACH / DETACH / |
| (vnode) | PROPERTY-CHANGE |
| +-----------+------------+
| | MIG RPC
| | (watch/notify channel
| | DISPATCH_SOURCE_TYPE_MACH_RECV)
v v
+---------------------+ +--------------------------+-----------+
| libgeom |<--+ diskarbitrationd |
| (in-process | | (C, libdispatch, libxpc) |
| enrichment only: | | |
| partition table, | | - per-disk DADisk* objects |
| UUID, label, | | - claim/unclaim arbitration |
| FS type via | | - mount-policy callbacks |
| g_classes walk) | | |
+---------------------+ +-+-------------------------------------+
|
| Mach service com.apple.DiskArbitration
| MIG IDL: da.defs
| DISPATCH_SOURCE_TYPE_MACH_RECV
v
+------------+------------+
| libDiskArbitration |
| (linked into apps; |
| mach_msg under the |
| DADisk* API; |
| callbacks dispatched |
| to app's runloop / |
| dispatch queue) |
+-------------------------+</div>
<h3>3.1 Sourcing storage events from <code>hwregd</code></h3>
<p>Apple's daemon at startup walks the IOKit I/O Registry to find every <code>IOMedia</code> object — that's its source of truth for "what disks exist, what their metadata is, are they whole disks vs partitions, what filesystem type per partition." This repo has <strong><code>hwregd</code></strong> at the Mach service <code>org.freebsd.hwregd</code>, which already aggregates <code>devctl(4)</code> + GEOM data into an IORegistry-shape tree behind a 10-routine MIG RPC surface plus a watch/notify channel. <code>diskarbitrationd</code> consumes that interface rather than walking GEOM directly:</p>
<ul>
<li><strong>Initial enumeration:</strong> open a Mach client to <code>org.freebsd.hwregd</code>; query the storage device class subtree (ATA / SCSI / NVMe / removable / optical) via the registry-walk RPCs. One <code>DADisk</code> per returned node.</li>
<li><strong>Hot-plug:</strong> register on <code>hwregd</code>'s watch/notify channel for the storage device class; ATTACH / DETACH / PROPERTY-CHANGE events arrive as Mach messages and drive <code>DADisk</code> instantiation / teardown / property refresh. <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code> on the notify port; no <code>devctl(4)</code> parser in <code>diskarbitrationd</code> itself.</li>
<li><strong>Identity / class:</strong> from the <code>hwregd</code> properties (subsystem = <code>DEVFS</code>; device name like <code>ada0</code> / <code>da0</code> / <code>nvd0</code> / <code>cd0</code>; class membership in <code>DISK</code> / <code>PART</code> / <code>CD</code>; published GEOM-class enrichment when <code>hwregd</code> has it).</li>
</ul>
<p><strong><code>libgeom</code> is retained for enrichment only, in-process.</strong> When <code>diskarbitrationd</code> needs metadata that <code>hwregd</code> doesn't publish — partition table contents, GPT UUIDs, GELI/ZFS labels, filesystem-type sniffing — it walks <code>g_classes</code> directly via <code>libgeom(3)</code> for that single disk. This is library-style use, not event-source use:</p>
<ul>
<li>FS type detection: <code>g_label</code> metadata; otherwise read the partition table directly (<code>libufs</code> for UFS, ZFS pool import probe, etc.).</li>
<li>UUIDs / labels: <code>gpart show -l</code>-equivalent via <code>libgeom</code> XML walk on the specific provider.</li>
</ul>
<p>The separation matters: hot-plug timing / event delivery comes from <code>hwregd</code>'s MIG channel (clean, structured, already plumbed); metadata enrichment is a synchronous in-process call against <code>libgeom</code> (no async surface). If <code>hwregd</code> grows GEOM-class enrichment later, the in-process <code>libgeom</code> use shrinks.</p>
<h3>3.2 Mach IPC retained — <code>da.defs</code> MIG IDL</h3>
<p>The Mach-IPC track keeps the framework ↔ daemon channel on Mach. <code>diskarbitrationd</code> registers Mach service <strong><code>com.apple.DiskArbitration</code></strong> (Apple-canonical) at startup via launchd's MachServices; <code>libDiskArbitration</code> looks the port up and sends <code>mach_msg</code> requests through MIG stubs generated from <code>da.defs</code>. Daemon side runs a <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code> source on the service port; libdispatch routes each request to the generated MIG demux. The IDL filename matches the <code>hwreg.defs</code> / <code>ipconfig.defs</code> shape elsewhere in this repo.</p>
<p>Routine sketch (final list TBD in Phase 2, but the shape is set):</p>
<table>
<thead><tr><th>Routine</th><th>Purpose</th></tr></thead>
<tbody>
<tr><td><code>da_session_create</code></td><td>Per-client session; receive port for callback delivery; client-name string for logging.</td></tr>
<tr><td><code>da_session_release</code></td><td>Tear down session; cancel all callbacks; drop claims.</td></tr>
<tr><td><code>da_register_disk_appeared</code></td><td>Client registers interest in disk-appeared events. Daemon delivers existing disks immediately, then streams new arrivals via the session's receive port.</td></tr>
<tr><td><code>da_register_disk_disappeared</code></td><td>Counterpart on detach.</td></tr>
<tr><td><code>da_register_disk_mount_approval</code></td><td>Client opts into mount-veto rights for the disk classes it cares about.</td></tr>
<tr><td><code>da_disk_claim</code></td><td>Client takes exclusive arbitration over a disk (suppresses auto-mount).</td></tr>
<tr><td><code>da_disk_unclaim</code></td><td>Release the claim.</td></tr>
<tr><td><code>da_disk_mount</code></td><td>Request mount with options dict (path, fs-type, flags). Daemon delegates to <code>mount(8)</code>.</td></tr>
<tr><td><code>da_disk_unmount</code></td><td>Request unmount with force-flag option.</td></tr>
<tr><td><code>da_disk_eject</code></td><td>Unmount + eject (delegates to <code>camcontrol</code> / <code>cdcontrol</code> as appropriate).</td></tr>
</tbody>
</table>
<p>Callback delivery is asynchronous Mach messages from daemon to the client's session port, dispatched through the framework into the app's runloop or dispatch queue — same shape Apple uses, same byte-for-byte client-side public API.</p>
<h3>3.3 Mount-policy arbitration</h3>
<p>One of DiskArbitration's defining features — <em>not just events, but veto rights</em>. When a new disk appears, before it auto-mounts, the daemon dispatches mount-approval callbacks to all registered subscribers. Each subscriber can:</p>
<ul>
<li><strong>Approve</strong> — let the mount proceed.</li>
<li><strong>Veto</strong> — provide a dissent dictionary explaining why.</li>
<li><strong>Claim</strong> — tell the daemon "I'm going to mount this myself, don't auto-mount."</li>
</ul>
<p>This is how disk-encryption tools (<code>FileVault</code> on macOS, <code>geli</code> on FreeBSD) intercept disk insertion: register an approval callback, veto the auto-mount, prompt the user for the password, then explicitly mount. Without DiskArbitration, every encryption tool has to monitor disk events independently and race the auto-mounter.</p>
<h3>3.4 Event sources (libdispatch)</h3>
<table>
<thead><tr><th>Source type</th><th>Watches</th><th>Reaction</th></tr></thead>
<tbody>
<tr><td><code>DISPATCH_SOURCE_TYPE_MACH_RECV</code></td><td><code>com.apple.DiskArbitration</code> service port</td><td>demux incoming MIG requests from framework clients (<code>da.defs</code> routines)</td></tr>
<tr><td><code>DISPATCH_SOURCE_TYPE_MACH_RECV</code></td><td><code>hwregd</code> watch/notify port</td><td>storage-device-class ATTACH / DETACH / PROPERTY-CHANGE; instantiate or destroy <code>DADisk</code> objects; fire callbacks</td></tr>
<tr><td><code>DISPATCH_SOURCE_TYPE_VNODE</code></td><td><code>/etc/fstab</code>, <code>/etc/auto_master</code></td><td>reload mount policy on edit</td></tr>
<tr><td><code>DISPATCH_SOURCE_TYPE_TIMER</code></td><td>per-pending-approval timeout</td><td>if no subscriber responds within N seconds, default to "approve"</td></tr>
<tr><td><code>DISPATCH_SOURCE_TYPE_SIGNAL</code></td><td>SIGTERM, SIGHUP</td><td>SIGTERM: clean shutdown. SIGHUP: full re-enumeration via <code>hwregd</code> registry walk.</td></tr>
</tbody>
</table>
<p>Per-client lifecycle (auto-cancel on client exit) is handled at the Mach layer: when a session's receive port goes dead, the daemon gets a <code>MACH_NOTIFY_NO_SENDERS</code> notification and tears down the session. No <code>DISPATCH_SOURCE_TYPE_PROC</code> needed.</p>
<h2 id="paths">4. Install paths</h2>
<table>
<thead><tr><th>Artifact</th><th>Path</th><th>Why</th></tr></thead>
<tbody>
<tr><td><code>diskarbitrationd</code> binary</td><td><code>/usr/libexec/diskarbitrationd</code></td><td>Daemon. Same tier as other system daemons.</td></tr>
<tr><td><code>DiskArbitrationAgent</code></td><td><code>/usr/libexec/DiskArbitrationAgent</code></td><td>Per-user GUI agent: shows mount-approval prompts.</td></tr>
<tr><td><code>libDiskArbitration.so</code></td><td><code>/System/Library/Libraries/libDiskArbitration.so</code></td><td>Client library; apps link.</td></tr>
<tr><td>Headers</td><td><code>/System/Library/Headers/DiskArbitration/*.h</code></td><td>Public API; apps <code>#include <DiskArbitration/DiskArbitration.h></code>.</td></tr>
<tr><td>Mach service name</td><td><code>com.apple.DiskArbitration</code></td><td>Registered via launchd MachServices; framework looks up by name through bootstrap.</td></tr>
<tr><td>MIG IDL</td><td><code>diskarb/mig/da.defs</code></td><td>Generates client + server stubs at build time.</td></tr>
<tr><td>launchd plists</td><td><code>/System/Library/LaunchDaemons/org.freebsd.diskarbitrationd.plist</code><br><code>/System/Library/LaunchAgents/org.freebsd.DiskArbitrationAgent.plist</code></td><td>System daemon + per-user agent.</td></tr>
</tbody>
</table>
<h2 id="decisions">5. Locked architectural decisions</h2>
<table>
<thead><tr><th>Decision</th><th>Choice</th></tr></thead>
<tbody>
<tr><td>Source baseline</td><td>Apple <code>DiskArbitration-79.3</code>. APSL 2.0. Latest tag.</td></tr>
<tr><td>Disk discovery</td><td>MIG RPC to <code>hwregd</code> for enumeration + hot-plug events; <code>libgeom(3)</code> in-process for enrichment only. <strong>No IOKit; no devctl reader in <code>diskarbitrationd</code>.</strong></td></tr>
<tr><td>Daemon ↔ framework IPC</td><td><strong>Mach IPC retained.</strong> MIG IDL <code>da.defs</code>; service name <code>com.apple.DiskArbitration</code>.</td></tr>
<tr><td>Event loop</td><td>libdispatch sources, predominantly <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code>.</td></tr>
<tr><td>Filesystem-type detection</td><td>libgeom metadata + partition-table inspection (in-process enrichment). Drop HFS+/APFS detection (FreeBSD doesn't mount them).</td></tr>
<tr><td>Mount mechanism</td><td>FreeBSD <code>mount(8)</code> (and ZFS <code>zfs mount</code> for ZFS volumes) invoked via <code>posix_spawn</code> + <code>waitpid</code>, NOT direct <code>mount(2)</code> syscalls. Matches Apple's pattern of delegating to <code>/sbin/mount</code>. (Apple uses <code>NSTask</code>; we stay in C / libdispatch shape with no Foundation dep.)</td></tr>
<tr><td>Per-user agent</td><td>Yes (Phase 4). Approval prompts surface to user via the agent + Workspace UI.</td></tr>
<tr><td>License (top-level)</td><td>BSD-2-Clause. Apple's DA source retains APSL 2.0 per-file.</td></tr>
</tbody>
</table>
<h2 id="files">6. File-by-file plan (<code>diskarb/src/</code>)</h2>
<p>Imported source: Apple <code>DiskArbitration-79.3</code>. 71 files, ~34.5k LOC. 10 Mach-tied (the Mach IPC layer + IOKit lookup hooks).</p>
<h3>6.1 Deleted on import</h3>
<ul>
<li><code>DiskArbitration.xcodeproj/</code> — Xcode</li>
<li><code>Modules/</code> — IOKit-based device-classification hooks; replaced by <code>hwregd</code> MIG queries</li>
<li>Apple's Mach IDL files (<code>*.defs</code>) — <strong>replaced</strong>, not dropped: our own <code>diskarb/mig/da.defs</code> takes their place. The Apple-shipped <code>.defs</code> references Darwin-private types and is regenerated from scratch.</li>
<li><code>autodiskmount/</code> — legacy pre-DiskArbitration mount mechanism; either drop entirely or keep a small shim that translates <code>autodiskmount</code> CLI invocations to DA calls. Decision: drop in Phase 1; add back in Phase 5 if any compat consumer turns up.</li>
</ul>
<h3>6.2 Retained — Phase 2 fate</h3>
<table>
<thead><tr><th>Directory / file</th><th>Apple LOC</th><th>Action</th></tr></thead>
<tbody>
<tr><td><code>diskarbitrationd/diskarbitrationd.{c,m}</code></td><td>~3k</td><td>Substantial rewrite. Replace IOKit registry walk with <code>hwregd</code> MIG enumeration; replace IOKit notifications with the <code>hwregd</code> watch/notify channel under <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code>. Keep the disk-state-machine + arbitration logic.</td></tr>
<tr><td><code>diskarbitrationd/DAMain.{c,m}</code></td><td>~2k</td><td>Daemon main; replace Apple's Mach service-loop boilerplate with <code>dispatch_main</code> + libdispatch sources. Service port still obtained via <code>bootstrap_check_in</code>.</td></tr>
<tr><td><code>diskarbitrationd/DAServer.{c,m}</code></td><td>~5k</td><td>The IPC server. Keep Mach IPC; regenerate against <code>da.defs</code>. Keep the request-routing + per-client-state.</td></tr>
<tr><td><code>diskarbitrationd/DADisk.{c,m}</code></td><td>~3k</td><td>Per-disk state object. Replace IOKit-derived metadata getters with <code>hwregd</code> property reads + <code>libgeom</code> enrichment calls. Heavy refactor; ~50% rewrite.</td></tr>
<tr><td><code>diskarbitrationd/DAMount.{c,m}</code></td><td>~2k</td><td>Mount/unmount/eject orchestration. Replace <code>diskutil</code> NSTask invocations with <code>mount(8)</code> / <code>umount(8)</code> / <code>zfs</code>.</td></tr>
<tr><td><code>diskarbitrationd/DAFileSystem*</code></td><td>~3k</td><td>Filesystem-type detection. Drop HFS+/APFS detection; keep + extend UFS / EXT / FAT / NTFS / ISO9660 / ZFS detection.</td></tr>
<tr><td><code>DiskArbitration/DiskArbitration.{h,c}</code> + family</td><td>~5k</td><td>Client library. Keep Mach IPC; regenerate MIG client stubs against <code>da.defs</code>. <strong>Public API must stay byte-for-byte stable</strong>: <code>DASessionCreate</code>, <code>DARegister*</code>, <code>DADisk*</code> functions all keep signatures.</td></tr>
<tr><td><code>DiskArbitrationAgent/</code></td><td>~3k</td><td>Per-user agent. Port last; depends on Workspace having UI surface for approval prompts.</td></tr>
<tr><td><code>datest/</code></td><td>~1k</td><td>Adapt as a self-test harness. Useful for verifying GEOM-based discovery matches IOKit-based behavior on Apple.</td></tr>
</tbody>
</table>
<p>Total post-Phase-2: roughly <strong>22-24k LOC</strong> vs Apple's ~34k. About 30% deletion, plus ~30% of remaining code is new <code>hwregd</code>-client logic + <code>libgeom</code> enrichment shims. Net: similar LOC but very different shape. Mach IPC scaffolding (MIG demux, service-port management, no-senders notifications) is largely preserved from Apple verbatim modulo the regenerated <code>da.defs</code> stubs.</p>
<h2 id="bsd-wins">7. FreeBSD-only wins (vs Apple's Darwin-tied DiskArbitration)</h2>
<table>
<thead><tr><th>Feature</th><th>Apple's daemon does</th><th>This port (FreeBSD-only)</th></tr></thead>
<tbody>
<tr><td>Disk enumeration</td><td>IOKit registry walk; <code>IOMediaClass</code> matching</td><td>MIG RPC to <code>hwregd</code> (storage device class subtree); <code>libgeom(3)</code> in-process for enrichment</td></tr>
<tr><td>Hot-plug notification</td><td>IOKit <code>IOServiceAddInterestNotification</code></td><td><code>hwregd</code> watch/notify Mach channel via <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code></td></tr>
<tr><td>Filesystem-type detection</td><td>HFS+, APFS, FAT, NTFS, ExFAT, plus IOKit-published metadata</td><td>UFS, ZFS, FAT, NTFS, ExFAT, ISO9660, EXT2/3/4 (via FreeBSD's <code>fusefs-ext4</code>), via <code>libgeom</code> labels + partition-table inspection (in-process enrichment)</td></tr>
<tr><td>Encryption-volume hooks</td><td>FileVault / FileVault2 keychain integration</td><td>Drop. GELI / ZFS-native encryption use their own tooling outside DA.</td></tr>
<tr><td>Auto-mount target</td><td><code>/Volumes/<name></code></td><td><code>/Volumes/<name></code> — same convention. The <code>Volumes</code> directory is already created by macOS-style overlays in our system.</td></tr>
<tr><td>Daemon IPC</td><td>Mach ports + MIG stubs</td><td>Mach ports + MIG stubs (retained). MIG IDL <code>da.defs</code>; service <code>com.apple.DiskArbitration</code>.</td></tr>
</tbody>
</table>
<h2 id="usecases">8. Use cases for gershwin</h2>
<h3>8.1 Workspace File Viewer (the headline)</h3>
<p>The "Devices" sidebar in File Viewer (Finder-equivalent) populates from DA events. When a user plugs in a USB stick:</p>
<ol>
<li>Kernel attaches the device; <code>devctl(4)</code> fires; <code>hwregd</code> publishes a storage-device-class ATTACH event on its notify channel.</li>
<li>diskarbitrationd receives the ATTACH via its <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code> source on the <code>hwregd</code> watch port; reads device properties via MIG; calls <code>libgeom</code> in-process for partition / FS-type enrichment; instantiates DADisk; runs approval callbacks (none registered for USB sticks by default → approves).</li>
<li>Auto-mount fires: <code>mount</code> is invoked with the right filesystem type at <code>/Volumes/<name></code>.</li>
<li>Mount-success callback fires; subscribers (Workspace) get notified.</li>
<li>Workspace adds the volume to the sidebar; user clicks → opens the mount point.</li>
</ol>
<p>On eject (drag to trash, sidebar eject button): Workspace calls <code>DADiskUnmount(disk, kDADiskUnmountOptionDefault)</code>; daemon orchestrates clean unmount; eject-success callback removes the sidebar entry.</p>
<h3>8.2 Disk-utility-style apps</h3>
<table>
<thead><tr><th>App scenario</th><th>DA-API role</th></tr></thead>
<tbody>
<tr><td>"Disk Utility" / format new disk</td><td>Claim a disk to prevent auto-mount; format it; release claim; mount.</td></tr>
<tr><td>Time Machine-equivalent backup</td><td>Watch for the backup target disk (specific UUID); on appearance, start backup; on disappearance, pause.</td></tr>
<tr><td>Disk-image mounter (gershwin's "mount this .iso")</td><td>Use <code>mdconfig</code> + DA registers the new md device; auto-mount.</td></tr>
<tr><td>Encryption volume unlock prompt</td><td>Register approval callback for GELI volumes; prompt for password before mount; release approval.</td></tr>
</tbody>
</table>
<h3>8.3 System-administration utilities</h3>
<ul>
<li>diskarbitrationd's logs surface in <code>asl</code> with structured fields: which disk, which subscriber claimed, mount destination, timing. Way better than parsing dmesg for <code>cdN: attached</code>.</li>
<li>A future <code>diskutil(1)</code>-equivalent CLI on FreeBSD could lean on DA events instead of direct GEOM scraping.</li>
</ul>
<h2 id="integration">9. launchd integration</h2>
<h3>9.1 The plist (system daemon)</h3>
<pre class="plist"><code><?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key> <string>org.freebsd.diskarbitrationd</string>
<key>ProgramArguments</key> <array><string>/usr/libexec/diskarbitrationd</string></array>
<key>RunAtLoad</key> <true/>
<key>KeepAlive</key> <true/>
<key>MachServices</key> <dict>
<key>com.apple.DiskArbitration</key> <true/>
</dict>
</dict>
</plist></code></pre>
<p>launchd creates the Mach service port and stashes it in the daemon's bootstrap namespace before <code>exec</code>; <code>diskarbitrationd</code> picks it up with <code>bootstrap_check_in("com.apple.DiskArbitration", &port)</code> at startup and wraps it in a <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code> source.</p>
<h3>9.2 Boot ordering</h3>
<p>diskarbitrationd needs the <code>hwregd</code> Mach service available (for storage-device subscription) + <code>libgeom</code> topology populated. <code>hwregd</code> is a peer system daemon launched by launchd; <code>diskarbitrationd</code> connects via bootstrap lookup and retries with backoff if <code>hwregd</code> isn't up yet. Order: starts in parallel with other system daemons; provides events from "now" forward (existing already-mounted root + critical filesystems aren't re-arbitrated).</p>
<h2 id="license">10. Licensing</h2>
<p>APSL 2.0 (same as configd / asl). Per-file headers preserved on Apple-derived files; top-level repo BSD-2-Clause.</p>
<table>
<thead><tr><th>Source</th><th>License</th><th>How we handle it</th></tr></thead>
<tbody>
<tr><td>Apple <code>DiskArbitration-79.3</code></td><td>APSL 2.0</td><td>Per-file headers preserved verbatim. Edits inherit APSL.</td></tr>
<tr><td>This repo's new code (GEOM bridge, FreeBSD shims, integration glue)</td><td>BSD-2-Clause</td><td>SPDX header on each new file.</td></tr>
<tr><td>libgeom (FreeBSD base)</td><td>BSD-2-Clause</td><td>Linked from base; nothing in our tree.</td></tr>
<tr><td>libdispatch, libxpc, libCoreFoundation (linked)</td><td>Apache 2.0 / APSL 2.0</td><td>Listed in NOTICE. (Same stack as configd / IPConfiguration / hwregd in this repo; no GNUstep Foundation dep.)</td></tr>
</tbody>
</table>
<h2 id="phases">11. Phased delivery</h2>
<div class="phase">
<h3>Phase 0 — repo scaffold + import</h3>
<ul>
<li>Create <code>diskarb/</code> at the top of <code>freebsd-launchd-mach</code>. Update <code>NOTICE</code>.</li>
<li>Add <code>diskarb/scripts/import-source.sh</code> at <code>DiskArbitration-79.3</code>.</li>
<li>One-shot import + amputation per §6.1 (Xcode + IOKit Modules + autodiskmount + Apple's <code>.defs</code> out).</li>
<li>Author <code>diskarb/mig/da.defs</code> from scratch (modeled on <code>hwreg.defs</code> / <code>ipconfig.defs</code> in this repo); wire MIG codegen into the Makefile.</li>
</ul>
</div>
<div class="phase">
<h3>Phase 1 — daemon foundation: <code>hwregd</code> subscription + Mach service skeleton</h3>
<ul>
<li>Implement the <code>compat/</code> bridge: <code>hwregd</code> MIG client wrappers (initial enumerate + watch/notify subscribe) replacing <code>IOServiceGetMatchingServices</code> / <code>IOServiceAddInterestNotification</code>; thin <code>libgeom</code> enrichment helpers (partition / UUID / FS-type) for in-process metadata.</li>
<li>Port <code>diskarbitrationd/diskarbitrationd.{c,m}</code>, <code>DAMain</code>, <code>DAServer</code>, <code>DADisk</code> against the <code>hwregd</code> + libgeom shims; stand up the <code>com.apple.DiskArbitration</code> Mach service port under a <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code> source; wire the generated <code>da.defs</code> server demux.</li>
<li>Daemon enumerates disks at startup via <code>hwregd</code> RPC, fires "disk appeared" callbacks; hot-plug via <code>hwregd</code> notify channel works.</li>
<li>Boot test extension: ssh in, run a small DA-API test program; verify it sees the boot disk and any USB stick attached during the test.</li>
</ul>
</div>
<div class="phase">
<h3>Phase 2 — libDiskArbitration framework (<code>da.defs</code> client side)</h3>
<ul>
<li>Port <code>DiskArbitration/</code> — the client library. Regenerate MIG client stubs from <code>da.defs</code>; wire <code>DASessionCreate</code> to <code>bootstrap_look_up("com.apple.DiskArbitration", &port)</code>; deliver callbacks on a session-owned receive port pumped from the app's runloop / dispatch queue. Keep public API stable.</li>
<li>Build + install <code>libDiskArbitration.so</code>.</li>
<li>Boot test: write a small client app, register a callback, plug a USB stick, verify callback fires.</li>
</ul>
</div>
<div class="phase">
<h3>Phase 3 — mount/unmount orchestration</h3>
<ul>
<li>Port <code>DAMount.{c,m}</code> + <code>DAFileSystem*</code>. Replace <code>diskutil</code> calls with <code>mount(8)</code> / <code>umount(8)</code> / <code>zfs</code>.</li>
<li>Implement filesystem-type detection for FreeBSD-relevant filesystems (UFS, ZFS, FAT, NTFS, ExFAT, ISO9660; EXT via fusefs-ext4 if available).</li>
<li>Mount-policy approval / disapproval / claim flow working end-to-end.</li>
</ul>
</div>
<div class="phase">
<h3>Phase 4 — per-user agent</h3>
<ul>
<li>Port <code>DiskArbitrationAgent/</code>. Per-user launchd plist; mount-approval prompts surface to user via Workspace UI.</li>
<li>Encryption-volume password prompts (when GELI / ZFS-native encryption integration eventually lands) hook here.</li>
</ul>
</div>
<div class="phase">
<h3>Phase 5+ — gershwin integration</h3>
<ul>
<li>Workspace's File Viewer "Devices" sidebar populated from DA events.</li>
<li>Eject button in sidebar calls <code>DADiskUnmount</code>.</li>
<li>Disk Utility-equivalent app uses DA claim/release for safe formatting.</li>
</ul>
</div>
<h2 id="open">12. Open questions</h2>
<div class="open-q">
<strong>Q1. Auto-mount destination layout.</strong> Apple uses <code>/Volumes/<name></code>. FreeBSD convention is <code>/mnt/...</code> or <code>/media/...</code>. Decision: follow Apple — <code>/Volumes/<name></code>. Matches gershwin / Apple-shaped expectations; create the directory in the rootfs overlay.
</div>
<div class="open-q">
<strong>Q2. ZFS pool import semantics.</strong> Plugging in a disk that's part of a ZFS pool is different from a single-filesystem disk — "mount" means "import the pool." Decision: detect via <code>zpool import -d</code>; surface as a special <code>DADisk</code> kind with the pool name; auto-import only if the user opts in via per-pool config.
</div>
<div class="open-q">
<strong>Q3. CD/DVD eject mechanism.</strong> Apple uses <code>DADiskEject</code> → IOKit eject. FreeBSD's <code>cdcontrol(8)</code> handles physical eject. Decision: <code>DADiskEject</code> dispatches to <code>cdcontrol eject <dev></code> via <code>posix_spawn</code> + <code>waitpid</code> (matches the rest of the daemon's C / libdispatch shape; no Foundation in this daemon). Same code path covers USB-attached optical drives.
</div>
<div class="open-q">
<strong>Q4. Per-user agent vs system-wide approvals.</strong> Apple's design has a per-user agent that handles GUI prompts. For headless / single-user / boot-time scenarios where no agent is running, the daemon falls back to a default policy. Decision: ship default policy = "auto-mount everything that's safely auto-mountable, ask only when an explicit approver is registered." User can install the agent later for richer prompts.
</div>
<div class="open-q">
<strong>Q5. Coexistence with FreeBSD's <code>autofs(5)</code>.</strong> FreeBSD has its own automount system. Decision: not fight. autofs handles its specific declarative-config-file scenarios; DA handles event-driven UX. They don't collide on actual mount/unmount because both ultimately call <code>mount(2)</code>; first writer to <code>/Volumes/<name></code> wins.
</div>
<h2 id="refs">13. References</h2>
<ul>
<li>Apple source: <a href="https://github.com/apple-oss-distributions/DiskArbitration">github.com/apple-oss-distributions/DiskArbitration</a> (latest tag <code>DiskArbitration-79.3</code>).</li>
<li>Target repo: <a href="https://github.com/pkgdemon/freebsd-launchd-mach">github.com/pkgdemon/freebsd-launchd-mach</a> (v2 / Mach-IPC track).</li>
<li>Companion plans: <a href="freebsd-launchd-mach-plan.html">launchd (Mach track)</a>, <a href="nextbsd-configd-plan.html">configd</a>, <a href="freebsd-hardware-registry-iokit-plan.html">hwregd (hardware registry)</a>, <a href="nextbsd-ipconfiguration-plan.html">IPConfiguration</a>, <a href="freebsd-asl-plan.html">asl</a>, <a href="freebsd-notifyd-plan.html">notifyd</a>, <a href="freebsd-mdnsresponder-plan.html">mDNSResponder</a>.</li>
<li>FreeBSD <code>geom(8)</code>, <code>libgeom(3)</code>, <code>devctl(4)</code> manpages.</li>
<li>Apple's DiskArbitration framework: <code>man 3 DiskArbitration</code> on macOS.</li>
<li>Apple Public Source License 2.0: <a href="https://opensource.apple.com/apsl/">opensource.apple.com/apsl/</a>.</li>
</ul>
<hr>
<p class="subtitle"><strong>Revision 2026-05-23.</strong> Refactored to target <code>freebsd-launchd-mach</code> (v2 / Mach-IPC track). Disk-event source pivoted from in-process <code>libgeom</code> + <code>devctl(4)</code> reader to MIG RPC against the in-repo <code>hwregd</code> daemon; <code>libgeom</code> retained for in-process partition / UUID / label / FS-type enrichment only. Daemon ↔ framework IPC retained on Mach (MIG IDL <code>da.defs</code>, service <code>com.apple.DiskArbitration</code>), <strong>not</strong> rebuilt on GNUstep Distributed Objects + AF_UNIX as the sibling <code>freebsd-launchd</code> repo plan does. Event loop uses <code>DISPATCH_SOURCE_TYPE_MACH_RECV</code> on the service port and on the <code>hwregd</code> notify port. launchd plist switched from <code>Sockets</code> to <code>MachServices</code>. Phased delivery, architecture diagram, file-by-file plan, and BSD-wins table updated to match.</p>
</body>
</html>