E14 ticket drafts — VM, paging and filesystems Filed 2026-09-20
Every issue for EPIC E14, written out in full — and now filed. This page is the companion to NextBSD swap and paging without a partition. It records the epic, the new area:vm label, all 23 child tickets and one migration: title, target repo, labels, dependencies, and the exact Markdown body for each. Filed on 2026-09-20 as epic nextbsd#468 and 23 child issues, attached to it as GitHub sub-issues. The bodies below are exactly what was filed; each issue also carries a footer naming its parent, its dependencies by issue number, and a link back here. The plan's ticket map is generated from the same source as this page, so the two cannot drift apart.
TL;DR
23 child tickets across four repos, plus the epic nextbsd#468 and the area:vm label, created in all four repos.
Tracks: K = kernel swap pager and dumper · Z = ZFS swapify · P = platform dump paths · U = userland · S/X = spike and upstream report.
All preconditions are done — Q3, R7 and the labels; see Preconditions. R7 added two pin-time requirements to K1 (nextbsd-kernel#214).
Order: K1 -> K2 / K3 / K4 / K7 -> K5 / K6 -> U1 -> U2. Z0 first for everything ZFS, then Z2 and Z1 (after K6) in parallel, then Z3 -> Z4. K8, Z5, S1 follow K1. U4 after Z0 and U2, once its design is refined. P2, U3 and X1 are independent.
Machine-readable: every field on this page is also embedded as JSON (<script id="e14-data">), so the issues can be recreated by script as well as by hand.
Preconditions
1. Q3 — done 2026-09-20: no local changes. none of the 50 patches in nextbsd-kernel (at ddad2c1) touch sys/vm/, sys/dev/md/, UFS/FFS, the VFS layer, GEOM, kern_shutdown.c or the bundled OpenZFS, and src-overlay/ only adds new files. The kernel builds from releng/15.1 (FREEBSD_BRANCH in .github/workflows/build.yml), where the VFCF_NETWORK test K1 relaxes is at swap_pager.c:2680. K1 is unchanged.
2. R7 — done 2026-09-20: soft-updates cannot see bypass writes. On releng/15.1, soft-updates' write hook (buf_start on a buffer's b_dep) and UFS snapshot copy-on-write (ffs_copyonwrite, called only at ffs_vfsops.c:2366 and :2378) both live in ffs_geom_strategy (ffs_vfsops.c:2334), so they run only for writes through the buffer cache; a bypass bio has no buffer and no dependency list. fsync(MNT_WAIT) flushes every inode dependency, every dirty buffer and then softdep_fsync, looping until nothing is dirty or in flight (ffs_vnops.c:226-258), and SU+J takes the same path (DOINGSOFTDEP). Two requirements follow, now in K1: fsync(MNT_WAIT) then vinvalbuf() at pin time, and documenting that UFS snapshots hold live swap contents for the swapfile.
3. Labels — done 2026-09-20.area:vm was created in all four repos, including nextbsd-freebsd-compat, which previously had no area:* labels at all.
Label
Name
area:vm
Description
Virtual memory, swap, paging, memory pressure, crash dumps, and the filesystems they back
## Summary
FreeBSD refuses `swapon(2)` on a local regular file. `sys_swapon()` accepts `VREG` only when the mount is `VFCF_NETWORK` (NFS), so local swapfiles go through `md(4)`, whose vnode path deadlocks under memory pressure.
Add a swapfile backend that resolves the file to a physical extent map **at swapon time** and writes raw bios to the underlying GEOM provider. After `swapon`, the filesystem is not on the I/O path.
## Why
Today's path: `mdstart_vnode` -> `vn_start_write` -> `vn_lock` -> `VOP_WRITE` -> `ffs_write` -> `getblk`/`allocbuf` -> `vm_wait`. The md kthread is not `pageproc`, so it gets no reserve and sleeps behind the pagedaemon, which is waiting on this very bio.
## Design
- `sys/vm/swap_pager.c:2680` (`releng/15.1`, the NextBSD kernel base) -- relax the `VFCF_NETWORK` test for filesystems that return a full, hole-free block map.
- At swapon, walk `VOP_BMAP` (UFS: `ufs_bmaparray`) as Linux's `generic_swapfile_activate()` does. Reject holes.
- Store the extents in `struct swdevt`.
- New `sw_strategy_t swapfile_strategy()`: translate `bp->b_blkno` through the extent table, `g_new_bio()` (non-sleeping), `g_io_request()` on a GEOM consumer of the mount's provider (`VFSTOUFS(mp)->um_cp` for UFS).
- Everything upstream in `swap_pager_putpages()` is already allocation-free: `blist` bitmap, preallocated `swwbuf` pbuf, `M_NOWAIT` metadata. **Only `sw_strategy` changes.**
- **At pin time** (from R7): `VOP_FSYNC(vp, MNT_WAIT)` after preallocation, then `vinvalbuf(vp, 0, ...)` before building the extent map. The fsync leaves no soft-updates dependency pending; the invalidate drops the file's cached buffers *and* pages (`bufobj_invalbuf` calls `vm_object_page_remove`), so no stale cached copy can mask blocks the bypass writes. `VV_SWAP` (K2) then stops new dirty buffers appearing
- **UFS snapshots** (from R7): bypass writes skip `ffs_copyonwrite`, so a snapshot's image of the swapfile holds live swap contents instead of what it held when the snapshot was taken. That is harmless -- nothing that uses snapshots (`dump -L`, background `fsck`) interprets swap or dump data -- so document it rather than refuse `swapon` while a snapshot exists
## Scope
~500-800 LOC. Low risk: the same device path swap partitions use today.
## Before starting
- [x] **Q3:** done 2026-09-20 -- no patch in `nextbsd-kernel` touches `swap_pager.c`, `md.c`, UFS/FFS, VFS, GEOM or OpenZFS, so this ticket is written against stock `releng/15.1`
- [x] **R7:** done 2026-09-20 -- soft-updates cannot see bypass writes, and nothing is left pending on a synced file. On `releng/15.1`, soft-updates' write hook (`buf_start` on a buffer's `b_dep`) and UFS snapshot copy-on-write (`ffs_copyonwrite`, called only at `ffs_vfsops.c:2366` and `:2378`) both live in `ffs_geom_strategy` (`ffs_vfsops.c:2334`), the strategy routine for FFS's device vnode -- so they run only for writes that go through the buffer cache. A bypass bio has no buffer and no dependency list. `fsync(MNT_WAIT)` flushes every inode dependency (`softdep_sync_metadata`), every dirty buffer, then `softdep_fsync`, and loops until nothing is dirty or in flight (`ffs_vnops.c:226-258`); the guard is `DOINGSOFTDEP`, so SU+J takes the same path
## Acceptance
- [ ] `swapon /private/var/vm/swapfile0` on UFS succeeds with no `mdconfig`
- [ ] `swapinfo` lists the file
- [ ] a larger-than-RAM allocation test pages out through the file without hanging
- [ ] the same test on a soft-updates root with an active UFS snapshot; `fsck` is clean afterwards
- [ ] `swapon` rejects a file with holes
## Refs
Plan sections 2, 3. `sys/vm/swap_pager.c:2680` on `releng/15.1`.
K2 · VV_SWAP: refuse write, truncate and unlink on a pinned swap or dump file ready
## Summary
Once a file is pinned, its extents must never move or be freed -- the kernel holds raw block addresses. Add a vnode flag `VV_SWAP` (in `v_vflag`), set at swapon/pin time.
## Design
With `VV_SWAP` set, return `ETXTBSY` from:
- `ffs_write`
- `ffs_truncate` / `VOP_SETATTR(va_size)`
- `ufs_remove`, rename
- `vop_allocate` / `vop_deallocate`
Mirrors xnu `VSWAP` (`bsd/sys/vnode_internal.h:301`) and Linux `S_SWAPFILE` (`-ETXTBSY` in `fs/read_write.c`, `fs/open.c`).
Note: `ffs_reallocblks` only relocates *dirty, not-yet-written* clusters, so a fully synced file does not move.
## Acceptance
- [ ] write, truncate and unlink on a pinned file fail with `ETXTBSY`
- [ ] after `swapoff` / unpin they succeed again
K3 · swapon(8): stop creating an md(4) device for local swapfiles ready
## Summary
`swapon(8)` currently runs `mdconfig -a -t vnode -f` for file entries (`sbin/swapon/swapon.c:417`, `swap_on_off_md`; fstab form `md none swap sw,file=...`). With K1 in place, call `swapon(2)` on the path directly.
`sbin/swapon` is currently **DEFER** in the srclist ("no swap configured"). This ticket brings it into the build.
## Acceptance
- [ ] `swapon /private/var/vm/swapfile0` activates the file directly, with no md device. Test it this way rather than through `/etc/fstab`: E15 stops shipping fstab (A1, nextbsd/nextbsd-overlays#4), and `swapd` (nextbsd/nextbsd-userland#178) is the activation path
- [ ] `mdconfig -l` is empty afterwards
## Note
This repo has **no `area:*` labels**. Create `area:vm` here before filing.
K4 · File-backed kernel dumper: write crash dumps through a pinned file's extent map ready
## Summary
Crash dumps without a dump device. **No `struct dumperinfo` change is needed**: netdump already registers `mediaoffset = 0`, `mediasize = 0` and ignores offsets entirely (`sys/netinet/netdump/netdump_client.c:584` and `:745`).
## Design
New `sys/kern/kern_dumpfile.c` (~400 LOC):
- obtain the leaf `dumperinfo` plus `extent[]` through a new `VOP_DUMPCTL(PIN|UNPIN)` (illumos analogue)
- register a wrapper with `dumper_insert(&di, "file:<path>", kda)`: `mediaoffset = 0`, `mediasize` = file length, `blocksize = 4096`, `maxiosize` = min leaf `d_maxsize`
- the `dumper` callback locates the extent and **splits at extent boundaries** -- mandatory, because arm64 `minidump_machdep.c` writes unaligned chunks via `blk_write` -- then calls the leaf `d_dump` with `leaf.mediaoffset + phys`
- the `(NULL, 0, 0)` flush fans out to every leaf
- unregister on unmount
EKCD encryption and zstd compression layer above `dump_write` and need no changes.
## Acceptance
- [ ] `dumpon /private/var/vm/kernelcore` registers
- [ ] a panic in a VM (`sysctl debug.kdb.panic=1`) followed by `savecore` (K5) recovers a valid vmcore
## Refs
Apple reference design: `kern_open_file_for_direct_io()` in `bsd/kern/kern_symfile.c`.
## Summary
`savecore` requires `DIOCGMEDIASIZE`/`DIOCGSECTORSIZE` (`sbin/savecore/savecore.c:975-977`) and reads the trailing header at `mediasize - sectorsize`. Add a regular-file mode to both tools.
`dumpon` is **DEFER** and `savecore` is **DROP** in the srclist -- this brings both back.
## Design
- `savecore`: `fstat()` for `st_size`; fixed 4096-byte sector size for `S_ISREG`; extract to `/private/var/crash` (~150 LOC)
- `dumpon`: an `S_ISREG` path routes `DIOCSKERNELDUMP` through `VOP_IOCTL` (~100 LOC)
- `savecore -c` clear-write must land in place: UFS `write(2)` is in place; ZFS must take the raw path (Z3)
## Acceptance
- [ ] full panic -> `savecore` round trip on a UFS VM
## Note
This repo has **no `area:*` labels**. Create `area:vm` here before filing.
K6 · g_part: allow GEOM::kerneldump on freebsd-ufs/freebsd-zfs partitions for the file dumper ready
## Summary
`sys/geom/part/g_part.c:2295-2316` refuses `GEOM::kerneldump` unless `G_PART_DUMPTO` passes, and `g_part_gpt_dumpto` (`g_part_gpt.c:803-811`) returns 1 only for swap-type GUIDs. The file dumper (K4) and ZFS dumpio (Z1) need the leaf `dumperinfo` of the partition that holds the root filesystem.
## Design
~15 LOC: permit the attribute when the requester is the file-dumper facility.
## Acceptance
- [ ] K4 registers on a `freebsd-ufs` partition
- [ ] Z1 caches a leaf `dumperinfo` on a `freebsd-zfs` partition
K7 · Swap low-space event: surface swp_sizecheck transitions to userland via devctl ready
## Summary
`swp_sizecheck()` flips `swap_pager_almost_full` at `nswap_lowat` (128 pages) / `nswap_hiwat` (512 pages) and only `printf`s -- there is **no userland event**. Without one, `swapd` (U1) must poll.
## Design
- `EVENTHANDLER_DECLARE(swap_lowspace, ...)`, invoked on `swp_sizecheck()` transitions
- deliver via `devctl_notify("VM", "swap", "LOWSPACE", ...)` (devd-consumable)
- make `nswap_lowat` / `nswap_hiwat` sysctls
- optional, Darwin-faithful: have `mach.ko` carry a `default_pager_space_alert`-shaped message to a registered port
## Scope
Days. Phase 3 (Grow) of the plan depends on it.
## Acceptance
- [ ] `devd` receives `LOWSPACE` when free swap crosses `nswap_lowat`
K8 · Defence-in-depth VM privileges: TDP_VMPRIV, sync dispatch for swap objects, ARC pageout throttle ready
## Summary
The cheap parts of the memory-reserve route, taken as **defence in depth only** -- not as the correctness argument. The bypass design (K1, Z3) is what makes swap safe.
## Why only the cheap parts
illumos built the full reserve design (`NOMEMWAIT()`, `KM_PUSHPAGE`, a ZFS-sized `pageout_reserve`, the ARC throttle) and still ships `pageout_deadman`, which panics after 90 s of stuck pageout. On FreeBSD it is weaker again:
- `KM_PUSHPAGE` is `#define`d to `M_WAITOK` (`include/os/freebsd/spl/sys/kmem.h:49`)
- `arc_memory_throttle()` is `return (0);` (`module/os/freebsd/zfs/arc_os.c:124`)
## Design
- `TDP_VMPRIV` bit in `td_pflags` plus a `vm_thread_privileged()` inline (`curproc == pageproc || (td->td_pflags & TDP_VMPRIV)`) at the ~10 existing `curproc == pageproc` sites (~300-500 LOC in `sys/vm`)
- synchronous dispatch for swap objects (no taskq hop)
- the `arc_memory_throttle` pageout branch, as Linux and illumos have
## Acceptance
- [ ] a thread marked `TDP_VMPRIV` allocates at `VM_ALLOC_SYSTEM` and waits pageproc-style
Z0 · kernel: compile ZFS into NEXTBSD (options ZFS, no module tree) and ship the ZFS userland ready
## Summary
NextBSD cannot use ZFS at all today. `config/NEXTBSD` has no `options ZFS`, stock FreeBSD builds ZFS only as `zfs.ko`, and NextBSD ships no module tree -- the same situation E15's C7 describes for UDF and LIBICONV. Compile ZFS into the kernel, and ship the userland that drives it.
## Why
Every ZFS ticket in E14 (Z1-Z5), the ZFS path of U2 (nextbsd/nextbsd-userland#179), and the installer's ZFS option need a kernel that can create and import a pool. Nothing ZFS works without this.
## Kernel
- add `options ZFS` to `config/NEXTBSD`. On `releng/15.1` it is defined at `sys/conf/options:279`, and **222 sources in `sys/conf/files` are `optional zfs`** (216 plain, 5 `optional zfs | dtrace`, 1 `optional zfs zstdio`)
- `ZSTDIO` is already enabled through `std.arm64`, which covers the one `zfs zstdio` source
- crypto acceleration for native encryption: check whether ZFS's ICP uses `armv8crypto` (arm64) / `aesni` (amd64) or its own implementations
- record the kernel size delta
## Boot
- the kernel mounts the root dataset from `vfs.root.mountfrom=zfs:zroot/ROOT/default`
- confirm NextBSD's loader can read ZFS on both layouts: gpt, and the Pi's mbr-fat. FreeBSD builds `loader.efi` with ZFS support by default; verify for NextBSD's boot chain rather than assume it
## Userland (lands in nextbsd-freebsd-compat)
The image does not ship ZFS userland today. The srclist keeps `zfs`/`zpool`/`zdb` **only inside the `/rescue` crunchgen**, and marks `sbin/zfsbootcfg` **DROP -- "UFS-only boot"**. Bring in `sbin/zfs`, `sbin/zpool`, `zdb`, `libzfs` and its dependencies, and reverse the `zfsbootcfg` decision.
## Not in this ticket
Importing pools and mounting their datasets at boot under launchd, with no `/etc/fstab` -- that is E15, nextbsd/nextbsd-userland#200.
## Acceptance
- [ ] NEXTBSD builds with `options ZFS` on arm64 and amd64 in CI
- [ ] `zpool create` / `zpool import` / `zfs create` work on a virtio disk in the CI VM
- [ ] a system boots from a ZFS root (`zroot/ROOT/default`) on the gpt layout
- [ ] the mbr-fat (Pi) result is recorded, supported or not
- [ ] kernel size delta recorded
Z1 · vdev_op_dumpio for vdev_geom, mirror and raidz ready
## Summary
OpenZFS removed the illumos dump I/O operation -- there is no `vdev_op_dumpio` in `include/sys/vdev_impl.h`. Restore it; it is the raw-I/O leaf that both swap (Z3) and dumps use.
## Design
- add `vdev_op_dumpio_t` to `vdev_ops_t`, `NULL` in every initializer (~40 LOC)
- `vdev_geom_dumpio` in `module/os/freebsd/zfs/vdev_geom.c` (~150 LOC): cache the leaf `dumperinfo` via `g_io_getattr("GEOM::kerneldump")` at open (needs K6); add `VDEV_LABEL_START_SIZE`; at panic call the cached `di.dumper` directly (`g_up`/`g_down` are not running), otherwise `vdev_geom_io()`
- mirror: port illumos `vdev_mirror.c:786-813` (~40 LOC)
- raidz: port illumos `vdev_raidz.c:1680-1769` onto `raidz_row_t`, refuse during expansion (~120-200 LOC). **Can be deferred** -- single disk and mirror first.
- dRAID: unsupported
## Acceptance
- [ ] dumpio round trip on a single-disk pool and a mirror pool in a VM
## Summary
~20 LOC, from illumos `dmu.c:1251-1269`: `dmu_buf_hold_array` plus `dmu_buf_will_not_fill()` on each dbuf. Allocates real DVAs **without writing data** (DB_NOFILL).
## Why it is safe to restore
The primitive is live code in OpenZFS, not a vestige:
- `dmu_buf_will_not_fill()` at `module/zfs/dbuf.c:2973`, called internally at 3153 and 3194
- the DB_NOFILL write path at `dbuf.c:5500-5501` still asserts `zp_checksum == ZIO_CHECKSUM_OFF || zp_checksum == ZIO_CHECKSUM_NOPARITY`
## Acceptance
- [ ] preallocating a 1 GiB object on a `checksum=off` dataset yields fully allocated, non-gang level-0 blocks
Z3 · Swapify: pin a ZFS file object -- property freeze, preallocation, DVA extent walk, DMU bypass ready
## Summary
Port illumos dumpify to a ZPL file object, in a new `module/zfs/zfs_dump.c` (~600-800 LOC). This one mechanism feeds **both** K1 (swap) and K4 (dumps) on ZFS.
## How illumos handles copy-on-write
It does not make ZFS COW-safe; it makes COW irrelevant. While pinned, **every** read and write takes the dumpio bypass, so nothing ever dirties the object through the DMU, so no block pointer is rewritten, so the captured DVAs stay valid.
## Design
Port from `usr/src/uts/common/fs/zfs/zvol.c`:
| illumos | lines |
|---|---|
| `zvol_dump_init` | 1908-2064 |
| `zvol_dumpify` | 2067-2120 |
| `zvol_get_lbas` | 315-334 |
| `zvol_map_block` | 261-301 |
| `zvol_dumpio` | 1134-1170 |
- `ZVOL_OBJ` becomes `zp->z_id`
- require dataset `zroot/private/var/vm` with `checksum=off compression=off dedup=off copies=1 recordsize=128K`, no encryption
- `traverse_dataset` filtered on `zb_object`; coalesce adjacent DVAs; **reject `BP_IS_GANG`** (`EFRAGS`)
- all I/O while pinned goes through dumpio -- no DMU, tx, ARC or ZIL -- which keeps the ARC coherent
- extent array preallocated: no allocation at panic
- rebuild the map every boot; never cache it across boots
## Acceptance
- [ ] `swapon` and `dumpon` a file in `zroot/private/var/vm`
- [ ] larger-than-RAM paging test completes
- [ ] VM panic round trip recovers a vmcore
Z4 · DSL guards for pinned objects -- and skip, not fail, them in recursive snapshot/send ready
## Summary
Operations that would move, share or reinterpret a pinned object's blocks must be refused while it is pinned.
## Refuse, with a clear error
- a snapshot aimed **directly** at the pinned dataset
- clone
- device removal (indirect vdevs remap DVAs)
- raidz expansion (relocates data)
- block cloning / BRT (`FICLONE` on the pinned file)
- `zfs destroy` of the dataset
## Skip, do not fail
`zfs snapshot -r` is atomic. If a pinned child refused, the **whole** recursive snapshot would fail -- silently breaking backup scripts, `sanoid` and `zfs-auto-snapshot`. So pinned datasets are **skipped** in recursive snapshot and `send -R`. Also set `com.sun:auto-snapshot=false` (the OpenZFS FAQ already recommends it for swap).
## Open
- **R4:** how resilver is ordered against concurrent bypass writes is unverified
## Acceptance
- [ ] each refused operation returns a clear error
- [ ] `zfs snapshot -r zroot/private@x` succeeds and excludes `vm`
Z5 · Pager-level swap encryption (geli cannot cover a sub-range of a provider) ready
## Summary
The usual FreeBSD encrypted-swap answer -- `geli onetime` on the swap device -- does not apply: geli cannot cover a sub-range of a provider, and an extent-mapped file is a set of sub-ranges. ZFS dataset encryption is incompatible with the bypass.
## Design
Encrypt in the pager, as xnu does (per-segment AES, `vm_swap_encrypt`): `crypto(9)` with an ephemeral per-boot key that is never persisted.
## Open
Plan Q2: port xnu's approach, or accept unencrypted swap initially?
## Acceptance
- [ ] swap blocks on disk are ciphertext
- [ ] the key does not survive reboot
P1 · rpi5: verify NVMe polled crash dump on the Pi 500+ ready
## Summary
`ndadump()` (`sys/cam/nvme/nvme_da.c:583`, installed as `d_dump` at `:1011`) polls through `cam_periph_runccb` when `dumping` (`cam_periph.c:1273-1300`) -> `nvme_sim_poll` -> `nvme_ctrlr_poll`. The Pi 500+ boots from NVMe over PCIe -> RP1.
Confirm the PCIe/RP1 path needs no interrupts at panic time and that a K4 dump completes.
## Acceptance
- [ ] a panic on the Pi 500+ produces a recoverable vmcore
P2 · dwmmc: add a dumping path so d_dump does not hang ready
## Summary
`dwmmc_request()` (`sys/dev/mmc/host/dwmmc.c:1237`, `msleep` at `:1279`) has no `dumping` path and would hang at panic. `sdhci` already spins when `dumping` (`sys/dev/sdhci/sdhci.c:2159-2164`). Mirror that.
## Scope
~30 LOC.
## Acceptance
- [ ] an `mmcsd` dump completes on a dwmmc host
P3 · CI: virtio-blk crash-dump harness so the dump path is tested before hardware ready
## Summary
`vtblk_dump` (`sys/dev/virtio/block/virtio_blk.c:644-667`) supports polled dumps, so the whole design can be exercised in the CI VM.
Add a stage: induce a panic, reboot, assert `savecore` recovers a vmcore. Pairs with #428 (iso-test/img-test declare PASS at the login prompt).
## Acceptance
- [ ] CI fails if a panic does not produce a recoverable dump
U1 · swapd: grow and reclaim swapfiles under memory pressure ready
## Summary
A launchd daemon that owns `/private/var/vm`. It is also the **activation path**: nothing under launchd runs `swapon -a` (#171).
## Design
- **Boot:** pin existing files; pin the corefile and register it with `dumpon`
- **Grow:** on `devctl` `VM/swap/LOWSPACE` (K7), create the next file on xnu's ladder, 256 MiB -> 1 GiB; back off 15 s on `ENOSPC`; refuse past `vm.swap_maxpages / 2` (4x RAM) unless `kern.maxswzone` was raised
- **Reclaim:** free swap above a high-water mark for N minutes -> `swapoff(2)` the newest file (handle `ENOMEM`; **never** default to `SWAPOFF_FORCE`) -> unlink
- `mlock` itself, and keep one pre-created file of headroom so growth is never on the critical path
## Known weakness
`swapd` must open and preallocate at exactly the moment memory is short. xnu's real answer is in-kernel creation (`vm_swapfile_create_thread`) -- see S1.
## Acceptance
- [ ] a WebKit-scale build on a 4 GB VM grows swap under pressure and reclaims it afterwards
U2 · installer: create /private/var/vm (ZFS: zroot/private/var/vm) -- no partition, no sizing ready
## Summary
`src/nextbsd-installer/engine/do-install.sh`. No `freebsd-swap` partition and no size decision.
## UFS
`mkdir /private/var/vm`
## ZFS
```
zroot/private mountpoint=/private canmount=off
zroot/private/var canmount=off
zroot/private/var/vm checksum=off compression=off dedup=off copies=1
recordsize=128K com.sun:auto-snapshot=false
zroot/private/var/crash exec=off setuid=off
```
The `canmount=off` parents keep `/private/etc` and most of `/private/var` inside `zroot/ROOT/default`, so they roll back with the boot environment.
**Create these early**, while the pool is unfragmented -- gang blocks are fatal to the extent walk.
Must work under both the mbr-fat (Pi) and gpt layouts.
## Acceptance
- [ ] a fresh install has the directory / datasets with the right properties
- [ ] `gpart show` contains no swap partition
U3 · vm_stat / pagesize report real numbers from vm.stats instead of the 'feature not available' stub ready
## Summary
freebsd-apple-userland-cmds v3 stubs the Mach VM introspection family. `vm_stat`'s columns map cleanly onto `sysctl vm.stats.vm.*` and `vm.swap_*`. Ship a FreeBSD backend for `vm_stat` and `pagesize`; keep the graceful stub only for jetsam-only fields.
## Acceptance
- [ ] `vm_stat` on the live ISO matches `top` / `sysctl` within one sample interval
U4 · installer: ZFS install path -- F8 on the disk picker, pool layout, options and review (preliminary design) design to refine
> **Preliminary design -- not final.** The linked draft is a starting point and will be refined before implementation begins. Treat its screens, keys and defaults as proposals, not a spec.
**Design draft:** https://pkgdemon.github.io/nextbsd-installer-zfs-design.html
## Summary
Add a ZFS install path to `nextbsd-installer`. On the disk picker, **F8** switches from the single-disk UFS install to a multi-disk ZFS one. The marked disks are arranged into vdevs on a two-pane layout screen, followed by pool options and a review screen that shows exactly what will be erased before anything is written.
## Where the installer is today
- `src/screen_disk.cpp` is a 76-line single-select FTXUI `Menu` that handles only Return and Escape, and the engine holds a single `disk_index`. ZFS needs multi-select and a new layout screen -- it is more than a toggle.
- F8 is unused.
## Scope, per the draft
- **F8** toggles UFS / ZFS on the disk picker; **Space** marks disks in ZFS mode
- **Pool Layout**: <- -> move disks into and out of vdevs, **T** cycles single / mirror / raidz1 / raidz2 / raidz3, **N** adds a vdev, **D** deletes one; usable size and failure tolerance update live
- **Validation**: mirror needs 2 disks, raidzN needs N+1 (`vdev_raidz_open()`, and `zpool`'s `is_grouping()` sets `mindev = nparity + 1`); no mixed replication levels; every marked disk placed before Continue
- **Pool Options**: name, ashift (auto from the disks), compression, and encryption of **user data only** -- FreeBSD's loader can open a pool that uses encryption but cannot decrypt a dataset, so an encrypted root would not boot
- **Review**: pool, partitions (EFI + `freebsd-zfs` on each disk, **no swap partition**), the E14 dataset tree, and every volume that will be destroyed
- **Engine**: `gpart`, `zpool create`, and the datasets under `zroot/private` with `canmount=off` parents, so the boot environment stays intact
- ZFS stays disabled on the Pi's mbr-fat layout until Z0 records whether its boot chain can read ZFS
## Open decisions (draft section 7)
- Space: mark disks, or keep it for "toggle details"?
- home location: `/Users` or `/home` (`do-install.sh:264` preserves both)
- compression default: lz4 or zstd
- an EFI partition on every disk, so any mirror member can boot
## Depends on
- Z0 nextbsd/nextbsd-kernel#230 -- ZFS in the kernel, and `zfs`/`zpool` in the image
- A6 nextbsd/nextbsd-userland#200 -- mounting the datasets at boot, and prompting for the user-data passphrase
- U2 nextbsd/nextbsd-userland#179 -- the dataset layout
## Acceptance
- [ ] the design is refined and signed off **before** implementation starts
- [ ] a mirror install and a raidz install each boot in a VM
- [ ] the review screen lists every volume on every marked disk before erasing
- [ ] UFS installs are unchanged
## Summary
xnu's pageout stays robust independent of everything else because reclaim does not wait on swap writes: pages are freed by compressing them, and `vm_swapout_thread` drains already-compressed segments asynchronously (`vm_swap_put`). A stalled swap write slows the system rather than wedging the pagedaemon.
## Questions
- what would compressor-style staging look like on FreeBSD's VM?
- could in-kernel swapfile creation (`vm_swapfile_create_thread`) replace U1's userland growth, closing its known weakness?
## Deliverable
A design note with scope and a recommendation.
## Summary
`module/os/freebsd/zfs/zfs_vnops_os.c` `zfs_putpages` takes `zfs_rangelock_enter` and **then** calls `dmu_tx_assign(tx, DMU_TX_WAIT)` in pageproc context, for dirty mmap'd pages. The file's own header comment (lines 131-155) documents exactly this as the deadlock pattern.
This exists on **every ZFS-root system today, with no swap involved**.
Compounding factors on FreeBSD:
- `KM_PUSHPAGE` is `#define`d to `M_WAITOK` (`include/os/freebsd/spl/sys/kmem.h:49`)
- `arc_memory_throttle()` returns 0 (`module/os/freebsd/zfs/arc_os.c:124`)
## Action
Decide whether to file with OpenZFS (plan Q4), and with what reproduction.
Moved under E14 on 2026-09-20, from E11 (#444). It is a VM measurement task. area:desktop was removed and area:vm kept, because the Roadmap mirrors each issue's Epic as its area:* label. E11's body now notes where it went.
Link, do not migrate
These stay in their current epics. Cross-link them from E14 and add area:vm where it applies.
ext4/btrfs -- the filesystems half of the epic title
Recreating in GitHub
Everything is filed; these commands are kept in case the issues ever need recreating. File the epic first, so that each child can name its parent, then the children in dependency order.
# 1. the label, in all four repos
gh label create area:vm --color 5319e7 \
--description "Virtual memory, swap, paging, memory pressure, crash dumps, and the filesystems they back" \
--repo nextbsd/nextbsd-freebsd-compat # repeat for each repo
# 2. any single ticket: copy its body from this page into a file
gh issue create --repo nextbsd/nextbsd-kernel \
--title "..." --label enhancement,area:vm --body-file K1.md
# 3. or all of them, from the embedded JSON
curl -s https://pkgdemon.github.io/nextbsd-e14-tickets.html | python3 -c '
import sys, re, json
page = sys.stdin.read()
m = re.search(r"<script type=\"application/json\" id=\"e14-data\">(.*?)</script>", page, re.S)
print(json.dumps(json.loads(m.group(1)), indent=1))' > e14.json
A script working from e14.json needs only the fields shown on this page: id, title, repo, labels, depends, body.