# DF-2624 VERDICT

**Status: reproduced (unbounded kstrdup overread past the 64KB DIO buffer, proven two ways: instrumented length 772 = 4 bytes past the buffer, and a kernel panic inside the stock kstrdup's strlen). Impact: panic (mount-time, layout-dependent) + silent adjacent-heap overread; no userspace disclosure found. Confidence: certain.**

## Root cause (line-precise)

```c
sys/vfs/hammer2/hammer2_vfsops.c:495   (hammer2_pfsalloc)
		pmp->pfs_names[j] = kstrdup((const char *)ripdata->filename, M_HAMMER2);
```

`ripdata->filename` is `unsigned char filename[HAMMER2_INODE_MAXNAME]`
(=256, disk.h:912/1012) inside the 1024-byte inode data block.  Nothing
on the ingestion path guarantees a NUL inside the array:

* the on-disk name length is `meta.name_len`, but `kstrdup` re-derives
  the length with `strlen` — a missing NUL makes the walk leave the array,
  continue through the rest of the inode block, and (when the block sits
  at a 64KB window tail) past the **end of the DIO buffer** into adjacent
  kernel memory until a zero byte happens to appear;
* there is no `strnlen` bound, and `HAMMER2_INODE_MAXNAME` is not
  consulted anywhere before :495 (grep over the whole call chain:
  update_pmps :1561 → pfsalloc :487-495).

Reachability: `hammer2_update_pmps()` (vfsops.c:1552-1571) pfsallocs
**every** INODE child under the sroot on *any* mount of the device — the
poison PFS does not need to be the mounted label (a non-NUL name can in
fact never match a label: the label scan at :1394 uses `strcmp`, whose
terminator requirement makes a non-NUL filename unmatchable).

## PoC

`forge_2624.py`: base image has a second PFS (created in-guest with
`hammer2 pfs-create`); the forger relocates its inode block to
`WIN|0xFC00|10` — a 1024-byte block ending **exactly at the 64KB buffer
end** (passes the window-fit KKASSERT at io.c:127), fills
`[0x100,0x400)` with 0x50 (no NUL), zeroes the bref's mirror_tid so
recovery does not recurse, and neutralizes ancestor checks (DF-2616
technique).  Mount `testvol` (healthy) → update_pmps → pfsalloc(poison)
→ kstrdup walks.

## Observed

1. Stock kernel #0 (`run_stock.log`, `console_excerpts.txt` [A]): mount
   succeeds; the overread is silent (adjacent page mapped, NUL found
   shortly past the buffer).  `hammer2 pfs-list` shows the poison PFS
   (`PPPPP` — the on-disk view via `meta.name_len`=5).
2. Instrumented kernel #1, mapped case (`console_excerpts.txt` [C]):
   `DF2624: ... kstrdup result len=772 (516 bytes past array)` —
   772 = 256 (filename array) + 512 (blockset filler) + **4 bytes read
   past the end of the 64KB DIO buffer**, all copied into
   `pmp->pfs_names[0]` of the poison pmp.
3. Instrumented kernel #1, unmapped case (`console_excerpts.txt` [B]):
   `Fatal trap 12 … panic: page fault` with backtrace
   `strlen() at strlen+0x14 ← hammer2_pfsalloc() at hammer2_pfsalloc+0x157`,
   fault VA page-granular = the page after the DIO buffer.  The
   instrumentation only *reports after* kstrdup returns; the faulting
   strlen is the stock `kstrdup` at :495 — the panic is stock behavior
   under that heap layout.

## Honest ceiling

The overread reads real adjacent kernel heap (bytes 768..771 of the walk
on the mapped run) into a kernel string, and can panic the kernel at
mount.  No path was found that returns those bytes to userspace:
`pfs_names` feeds thread names (truncated to MAXCOMLEN=16 — never
reaches the tail), `hammer2 pfs-list` prints the on-disk name, and the
vol-list ioctl reads `pmp->pfs_names[0]` of the *mounted* pmp only.
So: local DoS (mount-time panic, layout dependent) + in-kernel
over-read; Low/Medium boundary — filed Low, consistent.

## Fix

```diff
-		pmp->pfs_names[j] = kstrdup((const char *)ripdata->filename, M_HAMMER2);
+		{
+			size_t nlen = strnlen((const char *)ripdata->filename,
+					      HAMMER2_INODE_MAXNAME);
+			pmp->pfs_names[j] = kmalloc(nlen + 1, M_HAMMER2,
+						    M_WAITOK | M_ZERO);
+			bcopy(ripdata->filename, pmp->pfs_names[j], nlen);
+			if (nlen == HAMMER2_INODE_MAXNAME)
+				kprintf("hammer2_pfsalloc: PFS filename not "
+					"NUL-terminated (truncated)\n");
+		}
```

## Fix validation (kernel #2)

Same crafted image mounted three times: 3/3 succeed, **no panic**, the
truncation warning fires each time, `pfs-list` unchanged, testvol mount
unaffected (`console_excerpts.txt` [D]).
