# DF-3153 — volume-del on a volume carrying UNDO-zone big-blocks: KKASSERT(0) panic (INVARIANTS) / silent undo-FIFO wipe (production)

## What

`hammer_free_freemap` and `hammer_count_bigblocks` treat a layer2 entry in
zone `HAMMER_ZONE_UNDO_INDEX` (3) on the volume being removed as an
impossible state via bare `KKASSERT(0)`:

- sys/vfs/hammer/hammer_volume.c:553-554 (free_freemap, first pass)
- sys/vfs/hammer/hammer_volume.c:589-590 (free_freemap, second pass)
- sys/vfs/hammer/hammer_volume.c:803-804 (count_bigblocks — reached on the
  del path at hammer_volume.c:207 BEFORE any EBUSY check)

`hammer_ioc_volume_del` only refuses the ROOT volume
(hammer_volume.c:195-200). The undo FIFO is described by
`vol0_undo_array[]` on the root volume's ondisk (hammer_disk.h:789) whose
entries are zone2 offsets that the format permits pointing at ANY volume —
the array exists precisely because undo may span volumes. All in-tree
tooling happens to allocate undo big-blocks on the root volume only
(sbin/hammer/ondisk.c `alloc_undo_bigblock(root_vol)`), so reaching the
assertion requires a hand-crafted multi-volume image with a CRC-correct
zone-3 layer2 entry on a non-root volume, then `hammer volume-del` of that
volume (privileged).

## Impact

- INVARIANTS kernels (incl. stock audit guest): deterministic kernel panic
  from the admin volume-del ioctl (`KKASSERT(0)`).
- Production (non-INVARIANTS) kernels: KKASSERTs compile out; the case
  FALLS THROUGH — first pass treats the undo block as ignorable
  (`continue`), second pass falls into `default:` and **bzero()s the undo
  layer2 entry** (hammer_volume.c:591-596) while the root volume's
  undomap/vol0_undo_array still references those offsets. The undo FIFO is
  silently truncated: later undo writes/reads hit blocks the freemap now
  reports free (and the volume is being erased) → crash-recovery corruption
  (stale/garbage undo replay) with no error reported to the operator.

## Preconditions / why untested

Root (SYSCAP_NOVFS_IOCTL) issues the del on a crafted multi-volume HAMMER1
image. The code path is certain (dead switch on an on-disk-permitted
state); the trigger requires assembling a two-volume image with layer1/layer2
CRCs correct (hammer_blockmap.c:59 panics on layer1 CRC mismatch), which was
not built for this run — no PoC executed, verdict untested. Severity Low:
privileged operator + crafted metadata.

## Fix sketch

The del path must reject a volume hosting undo big-blocks explicitly
(`return EBUSY`/EINVAL with a clear operator message), never assert:

```diff
--- a/sys/vfs/hammer/hammer_volume.c
+++ b/sys/vfs/hammer/hammer_volume.c
@@ -550,7 +550,9 @@ hammer_free_freemap(hammer_transaction_t trans, hammer_volume_t volume)
 			switch (layer2->zone) {
 			case HAMMER_ZONE_UNDO_INDEX:
-				KKASSERT(0);
+				krateprintf(&hmp->kdiag, "volume %d holds "
					"undo big-blocks\n", volume->vol_no);
+				error = EBUSY;
+				goto end;
 			case HAMMER_ZONE_FREEMAP_INDEX:
```

(the same replacement applies at :589 and :803; at :803 count_bigblocks
should return EBUSY so the del aborts before the reblock).
