A port of Apple's DiskArbitration framework + diskarbitrationd to FreeBSD: disk attach/detach events, mount/unmount/eject coordination, mount-policy approval prompts, and the same DiskArbitration.framework C API that Apple-derived applications already use. Replaces ad-hoc kqueue(EVFILT_FS) polling with a clean event API. Companion to launchd, configd, hwregd, kmodloader, asl, notifyd, mDNSResponder, IPConfiguration.
freebsd-launchd (AF_UNIX / GNUstep Distributed Objects) repo where libgeom replaces IOKit and DO replaces Mach IPC. Refactored 2026-05-23 to target freebsd-launchd-mach (v2): this repo has hwregd — a MIG-served IORegistry-shape daemon at org.freebsd.hwregd that aggregates devctl(4) events + GEOM data — so DiskArbitration consumes hwregd's storage-device-class notifications instead of walking GEOM directly for hot-plug. Mach IPC is retained for the daemon's DiskArbitration.framework client API (MIG IDL da.defs, Mach service com.apple.DiskArbitration, DISPATCH_SOURCE_TYPE_MACH_RECV event loop). libgeom is used only for partition / UUID / label / FS-type enrichment, in-process, not as the event source.
diskarb/.kqueue(EVFILT_FS) + geom(8) walks with an Apple-shaped event-driven model: register a callback once, receive events for the lifetime of your process.DiskArbitration-79.3 (latest tag at apple-oss-distributions/DiskArbitration, APSL 2.0). 71 source files, ~34.5k LOC. 10 files have <mach/> includes — the daemon-client IPC plus IOKit's I/O Registry traversal.IOMedia objects; matches mount policies based on IOKit metadata (kIOMediaContentKey, etc.); subscribes to IOKit notifications for hot-plug. This repo has hwregd — a MIG-served IORegistry-shape daemon at org.freebsd.hwregd already aggregating devctl(4) + GEOM data with a 10-routine RPC surface and a watch/notify channel. The port replaces IOKit calls with MIG RPC to hwregd for storage-device-class events; libgeom is retained in-process only for partition-table / UUID / label / FS-type enrichment that hwregd 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.da.defs (matches the hwreg.defs / ipconfig.defs shape) defines a ~5–10 routine surface: session create/release, register disk-appeared / disappeared / mount-approval callbacks, claim, eject, mount, unmount. Service name com.apple.DiskArbitration (Apple-canonical). DiskArbitration.framework client-side calls keep mach_msg.DISPATCH_SOURCE_TYPE_MACH_RECV on the com.apple.DiskArbitration service port (client RPC); separate MIG subscription to hwregd's storage-device-class notify channel for hot-plug; DISPATCH_SOURCE_TYPE_VNODE on /etc/fstab for mount-policy reload; one timer source per pending mount-approval-prompt.kqueue in a few hundred LOC. DiskArbitration's value compounds with gershwin's full desktop UX maturity, not before. Phase 8+ work, sequenced after mDNSResponder.Provide a working diskarbitrationd + libDiskArbitration on FreeBSD so apps calling the standard DiskArbitration C API (DARegisterDiskAppearedCallback, DARegisterDiskMountApprovalCallback, DADiskUnmount, DADiskEject, DADiskClaim, etc.) work without source modification. Replace the IOKit-based disk-discovery layer with MIG RPC to hwregd (which already aggregates devctl + GEOM into an IORegistry-shape registry); use libgeom in-process only for enrichment metadata that hwregd 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 com.apple.DiskArbitration.
hwregd. Don't try to port IOKit-without-Mach — that's a configd-scale project on its own, and hwregd already shipped the IORegistry-shape surface DiskArbitration needs.Monorepo. Source under diskarb/ in freebsd-launchd-mach:
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
hwregdApple's daemon at startup walks the IOKit I/O Registry to find every IOMedia 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 hwregd at the Mach service org.freebsd.hwregd, which already aggregates devctl(4) + GEOM data into an IORegistry-shape tree behind a 10-routine MIG RPC surface plus a watch/notify channel. diskarbitrationd consumes that interface rather than walking GEOM directly:
org.freebsd.hwregd; query the storage device class subtree (ATA / SCSI / NVMe / removable / optical) via the registry-walk RPCs. One DADisk per returned node.hwregd's watch/notify channel for the storage device class; ATTACH / DETACH / PROPERTY-CHANGE events arrive as Mach messages and drive DADisk instantiation / teardown / property refresh. DISPATCH_SOURCE_TYPE_MACH_RECV on the notify port; no devctl(4) parser in diskarbitrationd itself.hwregd properties (subsystem = DEVFS; device name like ada0 / da0 / nvd0 / cd0; class membership in DISK / PART / CD; published GEOM-class enrichment when hwregd has it).libgeom is retained for enrichment only, in-process. When diskarbitrationd needs metadata that hwregd doesn't publish — partition table contents, GPT UUIDs, GELI/ZFS labels, filesystem-type sniffing — it walks g_classes directly via libgeom(3) for that single disk. This is library-style use, not event-source use:
g_label metadata; otherwise read the partition table directly (libufs for UFS, ZFS pool import probe, etc.).gpart show -l-equivalent via libgeom XML walk on the specific provider.The separation matters: hot-plug timing / event delivery comes from hwregd's MIG channel (clean, structured, already plumbed); metadata enrichment is a synchronous in-process call against libgeom (no async surface). If hwregd grows GEOM-class enrichment later, the in-process libgeom use shrinks.
da.defs MIG IDLThe Mach-IPC track keeps the framework ↔ daemon channel on Mach. diskarbitrationd registers Mach service com.apple.DiskArbitration (Apple-canonical) at startup via launchd's MachServices; libDiskArbitration looks the port up and sends mach_msg requests through MIG stubs generated from da.defs. Daemon side runs a DISPATCH_SOURCE_TYPE_MACH_RECV source on the service port; libdispatch routes each request to the generated MIG demux. The IDL filename matches the hwreg.defs / ipconfig.defs shape elsewhere in this repo.
Routine sketch (final list TBD in Phase 2, but the shape is set):
| Routine | Purpose |
|---|---|
da_session_create | Per-client session; receive port for callback delivery; client-name string for logging. |
da_session_release | Tear down session; cancel all callbacks; drop claims. |
da_register_disk_appeared | Client registers interest in disk-appeared events. Daemon delivers existing disks immediately, then streams new arrivals via the session's receive port. |
da_register_disk_disappeared | Counterpart on detach. |
da_register_disk_mount_approval | Client opts into mount-veto rights for the disk classes it cares about. |
da_disk_claim | Client takes exclusive arbitration over a disk (suppresses auto-mount). |
da_disk_unclaim | Release the claim. |
da_disk_mount | Request mount with options dict (path, fs-type, flags). Daemon delegates to mount(8). |
da_disk_unmount | Request unmount with force-flag option. |
da_disk_eject | Unmount + eject (delegates to camcontrol / cdcontrol as appropriate). |
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.
One of DiskArbitration's defining features — not just events, but veto rights. When a new disk appears, before it auto-mounts, the daemon dispatches mount-approval callbacks to all registered subscribers. Each subscriber can:
This is how disk-encryption tools (FileVault on macOS, geli 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.
| Source type | Watches | Reaction |
|---|---|---|
DISPATCH_SOURCE_TYPE_MACH_RECV | com.apple.DiskArbitration service port | demux incoming MIG requests from framework clients (da.defs routines) |
DISPATCH_SOURCE_TYPE_MACH_RECV | hwregd watch/notify port | storage-device-class ATTACH / DETACH / PROPERTY-CHANGE; instantiate or destroy DADisk objects; fire callbacks |
DISPATCH_SOURCE_TYPE_VNODE | /etc/fstab, /etc/auto_master | reload mount policy on edit |
DISPATCH_SOURCE_TYPE_TIMER | per-pending-approval timeout | if no subscriber responds within N seconds, default to "approve" |
DISPATCH_SOURCE_TYPE_SIGNAL | SIGTERM, SIGHUP | SIGTERM: clean shutdown. SIGHUP: full re-enumeration via hwregd registry walk. |
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 MACH_NOTIFY_NO_SENDERS notification and tears down the session. No DISPATCH_SOURCE_TYPE_PROC needed.
| Artifact | Path | Why |
|---|---|---|
diskarbitrationd binary | /usr/libexec/diskarbitrationd | Daemon. Same tier as other system daemons. |
DiskArbitrationAgent | /usr/libexec/DiskArbitrationAgent | Per-user GUI agent: shows mount-approval prompts. |
libDiskArbitration.so | /System/Library/Libraries/libDiskArbitration.so | Client library; apps link. |
| Headers | /System/Library/Headers/DiskArbitration/*.h | Public API; apps #include <DiskArbitration/DiskArbitration.h>. |
| Mach service name | com.apple.DiskArbitration | Registered via launchd MachServices; framework looks up by name through bootstrap. |
| MIG IDL | diskarb/mig/da.defs | Generates client + server stubs at build time. |
| launchd plists | /System/Library/LaunchDaemons/org.freebsd.diskarbitrationd.plist/System/Library/LaunchAgents/org.freebsd.DiskArbitrationAgent.plist | System daemon + per-user agent. |
| Decision | Choice |
|---|---|
| Source baseline | Apple DiskArbitration-79.3. APSL 2.0. Latest tag. |
| Disk discovery | MIG RPC to hwregd for enumeration + hot-plug events; libgeom(3) in-process for enrichment only. No IOKit; no devctl reader in diskarbitrationd. |
| Daemon ↔ framework IPC | Mach IPC retained. MIG IDL da.defs; service name com.apple.DiskArbitration. |
| Event loop | libdispatch sources, predominantly DISPATCH_SOURCE_TYPE_MACH_RECV. |
| Filesystem-type detection | libgeom metadata + partition-table inspection (in-process enrichment). Drop HFS+/APFS detection (FreeBSD doesn't mount them). |
| Mount mechanism | FreeBSD mount(8) (and ZFS zfs mount for ZFS volumes) invoked via posix_spawn + waitpid, NOT direct mount(2) syscalls. Matches Apple's pattern of delegating to /sbin/mount. (Apple uses NSTask; we stay in C / libdispatch shape with no Foundation dep.) |
| Per-user agent | Yes (Phase 4). Approval prompts surface to user via the agent + Workspace UI. |
| License (top-level) | BSD-2-Clause. Apple's DA source retains APSL 2.0 per-file. |
diskarb/src/)Imported source: Apple DiskArbitration-79.3. 71 files, ~34.5k LOC. 10 Mach-tied (the Mach IPC layer + IOKit lookup hooks).
DiskArbitration.xcodeproj/ — XcodeModules/ — IOKit-based device-classification hooks; replaced by hwregd MIG queries*.defs) — replaced, not dropped: our own diskarb/mig/da.defs takes their place. The Apple-shipped .defs references Darwin-private types and is regenerated from scratch.autodiskmount/ — legacy pre-DiskArbitration mount mechanism; either drop entirely or keep a small shim that translates autodiskmount CLI invocations to DA calls. Decision: drop in Phase 1; add back in Phase 5 if any compat consumer turns up.| Directory / file | Apple LOC | Action |
|---|---|---|
diskarbitrationd/diskarbitrationd.{c,m} | ~3k | Substantial rewrite. Replace IOKit registry walk with hwregd MIG enumeration; replace IOKit notifications with the hwregd watch/notify channel under DISPATCH_SOURCE_TYPE_MACH_RECV. Keep the disk-state-machine + arbitration logic. |
diskarbitrationd/DAMain.{c,m} | ~2k | Daemon main; replace Apple's Mach service-loop boilerplate with dispatch_main + libdispatch sources. Service port still obtained via bootstrap_check_in. |
diskarbitrationd/DAServer.{c,m} | ~5k | The IPC server. Keep Mach IPC; regenerate against da.defs. Keep the request-routing + per-client-state. |
diskarbitrationd/DADisk.{c,m} | ~3k | Per-disk state object. Replace IOKit-derived metadata getters with hwregd property reads + libgeom enrichment calls. Heavy refactor; ~50% rewrite. |
diskarbitrationd/DAMount.{c,m} | ~2k | Mount/unmount/eject orchestration. Replace diskutil NSTask invocations with mount(8) / umount(8) / zfs. |
diskarbitrationd/DAFileSystem* | ~3k | Filesystem-type detection. Drop HFS+/APFS detection; keep + extend UFS / EXT / FAT / NTFS / ISO9660 / ZFS detection. |
DiskArbitration/DiskArbitration.{h,c} + family | ~5k | Client library. Keep Mach IPC; regenerate MIG client stubs against da.defs. Public API must stay byte-for-byte stable: DASessionCreate, DARegister*, DADisk* functions all keep signatures. |
DiskArbitrationAgent/ | ~3k | Per-user agent. Port last; depends on Workspace having UI surface for approval prompts. |
datest/ | ~1k | Adapt as a self-test harness. Useful for verifying GEOM-based discovery matches IOKit-based behavior on Apple. |
Total post-Phase-2: roughly 22-24k LOC vs Apple's ~34k. About 30% deletion, plus ~30% of remaining code is new hwregd-client logic + libgeom 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 da.defs stubs.
| Feature | Apple's daemon does | This port (FreeBSD-only) |
|---|---|---|
| Disk enumeration | IOKit registry walk; IOMediaClass matching | MIG RPC to hwregd (storage device class subtree); libgeom(3) in-process for enrichment |
| Hot-plug notification | IOKit IOServiceAddInterestNotification | hwregd watch/notify Mach channel via DISPATCH_SOURCE_TYPE_MACH_RECV |
| Filesystem-type detection | HFS+, APFS, FAT, NTFS, ExFAT, plus IOKit-published metadata | UFS, ZFS, FAT, NTFS, ExFAT, ISO9660, EXT2/3/4 (via FreeBSD's fusefs-ext4), via libgeom labels + partition-table inspection (in-process enrichment) |
| Encryption-volume hooks | FileVault / FileVault2 keychain integration | Drop. GELI / ZFS-native encryption use their own tooling outside DA. |
| Auto-mount target | /Volumes/<name> | /Volumes/<name> — same convention. The Volumes directory is already created by macOS-style overlays in our system. |
| Daemon IPC | Mach ports + MIG stubs | Mach ports + MIG stubs (retained). MIG IDL da.defs; service com.apple.DiskArbitration. |
The "Devices" sidebar in File Viewer (Finder-equivalent) populates from DA events. When a user plugs in a USB stick:
devctl(4) fires; hwregd publishes a storage-device-class ATTACH event on its notify channel.DISPATCH_SOURCE_TYPE_MACH_RECV source on the hwregd watch port; reads device properties via MIG; calls libgeom in-process for partition / FS-type enrichment; instantiates DADisk; runs approval callbacks (none registered for USB sticks by default → approves).mount is invoked with the right filesystem type at /Volumes/<name>.On eject (drag to trash, sidebar eject button): Workspace calls DADiskUnmount(disk, kDADiskUnmountOptionDefault); daemon orchestrates clean unmount; eject-success callback removes the sidebar entry.
| App scenario | DA-API role |
|---|---|
| "Disk Utility" / format new disk | Claim a disk to prevent auto-mount; format it; release claim; mount. |
| Time Machine-equivalent backup | Watch for the backup target disk (specific UUID); on appearance, start backup; on disappearance, pause. |
| Disk-image mounter (gershwin's "mount this .iso") | Use mdconfig + DA registers the new md device; auto-mount. |
| Encryption volume unlock prompt | Register approval callback for GELI volumes; prompt for password before mount; release approval. |
asl with structured fields: which disk, which subscriber claimed, mount destination, timing. Way better than parsing dmesg for cdN: attached.diskutil(1)-equivalent CLI on FreeBSD could lean on DA events instead of direct GEOM scraping.<?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>
launchd creates the Mach service port and stashes it in the daemon's bootstrap namespace before exec; diskarbitrationd picks it up with bootstrap_check_in("com.apple.DiskArbitration", &port) at startup and wraps it in a DISPATCH_SOURCE_TYPE_MACH_RECV source.
diskarbitrationd needs the hwregd Mach service available (for storage-device subscription) + libgeom topology populated. hwregd is a peer system daemon launched by launchd; diskarbitrationd connects via bootstrap lookup and retries with backoff if hwregd 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).
APSL 2.0 (same as configd / asl). Per-file headers preserved on Apple-derived files; top-level repo BSD-2-Clause.
| Source | License | How we handle it |
|---|---|---|
Apple DiskArbitration-79.3 | APSL 2.0 | Per-file headers preserved verbatim. Edits inherit APSL. |
| This repo's new code (GEOM bridge, FreeBSD shims, integration glue) | BSD-2-Clause | SPDX header on each new file. |
| libgeom (FreeBSD base) | BSD-2-Clause | Linked from base; nothing in our tree. |
| libdispatch, libxpc, libCoreFoundation (linked) | Apache 2.0 / APSL 2.0 | Listed in NOTICE. (Same stack as configd / IPConfiguration / hwregd in this repo; no GNUstep Foundation dep.) |
diskarb/ at the top of freebsd-launchd-mach. Update NOTICE.diskarb/scripts/import-source.sh at DiskArbitration-79.3..defs out).diskarb/mig/da.defs from scratch (modeled on hwreg.defs / ipconfig.defs in this repo); wire MIG codegen into the Makefile.hwregd subscription + Mach service skeletoncompat/ bridge: hwregd MIG client wrappers (initial enumerate + watch/notify subscribe) replacing IOServiceGetMatchingServices / IOServiceAddInterestNotification; thin libgeom enrichment helpers (partition / UUID / FS-type) for in-process metadata.diskarbitrationd/diskarbitrationd.{c,m}, DAMain, DAServer, DADisk against the hwregd + libgeom shims; stand up the com.apple.DiskArbitration Mach service port under a DISPATCH_SOURCE_TYPE_MACH_RECV source; wire the generated da.defs server demux.hwregd RPC, fires "disk appeared" callbacks; hot-plug via hwregd notify channel works.da.defs client side)DiskArbitration/ — the client library. Regenerate MIG client stubs from da.defs; wire DASessionCreate to bootstrap_look_up("com.apple.DiskArbitration", &port); deliver callbacks on a session-owned receive port pumped from the app's runloop / dispatch queue. Keep public API stable.libDiskArbitration.so.DAMount.{c,m} + DAFileSystem*. Replace diskutil calls with mount(8) / umount(8) / zfs.DiskArbitrationAgent/. Per-user launchd plist; mount-approval prompts surface to user via Workspace UI.DADiskUnmount./Volumes/<name>. FreeBSD convention is /mnt/... or /media/.... Decision: follow Apple — /Volumes/<name>. Matches gershwin / Apple-shaped expectations; create the directory in the rootfs overlay.
zpool import -d; surface as a special DADisk kind with the pool name; auto-import only if the user opts in via per-pool config.
DADiskEject → IOKit eject. FreeBSD's cdcontrol(8) handles physical eject. Decision: DADiskEject dispatches to cdcontrol eject <dev> via posix_spawn + waitpid (matches the rest of the daemon's C / libdispatch shape; no Foundation in this daemon). Same code path covers USB-attached optical drives.
autofs(5). 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 mount(2); first writer to /Volumes/<name> wins.
DiskArbitration-79.3).geom(8), libgeom(3), devctl(4) manpages.man 3 DiskArbitration on macOS.Revision 2026-05-23. Refactored to target freebsd-launchd-mach (v2 / Mach-IPC track). Disk-event source pivoted from in-process libgeom + devctl(4) reader to MIG RPC against the in-repo hwregd daemon; libgeom retained for in-process partition / UUID / label / FS-type enrichment only. Daemon ↔ framework IPC retained on Mach (MIG IDL da.defs, service com.apple.DiskArbitration), not rebuilt on GNUstep Distributed Objects + AF_UNIX as the sibling freebsd-launchd repo plan does. Event loop uses DISPATCH_SOURCE_TYPE_MACH_RECV on the service port and on the hwregd notify port. launchd plist switched from Sockets to MachServices. Phased delivery, architecture diagram, file-by-file plan, and BSD-wins table updated to match.