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, kmodloader, asl, notifyd, mDNSResponder.
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. FreeBSD has no IOKit. The port replaces the IOKit layer with FreeBSD's libgeom (GEOM-based topology + metadata) plus devctl(4) hot-plug events. This is a real porting effort, not just a swap of the IPC layer.DiskArbitration.framework client-side calls swap mach_msg for NSConnection RPC.DISPATCH_SOURCE_TYPE_READ on devctl(4) for kernel disk events; DISPATCH_SOURCE_TYPE_VNODE on /etc/fstab for mount-policy reload; one 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 libgeom-based discovery; replace IOKit hot-plug notifications with devctl(4) events; 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).
Monorepo. Source under diskarb/:
freebsd-launchd/
├── src/ launchd
├── configd/ Apple configd
├── 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 (GEOM bridge)
│ └── 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)
└── make-diskarb.sh STANDALONE — builds + installs
Apple'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." On FreeBSD we use libgeom:
geom_gettree(3) returns the live GEOM topology (providers, consumers, classes).geom_provider_t) has metadata: name, sector size, mediasize, etc.NCDisk object per provider that's a "disk-shaped" thing (DISK or PART class).g_label metadata or by reading the partition table directly (libufs, ZFS pool import, etc.).For hot-plug: subscribe to devctl(4) events as kmodloader does. New !system=DEVFS type=CREATE for a disk-shaped device → walk libgeom for the new entry → instantiate NCDisk → fire DARegisterDiskAppearedCallback subscribers.
Same pattern as configd / notifyd. Daemon creates an NSConnection over /var/run/DiskArbitration.sock at startup; client framework's DARegister* calls go through DO proxy invocations. Client callbacks are delivered over the same DO connection — a remote message send back to the client's stub object.
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_READ | devctl(4) socket | parse disk attach/detach events; instantiate or destroy NCDisk objects; fire callbacks |
DISPATCH_SOURCE_TYPE_VNODE | /etc/fstab, /etc/auto_master | reload mount policy on edit |
DISPATCH_SOURCE_TYPE_PROC | each registered client PID | auto-cancel registrations on DISPATCH_PROC_EXIT |
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 of GEOM tree. |
| 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>. |
| Daemon socket | /var/run/DiskArbitration.sock | DO listener (launchd-opened). |
| 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 | libgeom(3) walk at startup; devctl(4) for hot-plug. No IOKit. |
| Mach IPC | None. Replaced with GNUstep DO over AF_UNIX. |
| Event loop | libdispatch dispatch sources only. |
| Filesystem-type detection | libgeom metadata + partition-table inspection. Drop HFS+/APFS detection (FreeBSD doesn't mount them). |
| Mount mechanism | FreeBSD mount(8) (and ZFS zfs mount for ZFS volumes) invoked as NSTask children, NOT direct mount(2) syscalls. Matches Apple's pattern of delegating to /sbin/mount. |
| 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 GEOM-based logic*.defs) wherever they exist in the repoautodiskmount/ — 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 libgeom enumeration; replace IOKit notifications with devctl(4) source. Keep the disk-state-machine + arbitration logic. |
diskarbitrationd/DAMain.{c,m} | ~2k | Daemon main; replace Mach service-loop with dispatch_main + libdispatch sources. |
diskarbitrationd/DAServer.{c,m} | ~5k | The IPC server. Replace Mach IPC with DO. Keep the request-routing + per-client-state. |
diskarbitrationd/DADisk.{c,m} | ~3k | Per-disk state object. Replace IOKit-derived metadata getters with libgeom-derived ones. 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. Replace Mach IPC with DO. 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 GEOM-bridging logic. Net: similar LOC but very different shape.
| Feature | Apple's daemon does | This port (FreeBSD-only) |
|---|---|---|
| Disk enumeration | IOKit registry walk; IOMediaClass matching | libgeom(3) tree walk; provider/consumer/class introspection |
| Hot-plug notification | IOKit IOServiceAddInterestNotification | devctl(4) via DISPATCH_SOURCE_TYPE_READ |
| 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 |
| 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 | GNUstep DO over AF_UNIX |
The "Devices" sidebar in File Viewer (Finder-equivalent) populates from DA events. When a user plugs in a USB stick:
devctl(4) fires.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>Sockets</key> <dict>
<key>Listeners</key> <dict>
<key>SockPathName</key> <string>/var/run/DiskArbitration.sock</string>
<key>SockType</key> <string>stream</string>
<key>SockPathMode</key> <integer>438</integer> <!-- 0666 -->
</dict>
</dict>
</dict>
</plist>
diskarbitrationd needs devctl(4) open + libgeom topology populated. Both available immediately after kernel boot. 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, GNUstep Foundation (linked) | Apache / LGPL | Listed in NOTICE. |
diskarb/ at the top of freebsd-launchd. Update NOTICE.diskarb/scripts/import-source.sh at DiskArbitration-79.3.compat/ GEOM bridge layer: replace IOServiceGetMatchingServices et al. with geom_gettree-based functions.diskarbitrationd/diskarbitrationd.{c,m}, DAMain, DAServer, DADisk with the GEOM/devctl substitutions.DAServer.DiskArbitration/ — the client library. Replace Mach IPC with DO proxy calls. 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 NSTask. 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.