← NextBSD Research

E15 ticket drafts — Disks, mounts and DiskArbitration All filed

Every issue for EPIC E15, written out in full so it can be filed in GitHub later. This page is the companion to Mounts, fstab and DiskArbitration. It records the epic, the new area:storage label, all 21 child tickets, 1 migration, 12 linked issues and 7 suggested correction comments: title, target repo, labels, dependencies and the exact Markdown body for each. The epic is filed as nextbsd#471, on the roadmap, with #467 migrated under it. All tickets are filed (links on each card) as sub-issues of #471, and the seven correction comments are posted. D3b became a NextBSD spike under E11 Desktop / Gershwin (#474) instead of an upstream libs-gui ticket.

TL;DR

Before filing

1. Choose A1's variant. Ship no fstab (Apple, recommended) or a comment-only one (quieter FreeBSD tooling). The A1 body is written for the first and names the second as the fallback.
2. Labels. area:storage now exists in nextbsd, nextbsd-userland and nextbsd-kernel. nextbsd-overlays has no area:* labels at all; Gershwin repos use their own labels.
3. #54 stays decided. B2 is drafted but parked until Linux support becomes default; file it as status:parked or hold it back.

Label

Namearea:storage
DescriptionDisks, volumes, mounts, fstab and DiskArbitration
Colour#8A6A3F
Create innextbsd, nextbsd-kernel, nextbsd-overlays, nextbsd-userland

Epic

EPIC: E15 Disks, mounts & DiskArbitration filed as #471

Reponextbsd
Labelsepic area:storage
## Summary

Who mounts what on NextBSD, done the Darwin way: stop shipping `/etc/fstab`, give the Linux ABI filesystems an owner, and grow `diskarbitrationd` from an iter-1 skeleton into the daemon that mounts disks at `/Volumes/<name>` and lets the desktop unmount and eject them without `sudo`.

**Read first:** https://pkgdemon.github.io/nextbsd-diskarbitration-mounts-plan.html

## Why

- Live media: launchctl's boot `mount -vat nonfs` tries to remount `/dev/ufs/ROOTFS` over the unionfs root (nextbsd#467).
- Apple stopped shipping `/etc/fstab` in 10.4; the vendored hook only runs if the file exists (`launchctl.c:2401`), and nothing on either medium needs the file.
- DiskArbitration today logs storage devices and sleeps. There is no framework, no mounting, no eject; Gershwin falls back to `sudo -A umount -f`.
- There is no automounter at all on NextBSD, and vermaden/automount needs `devd`, which NextBSD removed.

## Stages

- **A** -- fstab and boot -- no DiskArbitration code; closes nextbsd#467
- **B** -- Linux ABI mounts -- the manual path under no-fstab, and the parked org.nextbsd.linux one-shot
- **C** -- diskarbitrationd, the daemon -- GEOM disk model, fs bundles, mount executor, /Volumes
- **D** -- framework, policy, desktop -- client framework, unmount/eject, Gershwin bridge, console user (D3b is an E11 spike)

## Children

Replace IDs with issue numbers once filed.

