# DF-3012 VERDICT — hammer(1) mem-record leak on blockmap reservation failure

**Status: not_reproduced** (defect code-certain; trigger precondition not
reachable organically on the lab guest — see below). Guest stayed healthy
throughout; no panics.

## The defect (certain, by source)

`hammer_ip_add_bulk()` — sys/vfs/hammer/hammer_object.c:974-977:

```c
if (record->resv == NULL) {
        hdkprintf("reservation failed\n");
        hammer_rel_mem_record(record);   /* no DELETED_FE -> never freed */
        return (NULL);
}
```

`hammer_rel_mem_record()` (hammer_object.c:374-393) destroys a record only
when it carries `HAMMER_RECF_DELETED_FE|BE` or `HAMMER_RECF_COMMITTED`.
The just-allocated record carries none and was never inserted into the
inode's RB-tree, so dropping it to zero refs orphans it forever: ~224 B
per record in the "HAMMER-others" malloc zone (`hmp->m_misc`), plus
`hammer_count_records` never decrements.  Sibling site:
`hammer_ip_add_direntry()` :711-716 leaks record + entry data on the
ENOSPC namekey-exhaustion path.  The author's own error paths elsewhere
set the flag first — hammer_io.c:1793-1795 (`hammer_io_direct_write`
failure), hammer_inode.c:1360/1585/3183 — proving intent; these two sites
were missed.

## What Phase V did (guest: DragonFly 6.5-DEVELOPMENT #0, INVARIANTS)

The only caller is `hammer_vop_strategy_write()` (hammer_vnops.c:3251):
every 16K-aligned buffer flush → `hammer_ip_add_bulk()` →
`hammer_blockmap_reserve()`.  Reservation failure modes
(hammer_blockmap.c:419-623): zone wrap-twice → ENOSPC (real exhaustion),
freemap layer1/layer2 `hammer_bread` I/O errors, `hammer_bnew` errors.

Attempts to drive an unprivileged user into a failing reservation, all
with fresh HAMMER1 filesystems (vnconfig + newfs_hammer + mount, harness
per DF-2999), attacker processes run as `nobody`:

1. **Plain fill to ENOSPC + backlog sync** (800M fs, 512M undo): write(2)
   stops at the checkspace gate; all dirty buffers flush cleanly.
   `dmesg | grep -c "reservation failed"` = 0; zone unchanged.
2. **40,000 tiny-file spray** (unique contents, outruns the flusher's
   record accounting): all 40,000 created, 0 write failures, 0 reservation
   failures.  The rsv_recs/rsv_databytes estimate (hammer_blockmap.c:1274)
   tracks real consumption once records land at strategy time.
3. **mmap + msync(MS_SYNC) loops** (512 iters × 3 rounds): every msync
   returned success.  Investigation (probe.c) showed `vnode_pager_putpages`
   routes mmap flushes through **VOP_WRITE** (sys/vm/vnode_pager.c:768) —
   the same checkspace gate — so mmap cannot bypass the gate either; when
   the pager does fail it logs `vnode_pager_putpages: I/O error 28`
   without touching add_bulk (observed in dmesg).
   (Side observation, out of file scope: msync(2) returned 0 while the
   pager write failed with ENOSPC — error not propagated to userspace.)
4. **6-parallel-writer unique-data races** on tmpfs-backed (fast drain)
   and virtio/hammer2-backed (slow drain) images, nohistory mounts,
   pre-built random source for max dirty speed: fs filled to 100%
   cleanly every time; zero reservation failures.
5. **Deterministic attempt with the gate headroom removed**
   (`sysctl vfs.hammer.limit_dirtybufspace=2097152`, root-set; writes
   still by `nobody`): still zero failures — on this guest the buffer
   flush pipeline is effectively synchronous with write(2) (strategy
   runs inside the syscall path, so pending bytes are accounted before
   the next block's gate check), leaving no unaccounted in-flight window
   even at 2 MB headroom.

## Why it still matters

`hdkprintf("reservation failed")` exists because Dillon expected the path
to fire.  Operationally it can fire when the freemap cannot be read
(degraded hardware), during concurrent admin operations (reblock, prune,
mirror, volume-add), or on drain-lag topologies where the in-flight
buffer backlog exceeds the ~49 MB per-mount reserve against the ~103 MB
global dirty cap (hammer's reserve is half the system-wide capacity — a
designed-in 2× gap).  When it fires, each failed buffer flush leaks one
record permanently; a full-disk event under load leaks up to the backlog
depth (hundreds of MB of kernel heap in the worst case on real hardware),
unrecoverable without a reboot.

## Exploit chain

None — kernel heap resource leak (availability), not memory corruption.
No path to uid=0.

## Fix

fix.diff — one line per site (matches hammer_io.c:1793's pattern):

```diff
 	if (record->resv == NULL) {
 		hdkprintf("reservation failed\n");
+		record->flags |= HAMMER_RECF_DELETED_FE;
 		hammer_rel_mem_record(record);
 		return(NULL);
 	}
```

fix_status: not_testable — validating requires first reproducing a
reservation failure, which Phase V could not trigger (see above); the
diff is line-accurate against the read-only sys/ tree and mirrors the
author's own correct pattern one call-layer down.

## Kernel references

- sys/vfs/hammer/hammer_object.c:969-978  (leak, add_bulk)
- sys/vfs/hammer/hammer_object.c:711-716  (leak, add_direntry)
- sys/vfs/hammer/hammer_object.c:363-393  (rel_mem_record destroy-only-if-flagged)
- sys/vfs/hammer/hammer_vnops.c:3251      (sole caller, strategy write)
- sys/vfs/hammer/hammer_blockmap.c:419-623 (reserve failure modes)
- sys/vfs/hammer/hammer_blockmap.c:1267-1290 (checkspace estimate)
- sys/vfs/hammer/hammer_io.c:1793-1795    (correct sibling pattern)
