FreeBSD DiskArbitration — porting plan

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.

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

1.2 Non-goals (this iteration)

2. Repository

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

3. Architecture

+------------------+ +-----------------------+ | /etc/fstab | | devctl(4) socket | | /etc/auto_* | | (kernel disk | +--------+---------+ | attach/detach) | | +----------+------------+ | (vnode) | (READ source) v v +---------------------+ +-----------+----------------------+ | libgeom |<-+| diskarbitrationd | | (initial enumera- | | (ObjC, GNUstep, libdispatch) | | tion at startup, | | | | metadata queries) | | - per-disk DADisk* objects | +---------------------+ | - claim/unclaim arbitration | | - mount-policy callbacks | +-+--------------------------------+ | | DO via /var/run/DiskArbitration.sock v +------------+------------+ | libDiskArbitration | | (linked into apps; | | callbacks dispatched| | to app's runloop) | +-------------------------+

3.1 Replacing IOKit with libgeom

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:

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.

3.2 Replacing Mach IPC with DO

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.

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_READdevctl(4) socketparse disk attach/detach events; instantiate or destroy NCDisk objects; fire callbacks
DISPATCH_SOURCE_TYPE_VNODE/etc/fstab, /etc/auto_masterreload mount policy on edit
DISPATCH_SOURCE_TYPE_PROCeach registered client PIDauto-cancel registrations on DISPATCH_PROC_EXIT
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 of GEOM tree.

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>.
Daemon socket/var/run/DiskArbitration.sockDO listener (launchd-opened).
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 discoverylibgeom(3) walk at startup; devctl(4) for hot-plug. No IOKit.
Mach IPCNone. Replaced with GNUstep DO over AF_UNIX.
Event looplibdispatch dispatch sources only.
Filesystem-type detectionlibgeom metadata + partition-table inspection. Drop HFS+/APFS detection (FreeBSD doesn't mount them).
Mount mechanismFreeBSD 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 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 libgeom enumeration; replace IOKit notifications with devctl(4) source. Keep the disk-state-machine + arbitration logic.
diskarbitrationd/DAMain.{c,m}~2kDaemon main; replace Mach service-loop with dispatch_main + libdispatch sources.
diskarbitrationd/DAServer.{c,m}~5kThe IPC server. Replace Mach IPC with DO. Keep the request-routing + per-client-state.
diskarbitrationd/DADisk.{c,m}~3kPer-disk state object. Replace IOKit-derived metadata getters with libgeom-derived ones. 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. Replace Mach IPC with DO. 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 GEOM-bridging logic. Net: similar LOC but very different shape.

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

FeatureApple's daemon doesThis port (FreeBSD-only)
Disk enumerationIOKit registry walk; IOMediaClass matchinglibgeom(3) tree walk; provider/consumer/class introspection
Hot-plug notificationIOKit IOServiceAddInterestNotificationdevctl(4) via DISPATCH_SOURCE_TYPE_READ
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
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 stubsGNUstep DO over AF_UNIX

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.
  2. diskarbitrationd sees the new disk via libgeom; 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>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>

9.2 Boot ordering

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

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, GNUstep Foundation (linked)Apache / LGPLListed in NOTICE.

11. Phased delivery

Phase 0 — repo scaffold + import

Phase 1 — daemon foundation: GEOM + devctl + DO

Phase 2 — libDiskArbitration framework

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