- [ ] A1 Stop shipping rootfs/private/etc/fstab (closes nextbsd#467)
- [ ] A2 launchd: remount / with rw,noatime (the fstab root line was noatime's only source); CI asserts it
- [ ] A3 launchctl: compile out the dormant Darwin do_potential_fsck() path -- launchd owns root fsck and remount
- [ ] A4 Stale fstab comments in build.sh, the installer and its README; boot-test gate on the #467 strings
- [ ] A5 Decide: keep or drop empty_dir()'s force-unmount of mounts beneath /tmp and /var/run
- [ ] B1 Document the manual Linux ABI mount path under no-fstab: create /etc/fstab with the five compat lines, in order, never `late`
- [ ] B2 org.nextbsd.linux one-shot LaunchDaemon + /usr/libexec/nextbsd-linux --compat [--native] (parked until Linux support is default)
- [ ] C1 diskarbitrationd: rename the Mach service to com.apple.DiskArbitration.diskarbitrationd and drop the stale hwregd comments
- [ ] C2 diskarbitrationd: build the DADisk model from GEOM (libgeom walk on kernel-registry arrival), log-only
- [ ] C3 diskarbitrationd: adopt mounts it did not make (EVFILT_FS + getmntinfo diff -> DADiskCreateFromVolumePath)
- [ ] C4 Filesystem bundles: /System/Library/Filesystems/{msdos,exfat,ntfs,ufs,cd9660,udf}.fs with an fstyp-backed *.util shim (FSUR_* contract)
- [ ] C5 diskarbitrationd: stage machine and mount executor -- /Volumes/<name>[ N], untrusted-media defaults, no auto-repair, per-FS helper table, /Volumes owned by the OS
- [ ] C6 diskarbitrationd: /etc/fstab UUID=/LABEL=/DEVICE= mount-map overlay, exactly Apple's semantics
- [ ] C7 kernel: compile in UDF, LIBICONV and the msdosfs/cd9660/udf iconv tables so DiskArbitration can mount UDF and non-ASCII FAT names (NO_MODULES)
- [ ] D1 DiskArbitration client library: port DAServer.defs MIG and libDiskArbitration (sessions, Register* callbacks, CopyDescription, DiskListComplete/Idle replay); datest becomes a real client
- [ ] D2 diskarbitrationd: unmount and eject requests with approval broadcast, 10 s response timer, dissenters, EBUSY -> dissenter PID via libprocstat, eject via CAM START STOP UNIT / CDIOCEJECT
- [ ] D3a Workspace: DiskArbitration bridge -- post NSWorkspace mount/unmount notifications through the workspace center, replace the 1.5 s mount poll with DA callbacks, and make GWUnmountHelper call DADiskUnmount/DADiskEject instead of sudo umount -f
- [ ] D3b Spike: count volumes mounted under /Volumes as removable in GNUstep NSWorkspace (GSRemovableMediaPaths is exact-match)
- [ ] D3c Spike: disk images (dmg/iso/squashfs) as DiskArbitration disks -- how VolumeManager's darling-dmg/fuseiso/squashfuse mounts under /Volumes stop depending on vfs.usermount
- [ ] D4 Console user for diskarbitrationd: State:/Users/ConsoleUser in configd, published by LoginWindow; defer removable/external auto-mounts until login, unmount at logout; agent dialogs without XPC/CFUserNotification
- [ ] D5 Drop patch 0008 (vfs.usermount=1) once Workspace's image mounts and user unmounts go through DiskArbitration -- or narrow it if AppImage self-mounting still needs it
- [ ] nextbsd/nextbsd#467 (migrated)

## Linked, not migrated

nextbsd/nextbsd-userland#171, nextbsd/nextbsd-userland#54, nextbsd/nextbsd#460, nextbsd/nextbsd#461, nextbsd/nextbsd#278, nextbsd/nextbsd-kernel#62, gershwin-desktop/gershwin-desktop#49, gershwin-desktop/gershwin-desktop#60, gershwin-desktop/issues#22, gershwin-desktop/issues#26, gershwin-desktop/gershwin-workspace#78, https://pkgdemon.github.io/nextbsd-e14-tickets.html (E14 swap tickets, K3 and U1)

A2, A3 -> A1 -> A4; A5 independent. B1 with A1; B2 parked. C1 -> C2 -> C3 / C4 -> C5 -> C6; C7 independent (kernel). D1 after C1/C2 -> D2 -> D3a; D3b independent (upstream); D3c spike -> D5; D4 after C5; D5 last.

Index

IDTicketRepoDepends onStatus
A1Stop shipping rootfs/private/etc/fstab (closes nextbsd#467)nextbsd-overlaysA2, A3filed nextbsd-overlays#4
A2launchd: remount / with rw,noatime (the fstab root line was noatime's only source); CI asserts itnextbsd-userlandfiled nextbsd-userland#185
A3launchctl: compile out the dormant Darwin do_potential_fsck() path -- launchd owns root fsck and remountnextbsd-userlandfiled nextbsd-userland#186
A4Stale fstab comments in build.sh, the installer and its README; boot-test gate on the #467 stringsnextbsdA1filed nextbsd#472
A5Decide: keep or drop empty_dir()'s force-unmount of mounts beneath /tmp and /var/runnextbsd-userlandfiled nextbsd-userland#187
B1Document the manual Linux ABI mount path under no-fstab: create /etc/fstab with the five compat lines, in order, never `late`nextbsd-userlandfiled nextbsd-userland#189
B2org.nextbsd.linux one-shot LaunchDaemon + /usr/libexec/nextbsd-linux --compat [--native] (parked until Linux support is default)nextbsd-userlandB1filed nextbsd-userland#190
C1diskarbitrationd: rename the Mach service to com.apple.DiskArbitration.diskarbitrationd and drop the stale hwregd commentsnextbsd-userlandfiled nextbsd-userland#191
C2diskarbitrationd: build the DADisk model from GEOM (libgeom walk on kernel-registry arrival), log-onlynextbsd-userlandC1filed nextbsd-userland#192
C3diskarbitrationd: adopt mounts it did not make (EVFILT_FS + getmntinfo diff -> DADiskCreateFromVolumePath)nextbsd-userlandC2filed nextbsd-userland#193
C4Filesystem bundles: /System/Library/Filesystems/{msdos,exfat,ntfs,ufs,cd9660,udf}.fs with an fstyp-backed *.util shim (FSUR_* contract)nextbsd-userlandC2filed nextbsd-userland#194
C5diskarbitrationd: stage machine and mount executor -- /Volumes/<name>[ N], untrusted-media defaults, no auto-repair, per-FS helper table, /Volumes owned by the OSnextbsd-userlandC2, C4filed nextbsd-userland#195
C6diskarbitrationd: /etc/fstab UUID=/LABEL=/DEVICE= mount-map overlay, exactly Apple's semanticsnextbsd-userlandC5filed nextbsd-userland#196
C7kernel: compile in UDF, LIBICONV and the msdosfs/cd9660/udf iconv tables so DiskArbitration can mount UDF and non-ASCII FAT names (NO_MODULES)nextbsd-kernelfiled nextbsd-kernel#228
D1DiskArbitration client library: port DAServer.defs MIG and libDiskArbitration (sessions, Register* callbacks, CopyDescription, DiskListComplete/Idle replay); datest becomes a real clientnextbsd-userlandC1, C2filed nextbsd-userland#197
D2diskarbitrationd: unmount and eject requests with approval broadcast, 10 s response timer, dissenters, EBUSY -> dissenter PID via libprocstat, eject via CAM START STOP UNIT / CDIOCEJECTnextbsd-userlandD1, C5filed nextbsd-userland#198
D3aWorkspace: DiskArbitration bridge -- post NSWorkspace mount/unmount notifications through the workspace center, replace the 1.5 s mount poll with DA callbacks, and make GWUnmountHelper call DADiskUnmount/DADiskEject instead of sudo umount -fgershwin-workspaceD1, D2filed gershwin-workspace#119
D3bSpike: count volumes mounted under /Volumes as removable in GNUstep NSWorkspace (GSRemovableMediaPaths is exact-match)nextbsdfiled nextbsd#474
D3cSpike: disk images (dmg/iso/squashfs) as DiskArbitration disks -- how VolumeManager's darling-dmg/fuseiso/squashfuse mounts under /Volumes stop depending on vfs.usermountgershwin-workspaceD1, D2filed gershwin-workspace#120
D4Console user for diskarbitrationd: State:/Users/ConsoleUser in configd, published by LoginWindow; defer removable/external auto-mounts until login, unmount at logout; agent dialogs without XPC/CFUserNotificationnextbsd-userlandD1, D2, C5filed nextbsd-userland#199
D5Drop patch 0008 (vfs.usermount=1) once Workspace's image mounts and user unmounts go through DiskArbitration -- or narrow it if AppImage self-mounting still needs itnextbsd-kernelD1, D2, D3a, D3cfiled nextbsd-kernel#229

Tickets

Stage A — fstab and boot -- no DiskArbitration code; closes nextbsd#467

A1 · Stop shipping rootfs/private/etc/fstab (closes nextbsd#467) filed nextbsd-overlays#4

Reponextbsd-overlays
Labelsbug area:storage
Depends onA2, A3
ParentE15
## Summary

Delete `rootfs/private/etc/fstab` from the overlay seed (plan option A). This is the overlays-side change that closes nextbsd/nextbsd#467: with no `/etc/fstab`, launchctl's boot-time `mount -vat nonfs` (`src/launchd/support/launchctl.c:2401-2403`, gated on `path_check("/etc/fstab")`) never runs, so it can never try to union-mount `/dev/ufs/ROOTFS` over the live union.

The file has one commit (c0d0d1f) and one line:

```
/dev/ufs/ROOTFS  /  ufs  rw,noatime  1  1
```

Both claims in its header are false: root comes from the kernel's `ROOTDEVNAME` (nextbsd-kernel `config/NEXTBSD:15`, nextbsd#188), not `loader.conf`, and launchctl does run `mount -a` (#467, nextbsd-userland#171).

## Why

#467 has the failure analysis; this ticket only records why removing the file is safe. Every consumer of `/etc/fstab` on a NextBSD image was checked (plan section 3):

- **launchd's root remount** -- `launchd_root_make_writable()` (`src/launchd/src/launchd.c:443-475`) resolves `/` with `statfs()` and runs `fsck -p /` then `mount -uw /`. FreeBSD `fsck(8)` with an operand uses `getmntpoint()` and defaults to `ufs` when there is no fstab entry (`sbin/fsck/fsck.c:206-217`); `mount -u` takes the device from `getmntpoint()` in the `have_fstab == 0` branch (`sbin/mount/mount.c:357-393`). Same result without the file.
- **The loader** -- `getrootmount()` (`stand/common/boot.c:320-407`) reads `/etc/fstab` off the root device and sets `vfs.root.mountfrom` and `vfs.root.mountfrom.options` from the root line. That is why the CI kernel prints `Trying to mount root from ufs:/dev/ufs/ROOTFS [rw,noatime]` (run 35544120227, img-test). Without the file it sets nothing and the kernel falls through to `ROOTDEVNAME` (`sys/kern/vfs_mountroot.c:930-932`), or to the `vfs.root.mountfrom` the installer writes to `loader.conf` on EFI installs (`do-install.sh:300`). The Pi has no loader. Root discovery is unchanged on all three.
- **`df`, `mount` with no arguments, shutdown** -- `getmntinfo()` and `reboot(2)`; not fstab readers.
- **The installer** -- clones the running root with cpdup and only edits fstab `[ -f ]` (`do-install.sh:288`). Installs made from a fstab-less live root simply have no fstab. Existing installs keep theirs: the overlay is not package-owned (nextbsd `build.sh:7-9`), so `pkg upgrade` never touches it.
- **`swapon -a`, `umount -a`, bare `fsck`** -- nothing runs them at boot. Run by hand they print `fstab: /etc/fstab:0: No such file or directory` (libc `gen/fstab.c:264, 286-293`). launchd's own tools also trigger that line inside `getfsfile()`, but `launchd_run_tool()` logs a tool's stderr only on non-zero exit (`launchd.c:434-437`), so the console stays quiet.

Apple stopped shipping `/etc/fstab` on Macs in 10.4; the file is an admin overlay read by `diskarbitrationd`, `fsck`, `mount` and `umount`, never a boot mount list (plan section 2).

What the root line provides today, and what replaces it:

| Provided by the root line | After removal |
|---|---|
| `noatime` on `/` (via the loader, via `mount -uw /` merging fstab options at `mount.c:385-387`, and via `mount -vat`) | **A2**: launchd remounts with `-o rw,noatime` |
| a second-chance `fsck -fy` + `mount -uw /` in launchctl if launchd's remount failed (`launchctl.c:4060-4111`) | **A3**: that dormant Darwin path is compiled out; without fstab it would halt the machine instead |

## Design

**Option A (recommended): remove the file.** `git rm rootfs/private/etc/fstab`. Nothing else in this repo references it (the tree holds only `rootfs/boot/loader.conf.d/nextbsd.conf` and this file under `rootfs/`). This is what every Mac since 10.4 ships.

**Option B (fallback the maintainer may prefer): comment-only file.** Same effect on #467; `mount -vat nonfs` runs and mounts nothing; FreeBSD tools run by hand stop printing the "No such file" line, and admins have an obvious place for their own lines. Suggested content:

```
# /etc/fstab -- admin-maintained, optional. The kernel mounts the root
# filesystem (ROOTDEVNAME, or vfs.root.mountfrom from loader.conf) and
# launchd remounts it read-write; do not list / here. Lines added here are
# mounted in file order by `mount -vat nonfs` during launchctl bootstrap
# (support/launchctl.c). `late` entries are never mounted.
```

Not chosen: #467's option A (`noauto` on the root line) keeps an entry that names the wrong device on live media; #467's option B (live-only override) leaves the disk image and the install medium with different fstabs.

## Scope

One file. Image behaviour changes only for images built after this lands.

## Before starting

- [ ] A2 is merged and in the NextBSD-userland package the image build installs, so installed disks keep `noatime`
- [ ] A3 is merged, or explicitly accepted as a follow-up (it only changes the path taken when launchd's remount has already failed)

## Acceptance

- [ ] `rootfs/private/etc/fstab` is gone (A) or comment-only with no root line (B); `git grep fstab` in this repo finds nothing else
- [ ] CI iso-test transcript (amd64 and arm64) contains neither `Cannot union mount root filesystem` nor `fwexec(mount_tool` (A4 adds the gate)
- [ ] CI img-test transcript shows `/dev/ufs/ROOTFS on / (ufs, local, noatime, soft-updates` and the kernel line reads `Trying to mount root from ufs:/dev/ufs/ROOTFS []` (ROOTDEVNAME path); boot reaches `login:`
- [ ] A fresh install from the new live medium boots to `login:` with `/` read-write; `df /` and `mount` still show `/dev/ufs/NEXTBSD`
- [ ] nextbsd/nextbsd#467 is closed by the merging PR; its "header comment describes what processes the file" acceptance line is met by there being no header (A) or by the text above (B)

## Note

This repo has **no `area:*` labels** (GitHub defaults only). Create `area:storage` here before filing.

## Refs

Plan sections 1, 3, 7 (Stage A), 8: https://pkgdemon.github.io/nextbsd-diskarbitration-mounts-plan.html. nextbsd/nextbsd#467, nextbsd/nextbsd-userland#171, nextbsd/nextbsd#188. `launchctl.c:2401-2403`, `launchd.c:443-475`, `stand/common/boot.c:320-407`, `sbin/mount/mount.c:357-393`, `sbin/fsck/fsck.c:206-217`, `do-install.sh:288, 300`.

A2 · launchd: remount / with rw,noatime (the fstab root line was noatime's only source); CI asserts it filed nextbsd-userland#185

Reponextbsd-userland
Labelsenhancement area:storage area:boot
Depends on
ParentE15
## Summary

`launchd_root_make_writable()` remounts the root with `/sbin/mount -uw /` (`src/launchd/src/launchd.c:446`). Today `/` ends up `noatime` only because the shipped fstab root line says so. When A1 removes that line, `noatime` is lost. Move it into launchd:

```c
const char *mount_argv[] = { "/sbin/mount", "-u", "-o", "rw,noatime", "/", NULL };
```

and make CI assert it on the `/` mount line of the disk image.

## Why

Observed today (CI run 35544120227, img-test amd64 and arm64): `/dev/ufs/ROOTFS on / (ufs, local, noatime, soft-updates, ...)`. Three things put `noatime` there, and all three read the fstab root line:

1. The loader's `getrootmount()` (`stand/common/boot.c:320-407`) copies the line's options into `vfs.root.mountfrom.options`, so the kernel's initial read-only root mount already carries `noatime` (`vfs_mountroot.c:789-790, 1124-1166`: `rw` is dropped, other options pass through). Hence `Trying to mount root from ufs:/dev/ufs/ROOTFS [rw,noatime]` in the log.
2. `mount -uw /` finds the fstab entry for `/` and merges its options (`sbin/mount/mount.c:369-387`, `update_options(options, fs->fs_mntops, ...)`).
3. launchctl's `mount -vat nonfs` re-applies the line (`launchctl.c:2401-2403`).

Without the file, 1 and 3 do nothing and 2 passes only `noro` (`mount.c:388-391`, `update_options(options, NULL, ...)`). An `MNT_UPDATE` mount clears every `MNT_UPDATEMASK` flag that is not re-specified, and `MNT_NOATIME` is in that mask (`sys/kern/vfs_mount.c:1423-1430`; `sys/sys/mount.h:442-448`). So even a kernel-side `noatime` would not survive launchd's remount. The option has to be on launchd's command line.

This is also the Darwin shape: Apple keeps boot-critical mount options in boot code, not in fstab (plan section 2).

## Design

- `launchd.c:446`: `-u -o rw,noatime` in place of `-uw`. `rw` is `noro` to the kernel (`vfs_mount.c:930-937`); `noatime` sets `MNT_NOATIME` (`vfs_mount.c:894-895`). Stays filesystem-agnostic: both are generic `nmount` options.
- Optionally extend the `root-rw: / remounted read-write` notice (`launchd.c:471-472`) to say `read-write,noatime`, so the transcript shows the intent as well as the result.
- Works with the fstab still present (the merged options are identical) and without it, so this can land before A1.

**CI assertion.** The disk-image harness already runs `mount | grep ' / '` after login (nextbsd `tests/img-boot-test.sh:105-109`), but its `-re "ufs"` match fires on the echoed device name -- in the last run it printed `OK: ROOT-IS-UFS` after seeing `/dev/ufs/R` and before the rest of the line arrived -- so it proves nothing about mount flags. Add a transcript check in the verdict section of `tests/img-boot-test.sh` (lines 121-130, after `echo "==> verdict"` and before the `login:` check), in the same shape as the `DARWIN-SYSCTL-NOISE` gate in this repo's `tests/boot-test.sh:1527-1533` (`grep -aE` over `$LOG`, `echo "FAIL: MARKER -- reason"`, `exit 1`, else `echo "OK: MARKER"`):

```sh
# / must be mounted noatime by launchd's own remount, not by an fstab line
# (nextbsd-userland A2; the overlay ships no fstab after A1).
if ! grep -aE ' on / \(ufs, local, noatime' "$LOG"; then
    echo "FAIL: ROOT-NOATIME -- / is mounted without noatime"
    exit 1
fi
echo "OK: ROOT-NOATIME"
```

`$LOG` is `tests/img-boot.log` (`img-boot-test.sh:32`), the serial transcript, where the line is intact. The live ISO is out of scope: its `/` is `<above>:/cow on / (unionfs, local)`. This repo's own `tests/boot-test.sh` boots a native disk image but runs no `mount` command; adding one there is optional.

## Acceptance

- [ ] `launchd.c:446` passes `rw,noatime`; `grep -n '"-uw"' src/launchd/src/launchd.c` is empty
- [ ] With the current fstab still present, img-test shows `/dev/ufs/ROOTFS on / (ufs, local, noatime, soft-updates` (no behaviour change)
- [ ] With `/etc/fstab` absent, the same line still shows `noatime`
- [ ] `root-rw: / remounted read-write` is still logged once and no `root-rw: ... stderr:` line appears
- [ ] nextbsd `tests/img-boot-test.sh` fails when the `/` line lacks `noatime` (test by temporarily reverting the option) and passes otherwise

## Refs

Plan sections 1, 3 (recommendation, companion change 1): https://pkgdemon.github.io/nextbsd-diskarbitration-mounts-plan.html. nextbsd/nextbsd#467. `launchd.c:443-475`, `stand/common/boot.c:320-407`, `sbin/mount/mount.c:357-393`, `sys/kern/vfs_mount.c:894-895, 930-937, 1423-1430`, `sys/sys/mount.h:442-448`, nextbsd `tests/img-boot-test.sh:105-130`.

A3 · launchctl: compile out the dormant Darwin do_potential_fsck() path -- launchd owns root fsck and remount filed nextbsd-userland#186

Reponextbsd-userland
Labelsenhancement area:storage area:boot
Depends on
ParentE15
## Summary

Two pieces of code want to fsck and remount `/`:

- launchd (PID 1): `launchd_root_make_writable()` runs `fsck -p /` and `mount -uw /` before any job (`src/launchd/src/launchd.c:313, 443-475`). NextBSD-local, valid FreeBSD flags, runs first.
- launchctl (`bootstrap -S System`): `system_specific_bootstrap()` calls `do_potential_fsck()` (`src/launchd/support/launchctl.c:2357`). The function (`launchctl.c:4019-4115`) is byte-identical to Apple launchd-842.92.1 `support/launchctl.c:3992` and its flags are Darwin's.

Keep launchd's. Compile launchctl's out on FreeBSD, the way nextbsd-userland#176 did for `is_safeboot()` / `is_netboot()`.

## Why

`do_potential_fsck()` returns at `launchctl.c:4036-4038` because launchd already made `/` read-write. It only runs its body when launchd's remount failed (`launchd.c:467-469`, `mount -uw / exited N -- / left read-only`), and on FreeBSD that body is wrong twice:

1. `fsck -q` (`launchctl.c:4024, 4048`). FreeBSD `fsck(8)` accepts `BCdvpfFnyl:t:T:c:` (`sbin/fsck/fsck.c:111`); there is no `-q`, so `usage()` exits 1 (`fsck.c:573-581`). launchctl logs `fsck exited with status: 1` and falls through to
2. `fsck -fy` with no operand (`launchctl.c:4023, 4060`). With no operand fsck runs `checkfstab()` (`fsck.c:184-185`). With no `/etc/fstab` (A1) `preen.c:99-101` prints `Can't open checklist file` and returns 8; launchctl logs `Safe fsck exited with status: 8`, then `fsck failed! Shutting down in 3 seconds.` and `reboot(RB_HALT)` (`launchctl.c:4076-4078`). A read-only-root boot becomes a halt.

With today's fstab the same path instead fscks the root line and retries `mount -uw /` (`launchctl.c:4111`) -- a second-chance remount launchd's error path does not get. Removing the fstab removes that second chance, so this must be decided together with A1. A single owner is also what Apple's launchd did: the Mac's fsck/remount lived in one place (launchctl), never two.

Stale comments to fix in the same change: `launchd.c:307-312` ("Apple does this in launchctl's do_potential_fsck() ahead of the scan") and `launchd.c:381-392` ("this port dropped that bootstrapper. Restore the step here"). The bootstrapper is back (`launchd.c:355-373`); the comments should say launchd owns the transition and launchctl's Darwin path is compiled out.

## Design

**Option 1 (recommended): `#ifdef __FreeBSD__` inside the function.** Match `is_safeboot()` (`launchctl.c:4208-4212`) and `is_netboot()` (`launchctl.c:4228-4232`): FreeBSD body first, Apple's under `#else`, one comment naming this ticket. FreeBSD body: `statfs("/")`; if not `MNT_RDONLY` return; otherwise `launchctl_log(LOG_ERR, "/ is still read-only after launchd's remount (see root-rw: in the console log); continuing")` and return. No fsck, no remount, no halt. Leave the call at `launchctl.c:2357` alone to keep the diff against Apple small.

**Option 2: delete the function and the call.** Larger diff from upstream; nothing gained.

**Not: fix the flags** (`fsck -p /`, `fsck -fy /`). That keeps two owners and a second `fsck` of a filesystem launchd already checked.

Add a row for `do_potential_fsck()` to `docs/launchd-darwin-probes.md` (the disposition table at lines 15-27) so the dormant path is listed with the other stubbed Darwin probes.

## Scope

~15 lines in `launchctl.c`, comment edits in `launchd.c`, one doc row.

## Acceptance

- [ ] On FreeBSD builds launchctl never execs `fsck`: `grep -n 'fsck' src/launchd/support/launchctl.c` shows the strings only under `#else` / Apple's branch
- [ ] Forced failure test (e.g. make `/sbin/mount` non-executable in a scratch image): boot reaches `login:` with `/` read-only, the console shows launchd's `root-rw: mount -uw / exited` line and launchctl's new line, and the machine does not halt
- [ ] Normal boot unchanged: `root-rw: / remounted read-write` once; CI img-test and iso-test green
- [ ] `launchd.c:307-312` and `381-392` describe the current ownership
- [ ] `docs/launchd-darwin-probes.md` lists `do_potential_fsck()` with its disposition

## Refs

Plan section 3 (companion change 2) and section 1 step 3b: https://pkgdemon.github.io/nextbsd-diskarbitration-mounts-plan.html. nextbsd/nextbsd-userland#176, nextbsd/nextbsd#324. `launchctl.c:2357, 4019-4115`, `launchd.c:307-313, 381-392, 443-475`, `sbin/fsck/fsck.c:111, 184-185, 573-581`, `sbin/fsck/preen.c:99-101`.

A4 · Stale fstab comments in build.sh, the installer and its README; boot-test gate on the #467 strings filed nextbsd#472

Reponextbsd
Labelsdocumentation area:storage area:live-media area:ci
Depends onA1
ParentE15
## Summary

Two parts, one PR per repo:

1. Comments in nextbsd `build.sh` and in nextbsd-userland's installer say the shipped `/etc/fstab` is where the root entry lives. After A1 there is no such file, and the claims were already wrong (root comes from the kernel's `ROOTDEVNAME`, nextbsd#188).
2. The live-ISO harness gets a regression gate that fails on the two #467 strings, so the root remount can never come back unnoticed.

## Comments

nextbsd (this repo):

- `build.sh:7` -- the list of overlay-seeded `/etc` files names `fstab`. Drop it.
- `build.sh:318-319` -- "No /etc/fstab heredoc -- the nextbsd-overlays seed (rootfs/private/etc/fstab) carries the real root entry, and nextbsd-overlays rootfs/boot/loader.conf.d/ carries the loader settings". The root entry is `ROOTDEVNAME` in nextbsd-kernel `config/NEXTBSD:15`; `loader.conf.d/nextbsd.conf` says itself that `vfs.root.mountfrom` is gone (#188). Say: no fstab is shipped; the kernel mounts root from `ROOTDEVNAME`, launchd remounts it `rw,noatime` (A2).
- `build.sh:356-357` -- `"ROOTFS" matches loader.conf.d's vfs.root.mountfrom and the the nextbsd-overlays seed (rootfs/private/etc/fstab) entry`. It matches `ROOTDEVNAME`. Also "the the".

nextbsd-userland (`src/nextbsd-installer/`):

- `engine/do-install.sh:14-15` -- "the shipped /etc/fstab (ufs/ROOTFS) and the kernel's baked-in ufs:/dev/ufs/ROOTFS root both resolve with NO edits". Only the kernel default remains.
- `engine/do-install.sh:45` -- "The cloned fstab + a loader.conf override (step 5b) point the target here". Only the `loader.conf` override (EFI) or `cmdline.txt` (Pi, lines 353-388) does.
- `engine/do-install.sh:238-239` -- "the mount + fstab steps below".
- `engine/do-install.sh:282-291` -- step 5b's comment ("cpdup copied the source's fstab ... Repoint both") and the `sed` at line 288. The clone carries no fstab (under option B it carries no root line), so the `sed` is dead: remove it and reword the comment around the `loader.conf` override at line 300. The Pi paragraph at 289-291 ("the label in fstab above plus the kernel's compiled-in ROOTDEVNAME") becomes ROOTDEVNAME alone.
- `README.md:13-15` -- same claim as `do-install.sh:14-15`.

`build.sh:885-975` (the live `/init`) and `do-install.sh:300` are correct and need no change.

## Gate

The strings, verbatim from CI run 35544120227 (iso-test amd64 and arm64):

```
mount: /dev/ufs/ROOTFS: Cannot union mount root filesystem: Operation not supported
posix_assumes_zero: fwexec(mount_tool, NULL) == -1 (errno 2) at launchctl.c:2403
```

The first is mount(8) printing the kernel's nmount error; the second is launchctl's assert on the failed `mount -vat nonfs` (`launchctl.c:2403`).

Where: `tests/iso-boot-test.sh` verdict section (lines 130-140), which already greps the serial transcript `$LOG` (`tests/iso-boot.log`, line 25) for the pivot marker and `login:`. Match the shape of the `DARWIN-SYSCTL-NOISE` gate in nextbsd-userland `tests/boot-test.sh:1527-1533` -- `grep -aE` over `$LOG`, `echo "FAIL: MARKER -- reason"`, `exit 1`, else `echo "OK: MARKER"` -- and place it after `echo "==> verdict"` and before the PASS check:

```sh
# launchctl's boot-time `mount -vat nonfs` must never touch / (nextbsd#467).
if grep -aE 'Cannot union mount root filesystem|fwexec\(mount_tool' "$LOG"; then
    echo "FAIL: FSTAB-ROOT-REMOUNT -- launchctl mount -a tried to remount / on the live union (nextbsd#467)"
    exit 1
fi
echo "OK: FSTAB-ROOT-QUIET"
```

Add the same block to `tests/img-boot-test.sh` (verdict at lines 121-130, `$LOG` = `tests/img-boot.log`); the `fwexec(mount_tool` half catches any other `mount -a` failure on the disk image. Do not touch `tests/boot-test.sh`: `build.yml` runs only `img-boot-test.sh` (line 395) and `iso-boot-test.sh` (line 454).

The gate is red until A1 lands, so file it together with A1 or merge the comment fixes first and the gate with A1.

## Acceptance

- [ ] `grep -n fstab build.sh` in nextbsd and `grep -rn fstab src/nextbsd-installer` in nextbsd-userland list no line that presents fstab as the source of the root entry; `do-install.sh:288` is gone
- [ ] `tests/iso-boot-test.sh` and `tests/img-boot-test.sh` fail on either #467 string (verified by running the harness against an ISO built with the old fstab) and print `OK: FSTAB-ROOT-QUIET` on main after A1
- [ ] CI green on amd64 and arm64 for both harnesses

## Refs

Plan sections 1, 7 (Stage A), 8: https://pkgdemon.github.io/nextbsd-diskarbitration-mounts-plan.html. nextbsd/nextbsd#467, nextbsd/nextbsd#188. `build.sh:7, 318-319, 356-357`, `do-install.sh:14-15, 45, 238-239, 282-291, 300`, `README.md:13-15`, `tests/iso-boot-test.sh:130-140`, `tests/img-boot-test.sh:121-130`, nextbsd-userland `tests/boot-test.sh:1527-1533`.

A5 · Decide: keep or drop empty_dir()'s force-unmount of mounts beneath /tmp and /var/run filed nextbsd-userland#187

Reponextbsd-userland
Labelsarea:storage area:boot area:research
Depends on
ParentE15
## Summary

During `launchctl bootstrap -S System`, right after the fstab pass (`support/launchctl.c:2401-2403`), `empty_dir(_PATH_VARRUN)` and `empty_dir(_PATH_TMP)` run (`launchctl.c:2432-2433`). `empty_dir()` (`launchctl.c:4246-4316`, identical to Apple launchd-842.92.1 `support/launchctl.c:4207`) force-unmounts any entry whose `st_dev` differs from the directory's (`launchctl.c:4288-4289`, `unmount(de->d_name, MNT_FORCE)`), then deletes everything underneath. Decide whether NextBSD keeps that behaviour or skips mount points.

## What it does, precisely

- The baseline device is the directory itself (`lstat(thedir)`, `launchctl.c:4252-4258`). A tmpfs mounted **on** `/tmp` or `/var/run` is therefore not unmounted; it is just emptied (nothing to empty on a fresh tmpfs). The plan's section 1 line "an fstab tmpfs on /tmp is undone one step after it is mounted" overstates this.
- Anything mounted **beneath** them (`/tmp/scratch`, `/var/run/foo`) is force-unmounted; after a successful unmount the mount-point directory itself is kept (`launchctl.c:4294-4300`). This is the case that hits nextbsd-userland#171's suggested verification ("tmpfs on /tmp/scratch in fstab") -- it is mounted at line 2402 and unmounted at line 2433, and only a failed unmount would print anything.

Apple: 10.4-10.9 launchctl did exactly this. 10.10+ moved the sweep to a `dirs_cleaner` boot task (closed source). The plan says modern `dirs_cleaner` skips mount points; that is not verifiable from open source and should be treated as unconfirmed.

NextBSD today: nothing mounts beneath either path. The live `/init` mounts `/media`, `/rofs`, `/cow` and `/rofs/dev` (`build.sh:906-964`); no LaunchDaemon plist mounts; the installer scrubs the same directories at install time (`do-install.sh:274-277`). After A1 the only way for a mount to be there before `empty_dir()` runs is an admin `/etc/fstab` line.

## Options

**1. Keep Apple's behaviour and document it.** Zero code. Add a paragraph to `docs/launchd-darwin-probes.md` (or a new `docs/launchd-boot-sequence.md`) and to the fstab guidance B1 writes for #54/#171: "mounts beneath /tmp and /var/run are unmounted at boot; mount tmpfs on them, not under them".

**2. Skip mount points.** In `empty_dir()`, under `#ifdef __FreeBSD__`, when `psb->st_dev != sb.st_dev` log and `continue` instead of unmounting and recursing. About five lines. An fstab tmpfs on `/tmp/x` or `/var/run/x` then survives; whatever is on it is that mount's own business (a fresh tmpfs is empty anyway).

**3. Reorder: sweep before the fstab pass.** Considered and rejected: on a fresh boot there is no stale mount to catch, so it buys nothing over 2 while departing from Apple's sequence.

## Recommendation

Option 1 now. There is no consumer, a tmpfs on `/tmp` itself already works, and it keeps `empty_dir()` byte-identical to upstream. Revisit if a real requirement for a mount beneath `/var/run` or `/tmp` appears (Stage C/D do not add one: DiskArbitration mounts under `/Volumes`); option 2 is then a five-line change.

## Acceptance

- [ ] Decision recorded: a comment at `launchctl.c:2432` naming this issue, plus the doc paragraph
- [ ] #171's verification instruction is corrected (B1) so it does not test with a mount under `/tmp`
- [ ] If option 2 is chosen: a boot with `tmpfs /tmp/x tmpfs rw 0 0` in `/etc/fstab` leaves `/tmp/x` mounted and CI stays green

## Refs

Plan sections 1 (step 3e), 2, 7 (Stage A), 8: https://pkgdemon.github.io/nextbsd-diskarbitration-mounts-plan.html. nextbsd/nextbsd-userland#171, nextbsd/nextbsd-userland#54. `launchctl.c:2401-2403, 2432-2433, 4246-4316`; Apple launchd-842.92.1 `support/launchctl.c:2432-2433, 4207`.

Stage B — Linux ABI mounts -- the manual path under no-fstab, and the parked org.nextbsd.linux one-shot

B1 · Document the manual Linux ABI mount path under no-fstab: create /etc/fstab with the five compat lines, in order, never `late` filed nextbsd-userland#189

Reponextbsd-userland
Labelsdocumentation area:linux area:storage
Depends on
ParentE15
## Summary

Write down the supported manual path for the Linux ABI filesystems now that the image ships no `/etc/fstab` (E15 A1): **an admin who wants the Linuxulator mounts creates `/etc/fstab` with the five compat lines below, in this order, and never uses `late`.** Optionally add the four native-root lines for AppImages. Nothing else is needed: `launchctl bootstrap` already runs `mount -vat nonfs` whenever the file exists.

This is the documentation half of the #54 decision ("no LaunchDaemon at this time -- mounts stay manual"). It does not change that decision; it makes the manual path correct, complete and findable. B2 is the parked Darwin-shaped upgrade.

## Why

- #54 and #460 both say `/etc/fstab` is inert on NextBSD. It is not: `support/launchctl.c:2400-2405` runs `mount -vat nonfs` inside `if (path_check("/etc/fstab"))`, as Apple's launchd-842 did. #171's own reboot evidence (five compat mounts present after a reboot with no manual mount) is this code path working.
- #460's recorded fstab block carries a `lindebugfs ... rw,late` line that will never mount, and a header that says nothing runs `mount -a`. #54's procedure says "everything here is lost on reboot", which is only true for the by-hand `mount` commands, not for fstab lines.
- Under A1 the stock image ships no fstab at all, so "append these lines" becomes "create the file". Apple's admin hook is exactly this: `[ -f /etc/fstab ] && mount -vat nonfs`, skipped on every stock Mac (plan section 2).

## Design

The document says, in this order:

**1. The compat set (rc.d/linux's five mounts, `libexec/rc/rc.d/linux:74-80` in releng/15.0).** Create `/etc/fstab` (or append, on a system that still has one) with exactly:

```
linprocfs  /compat/linux/proc     linprocfs  rw,nocover                0  0
linsysfs   /compat/linux/sys      linsysfs   rw,nocover                0  0
devfs      /compat/linux/dev      devfs      rw,nocover                0  0
fdescfs    /compat/linux/dev/fd   fdescfs    rw,nocover,linrdlnk       0  0
tmpfs      /compat/linux/dev/shm  tmpfs      rw,nocover,mode=1777      0  0
```

- **Order matters and file order is mount order.** `mount -a` walks `getfsent()` sequentially (`sbin/mount/mount.c:300-323`), so `devfs` on `/compat/linux/dev` is mounted before `fdescfs` on `/compat/linux/dev/fd` (devfs supplies the `fd` directory) and before the tmpfs on `/compat/linux/dev/shm` (`linux_dev_shm_create()` puts `shm/` into every devfs instance at ABI init, `sys/compat/linux/linux.c:699-709`, so no `mkdir` is needed for either).
- **`linrdlnk` is not optional** (#54: Bun resolves cwd through `readlink("/dev/fd/N")`).
- **`nocover`** is what rc.d/linux passes; it keeps a second `mount -a` (or a stray manual mount) from stacking a filesystem on top of a live one. Optional but recommended.
- `/compat/linux/proc` and `/compat/linux/sys` must exist as directories: `linux_base-rl9` provides them (#171 observed them empty), `mount -a` does not `mkdir`.
- Optional: `failok` on each line keeps `launchctl`'s `posix_assumes_zero(fwexec(mount_tool))` quiet when `/compat/linux` is absent (`mount.c:311-314`: a `failok` line's failure does not set the exit status).

**2. Never `late`.** launchctl runs plain `-a`; `mount.c:307-310` skips any line carrying `late` unless `-l`/`-L` was given (`mount.c:226-243`), and nothing on NextBSD ever passes them. The FreeBSD Handbook's `late` on these lines exists to sequence against rc.d ordering, which NextBSD does not have. Also drop the `lindebugfs ... late` line from #460 (it never mounts; nothing in rc.d/linux mounts it either).

**3. The native set (optional, for AppImages; #54's "canonical" AppImage configuration).** After `mkdir -p /proc /sys`:

```
linprocfs  /proc     linprocfs  rw,nocover            0  0
linsysfs   /sys      linsysfs   rw,nocover            0  0
fdescfs    /dev/fd   fdescfs    rw,nocover,linrdlnk   0  0
tmpfs      /dev/shm  tmpfs      rw,nocover,mode=1777  0  0
```

with the warning #54 already records: `linprocfs` on the real `/proc` is a system-wide change that native binaries see too. Say so and leave it opt-in. (No `devfs` line: the kernel already mounts `/dev`; `/dev/fd` and `/dev/shm` exist inside it.)

**4. What runs, and when.** Step by step, cited: launchd remounts `/` rw (`src/launchd.c:443-475`); `launchctl bootstrap` applies `/etc/sysctl.conf` (`launchctl.c:2326`), runs `mount -vat nonfs` if fstab exists (`launchctl.c:2400-2405`), force-unmounts anything under `/var/run` and `/tmp` (`launchctl.c:2432-2433` -- so never put a tmpfs on `/tmp` in fstab), then `load -D all` starts the LaunchDaemons (`launchctl.c:2469-2476`). There is no in-process plist scan any more (`src/launchd.c:352-372`).

**5. After `pkg install linux_base-rl9` on a running system:** `mount -a` by hand is idempotent (`ismounted()` skips mounted lines, `mount.c:315-318`, and `nocover` refuses to stack), or `mount /compat/linux/proc` etc. by mount point. Chrome/claude-code need the mounts before they start.

**6. No sysctl is needed.** `kern.elf64.fallback_brand` is already 3 by kernel default (nextbsd-kernel patch 0009 / kernel#68). If an admin wants to change it, `/etc/sysctl.conf` is applied at bootstrap (`launchctl.c:2326`, `4330-4365`) -- mark this "not verified on a running box" until someone has.

**7. What is deliberately not done**: no LaunchDaemon (#54 decision), no autofs, no WatchPaths (inert on this launchd; see B2). Point at B2 as the path when Linux support becomes default.

### Where it lives

`nextbsd-userland/docs/linux-abi-mounts.md`, next to `docs/launchd-darwin-probes.md`. Reasons: the behaviour being documented is `launchctl bootstrap`'s (this repo); `docs/` already holds launchd behaviour notes in this format; a man page would be for a tool that does not exist under option H; the wiki is unused. Two pointers elsewhere: a paragraph in the `nextbsd-overlays` README (it owns `/etc` and says "mounts" are admin-owned) linking to the doc, and an edit to the top of #54 replacing the "everything here is lost on reboot" procedure with the fstab form and a link. The `nextbsd-overlays` fstab header fix itself is A1/A4, not this ticket.

## Scope

One Markdown file, one README paragraph, one issue edit, plus the #171/#460 corrections listed in the epic. No code.

## Before starting

- [ ] Decide A vs B for A1 (no file vs comment-only file), so the doc says "create" or "append" correctly. Draft it to work either way ("create `/etc/fstab` if it does not exist").

## Acceptance

- [ ] `docs/linux-abi-mounts.md` exists with the five compat lines verbatim, the ordering rule, the `late` rule, and the `launchctl.c` citations above.
- [ ] On a fresh image with `linux_base-rl9` installed and only those five lines in `/etc/fstab`, a reboot yields all five in `mount` output with no manual command; `sh -c 'exec 9</etc/passwd; readlink /dev/fd/9'` run under the Linuxulator returns a path (#54's check).
- [ ] The same file with a `late` line added: that line is absent from `mount` after reboot (documents the rule by test).
- [ ] `mount -a` run twice by hand does not stack mounts (`nocover`) and exits 0.
- [ ] `nextbsd-overlays` README and #54's top section link to the doc; #460's fstab block is marked superseded by it.

## Refs

Plan sections 1 (boot sequence), 3 (fstab audit), 5 (option H), 8 (corrections). `support/launchctl.c:2326, 2400-2405, 2432-2433, 2469-2476`; `src/launchd.c:352-372, 443-475`; FreeBSD `sbin/mount/mount.c:226-243, 300-323`; `libexec/rc/rc.d/linux:74-80`; `sys/compat/linux/linux.c:699-709`. nextbsd-userland#54, #171; nextbsd#460, #461.

B2 · org.nextbsd.linux one-shot LaunchDaemon + /usr/libexec/nextbsd-linux --compat [--native] (parked until Linux support is default) filed nextbsd-userland#190

Reponextbsd-userland
Labelsenhancement area:linux area:storage status:parked
Depends onB1
ParentE15
## Summary

**Parked.** The maintainer decision in #54 is "no LaunchDaemon at this time -- mounts stay manual" (B1 documents that path). This ticket records the Darwin-shaped design for when Linux support becomes default, so it is not re-derived from scratch. Do not start it until that decision is revisited.

`org.nextbsd.linux`: a one-shot LaunchDaemon (`RunAtLoad`, no `KeepAlive`) that runs `/usr/libexec/nextbsd-linux --compat [--native]`, the NextBSD owner of what `rc.d/linux` does on FreeBSD. It is idempotent, a no-op when `/compat/linux` is absent, ships in the package so installs get it on upgrade, and is re-run after a package install with `launchctl start org.nextbsd.linux`.

## Why

- Apple's pattern everywhere is that the daemon that switches a subsystem on also provisions its mounts and registers its exec handler: `vminitd` mounts proc/sysfs/devtmpfs/`/dev/shm`/devpts per container (`apple/containerization`, `Sources/Containerization/LinuxContainer.swift:445-456`, `defaultMounts()`), and the Rosetta guest agent mounts the virtiofs share and then writes the binfmt_misc registration (`Sources/Containerization/Vminitd+Rosetta.swift:21-34`, `Sources/ContainerizationOS/Linux/Binfmt.swift:73-82`). That registration is the analogue of `kern.elf64.fallback_brand`.
- On FreeBSD the same job is `libexec/rc/rc.d/linux` (releng/15.0): module loads (`:34-63`, gone on NextBSD -- the Linuxulator, linprocfs, linsysfs and fdescfs are static), `fallback_brand` only if `-1` (`:66-72`), and the five mounts with `nocover` (`:74-80`, gated by `linux_mounts_enable`).
- A fstab line is Apple's admin hook, not a product default. A shipped default belongs to the subsystem's own job, which reaches installed systems through `pkg upgrade` (the overlays repo is seed-once and never does).
- NextBSD precedent for an `org.nextbsd.*` daemon that owns an rc.d job's work: `overlay/System/Library/LaunchDaemons/org.nextbsd.wland.plist` (VAP creation was rc.conf's job on FreeBSD; its header explains the label choice -- ours, not `com.apple.*`, because Apple ships no such daemon).

## Design

### The tool: `/usr/libexec/nextbsd-linux`

A `/bin/sh` script is acceptable here (precedent: `overlay/usr/libexec/sshd-keygen-wrapper`). Behaviour:

1. `emul=$(sysctl -n compat.linux.emul_path)`; if `$emul` is not a directory, exit 0 silently. (Evaluation criterion 1 in #54: clean no-op with no Linux userland.)
2. `--compat`: for each of `rc.d/linux:75-79`, in that order:
   `linprocfs $emul/proc nocover`; `linsysfs $emul/sys nocover`; `devfs $emul/dev nocover`; `fdescfs $emul/dev/fd nocover,linrdlnk`; `tmpfs $emul/dev/shm nocover,mode=1777`.
   Idempotency has two layers: first `statfs(2)` on the mount point (skip if `f_fstypename` already matches, the `mount | grep` check of `rc.d/linux:22`), then `-o nocover` as the belt-and-braces guard, with `EBUSY` from `nocover` treated as "already mounted", exit 0. `mkdir -p` the two directory mount points only; `/dev/fd` and `/dev/shm` come from devfs (`sys/compat/linux/linux.c:699-709`).
3. `--native` (optional flag; off by default because it is a system-wide change, #54): `mkdir -p /proc /sys`, then `linprocfs /proc`, `linsysfs /sys`, `fdescfs /dev/fd linrdlnk`, `tmpfs /dev/shm mode=1777`, same guards. No devfs on `/dev` (kernel-mounted).
4. Re-assert `kern.elf64.fallback_brand=3` **only if it reads `-1`** (`rc.d/linux:66-68`), so a `loader.conf` override sticks. Never touch `kern.elf32.fallback_brand` (no 32-bit ABI is compiled in; kernel#68).
5. Exit non-zero only on a real mount failure; log one line per action to stderr.

### The plist: `overlay/System/Library/LaunchDaemons/org.nextbsd.linux.plist`

```
Label              org.nextbsd.linux
ProgramArguments   /usr/libexec/nextbsd-linux --compat        (add --native only by admin edit)
RunAtLoad          true
KeepAlive          (absent -- one-shot; true would respawn every ThrottleInterval, see #54 option A)
StandardOutPath    /var/log/nextbsd-linux.stderr
StandardErrorPath  /var/log/nextbsd-linux.stderr              (wland/ipconfigd log convention)
```

Header comment in the `org.nextbsd.wland.plist` style: why the label is ours, what the job replaces (`rc.d/linux`), and that there is no launchd ordering on this port, so the tool must not assume anything else is up.

### Ordering, races and re-runs

- RunAtLoad jobs are dispatched by `launchctl bootstrap`'s `load -D all` (`support/launchctl.c:2469-2476`), which runs **after** `mount -vat nonfs` (`:2400-2405`) and after the `/var/run`/`/tmp` force-unmounts (`:2432-2433`); there is no in-process plist scan any more (`src/launchd.c:352-372`). So an admin who also has B1's fstab lines gets no race: the tool's `statfs` check skips them.
- **After a package install** (the case that prompted #54): `launchctl start org.nextbsd.linux`. This launchctl is the launchd-842 CLI (`support/launchctl.c:233-264`); it has **no `kickstart`** subcommand, so the plan page's `launchctl kickstart -k system/org.nextbsd.linux` must read `launchctl start org.nextbsd.linux` (`LAUNCH_KEY_STARTJOB`, `launchctl.c:2858-2865` -> `src/ipc.c:406`). Optionally a pkg trigger `/usr/local/share/pkg/triggers/nextbsd-linux.ucl` with `path: ["/compat/linux"]` running that command, shipped by NextBSD's own base package rather than by the upstream `linux_base` port (#54 option D's ownership question).
- **WatchPaths is not an option on NextBSD.** `distill_fsevents()` (`support/launchctl.c:1611-1648`) rewrites `WatchPaths`, `QueueDirectories` and `KeepAlive.PathState` into `com.apple.fsevents.matching` LaunchEvents, and nothing delivers those: `semaphore_reason_t` in `src/core.c:322-332` has only `NETWORK_*`, `*_EXIT`, `CRASHED`/`DID_NOT_CRASH` and `OTHER_JOB_*` -- no `PATH_*` -- and the only file in the tree that mentions `com.apple.fsevents` is `launchctl.c` itself. #54 option B and nextbsd#278 "Lever D" both assumed otherwise.
- **Opt-in shipping**: `Disabled=true` plus `launchctl load -w` (overrides db) is the launchd-842 way to ship a job off. The getty plist header says the old in-process boot scan ignored `Disabled`; that scan is gone (`src/launchd.c:352-372`), so this needs re-testing under `load -D all` before being relied on.

### Relationship to fstab and DiskArbitration

The job does not read fstab and DiskArbitration will not either for these (plan section 4.1: `__DAMountMapCreate1` accepts only `UUID=`/`LABEL=`/`DEVICE=` specs, so a `linprocfs` line means nothing to it). If a `nullfs` projection of the native set into `/compat/linux` (#54 idea 3) is ever proven, the tool is the one place to switch it on.

## Scope

One shell tool (~80 lines), one plist, one optional pkg trigger, a CI boot-test line. No kernel or launchd change.

## Before starting

- [ ] The #54 decision is revisited: Linux support becomes a default feature (or the maintainer schedules this explicitly). Until then this ticket stays `status:parked`.
- [ ] B1 has landed (the manual path is the documented fallback and the doc gains a "or let org.nextbsd.linux do it" paragraph).
- [ ] Decide whether `--native` is ever on by default (plan section 9, open question 2: `linprocfs` on the real `/proc` is system-wide).

## Acceptance

- [ ] Fresh image, no `linux_base`: boot log shows the job ran and exited 0 with no mounts and no assert noise.
- [ ] `pkg install linux_base-rl9` then `launchctl start org.nextbsd.linux`: all five compat mounts present, in order, `linrdlnk` on fdescfs, `mode=1777` on shm; `launchctl start` a second time changes nothing and exits 0.
- [ ] Reboot with `linux_base` present: same five mounts, no fstab involved (`ls /etc/fstab` fails).
- [ ] With `kern.elf64.fallback_brand=-1` set in `loader.conf`, the job leaves it alone; with the kernel default 3 it is untouched.
- [ ] `--native` variant: `/proc`, `/sys`, `/dev/fd`, `/dev/shm` mounted; an AppImage from #460's test payload runs (the AppImage half stays with #460).
- [ ] CI boot test greps `org.nextbsd.linux` ran and asserts no `fwexec` failure.

## Refs

Plan sections 5.1 (Apple precedent), 5.2 option a, 8 (WatchPaths correction). `support/launchctl.c:233-264, 1611-1648, 2400-2405, 2432-2433, 2469-2476, 2858-2865`; `src/core.c:322-332`; `src/ipc.c:406`; `src/launchd.c:352-372`; `overlay/System/Library/LaunchDaemons/org.nextbsd.wland.plist`; `overlay/usr/libexec/sshd-keygen-wrapper`. FreeBSD `libexec/rc/rc.d/linux:22, 66-80`; `sys/compat/linux/linux.c:699-709`. apple/containerization `LinuxContainer.swift:445-456`, `Vminitd+Rosetta.swift:21-34`, `Binfmt.swift:73-82`. nextbsd-userland#54 (options A/B/D), #53 (unmerged draft), #171; nextbsd#278, #460, #461.

Stage C — diskarbitrationd, the daemon -- GEOM disk model, fs bundles, mount executor, /Volumes

C1 · diskarbitrationd: rename the Mach service to com.apple.DiskArbitration.diskarbitrationd and drop the stale hwregd comments filed nextbsd-userland#191

Reponextbsd-userland
Labelsenhancement area:storage area:mach
Depends on
ParentE15
## Summary

The iter-1 skeleton checks in as `com.apple.DiskArbitration` (`src/DiskArbitration/diskarbitrationd.c:97-104`, plist `MachServices` key at `overlay/System/Library/LaunchDaemons/com.apple.DiskArbitration.plist:35`, `datest.c:19-27`). Apple's name is `com.apple.DiskArbitration.diskarbitrationd` (`diskarbitrationd/DAInternal.h:41`, `_kDADaemonName`). Rename it now, before any client code exists, and refresh the comments that still describe the retired hwregd path.

## Why

The client framework looks the daemon up by that exact string: `DiskArbitration/DASession.c:530` and `:604` call `bootstrap_look_up2(bootstrapPort, _kDADaemonName, ...)`, and the daemon side checks in with the same constant (`DAServer.c:3483`). Apple's `SCDynamicStoreCreate` session is also named with it (`DAMain.c:481`). When D1 ports `DASession.c`, every lookup will fail against the current name. A one-string rename today costs nothing; a rename after D1 lands touches the framework, its tests and the CI markers at once.

Stale text: the plist header (`:9-16`) still says "iter 2 subscribes to hwregd's storage device class events" and cites hwregd's log convention; `da_iokit_subscribe.h:13-19` still says the caller "falls back to the legacy hwreg_subscribe_start() ... hwreg_subscribe.c stays compiled"; `Makefile:8` and `:27` still name "hwregd subscription" and `hwreg_subscribe.c` (the file no longer exists, `SRCS` at `:18` is `diskarbitrationd.c da_iokit_subscribe.c`); `da_iokit_subscribe.c:66-67` and `:89-90` still describe hwregd's pub/sub events; `diskarbitrationd.c:16-17` cites the retired 2026-05 plan URL. `da_iokit_subscribe.c:20-24` already says the truth ("No fallback ... hwregd was retired in #218").

## Design

- `diskarbitrationd.c`: `bootstrap_check_in(bootstrap_port, "com.apple.DiskArbitration.diskarbitrationd", &svc)`; define the string once (`#define _kDADaemonName`, same spelling as Apple's `DAInternal.h:41`) in a small `DAInternal.h` so C2+ and D1 include it.
- `datest.c`: look up the new name. Keep the `DA-BOOT-OK` / `DA-BOOT-FAIL` tokens unchanged (the gates grep only the token: `overlay/usr/tests/freebsd-launchd-mach/run.sh:1593-1600`, `tests/boot-test.sh:1178-1194` here and `tests/boot-test.sh:1076-1092` in nextbsd/nextbsd); update the parenthetical text in the `puts` lines.
- plist: `MachServices` key -> `com.apple.DiskArbitration.diskarbitrationd`. Keep `Label` = `com.apple.DiskArbitration` (it is what `run.sh:784` `LAUNCHD_REQUIRED` and `wedge-check.sh:150,156` key on; Apple's label is `com.apple.diskarbitrationd`, `com.apple.diskarbitrationd.plist`, and the vendored man page `diskarbitrationd.8:62` already cites that file name -- either leave the man page or rename the label in a later CI-touching change, not here). Rewrite the header comment: kernel registry via `/dev/ioregistry`, no hwregd, `RunAtLoad` + `KeepAlive` (Apple has `KeepAlive` and no `RunAtLoad`; keep ours, the daemon must exist before Stage C mounts anything).
- Comment refresh in `da_iokit_subscribe.h`, `Makefile`, `da_iokit_subscribe.c`, `diskarbitrationd.c` header (point at `https://pkgdemon.github.io/nextbsd-diskarbitration-mounts-plan.html` and this epic).
- `wedge-check.sh:150` comment: "no client tool on the image" stays true until D1; mention the new service name.

## Scope

Strings and comments only; no behaviour change. ~40 lines across 6 files plus the two `boot-test.sh` copies.

## Acceptance

- [ ] `grep -rn '"com.apple.DiskArbitration"' src/DiskArbitration overlay` returns only the plist `Label`
- [ ] CI boot test still prints `DA-BOOT-OK` (datest reaches the renamed service) and `DA-IOKIT-OK`
- [ ] `grep -rn hwreg src/DiskArbitration overlay/System/Library/LaunchDaemons/com.apple.DiskArbitration.plist` returns only historical references that say hwregd is retired
- [ ] `launchctl list` on the booted image still shows `com.apple.DiskArbitration` (label unchanged, `run.sh:784` untouched)

## Refs

Plan section 4.3 ("Mach service" row) and section 8 (2026-05 plan corrections). Apple DiskArbitration-535.120.1.0.1: `diskarbitrationd/DAInternal.h:41`, `DAServer.c:3483`, `DiskArbitration/DASession.c:530,604`, `diskarbitrationd/com.apple.diskarbitrationd.plist`.

C2 · diskarbitrationd: build the DADisk model from GEOM (libgeom walk on kernel-registry arrival), log-only filed nextbsd-userland#192

Reponextbsd-userland
Labelsenhancement area:storage area:base
Depends onC1
ParentE15
## Summary

Give the daemon a disk model. On every storage arrival the `/dev/ioregistry` watch already delivers (`da_iokit_subscribe.c:145-160`, `da_handle_storage`), walk the GEOM graph with libgeom and create one `DADisk` per DISK-class provider (whole disk) and per PART-class provider (partition), each carrying Apple's `kDADiskDescription*` dictionary. Log only: no probe, no mount. Apple's model is `diskarbitrationd/DADisk.c:261-983` (`DADiskCreateFromIOMedia`), which reads one `IOMedia` object plus its `IOBlockStorageDevice` parent and its device-tree bus node; this ticket is that function with GEOM and CAM as the sources.

## Why

Every later stage keys on this dictionary: C4 matches fs bundles on `Content Hint` / `Leaf` / `Whole` (`DAProbe.c:100-106`), C5 chooses trust and defer from `MediaRemovable` / `DeviceInternal` (`DAMount.c:940-972`) and `MediaWritable` (`:1527-1531`), and the D1 framework serialises exactly this dictionary to clients. Getting the key set and its sources right now means C3-C5 and D1 add behaviour without reshaping data.

## Design

**Trigger.** `da_handle_storage(verb="arrival", name, class)` (`da_iokit_subscribe.c:146`) -> `geom_gettree()` (`lib/libgeom/libgeom.h:138`) -> for each `DISK` geom whose provider name matches the arriving unit (`da0`, `ada0`, `nda0`/`nvd0`, `cd0`, `mmcsd0`, `vtbd0`; the registry reports the newbus driver `umass`/`ahcich`/`nvme`/`mmc`/`vtblk` per `da_iokit_subscribe.c:99-119`, so map driver -> CAM/disk units by walking `kern.disks` or re-walking the whole tree and diffing against the disk list) -> create the whole-disk `DADisk`, then one per `PART` provider hanging off it (recursively: GPT inside MBR slice, BSD label inside slice). Departures remove the disks whose provider vanished from the tree. Re-walk the tree on `GEOM::attachment`-less classes (`MD`) is out of scope.

**Identity.** `DADiskGetID` is the device path, `/dev/<provider>` (Apple: `_PATH_DEV` + `kIOBSDNameKey`, `DADisk.c:296-304`). `LABEL`-class providers (`/dev/gpt/EFI`, `/dev/ufs/NEXTBSD`) are aliases of a PART provider: record them as `_deviceLink` (Apple's `dev-name` link, `DADisk.c:938-960`), not as separate disks.

**Key table.** Sources are the libgeom `lg_config` XML keys that `g_part` and `geom_disk` emit, plus CAM.

| Key (Apple) | Type | NextBSD source |
|---|---|---|
| `DAMediaBSDName` | string | provider `lg_name` (`da0`, `da0p1`) |
| `DAMediaBSDUnit` | int | unit digits of the DISK provider; partitions inherit |
| `DAMediaBSDMajor`/`Minor` | int | `stat(/dev/<name>)` `st_rdev` (Apple: `kIOBSDMajorKey`, `DADisk.c:340-350`) |
| `DAMediaSize` | int64 | provider `lg_mediasize` (`DIOCGMEDIASIZE`, `disk.h:35`) |
| `DAMediaBlockSize` | int | provider `lg_sectorsize` (`DIOCGSECTORSIZE`, `disk.h:28`) |
| `DAMediaWhole` | bool | geom class == `DISK` (Apple: `kIOMediaWholeKey`, `:531-534`) |
| `DAMediaLeaf` | bool | provider has no consumer geoms of class `PART` below it (Apple: `kIOMediaLeafKey`, `:460-463`); a whole disk with a partition table is not a leaf |
| `DAMediaContent` | string | PART: `rawtype` (`g_part_gpt.c:786-788`, `g_part_mbr.c:335-336`) mapped as below; whole disk with a table: the scheme (`GUID_partition_scheme` / `FDisk_partition_scheme`, Apple's spelling); no table: empty string |
| `DAMediaUUID` | CFUUID | GPT `rawuuid` (`g_part_gpt.c:789-791`); absent for MBR/BSD and whole disks |
| `DAMediaName` | string | GPT `label` (`g_part_gpt.c:772-775`) if set, else the gpart alias `type` (`freebsd-ufs`, `ms-basic-data`; names at `g_part.c:66-`), else `GEOM::descr` for whole disks |
| `DAMediaPath` | string | `IORegistryEntryGetPath` of the newbus node the arrival came from + `/` + provider name; `DIOCGPHYSPATH` (`disk.h:105`) when set |
| `DAMediaKind` | string | `IOMedia`; `IOCDMedia` for `cd*` (Apple: `:400-453`) |
| `DAMediaRemovable` | bool | `da`: `kern.cam.da.<unit>.flags` contains `PACK_REMOVABLE` (`sys/cam/scsi/scsi_da.c:107`, string table `:132`, sysctl `:2401`); `cd*`, `mmcsd*`: true; `ada`/`nda`/`nvd`/`vtbd`: false |
| `DAMediaEjectable` | bool | same as removable (Apple: `kIOMediaEjectableKey`) |
| `DAMediaWritable` | bool | `open(O_RDWR)` on the provider succeeds; `cd*`: false |
| `DADeviceVendor`/`Model`/`Revision` | string | `GEOM::descr` via `DIOCGATTR` (how `diskinfo -v` prints "Disk descr", `usr.sbin/diskinfo/diskinfo.c:255-258`); CAM builds it as vendor + product; split on the CAM inquiry fields when available, else whole string in Model |
| `DADeviceInternal` | bool | bus driver: `umass` -> false; `ahcich`, `nvme`, `mmc`, `vtblk` -> true (Apple: `Physical Interconnect Location` Internal/External, `:591-606`) |
| `DADeviceProtocol` | string | `umass` -> `USB`; `ahcich` -> `SATA`; `nvme` -> `PCI-Express`; `mmc` -> `Secure Digital`; `vtblk` -> `Virtual Interface` (Apple's `kIOPropertyPhysicalInterconnectType*` strings) |
| `DADevicePath` | string | registry path of the newbus parent |
| `DADeviceUnit` | int | unit from `GEOM::attachment` (`geom_disk.c:531-532`; CAM sets it to `<sim><unit>`, `scsi_da.c:3033-3034`) |
| `DABusName`/`DABusPath` | string | newbus parent driver name and registry path (Apple: device-tree node, `:758-787`) |
| `DAAppearanceTime` | double | `CFAbsoluteTimeGetCurrent()` at creation (`:795-803`) |
| `DAVolumeNetwork` | bool | false (`:316`) |

Not produced: `DAMediaIcon`, `DAMediaType` (CD/DVD media type), `DADeviceGUID`, `DADeviceTDMLocked`. Volume keys (`DAVolumeKind/Name/UUID/Path/Mountable`) are C4 and C3.

**Content hint.** GPT: gpart prints `rawtype` in lower case (`sys/kern/kern_uuid.c:277`, `%08x-%04x-...`); Apple's `IOGUIDPartitionScheme.cpp:648` uses `uuid_unparse`, which on Darwin is upper case, and every fs-bundle key is upper case (`EBD0A0A2-B9E5-4433-87C0-68B6B72699C7`). Upper-case the string. MBR: `rawtype` is the decimal `dp_typ` (`g_part_mbr.c:335-336`); Apple names them through the `IOFDiskPartitionScheme` `Content Table` (IOStorageFamily `Info.plist`), falling back to `0x%02X` (`IOFDiskPartitionScheme.cpp:560-577`). Ship that table verbatim:

| `dp_typ` | Apple content hint |
|---|---|
| 1 | `DOS_FAT_12` |
| 4 | `DOS_FAT_16_S` |
| 6 | `DOS_FAT_16` |
| 7 | `Windows_NTFS` |
| 11 | `DOS_FAT_32` |
| 12 | `Windows_FAT_32` |
| 14 | `Windows_FAT_16` |
| 20 | `DOS_FAT_16_S_Hidden` |
| 22 | `DOS_FAT_16_Hidden` |
| 66 | `Windows_LDM` |
| 130 | `Linux_Swap` |
| 131 | `Linux` |
| 142 | `Linux_LVM` |
| 165 | `FreeBSD` |
| 166 | `OpenBSD` |
| 167 | `Apple_Rhapsody_UFS` |
| 168 | `Apple_UFS` |
| 169 | `NetBSD` |
| 171 | `Apple_Boot` |
| 174 | `Apple_Encrypted` |
| 175 | `Apple_HFS` |
| 253 | `Linux_RAID` |
| other | `0x%02X` |

BSD-label partitions (`da0s1a`, scheme `BSD`, `rawtype` = decimal `fs_type`) have no Apple equivalent; emit NextBSD-local `BSD_FFS` (7) and `BSD_Swap` (1), else `BSD_0x%02X`.

**Struct sketch** (C, CF-backed like Apple's `struct __DADisk`, `DADisk.c:50-78`):

```c
struct da_disk {
    TAILQ_ENTRY(da_disk)   link;          /* gDADiskList */
    char                  *id;            /* "/dev/da0p1"  (DADiskGetID) */
    CFMutableDictionaryRef description;   /* kDADiskDescription* keys above */
    uint32_t               state;         /* kDADiskStateStaged*, RequireRepair ... (Apple DADisk.h:43-66) */
    char                  *device_link[2];/* /dev/gpt/EFI, /dev/ufs/NEXTBSD aliases */
    struct da_disk        *whole;         /* the DISK-class disk this partition sits on; NULL if whole */
    char                  *geom_class;    /* "DISK" | "PART" */
    char                  *ioreg_path;    /* newbus node the arrival came from */
    uid_t                  user_uid;      /* owner for mounts (C5); 0 until a console user exists */
    gid_t                  user_gid;
};
```

**Log line and marker.** One line per disk: `DA-DISK: <id> whole=<0|1> leaf=<0|1> content=<hint> size=<bytes> removable=<0|1> internal=<0|1> bus=<driver> name="<DAMediaName>"`. New one-shot CI marker `DA-GEOM-OK: <n> disks from libgeom` after the first walk that yields at least one DISK-class disk (the qemu `vtbd0` root disk); `DA-GEOM-FAIL` if `geom_gettree` fails. Wire it into `run.sh`'s `da_iokit_gate` style (one definite marker, never blocks) and both `boot-test.sh` copies as a non-fatal `expect` block, like `DA-IOKIT` (`run.sh:1602-1660`).

**Reference code.** The installer already enumerates the same things in shell: `src/nextbsd-installer/engine/probe-disks.sh:46-60` (`kern.disks`, `diskinfo`, `gpart show -l`, `glabel status` at `:28`). Do it through libgeom's tree instead of parsing tool output.

## Scope

New `src/DiskArbitration/DADisk.[ch]` and `DAGeom.[ch]` (~600 lines), `-lgeom -lsbuf -lbsdxml` in the Makefile (`LIBADD+= geom`), marker plumbing in `run.sh` and the two `boot-test.sh` files.

## Acceptance

- [ ] on the CI image the log shows a `DA-DISK:` line for `vtbd0` (whole, not leaf, content `GUID_partition_scheme`) and one per partition, each with an upper-case GPT `rawtype` as content and a `DAMediaUUID`
- [ ] the ISO image logs `cd0` as whole+leaf, `IOCDMedia`, removable, not writable
- [ ] unplugging a USB stick removes its disks; re-plugging recreates them with a new `DAAppearanceTime`
- [ ] a `/dev/ufs/NEXTBSD` label appears as `_deviceLink` of the right partition, not as a disk
- [ ] `DA-GEOM-OK` is asserted by `boot-test.sh` in both repos

## Refs

Plan section 4.3 ("IOMedia -> GEOM mapping") and section 7 C2. Apple: `diskarbitrationd/DADisk.c:261-983`, `:50-78`, `DADisk.h:43-66`; IOStorageFamily `IOFDiskPartitionScheme.cpp:560-577`, `IOGUIDPartitionScheme.cpp:648`, `Info.plist` (`IOFDiskPartitionScheme` `Content Table`). FreeBSD releng/15.0: `lib/libgeom/libgeom.h:56,138`, `sys/geom/part/g_part_gpt.c:772-791`, `g_part_mbr.c:335-336`, `g_part.c:66`, `sys/kern/kern_uuid.c:277`, `sys/geom/geom_disk.c:531-532`, `sys/sys/disk.h:28,35,66,87,105`, `sys/cam/scsi/scsi_da.c:107,132,2401,3033-3034`, `usr.sbin/diskinfo/diskinfo.c:255-266`. NextBSD: `src/DiskArbitration/da_iokit_subscribe.c:99-160`, `src/nextbsd-installer/engine/probe-disks.sh:28,46-60`, `overlay/usr/tests/freebsd-launchd-mach/run.sh:1602-1660`.

C3 · diskarbitrationd: adopt mounts it did not make (EVFILT_FS + getmntinfo diff -> DADiskCreateFromVolumePath) filed nextbsd-userland#193

Reponextbsd-userland
Labelsenhancement area:storage area:base
Depends onC2
ParentE15
## Summary

Apple's daemon learns about mounts it did not perform (NFS, SMB, disk images, anything mounted by hand) and turns each into a `DADisk` whose ID is the mount point, so clients see every volume, not only the ones it mounted. Port that: watch the VFS with `kqueue(2)` `EVFILT_FS`, on `VQ_MOUNT` walk `getmntinfo(3)` and create a disk for every mount that is not already known; on `VQ_UNMOUNT` refresh the known volumes and drop the gone ones. Also seed the list at startup from `statfs("/")`.

## Why

Gershwin's Workspace polls `getmntinfo` every 1.5 s today (plan section 6.2) and nothing posts `NSWorkspaceDidMountNotification`. D3's bridge needs a single source of truth for "what is mounted", including union, tmpfs and network mounts that DiskArbitration will never initiate (plan section 4.2: "DiskArbitration never initiates network or pseudo-filesystem mounts"). This is also how the daemon notices `mount -vat nonfs` mounts from an admin fstab and its own C5 mounts completing.

## Design

Apple, macOS build: `notify_register_mach_port(kNotifyVFSMount/Unmount/Update)` (`DAMain.c:613-657`). Apple, embedded build: `dispatch_source_create(DISPATCH_SOURCE_TYPE_VFS, 0, DISPATCH_VFS_MOUNT|UNMOUNT|UPDATE, ...)` and `if (event_flags & VQ_MOUNT) _DAVolumeMountedCallback()` (`DAMain.c:659-676`). `DISPATCH_SOURCE_TYPE_VFS` is `EVFILT_FS` underneath, so the embedded branch is the one to port; NextBSD's libdispatch may or may not expose the VFS source type, so use a raw `kqueue` on the daemon's dispatch queue via `DISPATCH_SOURCE_TYPE_READ` on the kq fd, or a dedicated thread.

`_DAVolumeMountedCallback` (`DAServer.c:3362-3405`): `getmntinfo(&list, MNT_NOWAIT)`; for each entry look the disk up by `_DAVolumeGetID(fs)` (`DAInternal.c:591-614`: `f_mntfromname` when it starts with `/dev/`, else `"<f_mntonname>?owner=<f_owner>"`); if known and it has no `DAVolumePath` yet, refresh it (that is how a C5 mount completes); if unknown, skip `MNT_UNION` and `devfs` and call `_DADiskCreateFromFSStat` (`:3328-3357`) -> `DADiskCreateFromVolumePath(allocator, fs)` (`DADisk.c:985-1139`), which sets `DAVolumePath`, `DAVolumeMountable=true`, `DAVolumeKind=f_fstypename`, `DAVolumeName` (from the fs probe; until C4 use the last path component), `DAMediaBSDMajor/Minor` from `f_fsid`, `DAVolumeNetwork` from `!(f_flags & MNT_LOCAL)`, marks all stages done (`Probe|Peek|Mount`), and takes `_userUID` from `f_owner`. `_DAVolumeUnmountedCallback` (`:3407-3420`) refreshes every disk that has a volume path; the refresh drops the path when the mount is gone. Startup seed: `DAMain.c:138-146`.

NextBSD specifics:
- FreeBSD only signals two events: `vfs_event_signal(NULL, VQ_MOUNT, 0)` at the end of `vfs_domount_first` (`sys/kern/vfs_mount.c:1286`) and `VQ_UNMOUNT` in `dounmount` (`:2408`). `mount -u` (`vfs_domount_update`) signals nothing, so there is no `VQ_UPDATE`; a remount rw->ro is only seen at the next event or by a periodic refresh.
- `EVFILT_FS` knotes are `EV_CLEAR` and accumulate `fflags` (`sys/kern/vfs_subr.c:6535-6552`); `ident` is ignored; register once with `fflags = VQ_MOUNT|VQ_UNMOUNT` (`sys/sys/mount.h:734-735`).
- Skip the same pseudo mounts Apple skips plus NextBSD's: `devfs`, `MNT_UNION` (the live-media `/cow`+`/rofs` union), `fdescfs`, `linprocfs`, `linsysfs`, `procfs`, `tmpfs` on `/tmp`? No: Apple only skips devfs and union mounts; keep parity and let D1 clients filter, but log them at debug level only.
- Fallback if VQ_MOUNT does not arrive (see Before starting): FreeBSD also emits `devctl_notify("VFS", "FS", "MOUNT"/"UNMOUNT"/"REMOUNT", ...)` (`vfs_mount.c:2969-3009`; call sites `:1290`, `:2404`). With devd gone (`nextbsd-freebsd-compat scripts/superseded:17`), `/dev/devctl` has no reader, but it is single-consumer, and E14's K7/U1 plan a devctl reader for swap events; if DA needs it, the two must share one reader. Prefer `EVFILT_FS`.

## Before starting

- [ ] Verify `VQ_MOUNT` delivery on a NextBSD kernel. The claim "GhostBSD reported EVFILT_FS not working" is not what gershwin-desktop/issues#22 says: that code uses `EVFILT_VNODE` on the mount directory plus polling, and leaves "monitor mount/unmount events with pure kqueue" as a TODO. Nobody has tested `EVFILT_FS` on NextBSD. Build and run this on the CI image (or under `run.sh` as a `DA-VFSWATCH` probe):

```c
/* vfswatch.c -- cc -o vfswatch vfswatch.c; ./vfswatch & mount -t tmpfs t /mnt; umount /mnt */
#include <sys/types.h>
#include <sys/event.h>
#include <sys/mount.h>
#include <err.h>
#include <stdio.h>

int main(void)
{
	struct kevent kev, ev;
	int kq = kqueue();
	if (kq < 0) err(1, "kqueue");
	EV_SET(&kev, 0, EVFILT_FS, EV_ADD | EV_CLEAR, VQ_MOUNT | VQ_UNMOUNT, 0, NULL);
	if (kevent(kq, &kev, 1, NULL, 0, NULL) < 0) err(1, "kevent EV_ADD EVFILT_FS");
	printf("VFSWATCH-ARMED\n"); fflush(stdout);
	for (;;) {
		if (kevent(kq, NULL, 0, &ev, 1, NULL) < 0) err(1, "kevent wait");
		printf("VFSWATCH: fflags=0x%x%s%s\n", (unsigned)ev.fflags,
		    (ev.fflags & VQ_MOUNT) ? " VQ_MOUNT" : "",
		    (ev.fflags & VQ_UNMOUNT) ? " VQ_UNMOUNT" : "");
		fflush(stdout);
	}
}
```
  Expected: one `VQ_MOUNT` line after the mount and one `VQ_UNMOUNT` after the umount. If `EV_ADD` fails with `EINVAL`, check that the userland `<sys/event.h>` the daemon compiles against (freebsd-shims vs sysroot) has the same `EVFILT_FS` number as the kernel; nextbsd-kernel patch `0003-kqueue-reserve-evfilt-machport.patch` touches the filter table (`sys/kern/kern_event.c:355-375`).
- [ ] Decide whether NextBSD's libdispatch exposes `DISPATCH_SOURCE_TYPE_VFS`; if not, the kq-on-dispatch shape above.

## Acceptance

- [ ] `mount -t tmpfs t /mnt` from a shell produces a new disk with id `/mnt?owner=0`, `DAVolumeKind=tmpfs`, `DAVolumeNetwork=false`, logged within 1 s without polling; `umount /mnt` removes it
- [ ] an NFS mount produces a disk with `DAVolumeNetwork=true`
- [ ] the live ISO's union root and `devfs` mounts are not adopted
- [ ] after C5 mounts a stick, the same disk (id `/dev/da0p1`) gains `DAVolumePath` rather than a second disk appearing
- [ ] the `vfswatch` result (works / does not) is recorded in this issue before the code lands

## Refs

Plan section 4.2 ("Mounts it didn't make") and section 9 (EVFILT_FS open question). Apple: `diskarbitrationd/DAServer.c:3328-3420`, `DADisk.c:985-1139`, `DAInternal.c:563-614`, `DAMain.c:138-146,613-676`. FreeBSD releng/15.0: `sys/sys/mount.h:734-735`, `sys/kern/vfs_subr.c:6507-6552`, `sys/kern/vfs_mount.c:1286,1290,2404,2408,2969-3009`, `sys/kern/kern_event.c:355-375`. gershwin-desktop/issues#22.

C4 · Filesystem bundles: /System/Library/Filesystems/{msdos,exfat,ntfs,ufs,cd9660,udf}.fs with an fstyp-backed *.util shim (FSUR_* contract) filed nextbsd-userland#194

Reponextbsd-userland
Labelsenhancement area:storage area:base
Depends onC2
ParentE15
## Summary

Ship Apple's filesystem-bundle layout so the daemon's probe stage can be ported unchanged: one `<name>.fs/Contents/Info.plist` per filesystem under `/System/Library/Filesystems` (`FS_DIR_LOCATION`, `sys/loadable_fs.h:59`), each naming a `Contents/Resources/<name>.util` probe helper that follows the `FSUC_*` command / `FSUR_*` exit-code contract (`loadable_fs.h:66-98`). On FreeBSD the helper is one shared shim around `fstyp(8)` plus a small on-disk "is clean" check. Also add the probe stage itself (`DAProbe.c`, `DAFileSystem.c` probe path) so C2's disks gain `DAVolumeKind`, `DAVolumeName`, `DAVolumeUUID`, `DAVolumeMountable`; still no mount.

## Why

Apple's daemon does not know any filesystem. `DAFileSystemListRefresh` walks `*.fs` directories (`DASupport.m:400-470`, also `/Library/Filesystems`, `DABase.h:39`), builds a probe list from every `FSMediaTypes` entry sorted by `FSProbeOrder` (`:388-397`), and `DAProbe` runs the candidates whose `FSMediaProperties` match the disk's `Content Hint` / `Leaf` / `Whole` (`DAProbe.c:94-106`). Keeping that data-driven shape means adding a filesystem is a plist plus a helper, third parties can drop bundles into `/Library/Filesystems`, and Disk Utility-style tools read the same `FSPersonalities` (format name, `FSFormatExecutable`) Apple's do.

## Design

**Directory names** stay Apple's (`msdos.fs`, `exfat.fs`, `ntfs.fs`, `ufs.fs`, `cd9660.fs`, `udf.fs`) so anything written against macOS finds them. `CFBundleName` is the *FreeBSD vfs type*, because it is what the daemon passes to `mount -t` (`DAFileSystem.c:573-576` `DAFileSystemGetKind` = `CFBundleName`; `:745-778` `mount -t <kind> ...`) and what a fstab `vfstype` filter compares against (`DAMount.c:1325-1337`): `msdosfs`, `exfat`, `ntfs`, `ufs`, `cd9660`, `udf`.

**Probe protocol** (what C5's executor and this ticket's probe stage run, `DAFileSystem.c:917-928`, `:292-302`, `:359-387`, `DACommand.c:329`):
- `<name>.util -p <devname> removable readonly` -> stdout = volume name (trimmed, `:257-278`), exit `FSUR_RECOGNIZED` (-1) or `FSUR_UNRECOGNIZED` (-2). Apple passes the literal words `removable readonly` regardless of the media (`:925-926`); the shim ignores them. `DACommandExecute` reads the status as `(char)WEXITSTATUS` (`DACommand.c:329`), so `exit(-1)` (status 255) is read back as -1.
- `<name>.util -k <devname>` -> stdout = UUID string, exit `FSUR_IO_SUCCESS` (-3) (`:322-343`; Apple documents that the value is "a unique 64-bit number" it folds into a CFUUID via `___CFUUIDCreateFromString`).
- `<repair executable> -q <devpath>` -> exit 0 = clean, else dirty (`:359-387`; `DAStage.c:755-777` sets `kDADiskStateRequireRepair` when non-zero, and forces clean when the media is not writable).
- `-y` (repair) exists in the contract (`:1091-1108`) but C5 never runs it automatically.

**The shim** (`src/DiskArbitration/fsutil/fsutil.c`, installed as each bundle's `<name>.util` via symlink, dispatching on `argv[0]`):
- `-p`: run `fstyp -l /dev/<devname>` (`usr.sbin/fstyp/fstyp.c:168-171`, `:261-282`; `-l` appends the label, exit 1 when unrecognised); recognised iff `fstyp`'s first word equals the bundle's vfs type (`fstyp` names: `ufs`, `cd9660`, `hfs+`, `ext2fs`, `befs`, `apfs`, `exfat`, `hammer`, `hammer2`, `msdosfs`, `ntfs`, `zfs`, `fstyp.c:76-108`; `udf` is not in that table, so `udf.util` reads the UDF anchor at sector 256 itself, or returns unrecognised until `fstyp` grows it). Print the label; empty label -> print nothing and let the daemon use "Untitled" (`DAMount.c:754-757`).
- `-k`: print a UUID. v1: the GPT partition `rawuuid` from C2's model when the device is a GPT partition (passed in via `DA_PARTITION_UUID` env or a `-U` extra arg), else a name-based UUID v5 over `<disk ident>:<partition index>:<fstyp label>`. Real volume serials (FAT BPB volume id, UFS `fs_id`, NTFS/exFAT serial) are a follow-up; note it in the shim.
- `-q`: per type, read-only: `ufs` -> superblock `fs_clean` via libufs `sbread` (`fsck_ufs -p` would *repair* a dirty volume, and `-n` is a full scan); `msdosfs` -> FAT[1] clean-shutdown bit (FAT32 bit 27, FAT16 bit 15) from the BPB; `ntfs` -> `$Volume` dirty flag (or always "dirty" and let ntfs-3g decide); `exfat` -> `VolumeFlags` bit 1; `cd9660`/`udf` -> always clean. Exit 0 clean, 1 dirty, `FSUR_IO_FAIL` on I/O error.
- `-y`: exec the real checker (`fsck_msdosfs -y`, `fsck_ufs -y`, `fsck.exfat -y`) so a later explicit-repair request (D2) works; never invoked by the automatic path.

**`FSMediaTypes` per bundle** (`Content Hint` strings exactly as C2 emits them; `Partitionless` = `Leaf` + `Whole`, the whole-disk-no-table case, Apple's own key name):

| Bundle | `Content Hint` keys (FSProbeOrder) |
|---|---|
| `msdos.fs` | `EBD0A0A2-B9E5-4433-87C0-68B6B72699C7` (2000), `C12A7328-F81F-11D2-BA4B-00A0C93EC93B` (1000, `autodiskmount=false`: EFI system partitions are recognised but never auto-mounted, `DAProbe.c:113-117`), `DOS_FAT_12`, `DOS_FAT_16_S`, `DOS_FAT_16`, `DOS_FAT_32`, `Windows_FAT_32`, `Windows_FAT_16` (1000), `Partitionless` (4000) |
| `exfat.fs` | `EBD0A0A2-...` (2000), `Windows_NTFS` (2000), `Partitionless` (4000) |
| `ntfs.fs` | `EBD0A0A2-...` (2000), `Windows_NTFS` (1000), `Partitionless` (4000) |
| `ufs.fs` | `516E7CB6-6ECF-11D6-8FF8-00022D09712B` (freebsd-ufs, 1000), `BSD_FFS` (1000, NextBSD-local from C2), `Partitionless` (4000) |
| `cd9660.fs` | `Partitionless` (3000) -- `cd0` is a whole, leaf provider; Apple's `CD_ROM_Mode_1`/`CD_ROM_Mode_2_Form_1` hints come from a CD partition scheme FreeBSD does not have |
| `udf.fs` | `Partitionless` (1000) -- probes before cd9660 so UDF bridge discs mount as UDF, as on macOS |

These are the host macOS bundles' keys minus the Apple-only ones, checked against `/System/Library/Filesystems/{msdos,exfat,ntfs,udf,cd9660}.fs/Contents/Info.plist` on macOS 27.

**Complete example, `msdos.fs/Contents/Info.plist`** (Apple's, with FreeBSD helper names; the `FSMountExecutable` is consumed by C5 as `mount -o mountprog=`):

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>	<string>English</string>
	<key>CFBundleIdentifier</key>		<string>org.nextbsd.filesystems.msdosfs</string>
	<key>CFBundleInfoDictionaryVersion</key>	<string>6.0</string>
	<key>CFBundleName</key>			<string>msdosfs</string>
	<key>CFBundlePackageType</key>		<string>fs  </string>
	<key>CFBundleShortVersionString</key>	<string>1.0</string>
	<key>CFBundleVersion</key>		<string>1.0</string>
	<key>FSImplementation</key>		<array><string>kernel</string></array>
	<key>FSMediaTypes</key>
	<dict>
		<key>EBD0A0A2-B9E5-4433-87C0-68B6B72699C7</key>
		<dict>
			<key>FSMediaProperties</key>
			<dict>
				<key>Content Hint</key>	<string>EBD0A0A2-B9E5-4433-87C0-68B6B72699C7</string>
				<key>Leaf</key>		<true/>
			</dict>
			<key>FSProbeArguments</key>	<string>-p</string>
			<key>FSProbeExecutable</key>	<string>msdos.util</string>
			<key>FSProbeOrder</key>		<integer>2000</integer>
		</dict>
		<key>C12A7328-F81F-11D2-BA4B-00A0C93EC93B</key>
		<dict>
			<key>FSMediaProperties</key>
			<dict>
				<key>Content Hint</key>	<string>C12A7328-F81F-11D2-BA4B-00A0C93EC93B</string>
				<key>Leaf</key>		<true/>
			</dict>
			<key>FSProbeArguments</key>	<string>-p</string>
			<key>FSProbeExecutable</key>	<string>msdos.util</string>
			<key>FSProbeOrder</key>		<integer>1000</integer>
			<key>autodiskmount</key>	<false/>
		</dict>
		<key>DOS_FAT_12</key>
		<dict>
			<key>FSMediaProperties</key>
			<dict>
				<key>Content Hint</key>	<string>DOS_FAT_12</string>
				<key>Leaf</key>		<true/>
			</dict>
			<key>FSProbeArguments</key>	<string>-p</string>
			<key>FSProbeExecutable</key>	<string>msdos.util</string>
			<key>FSProbeOrder</key>		<integer>1000</integer>
		</dict>
		<!-- DOS_FAT_16_S, DOS_FAT_16, DOS_FAT_32, Windows_FAT_32, Windows_FAT_16: identical to DOS_FAT_12 with the key/hint changed -->
		<key>Partitionless</key>
		<dict>
			<key>FSMediaProperties</key>
			<dict>
				<key>Leaf</key>		<true/>
				<key>Whole</key>	<true/>
			</dict>
			<key>FSProbeArguments</key>	<string>-p</string>
			<key>FSProbeExecutable</key>	<string>msdos.util</string>
			<key>FSProbeOrder</key>		<integer>4000</integer>
		</dict>
	</dict>
	<key>FSPersonalities</key>
	<dict>
		<key>MS-DOS</key>
		<dict>
			<key>FSName</key>			<string>MS-DOS (FAT)</string>
			<key>FSMountExecutable</key>		<string>/sbin/mount_msdosfs</string>
			<key>FSMountArguments</key>		<string></string>
			<key>FSRepairExecutable</key>		<string>msdos.util</string>
			<key>FSRepairArguments</key>		<string>-y</string>
			<key>FSVerificationExecutable</key>	<string>/sbin/fsck_msdosfs</string>
			<key>FSVerificationArguments</key>	<string>-n</string>
			<key>FSFormatExecutable</key>		<string>/sbin/newfs_msdos</string>
			<key>FSFormatArguments</key>		<string>-F 32</string>
			<key>FSFormatContentMask</key>		<string>DOS_FAT_32</string>
			<key>FSFormatMinimumSize</key>		<integer>32768</integer>
			<key>FSSubType</key>			<integer>2</integer>
		</dict>
	</dict>
</dict>
</plist>
```
`FSRepairExecutable` points at the shim (so `-q` is the shim's clean check) and the shim's `-y` execs `/sbin/fsck_msdosfs -y`; Apple's daemon resolves relative names inside `Contents/Resources` (`DAFileSystem.c:856-866`), absolute paths are a NextBSD extension the executor must accept.

**Helpers, base vs ports.** In the base image: `mount_msdosfs`, `fsck_msdosfs`, `newfs_msdos`, `mount` (ufs), `fsck_ufs`, `newfs`, `mount_cd9660`, `mount_udf`, `fstyp` (none are in `nextbsd-freebsd-compat scripts/superseded`). Ports only, **not in the base image**: `sysutils/fusefs-exfat` (`/usr/local/sbin/mount.exfat-fuse`), `sysutils/exfat-utils` (`fsck.exfat`), `sysutils/fusefs-ntfs` (`/usr/local/bin/ntfs-3g`; FreeBSD base has had no `mount_ntfs` since 10, vermaden's fallback at `automount:616-618` is dead). Ship `exfat.fs` and `ntfs.fs` anyway with those `FSMountExecutable` paths: `fstyp` recognises both without the ports, so the disk is "mountable" in the model and the mount fails with a clear log line until the package is installed (D4's agent surfaces it). Kernel side: `MSDOSFS` and `CD9660` are in GENERIC (`sys/amd64/conf/GENERIC:54-55`); `UDF`, `EXT2FS` and all `*_ICONV` are modules upstream and absent under `NO_MODULES` -- see C7. `udf.fs` ships but cannot mount until C7 lands.

**Probe stage.** Port `DAProbe.c` and the probe half of `DAFileSystem.c` / `DASupport.m` (list refresh gated on directory mtime, `DASupport.m:1417-1462`; sort; match; run `-p`, `-k`, `-q`; set `DAVolumeKind/Name/UUID/Mountable`, `RequireRepair`). Log `DA-PROBE: <id> kind=<vfs> name="<label>" uuid=<uuid> clean=<0|1>` and a one-shot `DA-PROBE-OK` marker when the qemu root partition probes as `ufs`.

## Scope

Six plists (~350 lines), one shim (~500 lines, libufs for `-q`), probe-stage port (~800 lines from Apple), install rules in `build-userland.sh` next to the `diskarbitrationd` block (`:805-815`), CI marker.

## Acceptance

- [ ] `ls /System/Library/Filesystems` shows the six bundles, each with `Contents/Info.plist` and `Contents/Resources/<name>.util`
- [ ] `/System/Library/Filesystems/msdos.fs/Contents/Resources/msdos.util -p da0p1 removable readonly` on a FAT stick prints its label and exits 255; on a UFS partition exits 254
- [ ] `ufs.util -q /dev/vtbd0p3` exits 0 on the clean CI root; after `dd`-ing a superblock with `fs_clean=0` it exits 1 without modifying the device
- [ ] the daemon log shows `DA-PROBE:` for the CI root (`kind=ufs`) and `DA-PROBE-OK` gates in `boot-test.sh`
- [ ] the EFI partition probes as `msdosfs` with `autodiskmount=false` recorded on the disk
- [ ] a plist added under `/Library/Filesystems/foo.fs` after boot is picked up at the next arrival without restarting the daemon

## Refs

Plan section 4.2 (Probe) and section 7 C4. Apple: `sys/loadable_fs.h:59-108`; `diskarbitrationd/DAFileSystem.c:113-128,244-420,501-600,799-950,1007-1128`; `DACommand.c:329`; `DASupport.m:388-470,1417-1462`; `DABase.h:39`; `DAProbe.c:94-117`; `DAStage.c:717-810`; macOS 27 `/System/Library/Filesystems/*.fs/Contents/Info.plist`; DiskArbitration-535 `msdos`/`ntfs`/`hfs` bundle plists. FreeBSD releng/15.0: `usr.sbin/fstyp/fstyp.c:76-108,168-171,261-282`, `sys/amd64/conf/GENERIC:54-55`. vermaden `automount:253-295` (probe order with file(1) then fstyp), `:608-620`. NextBSD: `build-userland.sh:805-815`.

C5 · diskarbitrationd: stage machine and mount executor -- /Volumes/<name>[ N], untrusted-media defaults, no auto-repair, per-FS helper table, /Volumes owned by the OS filed nextbsd-userland#195

Reponextbsd-userland
Labelsenhancement area:storage area:base
Depends onC2, C4
ParentE15
## Summary

Mount things. Port Apple's stage machine (`DAStage.c:139-559`: Probe -> Peek -> Mount -> Appeared, driven by `DAStageSignal`) and the mount path (`DAMount.c` + `DAFileSystem.c` mount half + `DACommand.c` executor), with FreeBSD's `mount(8)` and helpers behind it. Volumes go to `/Volumes/<name>`, `<name> 1` ... `<name> 99`; untrusted media gets Apple's defaults mapped onto FreeBSD's option set; dirty volumes are never repaired automatically, they mount read-only; the daemon creates `/Volumes` at startup and sweeps stale entries out of it. That last point makes the OS, not the desktop, the owner of `/Volumes`, which answers gershwin-internal#22.

The fstab `UUID=`/`LABEL=` overlay is split out as C6; this ticket mounts with built-in policy only.

## Why

Plan section 6: vermaden's `automount` needs devd (removed: `nextbsd-freebsd-compat scripts/superseded:17`), sets no `nosuid`, runs `fsck -y` on every plug (`automount:655-669`), uses one static owner (`:216-220`, `USER=root`), and its detach step runs `find $MNT_PREFIX -depth 1 -empty -prune -delete` (`:343-349`), which under `/Volumes` would remove Gershwin's disk-image and network mount points. dsbmd needs devd too. Gershwin already polls `getmntinfo` for `/Volumes` (plan 6.2), so anything mounted here appears with zero desktop changes; gershwin-desktop#60 asks for exactly `/Volumes` on the live ISO.

## Design

**Stage machine.** Port `DAStage.c` as is: `__DAStageDispatch` walks `gDADiskList`, and for each disk without `CommandActive` runs the first stage not yet done (`:200-230`); the first pass after idle refreshes the fs-bundle list (`:213-220`; C6 adds the fstab map refresh at the same point). `__DAStageMount` (`:561-575`) calls `DADiskMountWithArguments(disk, NULL, kDADiskMountOptionDefault, NULL, "automatic")` unless the `DisableAutoMount` preference is set. Peek callbacks are a D1 concern; stage Peek is a no-op until sessions exist.

**Mount decision** (`DAMountWithArguments`, `DAMount.c:1150-1717`, minus the APFS/TDM/FSKit blocks):
1. `automatic` and the fs-bundle said `autodiskmount=false` (EFI system partitions, `DAProbe.c:113-117`) -> `ECANCELED`, no mount.
2. Defer: Apple does not auto-mount removable/external media until a console user exists (`:1498-1521`; preference defaults `:905-935`: internal not deferred, external deferred). NextBSD has no console-user source yet (D4). Until D4, treat "no console user" as `uid 0`: mount removable media at arrival, owned by root, and let D4 add the deferral and re-ownership. Log `DA-MOUNT-DEFER-NOOP` once so the gap is visible.
3. Media not writable -> prepend `rdonly` (`:1527-1531`); FreeBSD spelling `ro`.
4. Trust (`:940-972`): removable -> untrusted; not removable and internal -> trusted; external -> untrusted. Untrusted gets Apple's `nosuid,noowners,nodev` (`:1533-1543`; strings at `DAFileSystem.c:114-121`). FreeBSD mapping:
   - `nosuid` -> `nosuid` (`sbin/mount/mount.8:267`).
   - `nodev` -> nothing. FreeBSD dropped `MNT_NODEV` years ago; device nodes only exist on devfs, so a device special file on a FAT/UFS stick is inert. Document, do not pass (`mount.8` has no `nodev`).
   - `noowners` (`MNT_IGNORE_OWNERSHIP`) -> no FreeBSD equivalent. For ownership-less filesystems (msdosfs, exfat, ntfs, cd9660, udf) the same effect comes from the helper's owner options: `-u <uid> -g <gid>` of the console user (C5: root; D4: the logged-in user) and `-m 755 -M 755` -- mirroring what Apple does for hfs (`-m=0755,-g=,-u=` at `DAMount.c:1546-1567`; note Apple applies that block only to `hfs`, not to msdos, because Darwin's msdos mount inherits ownership from the mounting uid). For UFS/ext media from another machine the foreign uids show through; accepted gap, same as FreeBSD proper.
   - `noatime` -> add for untrusted removable media (both vermaden `:205,530-532` and dsbmd `:61,70` default to it; flash write reduction). Apple does not; this is a NextBSD choice, say so in the log line.
   - `noexec` -> **open question 3 in the plan**: Apple does not set it; it would stop AppImages running straight off a stick. Ship without `noexec`, as a `DAPreference` key (`kDAPreferenceMountTrustRemovable`-style plist under `/Library/Preferences/SystemConfiguration/autodiskmount.plist`, Apple's file) so it can be flipped without code.
5. Clean check: Apple runs the repair executable `-y` when `RequireRepair` is set and the mount is automatic (`:1573-1598`, `:1682-1690`), and on repair failure retries with `force,rdonly` (`:74-84`). NextBSD: **no auto-repair**. When C4's `-q` said dirty, skip the repair stage entirely and go straight to the `force` + `ro` retry path: mount read-only, set `DAVolumeMountable=true` and mark the disk `RequireRepair` so D4's agent can offer "Repair" (which is when `-y` runs, on the user's say-so). Also skip `RepairQuotas`.
6. Mount point (`DAMountCreateMountPointWithAction`, `:725-898`): name = `DAVolumeName` or localised "Untitled" (`:754-757`); truncate to `MNAMELEN - 20` (`:760`); replace `/` with `:` (`:766-769`); try `/Volumes/<name>` then `/Volumes/<name> 1` ... `<name> 99` (`:782-790`); `mkdir(path, 0111)` then `chown(path, userUID, -1)` if the user is not root (`:840-847`); remember it in `gDAMountPointList` (`:198`) so unmount removes only directories the daemon made (`DAMountRemoveMountPoint`).
7. Execute `/sbin/mount -t <kind> -o <options> <device> <mountpoint>` (`DAFileSystem.c:655,745-778`). Drop Apple's `-k` (FreeBSD `mount.c:224` getopt has no `-k`). Run as root (Apple runs it as the disk's owner uid; on FreeBSD an unprivileged mount needs `vfs.usermount=1` (kernel patch 0008, which D5 wants dropped) plus device-node access, so root is the honest choice). For helpers outside `/sbin`, pass `-o mountprog=<FSMountExecutable>` (`mount.8:201`) so the command shape stays `mount -t`. Owner options for ownership-less filesystems are appended per the table below. `DACommandExecute` (`DACommand.c`) ports as is: fork/exec on a dispatch queue, capture stdout for `-p`/`-k`, `(char)WEXITSTATUS`.
8. On success C3's `VQ_MOUNT` path attaches `DAVolumePath`; `DADiskDescriptionChangedCallback(VolumePath)` is the "mounted" signal clients watch (plan 4.2), which D3 turns into `NSWorkspaceDidMountNotification`.

**Per-filesystem helper table** (transcribed from vermaden `automount:535-649` and dsbmd `dsbmd.conf.tmpl:54-103`, corrected for FreeBSD 15 and for the trust defaults above; `kind` is the bundle's `CFBundleName`):

| Kind | Helper (base/port) | Untrusted option string | Check (`-q`) | Source lines |
|---|---|---|---|---|
| `msdosfs` | `/sbin/mount_msdosfs` (base) | `ro?,nosuid,noatime,longnames,-u=<uid>,-g=<gid>,-m=755,-M=755` (`-L <locale> -D cp437` only after C7: `mount_msdosfs.c:294-311` kldloads `msdosfs_iconv`, impossible under `NO_MODULES`; `-o large` is gone on 12+, vermaden `:588-593`) | shim | vermaden `:587-597`, dsbmd `:56-58` |
| `exfat` | `/usr/local/sbin/mount.exfat-fuse` (port `sysutils/fusefs-exfat`) | `-o uid=<uid>,gid=<gid>,umask=022,nosuid,noatime` | shim (`fsck.exfat` only with `sysutils/exfat-utils`) | vermaden `:598-607`, dsbmd `:103` |
| `ntfs` | `/usr/local/bin/ntfs-3g` (port `sysutils/fusefs-ntfs`) | `-o uid=<uid>,gid=<gid>,nosuid,noatime` (no `recover`: dirty -> `ro`) | shim | vermaden `:608-620`, dsbmd `:100` |
| `ufs` | `mount -t ufs` (base) | `ro?,nosuid,noatime` | shim (superblock `fs_clean`) | vermaden `:541-546` (its `fsck_ufs -C -y` is exactly what we do not do), dsbmd `:61` |
| `cd9660` | `/sbin/mount_cd9660` (base) | `ro,nosuid,-e` (`-C UTF-8` needs `CD9660_ICONV`, C7) | none | vermaden `:535-540` (`-o -e,-C=UTF-8`) |
| `udf` | `/sbin/mount_udf` (base; kernel `UDF` needed, C7) | `ro,nosuid` | none | -- |
| `ext2fs`, `hfs+`, `xfs` | not in v1 (`EXT2FS` is a module upstream; `hfsfuse`/`lklfuse` are ports) | -- | -- | vermaden `:547-586` |

Retry: vermaden retries the mount 5 times, 2 s apart, then falls back to `-o ro` (`:680-713`). Apple does not retry; a failed mount is a failed mount (`__DAMountWithArgumentsCallbackStage2`), and the `ro` fallback is only for the dirty case. Keep Apple's behaviour.

**/Volumes at startup** (`__DAMainCreateMountPointFolder`, `DAMain.c:150-246`, called from `main` at `:685`; `exit(EX_SOFTWARE)` on failure): if `/Volumes` is missing, `mkdir 0755`; else `chmod 0755`, `chown -1:wheel` (`:170-186`), then for every entry: directories get their `.autodiskmounted` cookie unlinked (`:78,213-219`) and `rmdir` (`:225`; `rmdir` fails with `EBUSY`/`ENOTEMPTY` on a live mount or a populated directory, so nothing in use is touched), symlinks are unlinked (`:227-233`). This is the whole of "the OS owns `/Volumes`": the daemon guarantees the directory exists with the right mode, and it is the only thing that creates or removes entries under it. Consequences to record on gershwin-internal#22: `dscli init` should stop creating `/Volumes` (the issue's own conclusion), Gershwin's VolumeManager/NetworkVolumeManager mount points survive the sweep because they are non-empty or mounted, and vermaden-style `find -delete` must never run there.

## Scope

Ports of `DAStage.c` (966 lines), `DAMount.c` (1717, roughly half survives), `DACommand.c` (576), the mount half of `DAFileSystem.c`, `DAMain.c:150-246`; new `DAMountFreeBSD.c` for the option mapping and helper table (~300 lines). Preferences plist reader from `DASupport.m` (`gDAPreferenceList`). No client API, no unmount/eject (D2), no console user (D4).

## Before starting

- [ ] C4 landed (probe results feed the mount decision) and C2's `DAMediaRemovable`/`DeviceInternal` are populated for USB (`umass`) and virtio in CI.
- [ ] Decide `noatime` for untrusted media (recommended yes) and confirm `noexec` stays off by default (plan open question 3); both are one-line preference defaults.

## Acceptance

- [ ] plugging a FAT stick labelled `STICK` into a booted image mounts `/dev/da0p1` on `/Volumes/STICK` with `nosuid,noatime`, owned by the console user (root until D4), mode 0755 files; a second stick with the same label lands on `/Volumes/STICK 1`
- [ ] an unlabelled stick mounts on `/Volumes/Untitled`
- [ ] a stick with a dirty FAT (unplugged mid-write elsewhere) mounts read-only, nothing writes to it, the log says `RequireRepair`
- [ ] the EFI partition of the boot disk is never mounted; the UFS root partition is not re-mounted
- [ ] the ISO image mounts nothing at boot (its `cd0` is the root medium, already mounted by `/init`) and still creates `/Volumes` with mode 0755 root:wheel
- [ ] a stale empty `/Volumes/OLD` directory left from a previous boot is removed at startup; a non-empty directory or a live mount under `/Volumes` is left alone
- [ ] `mount | grep /Volumes` shows no `mount_msdosfs` invocation with `-L`/`-D` until C7 lands
- [ ] CI: new `DA-MOUNT-OK` marker from a virtio disk image attached with a FAT partition (extend `run.sh` to attach one), gated in both `boot-test.sh` copies

## Refs

Plan sections 4.2 (Mount, Mount point, Trust defaults, Defer, Dirty volumes), 6.1, 6.2, 7 C5, 9 (open question 3). Apple: `diskarbitrationd/DAStage.c:139-575`, `DAMount.c:66-236,725-898,905-972,1150-1717`, `DAFileSystem.c:113-128,621-798`, `DACommand.c`, `DAMain.c:78,150-246,685`, `DAProbe.c:113-117`. FreeBSD releng/15.0: `sbin/mount/mount.8:201,238,258,267`, `sbin/mount/mount.c:224`, `sbin/mount_msdosfs/mount_msdosfs.8:81-124`, `mount_msdosfs.c:81,294-311`. vermaden `automount:199-220,343-349,530-717`; dsbmd `dsbmd.conf.tmpl:35,45,54-103`. NextBSD: `nextbsd-freebsd-compat scripts/superseded:17` (`/sbin/devd`), nextbsd-kernel patch 0008. gershwin-internal#22, gershwin-desktop/gershwin-desktop#60.

C6 · diskarbitrationd: /etc/fstab UUID=/LABEL=/DEVICE= mount-map overlay, exactly Apple's semantics filed nextbsd-userland#196

Reponextbsd-userland
Labelsenhancement area:storage area:base
Depends onC5
ParentE15
## Summary

Port Apple's fstab overlay: `/etc/fstab` is consulted by the daemon for *policy on disks it has already discovered* (mount point, options, auto/noauto, type filter), keyed by `UUID=`, `LABEL=` or `DEVICE=`. It is never a list of things to mount. Under Stage A's no-fstab default the file does not exist and this is a no-op; when an admin creates one, this is how a stick gets a fixed mount point or `noauto`.

## Why

Plan section 4.1: DiskArbitration ships its own `getfsent` copy (`diskarbitrationd/fstab.c`) and builds the map in `__DAMountMapCreate1` (`DASupport.m:1488-1634`); the map is consulted only inside `DAMountWithArguments` (`DAMount.c:1315-1418`); nothing iterates fstab to mount. `diskarbitrationd.8:45-56` (already vendored in `src/DiskArbitration`) documents the user-facing form. This is also the admin hook that keeps the "no fstab in the image" decision honest: anyone who wants `/dev/da0p1` on `/export` has a supported way to say so.

## Design

**Parsing** (`DASupport.m:1488-1634`), one map entry per fstab line:
- Lines with `fs_type == "sw"` are skipped (`:1492`).
- `fs_spec` is split on `=`; only `UUID=<uuid>` (-> CFUUID, `:1506-1509`), `LABEL=<name>` (-> string, matched against `DAVolumeName`, `:1510-1513`) and `DEVICE=<xml plist>` (-> dictionary matched against the media's properties, `:1514-1517`) produce an entry. `/dev/da0p1`, `tmpfs`, `host:/export`, `none`, `md` produce nothing (`entryID == NULL`). `DEVICE=` on NextBSD matches against C2's description dictionary (Apple uses `IOServiceMatchPropertyTable`, `DAMount.c:1365-1377`); implement as exact-value match on `kDADiskDescription*` keys.
- `fs_mntops`: `auto` -> `DAMountAutomatic=true`, `noauto` -> `false`; every other token is passed through verbatim into `DAMountOptions` (`:1526-1553`).
- `fs_file`: `none` means the default `/Volumes/<name>`; anything else becomes `DAMountPath` (`:1568-1578`).
- `fs_vfstype`: `auto` means no filter; anything else is `DAProbeKind` and must equal the matched bundle's `CFBundleName` -- the FreeBSD vfs type after C4 (`:1583-1592`; compared at `DAMount.c:1325-1337`).
- `fs_freq`, `fs_passno`: ignored.

**Consulting** (`DAMount.c:1315-1418`): scan `gDAMountMapList1` in file order; first entry whose kind filter passes and whose id matches the disk's `DAVolumeUUID` (`:1341-1352`), `DAVolumeName` (`:1353-1364`) or device dictionary (`:1365-1377`) wins. If it says `auto`, the disk is marked `MountAutomatic` and `MountAutomaticNoDefer` (`:1385-1393`, so an fstab `auto` line mounts even before a console user exists); `noauto` cancels the automatic mount. Its options are prepended before the trust defaults, so a fstab `suid` or `owners` overrides them (`:1400-1405`). Its mount point is used only when the caller gave none (`:1408-1418`); C5's mount-point creation then `mkdir`s it if missing.

**Refresh** (`DAMountMapListRefresh1`, `DASupport.m:1636-1685`): `stat(_PATH_FSTAB)`; rebuild only when the mtime changed; called at startup (`DAMain.c:744`) and at the first stage dispatch after idle (`DAStage.c:213-220`). There is no file watch; editing fstab takes effect at the next disk event. Missing file -> empty map, no error.

**Parser choice.** Vendor Apple's `diskarbitrationd/fstab.c` (244 lines, APSL like the rest) rather than libc's `getfsent(3)`: FreeBSD's `lib/libc/gen/fstab.c:182-213` returns `EFTYPE` ("bad entry") for any line whose options contain none of `rw`/`rq`/`ro`/`sw`/`xx`, so `UUID=... none msdosfs noauto` would be rejected outright; Apple's copy sets `fs_type = "??"` and carries on (`fstab.c:135`). Also keep `launchctl`'s `mount -vat nonfs` (`src/launchd/support/launchctl.c:2401-2402`) in mind: if a fstab exists at boot, that path will try to mount any line with a *real* device spec before the daemon runs, which is Apple's behaviour too and the reason `UUID=`/`LABEL=` lines are the recommended form (FreeBSD `mount -a` cannot resolve them, it just logs and skips).

**vsdb.** Apple's second map (`/var/db/volinfo.database`, `DAMountMapListRefresh2`, `DASupport.m:1690-1780`; the `vsdb(8)` per-volume "ignore ownership" table) is not ported. `noowners` has no FreeBSD equivalent (C5), so the database would carry no information.

## Scope

`fstab.c` vendored (244 lines), `DASupport.m` map section (~300 lines), the map scan in `DAMount.c` (already inside C5's port; this ticket switches it on), refresh call sites. Update `diskarbitrationd.8` examples to FreeBSD vfs names (`msdosfs` for `msdos`, drop `hfs`).

## Acceptance

- [ ] with `/etc/fstab` containing `UUID=<stick uuid> /export msdosfs rw` the stick mounts on `/export`, not `/Volumes/STICK`; removing the line and re-plugging returns it to `/Volumes/STICK`
- [ ] `LABEL=STICK none msdosfs rw,noauto` leaves the stick unmounted at plug; `LABEL=The\040Volume\040Name` matches a label with spaces
- [ ] `LABEL=STICK none ufs rw` (wrong type filter) is ignored and the stick mounts at the default place
- [ ] a line `/dev/da0p1 /mnt msdosfs rw` produces no map entry and does not change daemon behaviour
- [ ] a line without `rw`/`ro` (`UUID=... none msdosfs noauto`) parses (vendored `fstab.c`), and `swapfile ... sw` lines are skipped
- [ ] editing fstab while the daemon runs takes effect at the next plug without restart; a missing fstab logs nothing
- [ ] E14's `swapon /private/var/vm/swapfile0` line in fstab is not treated as a disk

## Refs

Plan sections 2 ("What fstab is for on modern macOS"), 4.1, 7 C5/C6. Apple: `diskarbitrationd/DASupport.m:1476-1480,1488-1685,1690-1780`, `DAMount.c:1315-1418`, `DAMain.c:744`, `DAStage.c:213-220`, `fstab.c:135`, `diskarbitrationd.8:45-56`. FreeBSD releng/15.0: `lib/libc/gen/fstab.c:182-213`. NextBSD: `src/launchd/support/launchctl.c:2401-2402`.

C7 · kernel: compile in UDF, LIBICONV and the msdosfs/cd9660/udf iconv tables so DiskArbitration can mount UDF and non-ASCII FAT names (NO_MODULES) filed nextbsd-kernel#228

Reponextbsd-kernel
Labelsenhancement area:storage area:base
Depends on
ParentE15
## Summary

`config/NEXTBSD` is `include GENERIC` plus a short options list (`GEOM_UZIP`, `TARFS`, `UNIONFS`, `NULLFS`, `FUSEFS`, `FDESCFS`, `P1003_1B_MQUEUE`, ...). GENERIC compiles in `MSDOSFS` and `CD9660` (`sys/amd64/conf/GENERIC:54-55`) but upstream ships `UDF`, `EXT2FS`, `LIBICONV`, `MSDOSFS_ICONV`, `CD9660_ICONV` and `UDF_ICONV` only as modules, and NextBSD has no `.ko` tree (`NO_MODULES=yes`). Add the options the E15 fs bundles need, same pattern as `FUSEFS` and `FDESCFS` in the same file.

## Why

- `mount_msdosfs -L <locale> -D <codepage>` (long-name conversion; vermaden's and every desktop's default) calls `set_charset()`, which `kldload("msdosfs_iconv")`s and fails if `modfind` cannot see it (`sbin/mount_msdosfs/mount_msdosfs.c:294-311`, `err(EX_OSERR, "msdosfs_iconv")`). Compiled in, `modfind` succeeds and `msdosfs_iconv` is non-NULL (`sys/fs/msdosfs/msdosfs_vfsops.c:135-149`). Without it, C5 must mount FAT without `-L`, and non-ASCII long names come out mangled.
- `mount_cd9660 -C UTF-8` needs `CD9660_ICONV` the same way.
- `udf.fs` (C4) cannot mount at all without `options UDF`; `UDF_ICONV` for its names.
- `EXT2FS` is optional: not in the v1 bundle set, but it is the only way ext2/3/4 sticks could ever mount without a FUSE port. Include it if the size cost is acceptable; otherwise leave a note.

## Design

Append to `config/NEXTBSD`, with a comment block in the file's style:

```
options 	LIBICONV
options 	MSDOSFS_ICONV
options 	CD9660_ICONV
options 	UDF
options 	UDF_ICONV
# optional
options 	EXT2FS
```

All are declared in the MI `sys/conf/options` (not `options.amd64`), so they are safe on both matrix legs including `NEXTBSD-RPI5`, which includes this file. Nothing in `src-overlay/` is involved, so nextbsd-kernel-extensions' KBI-matched rebuild keeps working (the constraint the config's Linuxulator comment spells out).

## Acceptance

- [ ] `kldstat -v | grep -c iconv` on the booted image shows the static `libiconv`, `msdosfs_iconv`, `cd9660_iconv`, `udf_iconv` modules
- [ ] `mount_msdosfs -L en_US.UTF-8 -D cp437 /dev/da0p1 /mnt` succeeds and a file named with non-ASCII characters lists correctly
- [ ] `mount -t udf /dev/cd0 /mnt` on a UDF disc succeeds
- [ ] kernel size delta recorded in the PR; both matrix legs green

## Refs

Plan section 7 C4/C5 (helper table), nextbsd-kernel `config/NEXTBSD` (`FUSEFS` and `FDESCFS` comment blocks). FreeBSD releng/15.0: `sys/amd64/conf/GENERIC:54-55`, `sbin/mount_msdosfs/mount_msdosfs.c:294-311`, `sys/fs/msdosfs/msdosfs_vfsops.c:113,135-149`. vermaden `automount:588-597` (`-L`/`-D` defaults, `large` dropped on 12+).

Stage D — framework, policy, desktop -- client framework, unmount/eject, Gershwin bridge, console user (D3b is an E11 spike)

D1 · DiskArbitration client library: port DAServer.defs MIG and libDiskArbitration (sessions, Register* callbacks, CopyDescription, DiskListComplete/Idle replay); datest becomes a real client filed nextbsd-userland#197

Reponextbsd-userland
Labelsenhancement area:storage area:base
Depends onC1, C2
ParentE15
## Summary

Port Apple's `DiskArbitration/` client framework (DiskArbitration-535.120.1.0.1, pure C over CF + Mach, 7782 lines including the 2903-line legacy `DiskArbitrationPrivate.c` which we drop) and the `DAServer.defs` MIG subsystem, so that `DASessionCreate`, `DASessionSetDispatchQueue` / `DASessionScheduleWithRunLoop`, `DARegisterDisk{Appeared,Disappeared,DescriptionChanged,Peek,MountApproval,UnmountApproval,EjectApproval}Callback`, `DADiskCreateFromBSDName` / `DADiskCreateFromVolumePath` and `DADiskCopyDescription` work against `diskarbitrationd`. `datest` stops being a `bootstrap_look_up` liveness probe and becomes a real client modelled on Apple's `datest/datest.m` (2048 lines, ObjC; ours stays C).

Today the tree has no framework, no DA headers and no `.defs`: `src/DiskArbitration/` is `diskarbitrationd.c` (132 lines, `while (!got_term) sleep(60)` at :127-128), `da_iokit_subscribe.c`, `datest.c` (29 lines) and a Makefile whose header still calls the framework "iter 2+".

## Why

- Every Stage D consumer (D2 unmount/eject, D3 Gershwin bridge, D4 agent) is a DA client. Nothing can be tested end to end without the client side.
- Apple's delivery model is cheap and already fits NextBSD: the daemon rings a doorbell (an empty `mach_msg` with `msgh_id 0` to the session's client port, `diskarbitrationd/DASession.c:308-332`, sent only when the queue goes 0 -> 1 at :314) and the client pulls the serialized queue with `_DAServerSessionCopyCallbackQueue` (`DiskArbitration/DASession.c:235-276`; server side `DAServer.c:2328-2390`). That is plain Mach + CF, both of which NextBSD ships.

## Design

**Layout (decision).** Flat, like every other Darwin library in this tree, not a `.framework` bundle:
- `src/libDiskArbitration/` -> `/usr/lib/system/libDiskArbitration.so.1` (`LIBDIR= ${PREFIX}/lib/system`, `SHLIB_MAJOR= 1`, rpath `/usr/lib/system`), same as `src/libIOKit/Makefile:18-19,68-69` and `src/libSystemConfiguration/Makefile:20-21,90-91`.
- Public headers `/usr/include/DiskArbitration/{DiskArbitration.h,DADisk.h,DASession.h,DADissenter.h}` (Apple's public set per `DiskArbitration.xcodeproj/project.pbxproj:139-144`; `DiskArbitration.h:29-31` is the umbrella). `DASession.h:93-118` also carries `DAApprovalSessionRef` / `DAApprovalSessionCreate`; there is no `DAApprovalSession.h`.
- `build-userland.sh` asserts `/usr/lib/system/libDiskArbitration.so.1` and `/usr/include/DiskArbitration/DADisk.h`, as it does for SC at :535-540 and IOKit at :549-553.
- The libxpc install-layout spike (`freebsd-libxpc-install-layout-spike.html` section 14, :376-386) decided `/System/Library/Frameworks/DiskArbitration.framework/` with headers inside the bundle. The tree has since overridden that same page for SystemConfiguration (:293 "flat") and CoreFoundation (`/usr/lib/system/libCoreFoundation.so.6`, `src/libCoreFoundation/Makefile:199`), and `grep -n Frameworks build-userland.sh` finds nothing. DA follows the tree. Update the spike page's section 14 and section 18 in the same PR (or file a docs note).

**MIG.** `diskarbitrationd/DAServer.defs` (`subsystem DAServer 0` at :28, `UseSpecialReplyPort 1` :38, `MsgOption MACH_SEND_PROPAGATE_QOS` :40) has 19 routines (:50-145). Drop the three inside `#if TARGET_OS_OSX` (`_DAServerSessionSetAuthorization`, `_DAServermkdir`, `_DAServerrmdir`, :128-135; Authorization Services has no port, see below) and `_DAServerSessionQueueRequestWithUserToken` (:97). Run `mig` from `build-userland.sh` exactly as configd does (`run_mig "$CONFIGD_MIG" ...` :520-526, `MIGOUT=` :536): `DAServerUser.c` links into the library, `DAServerServer.c` into the daemon. Check whether NextBSD's `mig`/launchd honour `ServerAuditToken` (used by `_DAServerSessionCreate` :82, `_DAServerDiskSetAdoption` :66, `_DAServerSessionQueueRequest` :87); if not, the daemon takes the caller uid from `mach_msg_audit_trailer_t` or, failing that, from the pid passed by the client (document which).

**Daemon-side dispatch.** Apple demuxes with `dispatch_mach_mig_demux` on a `dispatch_mach_create_f` channel (`DAServer.c:1787-1812`, `DAServerInit` :3471-3496; per-session channels `diskarbitrationd/DASession.c:194,355`). NextBSD's libdispatch is not known to carry `dispatch_mach`; use the pattern configd uses (a `mach_msg` loop over the MIG-generated `DAServer_server()` demux, `src/configd/Makefile:7`) on a `DISPATCH_SOURCE_TYPE_MACH_RECV` source. Session cancel on `MACH_NOTIFY_NO_SENDERS` (`DAServer.c:1802-1804`) needs `mach_port_request_notification`; see the mach-port-request-notification plan page for its status on NextBSD.

**Service name.** The client looks up `_kDADaemonName` = `com.apple.DiskArbitration.diskarbitrationd` (`diskarbitrationd/DAInternal.h:41`, `DiskArbitration/DASession.c:530,604`), which is why C1 renames the service first. `datest.c:19` and the plist `overlay/System/Library/LaunchDaemons/com.apple.DiskArbitration.plist:31` still say `com.apple.DiskArbitration`.

**Client-side replay.** On `_DAServerSessionRegisterCallback` for `_kDADiskAppearedCallback` the daemon replays every staged disk (`DAServer.c:2947-2961`), then `_kDADiskListCompleteCallback` (:2967) and, if `gDAIdle`, `_kDAIdleCallback` (:2969-2973); registering Idle alone fires immediately when idle (:2976-2986). Port as-is. The description a client receives carries `DAVolumePath` (`DAInternal.c:71`, serialized :423-431) once C5 mounts something; "volume mounted" for consumers is DescriptionChanged on `kDADiskDescriptionVolumePathKey` (updated at `DARequest.c:810-814`).

**Darwin-only calls in the client and what replaces them** (all verified by grep):
- `bootstrap_look_up2(bp, name, &port, 0, BOOTSTRAP_PRIVILEGED_SERVER)` (`DASession.c:530,604`, `<bootstrap_priv.h>` :31): **no shim needed**. NextBSD's liblaunch declares it (`src/launchd/liblaunch/bootstrap_priv.h:52`, `BOOTSTRAP_PRIVILEGED_SERVER` :35) and defines it (`libbootstrap.c:261`); Libnotify, syslog and kext_tools already call it. Verify the privileged-server flag is enforced by our launchd; if it is ignored, note it and keep the call.
- `_dyld_get_image_name(0)` (`DASession.c:543-550,617-624`, session name sent to `_DAServerSessionCreate`) -> `getprogname(3)`. There is no dyld shim in the tree (`_dyld_get_image_name` appears only inside `BINARY_SUPPORT_DYLD` blocks of CF).
- `AuthorizationCreate` / `AuthorizationMakeExternalForm` (`DASession.c:304-310`, `<Security/Authorization.h>` :43): delete; no Authorization port exists (`kext_tools` only vendors the header reference and does not compile `KextManager.c`).
- `os_log` (:745,958) -> the tree's syslog/asl; `OSAtomicIncrement32` (:163) -> `<stdatomic.h>`.
- `notify_register_dispatch("com.apple.diskarbitrationd.launched")` (:1011, `_DASessionRecreate` :950): keep; Libnotify is ported.
- `DADisk.c:279-321` IOKit (`IOServiceGetMatchingService`/`IOBSDNameMatching`, `IORegistryEntryCreateCFProperty`) in `DADiskCreateFromBSDName`: replace with a `stat("/dev/<name>")` existence check and let `_DAServerDiskCopyDescription` answer; `DADisk.c:413` `___statfs` for `DADiskCreateFromVolumePath` maps to `statfs(2)` `f_mntfromname`.
- `audit_token_to_pid` / `audit_token_to_euid` (`DiskArbitration.c:40,342`, `<bsm/libbsm.h>`): `getpid()` / `geteuid()` on the client.
- Not present in the client, nothing to do: `sandbox_check`, `proc_pidpath`, `xpc_*`, `SecTask`.

**datest.** Rewrite as a C port of the useful subset of Apple's `datest.m` (`--list`, `--watch`, `--describe <bsdname>`, later `--unmount/--eject` for D2, `--testDASessionKeepAliveWithDAIdle` style checks from `datest.m:116-123`). Build in `build-userland.sh:980` against `-lDiskArbitration -lCoreFoundation`. The CI gate `tests/boot-test.sh:1178-1193` (`DA-BOOT-OK`) and `overlay/usr/tests/freebsd-launchd-mach/run.sh:1593-1599` change from "service reachable" to "session created and root disk described".

## Scope

Library ~2,000 lines after dropping `DiskArbitrationPrivate.c`; MIG stubs generated; daemon gains `DAServer.c`'s session table, `DASession.c`, `DAQueue.c`, `DACallback.c` (the request side comes with D2). Copy Apple's `Info.plist`/modulemaps are not needed in a flat layout.

## Before starting

- [ ] C1 landed (service renamed to `com.apple.DiskArbitration.diskarbitrationd`, plist label `com.apple.diskarbitrationd` as in Apple's `diskarbitrationd/com.apple.diskarbitrationd.plist`)
- [ ] C2 landed (there are `DADiskRef`s with descriptions to replay)
- [ ] confirm `ServerAuditToken` support in NextBSD `mig` + launchd, and `mach_port_request_notification(MACH_NOTIFY_NO_SENDERS)` on a session port

## Acceptance

- [ ] `/usr/lib/system/libDiskArbitration.so.1` and `/usr/include/DiskArbitration/{DiskArbitration,DADisk,DASession,DADissenter}.h` are installed; `build-userland.sh` fails the build if they are missing
- [ ] `datest --list` prints one description per GEOM disk/partition with `DAMediaBSDName`, `DAMediaWhole`, `DAMediaLeaf`, `DAMediaSize`, `DAMediaContent` and, for mounted volumes, `DAVolumePath`
- [ ] `datest --watch` receives DiskAppeared for every existing disk, then DiskListComplete, then Idle, and no further callbacks until a disk event
- [ ] `datest --describe vtbd0` (CI) / `da0` (hardware) returns the same dictionary as `--list`
- [ ] a session that registers `DADiskDescriptionChangedCallback` on `kDADiskDescriptionVolumePathKey` is woken once by the doorbell and drains the whole queue in one `_DAServerSessionCopyCallbackQueue` pull (log/trace)
- [ ] killing `diskarbitrationd` delivers `MACH_NOTIFY_NO_SENDERS` to the client (`DASession.c:724-746` path) and `_DASessionRecreate` reconnects when the daemon respawns (`KeepAlive` plist)
- [ ] CI `DA-BOOT-OK` marker now means "DASessionCreate + DADiskCopyDescription(root disk) succeeded"; `tests/boot-test.sh:1178` comment updated
- [ ] `datest` still exits non-zero with `DA-BOOT-FAIL` when the service is absent

## Refs

Plan section 4.2, 4.3, 7 (Stage D). Apple: `DiskArbitration/DASession.c:235,370,508,569,666`, `DiskArbitration.c:465,805,1313-1363`, `DADisk.c:233,332,398`, `diskarbitrationd/DAServer.defs:28-145`, `DAServer.c:1787,2328,2895,3471`, `diskarbitrationd/DASession.c:308,369`, `DAQueue.c:450-455,727-734`. NextBSD: `src/DiskArbitration/Makefile:14,83`, `datest.c:19`, `build-userland.sh:520-536,980`, `tests/boot-test.sh:1178-1193`, `src/launchd/liblaunch/bootstrap_priv.h:35,52`. Install layout: `freebsd-libxpc-install-layout-spike.html` section 14 (superseded by this ticket's decision).

D2 · diskarbitrationd: unmount and eject requests with approval broadcast, 10 s response timer, dissenters, EBUSY -> dissenter PID via libprocstat, eject via CAM START STOP UNIT / CDIOCEJECT filed nextbsd-userland#198

Reponextbsd-userland
Labelsenhancement area:storage area:base
Depends onD1, C5
ParentE15
## Summary

Port `DADiskUnmount` / `DADiskEject` end to end: the client request (`DiskArbitration.c:1307`, `:957`), the daemon request machine (`DARequest.c`), the approval broadcast with its 10 s timer and first-dissenter-wins rule (`DAQueue.c`), `DADissenterRef` with `kDAReturn*` codes and an offending PID, and the two FreeBSD eject paths.

## Why

This is the one thing Gershwin cannot do today without root: `GWUnmountHelper.m:95-102` falls back to `sudo -A -E umount [-f]`, and there is no real USB eject anywhere (libs-gui `NSWorkspace.m:2113-2128` runs `eject cdrom`/`eject floppy` by directory name, else "Don't know how to eject"). D3 replaces that with DA calls; this ticket makes the calls work.

## Design

**Request flow (port as-is).** `DARequestDispatch` (`DARequest.c:1927`) -> `__DARequestUnmount` (:1324): root/Data volumes answer `kDAReturnBusy` immediately (:1373-1382); writable disks are tickled first (:1407-1410); then `DADiskUnmountApprovalCallback(disk, __DARequestUnmountApprovalCallback, request)` (:1415) broadcasts `_kDADiskUnmountApprovalCallback` to every session that registered one (`DAQueue.c:440-445`, per-session enqueue :596-660 with a `responseID` and `DACallbackSetTime` :619-623). `__DARequestUnmountUnmount` (:1656) calls `unmount(2)` (:1700) with `MNT_FORCE` when `kDADiskUnmountOptionForce` (:1680-1683). Eject: `__DARequestEject` (:299) refuses non-whole disks with `kDAReturnUnsupported` (:318-326), broadcasts `DADiskEjectApprovalCallback` (:346), then `__DARequestEjectEject` on a worker thread (:381, :472-497). `kDADiskUnmountOptionWhole` expands to all partitions and links the sub-requests (`DAQueue.c:906-1008`); `__DARequestDispatchCallback` (:122-157) reports the first linked dissenter.

**Timer.** `__kDAResponseTimerLimit = 10` and grace 1 (`DAQueue.c:45-46`); `__DAResponseTimerRefresh` (:209-251) uses `dispatch_after` on the server queue, no CFRunLoopTimer. On expiry the session is logged "not responding" (:191), marked `kDASessionStateTimeout` (:193) and skipped for future approvals until it pulls its queue again (`DAServer.c:2390` clears the state). A timeout is not a dissent. Sessions with `kDASessionOptionNoTimeout` are exempt (:174).

**First dissenter wins.** `_DAResponseDispatch` (`DAQueue.c:256-299`) keeps only the first non-NULL dissenter per request; `__DAResponseComplete` (:94-121) fires the request when no outstanding responses remain. Force and Hibernate bypass approval dissent (`DARequest.c:1632-1636`); the magic status `0xF8DAFF01` (:1642) is legacy-client only and can go.

**Dissenters.** Client `DADissenterCreate(allocator, status, string)` (`DiskArbitration/DADissenter.c:31`); daemon `DADissenterCreate(allocator, status)` (`diskarbitrationd/DADissenter.c:28`). Codes `kDAReturnSuccess..kDAReturnUnsupported` = `0xF8DA0000..0xF8DA000C` (`DADissenter.h:46-58`). Unix errors are wrapped with `unix_err()` (:1542, :420).

**EBUSY -> PID (FreeBSD replacement for `proc_listpidspath`).** Apple forces `status = EBUSY` on any unmount failure (`DARequest.c:1540`, inside `///w:` markers) and runs `__DARequestUnmountGetProcessID` on a thread (:1548, :1734-1760, `proc_listpidspath(... PROC_LISTPIDSPATH_PATH_IS_VOLUME ...)` :1752). Nothing in NextBSD links libprocstat yet and launchd's `freebsd-shims/libproc.h` has no `proc_listpidspath`. Implement `___proc_listpidspath_volume(path)` with `fuser -c`'s algorithm: `procstat_open_sysctl()`, `procstat_getprocs(KERN_PROC_PROC)`, `procstat_getfiles()`, `procstat_get_vnode_info()` and match `vn_fsid == st_dev` of the mount point (`usr.bin/fstat/fuser.c:152,233-303`, `lib/libprocstat/libprocstat.h:221-266`), `LIBADD+= procstat`. Report the first PID via `DADissenterSetProcessID`. Only force `EBUSY` when `errno == EBUSY`; keep the real errno otherwise (drop the `///w:` override). Skip Apple's `vfs.generic.print_busy_vnodes` retry (:1704-1722); the sysctl does not exist on FreeBSD. For eject, Apple walks child media nodes with an uninitialized `int flags` (`DARequest.c:1775` -> :1829); do not copy that: for a whole disk, check every mounted child partition with the same fsid lookup.

**Eject.** Apple: `open(bsdpath) + ioctl(DKIOCEJECT)` with ENOTTY as success (`DARequest.c:487-491`). FreeBSD:
- `cd(4)`: `ioctl(fd, CDIOCEJECT)` (`sys/sys/cdio.h:201`; driver `sys/cam/scsi/scsi_cd.c:2516-2519` -> `cdstopunit(periph, 1)`), after `CDIOCALLOW` (:213) if the tray was locked.
- `da(4)` (USB sticks, external SATA): CAM `START STOP UNIT` with the eject bit, i.e. what `camcontrol eject` does (`sbin/camcontrol/camcontrol.c:190`, `scsistart` :999, `scsi_start_stop(... load_eject ...)` :1020-1025): `cam_open_device()` on the whole disk's pass device, `scsi_start_stop(&ccb->csio, /*start*/0, /*load_eject*/1, /*immediate*/0)`, `cam_send_ccb()`; `LIBADD+= cam`. Treat `ENOTTY`/no pass device (vtbd, md, nvd) as success like Apple does with ENOTTY.
- All unmounts of child partitions precede the eject (the Whole expansion above); the eject itself runs as root inside the daemon.

**Who may unmount.** `_DAServerSessionQueueRequest` (`DAServer.c:2505`) takes the caller's euid/egid from the MIG audit token (`audit_token_to_euid(_token)`, :2558) and calls `DAAuthorize(session, _kDAAuthorizeOptionIsOwner, disk, euid, egid, _kDAAuthorizeRightUnmount)` (:2566, again :2761 for eject), answering `kDAReturnNotPrivileged` otherwise. Keep that rule, minus Authorization Services: a user can unmount volumes DA mounted for them (C5 sets the owner) and disks they own; anything else needs root. This is why D1 must settle where the caller uid comes from when `ServerAuditToken` is unavailable. Until D4 exists there is no console-user notion, so the check is uid == owner || uid == 0.

## Scope

`DARequest.c` (2,000 lines, of which unmount/eject/dissenter ~900), `DAQueue.c` response side (~300), `DADissenter.[ch]`, `DAThread.c`; FreeBSD-specific: ~150 lines libprocstat, ~80 lines libcam/cdio. `datest --unmount|--eject [--force] [--whole]` and a `--approve|--dissent` mode for tests.

## Acceptance

- [ ] `datest --unmount /Volumes/X` on an idle volume returns success and `kDADiskDescriptionVolumePathKey` is removed in a DescriptionChanged callback (`DARequest.c:1585-1587`); the mount point directory is removed (`DAMountRemoveMountPoint`, :1570)
- [ ] with a shell `cd`'d into `/Volumes/X`, the same call fails with `kDAReturnBusy`-class dissenter (`unix_err(EBUSY)`) whose `DADissenterGetProcessID` is that shell's PID; `--force` succeeds
- [ ] a second `datest --dissent` session registered for unmount approval blocks the unmount and its `DADissenterGetStatusString` reaches the requester; `--force` bypasses it
- [ ] a session registered for approval that never answers delays the request by 10 s (+1 grace), is logged "not responding", and is not consulted for the next request until it pulls its queue
- [ ] `datest --eject /dev/vtbd1` (CI: a hot-added virtio disk or `md`, whichever C2 models) unmounts every child first and returns success (ENOTTY path); on hardware `--eject da0` sends START STOP UNIT (visible in `camcontrol devlist`/dmesg: the stick goes away) and `--eject cd0` opens the tray
- [ ] `--eject /dev/da0p1` returns `kDAReturnUnsupported`
- [ ] unmounting `/` returns `kDAReturnBusy` without broadcasting
- [ ] a non-owner, non-root uid gets `kDAReturnNotPrivileged`

## Refs

Plan section 4.2 ("Unmount and eject"), 6.2, 7 (Stage D). Apple: `DARequest.c:122,299,318-326,346,381,389,472-497,1324,1373-1382,1415,1478,1540-1548,1622-1644,1656,1680-1700,1734-1760,1769-1829`, `DAQueue.c:45-46,94,152-193,209-251,256-299,440-445,596-660,906-1008`, `DADissenter.h:46-58`. FreeBSD releng/15.0: `sys/sys/cdio.h:201,213`, `sys/cam/scsi/scsi_cd.c:2516-2519`, `sbin/camcontrol/camcontrol.c:190,999,1020-1052`, `usr.bin/fstat/fuser.c:152,233,236,283,289,293`, `lib/libprocstat/libprocstat.h:221,223,237,266`. Gershwin: `gershwin-workspace/Workspace/GWUnmountHelper.m:86-111`.

D3a · Workspace: DiskArbitration bridge -- post NSWorkspace mount/unmount notifications through the workspace center, replace the 1.5 s mount poll with DA callbacks, and make GWUnmountHelper call DADiskUnmount/DADiskEject instead of sudo umount -f filed gershwin-workspace#119

Repogershwin-workspace
Labelsenhancement
Depends onD1, D2
ParentE15
## Summary

Make Workspace a DiskArbitration client on NextBSD (and any host with `libDiskArbitration`), so that (1) every GNUstep app learns about mounts and unmounts through `NSWorkspaceDidMountNotification` / `NSWorkspaceWillUnmountNotification` / `NSWorkspaceDidUnmountNotification`, (2) desktop volume icons come from DA callbacks instead of a 1.5 s poll, and (3) unmount/eject go through `DADiskUnmount` / `DADiskEject` with dissenters instead of `sudo -A -E umount -f`. Cross-repo: needs nextbsd-userland D1 (library) and D2 (unmount/eject); the removable-media prefix fix is D3b in gnustep/libs-gui.

## Why

- Nothing on FreeBSD/NextBSD posts `NSWorkspaceDidMountNotification` today; the only poster in libs-gui is `mountNewRemovableMedia` (`NSWorkspace.m:2186`), which runs `mount <path>` for exact `GSRemovableMediaPaths` entries (:2174).
- The Gershwin-side posters and observers use different centers. `VolumeManager.m:958,1058`, `GWUnmountHelper.m:65-66`, `Workspace.m:2667-2669` and `Network/NetworkVolumeManager.m:646` post Will/DidUnmount on `[NSNotificationCenter defaultCenter]`, but `GWDesktopManager.m:155-168` and `FileViewer/GWViewersManager.m:122-133` observe on `[ws notificationCenter]`, which is the separate `_GSWorkspaceCenter` instance (`NSWorkspace.m:327,794,2382-2385`). Only that center rebroadcasts over `NSDistributedNotificationCenter` with object `GSWorkspaceNotification` (`NSWorkspace.m:133,349-355,381-400`) and re-delivers locally (:439-456). So other apps never see Gershwin's own unmounts, and possibly Workspace's own observers don't either (not tested).
- `GWUnmountHelper.m` tries `umount` (:86), then `sudo -A -E umount` (:95), then `sudo -A -E umount -f` (:102); `VolumeManager.m:963-964` and `Workspace.m:2672` route through it. There is no eject except libs-gui's `eject cdrom`/`eject floppy` guess (`NSWorkspace.m:2113-2128`).
- This is the bridge `gershwin-desktop/issues#26` ("Automounter") asked for: a tool "would use Distributed Notification Center to inform Workspace about the mounting/unmounting of volumes". With DA the daemon side already exists; the bridge lives in Workspace.

## Design

1. **One center.** Post all four workspace notifications via `[[NSWorkspace sharedWorkspace] notificationCenter]` (userInfo `NSDevicePath` = mount point, as `GWUnmountHelper.m:64` already builds). Change the five posters listed above. `Tools/gw-unmount/gw-unmount.m:253-257` posts its own `GWWorkspaceWillUnmountNotification` on the distributed center (observed at `Workspace.m:1072`); keep it, or fold it into the standard name once Workspace posts through the workspace center.

2. **`GWDiskArbitration` (new, `Workspace/GWDiskArbitration.m`, `#if HAVE_DISKARBITRATION`).** A singleton that owns a `DASessionRef` scheduled on the main run loop (`DASessionScheduleWithRunLoop`) and registers:
   - `DADiskAppeared` / `DADiskDescriptionChanged` on `kDADiskDescriptionVolumePathKey` -> post `NSWorkspaceDidMountNotification` when a `DAVolumePath` appears; also drives `mountedVolumesDidChange` (`GWDesktopManager.m:1230`).
   - `DADiskDescriptionChanged` losing `DAVolumePath`, or `DADiskDisappeared` -> `NSWorkspaceDidUnmountNotification`.
   - `DADiskUnmountApproval` -> post `NSWorkspaceWillUnmountNotification`, return `NULL` (Workspace never dissents; it closes viewers on the volume as it does today at `GWViewersManager.m:1428-1458`).
   - `DADiskDisappeared` with `DAVolumePath` still set -> "Disk Not Ejected Properly" panel (this is the D4 agent's removal dialog rendered client-side; see D4).
   Configure check: `pkg-config`-free, `#include <DiskArbitration/DiskArbitration.h>` + `-lDiskArbitration` in GNUmakefile when the header exists.

3. **Poll becomes fallback.** `MPointWatcher` (`GWDesktopManager.m:1131-1146`: `NSTimer ... 1.5 ... watchMountPoints:`, body :1202-1235, `effectiveDesktopVolumes` :1249-1268 from `[ws mountedRemovableMedia]` + `[ws mountedLocalVolumePaths]` prefix-matched against `+[Workspace volumeMountRoots]` `Workspace.m:5493-5505`) stays for hosts without DA (Linux, stock FreeBSD) and is not started when the DA session connects.

4. **GWUnmountHelper.** New first step when DA is available: `DADiskCreateFromVolumePath` -> `DADiskUnmount(disk, kDADiskUnmountOptionDefault, cb, ctx)`; on a dissenter show `DADissenterGetStatusString` and, when `DADissenterGetProcessID` is set, the process name (`kDAReturnBusy` "in use by Terminal (pid 1234)"); offer Force (`kDADiskUnmountOptionForce`) only after the user confirms. `eject:YES` -> `DADiskEject` on `DADiskCopyWholeDisk(disk)` after a `kDADiskUnmountOptionWhole` unmount. The `sudo` ladder (:84-113) remains only for `#if !HAVE_DISKARBITRATION` and Linux. `unmountAndEjectDeviceAtPath:` (:76) is no longer called on DA hosts.

5. **`/Volumes` ownership.** `dscli init` creates `/Volumes` mode 01777 (`gershwin-components/DirectoryServices/dscli/main.m:1085-1092`) so users can mkdir mount points for images (`VolumeManager.m:300,346`, dirs created as the user at :334). Under E15 the OS owns `/Volumes` (C5 creates it root-owned and sweeps stale entries at startup, Apple `DAMain.c:150-246`; plan section 4.2 says this "settles gershwin-internal#22"). Leave the 01777 until D3c moves image mounts under DA, then drop it from `dscli init` (gershwin-components) -- note in that repo when filing.

Not in scope: `DiskUtility` backends (`gershwin-components/DiskUtility/Sources/Backend/*`, FreeBSD backend mounts under `/media/` at `DUFreeBSDStorageBackend.m:1381-1382` with `sudo -A`), `ISOWrite`, `NetworkVolumeManager` sshfs mounts. They become DA clients later; `gershwin-desktop#64` (installer must unmount all volumes on the target disk) and `gershwin-workspace#78` (drag-and-drop install) want the same disk/partition/volume model.

## Before starting

- [ ] nextbsd-userland D1 installed (`/usr/include/DiskArbitration/DiskArbitration.h`, `libDiskArbitration.so.1`) in the gershwin-desktop `targets/nextbsd` rootfs
- [ ] D2 unmount/eject working from `datest`

## Acceptance

- [ ] plugging a FAT stick on the NextBSD target shows the volume on the desktop within one DA callback (no 1.5 s poll running: `MPointWatcher` timer not scheduled when DA is connected)
- [ ] a second GNUstep app observing `NSWorkspaceDidMountNotification` via `[[NSWorkspace sharedWorkspace] notificationCenter]` receives it with `NSDevicePath` = `/Volumes/<name>`; `gdnc` shows the `GSWorkspaceNotification` object
- [ ] unmounting from the desktop menu with a Terminal `cd`'d into the volume shows a panel naming the process and PID, and does not run `sudo`; Force after confirmation succeeds
- [ ] "Eject" on a USB stick unmounts all its partitions, ejects via DA, and the icon disappears; unplugging without eject shows "Disk Not Ejected Properly"
- [ ] all Will/DidUnmount posts in Workspace use the workspace center; `grep -n "defaultCenter\] postNotificationName: NSWorkspace" Workspace/` is empty
- [ ] on Linux (no DA) behaviour is unchanged: poll runs, sudo ladder used

## Refs

Plan section 6.2, 7 (Stage D). gershwin-workspace@817491d: `Workspace/GWUnmountHelper.m:24,64-66,76,84-113,140-142`, `Workspace/Desktop/GWDesktopManager.m:155-168,1131-1146,1155,1171,1202-1235,1249-1268,1278`, `Workspace/Workspace.m:1042-1045,2636,2667-2672,2868,5493-5505,5507-5533`, `Workspace/VolumeManager.m:300,334,346,956-964,1056-1058`, `Workspace/Network/NetworkVolumeManager.m:239-240,644-646`, `Tools/gw-unmount/gw-unmount.m:213,253-257,292`. libs-gui@f285e90: `Source/NSWorkspace.m:133,327-457,794,2061-2140,2150-2196,2382-2385`. gershwin-components: `DirectoryServices/dscli/main.m:1085-1092`. Issues: gershwin-desktop/issues#26, issues#22, gershwin-internal#22, gershwin-desktop#60, gershwin-desktop#64, gershwin-workspace#78.

D3b · Spike: count volumes mounted under /Volumes as removable in GNUstep NSWorkspace (GSRemovableMediaPaths is exact-match) filed nextbsd#474

Reponextbsd
Labelsarea:desktop area:research
Depends on
ParentE15
## Spike question

Gershwin's `NSWorkspace` never treats anything mounted under `/Volumes` as removable, because libs-gui's `GSRemovableMediaPaths` test is an exact match. Decide how NextBSD/Gershwin fixes that **without filing upstream for now**:

1. **Carry a small libs-gui patch** in the Gershwin/NextBSD build: make the test "equal to an entry, or under an entry" (design below).
2. **Work around it in Gershwin only.** Workspace already keeps its own prefix list (`Workspace.m:5493-5505`) but still calls `mountedRemovableMedia`. Stop relying on it, or wrap it.
3. **Defer until DiskArbitration lands.** Workspace asks DA (`kDADiskDescriptionMediaRemovableKey`) instead of NSWorkspace (gershwin-desktop/gershwin-workspace#119, D3a).

Outcome: a recommendation, and if it's option 1, where the patch lives in the Gershwin build. Upstreaming to gnustep/libs-gui is explicitly out of scope for now.

## Background

`-[NSWorkspace getFileSystemInfoForPath:isRemovable:isWritable:isUnmountable:description:type:]` decides "removable" on non-Linux hosts solely by an exact-match test against the `GSRemovableMediaPaths` default: `NSWorkspace.m:1320` reads the array, `:1323` `if ([removables containsObject: fullPath])`. Gershwin ships `/mnt/floppy`, `/mnt/cdrom`, `/Volumes` in that default (`gershwin-system/Library/Preferences/GlobalDefaults/NSGlobalDomain.plist:18-22`), so `/Volumes/USB` is never removable and `mountedRemovableMedia` (`:2198-2229`, which filters `mountedLocalVolumePaths` through this flag) never returns anything mounted by an automounter or by DiskArbitration. Change the test to "equal to an entry, or under an entry" (`[fullPath hasPrefix: [entry stringByAppendingString: @"/"]]`).

## Why

Any automounter that uses a mount-point directory (macOS-style `/Volumes/<name>`, Linux `/media/<user>/<name>`, FreeBSD vermaden `/media/<name>`) mounts *under* a fixed prefix; an exact-match list can only describe fixed mount points like `/mnt/cdrom`. Gershwin works around this with its own prefix list (`gershwin-workspace/Workspace/Workspace.m:5493-5505`, matched at `GWDesktopManager.m:1256-1259`) and still ends up calling `mountedRemovableMedia` (`GWDesktopManager.m:1252`, `GWDesktopView.m:341`). The `/sys/block/<dev>/removable` check (`:1327-1373`) only runs under `HAVE_GETMNTENT && MNT_MEMB`, i.e. Linux.

## Design

- `getFileSystemInfoForPath:` (`:1307`): removable if `fullPath` equals an entry or has `entry + "/"` as a prefix; keep the Linux sysfs OR-in at `:1373`.
- `mountNewRemovableMedia` (`:2150-2196`) must keep mounting only *exact* entries (`mount <path>` at `:2174`); a prefix entry such as `/Volumes` is a directory, not a mount, so leave its loop as is (it already skips nothing there; running `mount /Volumes` fails harmlessly today, but do not make it worse). Document in the header that a trailing-slash-free directory entry means "anything mounted below here is removable".
- Optional, same area: `mountedLocalVolumePaths` on `HAVE_GETMNTINFO` keeps only `f_mntfromname` starting with `/dev/` (`:2303`); FUSE mounts on FreeBSD report `/dev/fuse`, so they pass; NFS/SMB (`host:/export`) do not, which matches "local". No change proposed, noted for reviewers.
- `unmountAndEjectDeviceAtPath:` (`:2061-2140`) is untouched here; it runs plain `umount` as the user (`:2079`) and guesses `eject cdrom`/`eject floppy` from the directory name (`:2113-2123`). On a DiskArbitration host the desktop calls DA directly (Gershwin D3a); a follow-up could let this method use DA when `<DiskArbitration/DiskArbitration.h>` is present at configure time.

## Acceptance

- [ ] with `GSRemovableMediaPaths = (/Volumes)` and a filesystem mounted at `/Volumes/USB`, `getFileSystemInfoForPath:@"/Volumes/USB"` sets `isRemovable = YES`; `/Volumes` itself and `/Volumes2/x` do not
- [ ] `mountedRemovableMedia` returns `/Volumes/USB`
- [ ] `mountNewRemovableMedia` does not attempt to mount `/Volumes` more than it did before (no new `mount` invocations for prefix entries)
- [ ] existing exact entries (`/mnt/cdrom`) behave as before on Linux and BSD

## Refs

libs-gui@f285e90 `Source/NSWorkspace.m:1307,1320-1324,1327-1373,2150-2196,2174,2198-2229,2231-2377,2303`. Gershwin consumers: `gershwin-workspace/Workspace/Desktop/GWDesktopManager.m:1155,1252-1259`, `GWDesktopView.m:334-341`, `gershwin-systempreferences/Modules/Volumes/Volumes.m:371,387`. Context: NextBSD E15 (https://pkgdemon.github.io/nextbsd-diskarbitration-mounts-plan.html) section 6.2, Gershwin ticket gershwin-desktop/gershwin-workspace#119 (D3a).

D3c · Spike: disk images (dmg/iso/squashfs) as DiskArbitration disks -- how VolumeManager's darling-dmg/fuseiso/squashfuse mounts under /Volumes stop depending on vfs.usermount filed gershwin-workspace#120

Repogershwin-workspace
Labelsenhancement question
Depends onD1, D2
ParentE15
## Summary

Decide, and prototype, how Workspace's image mounts go "through DiskArbitration" -- the precondition nextbsd-kernel#62 (PR, merged) set for dropping `vfs.usermount=1`: "Workspace asks diskarbitrationd to mount on the user's behalf -- exactly how a `.dmg` is mounted -- and no unprivileged-mount policy is needed at all. [...] When that lands, this patch should be dropped." Cross-repo with nextbsd-userland (any daemon-side extension) and nextbsd-kernel (D5).

## Why

`VolumeManager.m` runs the FUSE tools as the logged-in user via `NSTask` -- `darling-dmg`/`apfs-fuse` (`:579-583`, `:440-441`), `fuseiso` (`:649-657`), `squashfuse`/`fuseiso` for AppImage-style images (`:810-820`) -- against a mount point the user created under `/Volumes` (`:300`, `:346`, `NSFileManager` mkdir at `:334`). That only works because the kernel allows unprivileged mounts (patch `0008-NextBSD-enable-unprivileged-mounts-vfs-usermount-1.patch`; on the FreeBSD target `gershwin-system/Library/Scripts/SystemPrepare.sh:781` sets the same sysctl) and because `dscli init` makes `/Volumes` 01777 (`gershwin-components/DirectoryServices/dscli/main.m:1085-1092`). Apple's model is different: `hdiutil attach` asks `diskimages-helper` to create a block device, DiskArbitration then sees an ordinary disk and mounts it (plan section 4.2). DA has no "mount this file" API: `DADiskMount` takes a `DADiskRef` (`DiskArbitration.c:1003`).

## Design (options to evaluate)

- **A. DA-side attach, Apple shape.** A NextBSD-private MIG routine (`_DAServerDiskImageAttach(path, uid)`) makes the daemon run the FUSE tool as root with `-o allow_other,uid=<console user>` on `/Volumes/<name>` (C5's mount-point rules), track the FUSE pid, and create a `DADiskRef` with `DAVolumePath` and a NextBSD-private `DAMediaImagePath` key so Workspace's icon/eject flow is the same as for a USB stick; `DADiskUnmount` unmounts and reaps the tool. This is `DACommandExecute`-shaped (`DACommand.c`). Risks: FUSE tools running as root on untrusted images; each tool's option surface; `mount_fusefs` still needs the kernel's `allow_other` policy (`vfs.usermount` is irrelevant once root mounts).
- **B. `md(4)` + real filesystems.** `mdconfig -a -t vnode -f image` (needs root; DA does it) turns ISO/UFS/FAT images into GEOM disks that C2 already models; C4's fs bundles mount them. Covers `.iso`, raw `.img`, not `.dmg` (darling-dmg is FUSE) or squashfs (no kernel squashfs on FreeBSD). Cleanest for what it covers.
- **C. Keep `vfs.usermount` for FUSE only.** Leave user-space FUSE mounts to the user (as stock FreeBSD does) and let DA adopt them via C3 (`getmntinfo` diff) so they appear as disks with `DAVolumePath`; D5 then becomes "keep patch 0008" or "narrow it". Also required if AppImage runtimes self-mount as the user (nextbsd-userland#460's untested end-to-end item; the Linuxulator translates `mount("fuse.*")` into a fusefs mount, `linux_file.c:1108-1132`).

Deliverable: a short write-up under the plan page (or a comment on the epic) choosing A/B/C per image type, and a prototype of the chosen path for `.iso`.

## Acceptance

- [ ] a decision per image type (dmg, iso, squashfs/AppImage, raw img) recorded on the epic, with the daemon-side API (if any) named
- [ ] prototype: double-clicking an `.iso` on the NextBSD target mounts it under `/Volumes/<label>` with `vfs.usermount=0`, it appears as a DA disk (`datest --list` shows `DAVolumePath`), and Eject from the desktop unmounts and detaches it
- [ ] states explicitly whether AppImage execution still needs `vfs.usermount` (input to D5)

## Refs

Plan section 4.2, 5 (FUSE for AppImage), 6.2, 7 (D5). gershwin-workspace@817491d `Workspace/VolumeManager.m:298-346,429-441,579-583,649-657,810-820,963-964,985`; gershwin-components `DirectoryServices/dscli/main.m:1085-1092`; gershwin-system `Library/Scripts/SystemPrepare.sh:780-781`. nextbsd-kernel `patches/0008-NextBSD-enable-unprivileged-mounts-vfs-usermount-1.patch:38-42`, nextbsd-kernel#62, #60, #61; nextbsd-userland#460. Apple `DiskArbitration.c:1003`, `DACommand.c`.

D4 · Console user for diskarbitrationd: State:/Users/ConsoleUser in configd, published by LoginWindow; defer removable/external auto-mounts until login, unmount at logout; agent dialogs without XPC/CFUserNotification filed nextbsd-userland#199

Reponextbsd-userland
Labelsenhancement area:storage area:desktop
Depends onD1, D2, C5
ParentE15
## Summary

Give `diskarbitrationd` the "who is at the console" input that Apple gets from `SCDynamicStore` (`DAMain.c:338-339`, `:481-489`, `SCDynamicStoreKeyCreateConsoleUser` :576-584; wrappers `DABase.c:683,761`), port the login/logout handling in `_DAConfigurationCallback` (`DAServer.c:879-1290`) and the mount deferral (`DAMount.c:1497-1519`), and decide how the three `DiskArbitrationAgent` dialogs ("Disk Not Ejected Properly", unreadable, unrepairable) are shown on NextBSD/Gershwin, where Apple's XPC-to-a-per-user-agent path does not exist. Cross-repo: the publisher is Gershwin's `LoginWindow` (gershwin-components); the removal dialog is rendered by Workspace (D3a).

## Why

- Apple does not auto-mount removable or external media until a console user exists: `DAMountWithArguments` at `DAMount.c:1497-1519` (`if ( gDAConsoleUserList == NULL )` :1501, `kDAMountPreferenceDefer` :1506, "No console users yet, delaying mount" :1508, `ECANCELED` :1515-1519). Defaults: `DAMountDeferRemovable` true, `DAMountDeferExternal` true, `DAMountDeferInternal` false (`DAMount.c:906-937`, keys `DASupport.m:1803-1805`); `AutomountDisksWithoutUserLogin` turns both off (`DASupport.m:1869-1874`). C5's mounts are owned by the console user (`-u/-g` for msdos), which is meaningless without a console user.
- Nothing on NextBSD publishes one today. `grep -rn ConsoleUser|State:/Users src overlay` is empty; configd is ported (clean-room MIG store, `src/configd/configd.c:1-8`, routines `config.defs:72-191`) and `SCDynamicStoreSetValue` / `SCDynamicStoreSetNotificationKeys` / `SCDynamicStoreSetDispatchQueue` exist (`src/libSystemConfiguration/SCDynamicStore.c:432`, `SCNotify.c:36,441`), but no `SCDynamicStoreCopyConsoleUser`. The base image logs in through getty (`org.nextbsd.getty.ttyv0.plist:92-94`: "WHEN A DISPLAY SERVER / loginwindow ARRIVES, IT MUST TAKE THIS TTY ... the thing Apple actually runs on the console: loginwindow"); there are no LaunchAgents and no per-user launchd session. The Gershwin NextBSD target runs `LoginWindow` as a root LaunchDaemon (`gershwin-desktop/targets/nextbsd/overlays/System/Library/LaunchDaemons/io.github.gershwin-desktop.loginwindow.plist`), and `LoginWindow.m` is the only process that knows the session: PAM `pam_open_session` (`LoginWindowPAM.m:244,279,401`), `setuid` (`LoginWindow.m:1579`), `sessionPid` (`:1744,2203`), `waitpid` (`:2232`), `lastLoggedInUser` plist (`:2738-2789`). `dshelper`/`dscli` are a users-and-groups directory (`dshelper.h:3`), not a session tracker.

## Design

**1. The key (libSystemConfiguration + configd).** Add `SCDynamicStoreKeyCreateConsoleUser`, `SCDynamicStoreCopyConsoleUser(store, &uid, &gid)` and `SCDynamicStoreSetConsoleInformation(store, user, uid, gid, sessions)` to `libSystemConfiguration`, backed by `State:/Users/ConsoleUser` (dictionary: `Name`, `UID`, `GID`, `SessionInfo` array) via the existing set/notify routines. Same schema as Apple so `DAMain.c`/`DABase.c` port unchanged. configd should restrict writers of `State:/Users/*` to uid 0 (the store has an audit/uid on `configset`? -- verify; if not, note it as a hole shared with every other key).

**2. The publisher.** Ship `/usr/libexec/consoleuser` (`set <name> [tty]` / `clear`, root only; ~80 lines, links SystemConfiguration). Gershwin's `LoginWindow` execs it after `pam_open_session` + fork and again after `waitpid` returns (cross-repo change in gershwin-components, `LoginWindow.m` around :1744/:2232; it is already root there). A CLI keeps LoginWindow free of a NextBSD-only link dependency and gives an admin/getty hook (`/etc/pam.d/login` `pam_exec` is possible later). Alternative considered: LoginWindow links SystemConfiguration under `#ifdef __NextBSD__`. nextbsd-userland#64 (no `scutil`) is related but not required.

**3. Daemon.** Port `DAMain.c:338-339,481-489,576-584` and `_DAConfigurationCallback` (`DAServer.c:879`): on login (`gDAConsoleUserList` becomes non-NULL, `:1087-1159`) mount every deferred disk (`:1150-1153` `DADiskMountWithArguments(... "automatic")`); on a uid leaving the console (`:970-1080`) unmount that uid's volumes with Force (`:1029-1035`, `:1048-1049`) and eject whole disks it owns (`:1076`, note Apple passes `kDADiskUnmountOptionForce` to `DADiskEject`); on no console user (`:1161-1290`) unmount deferred volumes (`:1231-1273`). Drop the SystemUIServer wait (`:903-951`, `:2997-3009`), the `/dev` node chown (`:1111-1136`; devfs rules are the FreeBSD tool, out of scope), the `.SoftwareUpdateAtLogout` and CoreStorage branches. Keep `DAStage.c:308` and `DAProbe.c:274` checks.

**4. Headless policy.** With no publisher (base image, getty login) deferral means removable media are never auto-mounted, which is exactly a Mac at its login window. Ship Apple's defaults; document `AutomountDisksWithoutUserLogin` (daemon preference plist read by `DAPreferenceListRefresh`, `DAServer.c:1183`) as the server knob. Internal media (`DAMountDeferInternal` false) still mounts at boot.

**5. Agent dialogs (decision + what is unknown).** Apple: `DADialog.c:33-76` sends an XPC message to `com.apple.DiskArbitration.DiskArbitrationAgent` (`DAAgent.h:33`) with `xpc_connection_set_target_uid(gDAConsoleUserUID)` (:72); the agent (`DiskArbitrationAgent/DiskArbitrationAgent.m:142`, ObjC) shows removal via `NSUserNotificationCenter` (`DADialog.m:62`, "Disk Not Ejected Properly" :41) and unreadable/unrepairable via `CFUserNotificationCreate` (:164, :271-276). On NextBSD: libxpc has `xpc_connection_create_mach_service` (`src/libxpc/xpc_connection.c:120`) but no `xpc_connection_set_target_uid`; there are no LaunchAgents or per-user bootstrap subsets to host an agent; CF installs `CFUserNotification.h` (`src/libCoreFoundation/Makefile:288`) with no implementation. Decision for this ticket: **no XPC agent**. The daemon keeps the three call sites (`DAServer.c:1738,1780` removal; `DAStage.c:527` unreadable; `:548` unrepairable) but `DADialogShow*` delivers a private callback kind (`_kDADiskAgentCallback`, action + disk, registered with a private `DARegisterDiskAgentCallback` in `DiskArbitrationPrivate.h` style) on the normal session queue, so any DA client in the console session can render it. Workspace (D3a) is that client: removal is also derivable client-side (DiskDisappeared with `DAVolumePath` still set), unreadable/unrepairable need the callback. Unknown, to be checked before an Apple-shaped agent is attempted: whether launchd 842's per-user sessions/`LaunchAgents` work on NextBSD at all, and whether libxpc can target a uid. If both become true, Apple's `DADialog.c` drops back in unchanged.

## Scope

libSystemConfiguration ~150 lines; `consoleuser` CLI ~80; daemon ~400 (port of the callback minus Darwin branches); Gershwin LoginWindow ~20 (separate PR, gershwin-components).

## Before starting

- [ ] C5 landed (there are automatic mounts to defer, owner uid used for msdos `-u/-g`)
- [ ] D1 + D2 landed (mount/unmount at login/logout use the request machine)
- [ ] verify configd delivers `SCDynamicStoreSetNotificationKeys` changes to a dispatch queue set with `SCDynamicStoreSetDispatchQueue` (`SCNotify.c:441`) from another process, and whether `configset` records the writer uid
- [ ] agree with gershwin-components that `LoginWindow` execs `/usr/libexec/consoleuser` on the NextBSD target (or links SC)

## Acceptance

- [ ] `consoleuser set alice` then `datest --console` (or `scutil`-style read) shows `State:/Users/ConsoleUser` = `{Name=alice, UID=1001, GID=1001}`; `consoleuser clear` removes it; a non-root caller is refused
- [ ] boot with a USB stick attached and no login: `datest --list` shows the partition with no `DAVolumePath`, log has "No console users yet, delaying mount"; after `consoleuser set alice` it mounts under `/Volumes/<name>` owned by alice
- [ ] `consoleuser clear` unmounts alice's volumes (Force) and the whole disk is ejected; the mount points are gone
- [ ] internal (non-removable, non-external) media mounts at boot regardless
- [ ] `AutomountDisksWithoutUserLogin = true` in the daemon preferences mounts the stick with no console user
- [ ] Gershwin NextBSD target: log in via LoginWindow -> stick mounts; log out -> unmounted (LoginWindow PR referenced from this issue)
- [ ] pulling a mounted stick delivers the removal agent callback (and DiskDisappeared with `DAVolumePath`) to a registered client; `datest --agent` prints "Disk Not Ejected Properly: <name>"; plugging an unformatted stick delivers the unreadable action

## Refs

Plan section 4.2 ("Defer", "Trust defaults"), 7 (D4), 9 (open question 4). Apple: `DAMain.c:63,82-85,338-339,481-489,576-584`, `DABase.c:683,761`, `DAMount.c:900-937,1497-1519`, `DASupport.m:1803-1805,1869-1874`, `DAServer.c:879-1290,1738,1780,2997-3009`, `DAStage.c:308,527,548`, `DAProbe.c:274`, `DADialog.c:33-121`, `DAAgent.h:33-41`, `DiskArbitrationAgent/DiskArbitrationAgent.m:47-142`, `DiskArbitrationAgent/DADialog.m:39-49,62,164,271-276`, `DiskArbitrationAgent/com.apple.DiskArbitrationAgent.plist`. NextBSD: `src/configd/config.defs:72-191`, `src/libSystemConfiguration/SCDynamicStore.c:432`, `SCNotify.c:36,441`, `src/libxpc/xpc_connection.c:120`, `src/libCoreFoundation/Makefile:288`, `overlay/System/Library/LaunchDaemons/org.nextbsd.getty.ttyv0.plist:92-94`, nextbsd-userland#64. Gershwin: `gershwin-components/LoginWindow/LoginWindow.m:1437,1559,1579,1744,2203,2232,2349,2738-2789`, `LoginWindowPAM.m:244,279,401`, `gershwin-desktop/targets/nextbsd/overlays/System/Library/LaunchDaemons/io.github.gershwin-desktop.loginwindow.plist`.

D5 · Drop patch 0008 (vfs.usermount=1) once Workspace's image mounts and user unmounts go through DiskArbitration -- or narrow it if AppImage self-mounting still needs it filed nextbsd-kernel#229

Reponextbsd-kernel
Labelsenhancement area:storage
Depends onD1, D2, D3a, D3c
ParentE15
## Summary

Remove `patches/0008-NextBSD-enable-unprivileged-mounts-vfs-usermount-1.patch` (line 8 of `patches/series`; it flips `sys/kern/vfs_mount.c` `static int usermount = 0` to `1` and `CTLFLAG_RW` to `CTLFLAG_RWTUN`, patch lines 56-59) when the reason it exists is gone. The patch's own text, and PR nextbsd-kernel#62 that merged it (2026-08-02, "kernel: enable unprivileged mounts (vfs.usermount = 1) so users can mount AppImages"), say: "This is an interim mechanism. The intended end state is macOS-shaped: Workspace asks diskarbitrationd to mount on the user's behalf, as it does for a .dmg, and no unprivileged-mount policy is needed at all. NextBSD already ships com.apple.DiskArbitration. When that lands this patch should be dropped." (patch :38-42; the PR adds "which is why it is a standalone series entry rather than folded into anything else").

## Why

- Unprivileged mounts are a system-wide policy the desktop needed only because `VolumeManager.m` runs `darling-dmg`/`fuseiso`/`squashfuse` as the user (`:440-441`, `:656-657`, `:819-820`) and `GWUnmountHelper` had no privileged unmount path. With D2 (unmount/eject via DA), D3a (Workspace calls DA) and D3c (image mounts via DA), the desktop no longer needs it.
- The kernel already forces `MNT_NOSUID|MNT_USER` on unprivileged mounts (patch :32-34 citing `vfs_mount.c:1644-1648`), but the policy also lets any user mount anything the fs allows anywhere they own; DA's model (root daemon, untrusted media `nosuid,nodev,noowners`, `/Volumes` owned by the OS) makes that unnecessary.

## Design

- Delete the patch and its `series` line; `sysctl vfs.usermount` reads 0 on the next kernel.
- `RWTUN` is the only thing an admin loses; if someone wants the old behaviour, `vfs.usermount=1` via `/etc/sysctl.conf` works because launchctl applies it at bootstrap (`launchctl.c:2326`; the plan's section 8 notes the patch "may not have needed to be a kernel patch"). Document that in the commit.
- **Not affected:** Gershwin's stock-FreeBSD target keeps `vfs.usermount=1` in `gershwin-system/Library/Scripts/SystemPrepare.sh:781`; that is their sysctl, not this patch.

## Before starting

- [ ] D3a merged: no `sudo` and no user-run `umount` on the NextBSD target
- [ ] D3c decided and its `.iso` prototype merged; **and** D3c's answer on AppImage: kernel#62's title says the patch exists "so users can mount AppImages" (Refs #60, #61: FUSEFS baked in). If AppImage runtimes still self-mount via FUSE as the user (nextbsd-userland#460 lists the end-to-end unprivileged mount as untested; `linux_file.c:1108-1132` translates Linux `mount("fuse.*")`), this ticket becomes "keep 0008, or narrow it to fusefs only", not "drop". Record the decision on the epic.
- [ ] CI image boots and the Gershwin NextBSD target's mount/eject flow passes with `vfs.usermount=0` set in `loader.conf` on a patched kernel (dry run before deleting the patch)

## Acceptance

- [ ] `patches/0008-...` removed, `patches/series` has no line 8 entry for it, kernel builds
- [ ] `sysctl vfs.usermount` = 0 on the CI boot log
- [ ] as a non-root user on the Gershwin NextBSD target: mounting a `.iso` from the desktop, ejecting a USB stick and unmounting a network volume all still work (through DA)
- [ ] `mount -t tmpfs tmpfs ~/x` as a user fails with EPERM (policy actually off)
- [ ] either an AppImage still runs (D3c path) or this issue is closed with the narrowed/kept decision and a pointer to why

## Refs

Plan section 5 (FUSE for AppImage), 7 (D5), 8 (sysctl.conf correction). nextbsd-kernel `patches/0008-NextBSD-enable-unprivileged-mounts-vfs-usermount-1.patch:4,26-29,32-34,38-44,56-59`, nextbsd-kernel#62 (merged PR), #60, #61. nextbsd-userland `launchctl.c:2326`, #460. gershwin-workspace `Workspace/VolumeManager.m:440-441,656-657,819-820`; gershwin-system `Library/Scripts/SystemPrepare.sh:780-781`.

Existing issues

Migrate under E15

IssueTitleAction
nextbsd#467Live media: launchctl's boot-time mount -a tries to union-mount ROOTFS on / (fstab lists root)Move under E15 as the bug A1 closes: add area:storage (keep area:boot, area:live-media), reference the epic and the plan (section 3), and note in a comment that the fix is A1 (stop shipping the root line -- option A no fstab, or B comment-only) rather than #467's own option A (`noauto`), because a root line is never fstab's job on Darwin and the file is the only reason `mount -vat nonfs` runs at all. Its acceptance line 3 (non-root fstab entries still mount on live and disk) stays valid for admin-created fstabs and is the test B1 relies on.

Link, do not migrate

These stay where they are. Cross-link them from E15.

IssueEpicWhy it relates
nextbsd-userland#171E13The user-visible symptom (no /compat/linux mounts on a stock image) is real, but the title and premise are wrong: launchctl.c:2400-2405 runs `mount -vat nonfs` whenever /etc/fstab exists, which is what its own reboot evidence shows. Suggested retitle: "Stock image ships no Linux ABI mounts -- /compat/linux is unmounted until an admin adds the five fstab lines". B1 is the answer under no-fstab; the proposed org.nextbsd.mount `mount -a` daemon is redundant with launchctl bootstrap.
nextbsd-userland#54E13The design record and the maintainer decision (option H, mounts stay manual). B1 documents H under no-fstab; B2 is its option A/B rewritten for a launchd where WatchPaths is inert and `kickstart` does not exist. Stays open as the record; its option F ('fstab -- rejected: nothing runs mount -a') and option B ('WatchPaths verified present') need the corrections below.
nextbsd#460E13AppImage support and the packaging side of mount provisioning. Its recorded fstab block is the compat set B1 documents, minus the `lindebugfs ... late` line (never mounted by plain `mount -a`) and with the wrong header. The FUSE/vfs.usermount half stays in E13; D5 (drop patch 0008) is the E15 side of that.
nextbsd#461E13The E13 epic. Its 'Why' paragraph repeats 'nothing ever runs mount -a'; Stage B of E15 is the mount-provisioning answer E13 routes through #54 for. Cross-link both epics; do not move #54/#171/#460 out of E13.
nextbsd#278E9Its locked design's 'Lever D' is a WatchPaths daemon over /usr/local/libdata/ldconfig. WatchPaths is inert on this launchd (launchctl.c:1611-1648 rewrites it to fsevents LaunchEvents nothing delivers; core.c:322-332 has no PATH_* semaphore), so Lever D's own stated fallback (pkg trigger) is the path. Same finding B2 relies on; not an E15 child.
nextbsd-kernel#62E13Merged PR for patch 0008 (vfs.usermount=1). It says 'when [DiskArbitration mounting] lands, this patch should be dropped' -- that is E15 D5, which depends on the D-stage. Its rationale line '/etc/sysctl.conf is never read' is wrong (launchctl.c:2326), though the loader.conf/RWTUN and reaches-installs-on-upgrade arguments for a kernel default still stand. Not listed in #461's PR list; AppImage prerequisite.
gershwin-desktop#49external (Gershwin)The user-visible report of the live-media `Cannot union mount root filesystem` / `fwexec(mount_tool)` boot noise; the maintainer comment there already calls it noise from the fstab remount. Fixed by A1 via nextbsd#467; link so the reporter sees the fix land.
gershwin-desktop#60external (Gershwin)Asks that live-ISO volumes mount under /Volumes instead of /sysroot/media (vermaden MNT_PREFIX on the FreeBSD target). On NextBSD /Volumes is owned by diskarbitrationd (Stage C5: create and sweep /Volumes at startup), not by an automount prefix setting.
issues#22external (Gershwin)'Mount points for removable media' on GhostBSD, with a kqueue/EVFILT_FS watcher prototype and the report that VQ_MOUNT delivery was 'not working'. That is the open question behind C3 (adopt mounts DiskArbitration did not make) and D3 (post NSWorkspaceDidMount as a distributed notification).
issues#26external (Gershwin)'Automounter' -- proposes vermaden/automount plus a small tool posting distributed notifications to Workspace. Plan section 6 recommends against vermaden on NextBSD (needs devd, no nosuid/nodev, fsck -y on plug, deletes empty dirs under the prefix) and answers the notification half with D3. Link, do not adopt.
gershwin-workspace#78external (Gershwin)Drag-and-drop install (and disk images both ways) needs a device/partition/volume model and unmount/eject with dissenters: C2 (DADisk from GEOM), D1 (client framework), D2 (unmount/eject). Consumer, not a child.
https://pkgdemon.github.io/nextbsd-e14-tickets.html (E14 swap tickets, K3 and U1)E14K3's acceptance line 'an fstab entry for /private/var/vm/swapfile0 activates via swapon -a with no md device' should become '`swapon /private/var/vm/swapfile0` succeeds with no md device': under E15 there is no fstab and swapd (U1) is the activation path. U1's 'nothing under launchd runs swapon -a (#171)' is correct as stated, but the epic body's 'launchd runs no rc.d, so a swap entry in fstab is inert' should read 'nothing runs swapon -a' -- mount -a does run (launchctl.c:2400-2405), swapon -a does not.

Suggested correction comments

Existing issues whose stated facts are wrong per the plan’s §8. Post these as comments (or fold them into the issue body) when E15 is filed.

nextbsd-userland#171 posted

Answering the three open questions from the follow-up comment, from the code on `main` (nextbsd-userland@f035d62): (1) the component is `launchctl bootstrap -S System` itself -- `support/launchctl.c:2400-2405` runs `mount -vat nonfs` inside `if (path_check("/etc/fstab"))`, Apple's launchd-842 code; there is no mount job in `LaunchDaemons/` and none is needed. (2) It is plain `-a` (with `-v` and `-t nonfs`), never `-l`/`-L`, so FreeBSD `sbin/mount/mount.c:307-310` skips every line carrying `late`; the entries you added without `late` are the right shape. (3) Yes, the remaining gap is only that the stock image ships no compat lines -- and under the E15 plan (section 3) the image will ship no fstab at all, so the admin path becomes "create `/etc/fstab` with the five lines" (E15 B1). Two smaller things: verification step 4 (a tmpfs on `/tmp/scratch`) will appear to fail even though it mounts, because `launchctl.c:2432-2433` force-unmounts everything under `/var/run` and `/tmp` one step after `mount -a` (`empty_dir()`, `:4246`, `unmount(..., MNT_FORCE)` at `:4289`); and the fstab header comment this issue quoted is being fixed by E15 A1/A4. Suggested retitle: "Stock image ships no Linux ABI mounts -- /compat/linux is unmounted until an admin adds the five fstab lines". Not verified on a running box: only your own reboot evidence, which matches the code.

nextbsd#460 posted

Two facts in the recorded fstab block are wrong per the E15 plan (section 8). The header says nothing runs `mount -a`: `launchctl bootstrap` does (`nextbsd-userland support/launchctl.c:2400-2405`, `mount -vat nonfs` whenever `/etc/fstab` exists), which is why #171's reboot test worked. And `lindebugfs /compat/linux/sys/kernel/debug lindebugfs rw,late` will never mount: launchctl passes plain `-a`, and FreeBSD `sbin/mount/mount.c:307-310` skips `late` lines unless `-l`/`-L` is given, which nothing on NextBSD does -- so the 'settle late vs non-late' item resolves to 'never `late`'. (Whether `LINDEBUGFS` is even compiled into the NextBSD kernel was not checked; `rc.d/linux` does not mount it either.) The 'ordering under mount -a needs checking' note also resolves: `mount -a` walks `getfsent()` in file order (`mount.c:300-323`), so `devfs` before `fdescfs` is guaranteed by line order. The canonical block is being written up as E15 B1 (`nextbsd-userland/docs/linux-abi-mounts.md`); suggest marking this block superseded by it once that lands.

nextbsd-kernel#62 posted

For the record (no change requested; the patch is merged): the rationale line '`/etc/sysctl.conf` is never read -- launchd is PID 1 and there is no `rc.d`' is not right. `launchctl bootstrap` applies it: `nextbsd-userland support/launchctl.c:2326` calls `apply_sysctls_from_file("/etc/sysctl.conf")`, which runs one `sysctl -w <line>` per non-comment line (`:4330-4365`). Today it does nothing only because `nextbsd-overlays` ships no `sysctl.conf`. So `vfs.usermount=1` could have been a sysctl.conf line; the stronger arguments in this PR still hold -- a kernel default reaches installed systems on `pkg upgrade` while the overlay is seed-once, and `RWTUN` gives a `loader.conf` off switch. Caveat: that `sysctl.conf` really applies at runtime has not been verified on a running box, only read from the code. E15 D5 is the 'drop this patch when DiskArbitration owns Workspace's mounts' item this PR asks for.

nextbsd-kernel#68 posted

Same note as on #62, for the record only: 'NextBSD ... never reads `/etc/sysctl.conf`' is not accurate -- `launchctl bootstrap` applies the file (`nextbsd-userland support/launchctl.c:2326`, `apply_sysctls_from_file`, `:4330-4365`), so `kern.elf64.fallback_brand=3` could also have lived there. The kernel default is still the better choice for the reasons the PR gives (reaches installs on upgrade, `RWTUN` loader.conf override, and the overlay that owns `/etc` is seed-once). Runtime application of sysctl.conf is unverified on a running box. E15 B2 (parked) keeps rc.d/linux's 'set only if -1' guard so a `loader.conf` override is never clobbered.

nextbsd#231 posted

Informational, on a closed ticket: the Background paragraph says 'launchd (PID 1) bypasses `rc.d` (so `/etc/sysctl.conf` is never applied)'. It is applied -- `launchctl bootstrap` runs `apply_sysctls_from_file("/etc/sysctl.conf")` (`nextbsd-userland support/launchctl.c:2326`, implementation `:4330-4365`), Apple's own launchd-842 behaviour; the overlay simply ships no such file. The kernel-default patch (0005) was still the right call because a kernel default survives `pkg upgrade` and the seed-once overlay would not have delivered a new sysctl.conf to installed systems. Not verified at runtime, only in the code. Noting it so the 'never read' claim is not copied into further tickets (it already reached kernel#62 and kernel#68).

nextbsd-userland#54 posted

Two corrections to the options table, from the code on `main`, neither of which changes the option H decision. Option B: `WatchPaths` is not 'verified present' -- it is inert on this port. `support/launchctl.c:1611-1648` (`distill_fsevents()`) strips `WatchPaths`, `QueueDirectories` and `KeepAlive.PathState` from the plist and rewrites them as `com.apple.fsevents.matching` LaunchEvents, which is why the `LAUNCH_JOBKEY_WATCHPATHS` parse could not be found in `core.c`; and `semaphore_reason_t` in `src/core.c:322-332` has no `PATH_EXISTS`/`PATH_MISSING`/`PATH_CHANGES` members (only `NETWORK_*`, `*_EXIT`, `CRASHED`/`DID_NOT_CRASH`, `OTHER_JOB_*`). Nothing in the tree provides that event stream, so open question 1 is answered: no. Option F: 'nothing runs `mount -a`' is wrong -- `launchctl bootstrap` runs `mount -vat nonfs` whenever `/etc/fstab` exists (`launchctl.c:2400-2405`), which is what #171's reboot evidence shows; fstab is therefore the admin hook, not a rejected option, and under the E15 plan (section 5, option H) the documented manual path becomes 'create `/etc/fstab` with the five compat lines, never `late`' (E15 B1). Also, this launchctl has no `kickstart` subcommand (`launchctl.c:233-264`); option A's re-run command is `launchctl start <label>`. The Darwin-shaped upgrade (an `org.nextbsd.linux` one-shot, E15 B2) is parked until Linux support becomes default, per this decision.

nextbsd#278 posted

On 'Lever D -- launchd `WatchPaths` daemon': the parenthetical fallback is the path, because `WatchPaths` activation is not ported. `nextbsd-userland support/launchctl.c:1611-1648` (`distill_fsevents()`) rewrites `WatchPaths`/`QueueDirectories`/`KeepAlive.PathState` into `com.apple.fsevents.matching` LaunchEvents, and nothing on NextBSD delivers those: `semaphore_reason_t` in `src/core.c:322-332` has no `PATH_*` members and the only file mentioning `com.apple.fsevents` is `launchctl.c` itself. A plist with `WatchPaths` loads and simply never fires. So the ldconfig-on-package-change step needs the pkg trigger (or an explicit `launchctl start`), not a path watch. Same finding recorded for nextbsd-userland#54 option B; see the E15 plan, section 8.

Recreating in GitHub

File the epic first, so that each child can name its parent. Then file the children in dependency order, and replace the A1D5 placeholders in the epic’s checklist with the real issue numbers.

# 1. the label, in each NextBSD repo
gh label create area:storage --color 8A6A3F \
  --description "Disks, volumes, mounts, fstab and DiskArbitration" \
  --repo nextbsd/nextbsd-userland        # repeat for each repo

# 2. any single ticket: copy its body from this page into a file
gh issue create --repo nextbsd/nextbsd-userland \
  --title "..." --label enhancement,area:storage --body-file C1.md

# 3. or all of them, from the embedded JSON
curl -s https://pkgdemon.github.io/nextbsd-e15-tickets.html | python3 -c '
import sys, re, json
page = sys.stdin.read()
m = re.search(r"<script type=\"application/json\" id=\"e15-data\">(.*?)</script>", page, re.S)
print(json.dumps(json.loads(m.group(1)), indent=1))' > e15.json

A script working from e15.json needs only the fields shown on this page: id, title, repo, labels, depends, body.