FreeBSD DiskArbitration — porting plan freebsd-launchd-mach (v2) effort

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.

Revision 2026-05-23. This plan originally targeted the sibling 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.

Status: planning v0 — deferred

1. Goal & non-goals

1.1 Goal

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.

1.2 Non-goals (this iteration)

2. Repository

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

3. Architecture

+------------------+ +------------------------+ | /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) | +-------------------------+

3.1 Sourcing storage events from hwregd

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." 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:

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:

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.

3.2 Mach IPC retained — da.defs MIG IDL

The 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):

RoutinePurpose
da_session_createPer-client session; receive port for callback delivery; client-name string for logging.
da_session_releaseTear down session; cancel all callbacks; drop claims.
da_register_disk_appearedClient registers interest in disk-appeared events. Daemon delivers existing disks immediately, then streams new arrivals via the session's receive port.
da_register_disk_disappearedCounterpart on detach.
da_register_disk_mount_approvalClient opts into mount-veto rights for the disk classes it cares about.
da_disk_claimClient takes exclusive arbitration over a disk (suppresses auto-mount).
da_disk_unclaimRelease the claim.
da_disk_mountRequest mount with options dict (path, fs-type, flags). Daemon delegates to mount(8).
da_disk_unmountRequest unmount with force-flag option.
da_disk_ejectUnmount + 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.

3.3 Mount-policy arbitration

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.

3.4 Event sources (libdispatch)

Source typeWatchesReaction
DISPATCH_SOURCE_TYPE_MACH_RECVcom.apple.DiskArbitration service portdemux incoming MIG requests from framework clients (da.defs routines)
DISPATCH_SOURCE_TYPE_MACH_RECVhwregd watch/notify portstorage-device-class ATTACH / DETACH / PROPERTY-CHANGE; instantiate or destroy DADisk objects; fire callbacks
DISPATCH_SOURCE_TYPE_VNODE/etc/fstab, /etc/auto_masterreload mount policy on edit
DISPATCH_SOURCE_TYPE_TIMERper-pending-approval timeoutif no subscriber responds within N seconds, default to "approve"
DISPATCH_SOURCE_TYPE_SIGNALSIGTERM, SIGHUPSIGTERM: 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.

4. Install paths

ArtifactPathWhy
diskarbitrationd binary/usr/libexec/diskarbitrationdDaemon. Same tier as other system daemons.
DiskArbitrationAgent/usr/libexec/DiskArbitrationAgentPer-user GUI agent: shows mount-approval prompts.
libDiskArbitration.so/System/Library/Libraries/libDiskArbitration.soClient library; apps link.
Headers/System/Library/Headers/DiskArbitration/*.hPublic API; apps #include <DiskArbitration/DiskArbitration.h>.
Mach service namecom.apple.DiskArbitrationRegistered via launchd MachServices; framework looks up by name through bootstrap.
MIG IDLdiskarb/mig/da.defsGenerates 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.

5. Locked architectural decisions

DecisionChoice
Source baselineApple DiskArbitration-79.3. APSL 2.0. Latest tag.
Disk discoveryMIG RPC to hwregd for enumeration + hot-plug events; libgeom(3) in-process for enrichment only. No IOKit; no devctl reader in diskarbitrationd.
Daemon ↔ framework IPCMach IPC retained. MIG IDL da.defs; service name com.apple.DiskArbitration.
Event looplibdispatch sources, predominantly DISPATCH_SOURCE_TYPE_MACH_RECV.
Filesystem-type detectionlibgeom metadata + partition-table inspection (in-process enrichment). Drop HFS+/APFS detection (FreeBSD doesn't mount them).
Mount mechanismFreeBSD 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 agentYes (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.

6. File-by-file plan (diskarb/src/)

Imported source: Apple DiskArbitration-79.3. 71 files, ~34.5k LOC. 10 Mach-tied (the Mach IPC layer + IOKit lookup hooks).

6.1 Deleted on import

6.2 Retained — Phase 2 fate

Directory / fileApple LOCAction
diskarbitrationd/diskarbitrationd.{c,m}~3kSubstantial 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}~2kDaemon 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}~5kThe IPC server. Keep Mach IPC; regenerate against da.defs. Keep the request-routing + per-client-state.
diskarbitrationd/DADisk.{c,m}~3kPer-disk state object. Replace IOKit-derived metadata getters with hwregd property reads + libgeom enrichment calls. Heavy refactor; ~50% rewrite.
diskarbitrationd/DAMount.{c,m}~2kMount/unmount/eject orchestration. Replace diskutil NSTask invocations with mount(8) / umount(8) / zfs.
diskarbitrationd/DAFileSystem*~3kFilesystem-type detection. Drop HFS+/APFS detection; keep + extend UFS / EXT / FAT / NTFS / ISO9660 / ZFS detection.
DiskArbitration/DiskArbitration.{h,c} + family~5kClient 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/~3kPer-user agent. Port last; depends on Workspace having UI surface for approval prompts.
datest/~1kAdapt 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.

7. FreeBSD-only wins (vs Apple's Darwin-tied DiskArbitration)

FeatureApple's daemon doesThis port (FreeBSD-only)
Disk enumerationIOKit registry walk; IOMediaClass matchingMIG RPC to hwregd (storage device class subtree); libgeom(3) in-process for enrichment
Hot-plug notificationIOKit IOServiceAddInterestNotificationhwregd watch/notify Mach channel via DISPATCH_SOURCE_TYPE_MACH_RECV
Filesystem-type detectionHFS+, APFS, FAT, NTFS, ExFAT, plus IOKit-published metadataUFS, ZFS, FAT, NTFS, ExFAT, ISO9660, EXT2/3/4 (via FreeBSD's fusefs-ext4), via libgeom labels + partition-table inspection (in-process enrichment)
Encryption-volume hooksFileVault / FileVault2 keychain integrationDrop. 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 IPCMach ports + MIG stubsMach ports + MIG stubs (retained). MIG IDL da.defs; service com.apple.DiskArbitration.

8. Use cases for gershwin

8.1 Workspace File Viewer (the headline)

The "Devices" sidebar in File Viewer (Finder-equivalent) populates from DA events. When a user plugs in a USB stick:

  1. Kernel attaches the device; devctl(4) fires; hwregd publishes a storage-device-class ATTACH event on its notify channel.
  2. diskarbitrationd receives the ATTACH via its 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).
  3. Auto-mount fires: mount is invoked with the right filesystem type at /Volumes/<name>.
  4. Mount-success callback fires; subscribers (Workspace) get notified.
  5. Workspace adds the volume to the sidebar; user clicks → opens the mount point.

On eject (drag to trash, sidebar eject button): Workspace calls DADiskUnmount(disk, kDADiskUnmountOptionDefault); daemon orchestrates clean unmount; eject-success callback removes the sidebar entry.

8.2 Disk-utility-style apps

App scenarioDA-API role
"Disk Utility" / format new diskClaim a disk to prevent auto-mount; format it; release claim; mount.
Time Machine-equivalent backupWatch 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 promptRegister approval callback for GELI volumes; prompt for password before mount; release approval.

8.3 System-administration utilities

9. launchd integration

9.1 The plist (system daemon)

<?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.

9.2 Boot ordering

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).

10. Licensing

APSL 2.0 (same as configd / asl). Per-file headers preserved on Apple-derived files; top-level repo BSD-2-Clause.

SourceLicenseHow we handle it
Apple DiskArbitration-79.3APSL 2.0Per-file headers preserved verbatim. Edits inherit APSL.
This repo's new code (GEOM bridge, FreeBSD shims, integration glue)BSD-2-ClauseSPDX header on each new file.
libgeom (FreeBSD base)BSD-2-ClauseLinked from base; nothing in our tree.
libdispatch, libxpc, libCoreFoundation (linked)Apache 2.0 / APSL 2.0Listed in NOTICE. (Same stack as configd / IPConfiguration / hwregd in this repo; no GNUstep Foundation dep.)

11. Phased delivery

Phase 0 — repo scaffold + import

Phase 1 — daemon foundation: hwregd subscription + Mach service skeleton

Phase 2 — libDiskArbitration framework (da.defs client side)

Phase 3 — mount/unmount orchestration

Phase 4 — per-user agent

Phase 5+ — gershwin integration

12. Open questions

Q1. Auto-mount destination layout. Apple uses /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.
Q2. ZFS pool import semantics. 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 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.
Q3. CD/DVD eject mechanism. Apple uses 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.
Q4. Per-user agent vs system-wide approvals. 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.
Q5. Coexistence with FreeBSD's 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.

13. References


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.