# DF-3011 — VERDICT

**status: reproduced (twice)** | **impact: panic (kernel stack overflow; silent
kstack smash into adjacent kernel_map memory on production builds)** |
**confidence: certain** | fix validated: **fixed**

## What was verified on the guest (stock INVARIANTS kernel #0)

1. **The recursion is attacker-depth-controlled.** Depth-4 crafted chain →
   console backtrace shows exactly 4 `btree_remove` recursion frames
   (`btree_remove+0x26a` ×3 under `btree_remove+0x2fd`, panic_layer2_d4.txt /
   run_control_d4.log — that run later tripped an *environmental* freemap
   assert during the unwind frees, see "artifact" below). Depth-64: recursion
   completes (panic_d64_layer2artifact.txt — again the artifact, after 64
   successful levels). Depth-200: **DOUBLE FAULT** before any unwind
   (panic_d200.txt, panic_d200_run2.txt — two independent fresh-mount runs):
   ```
   DOUBLE FAULT
   rip = 0xffffffff80949d20   -> hammer_ref_buffer+0x0  (nm /boot/kernel/kernel)
   rsp = 0xfffff80118bb1000   -> page-aligned kstack floor (16KB LWKT stack)
   panic: double fault
   ```
2. **The mount itself is clean.** `mount_hammer` + `ls /mnt` succeed on the
   crafted image (original subtree intact under the grafted root R2); the
   original records remain reachable. Only the record *deletion* triggers the
   overflow — via `hammer snaprm` (HAMMERIOC_DEL_SNAPSHOT) which calls
   `hammer_delete_at_cursor(HAMMER_DELETE_DESTROY)` → `hammer_btree_delete` →
   `btree_remove` synchronously in the ioctl thread.
3. **All forged fields are in-range.** deepforge writes count=1/2, type
   'I'/'L', and recomputes every node CRC32C — the DF-0776 count/type
   validation (evidence pack DF-0776/fix.diff, hammer_ondisk.c) passes every
   crafted node. The attacker-controlled quantity is the *chain length* (tree
   depth), which nothing validates anywhere in HAMMER1.

## Root cause trace (path:line)

* `sys/vfs/hammer/hammer_btree.c:932-935` — empty leaf → `btree_remove(cursor, ndelete)`.
* `sys/vfs/hammer/hammer_btree.c:1979` — `if (parent->ondisk->count == 1)`: every
  single-element internal level recurses.
* `sys/vfs/hammer/hammer_btree.c:2002-2007` — `hammer_cursor_up_locked(cursor)` then
  `error = btree_remove(cursor, ndelete);` — one C-stack frame per B-tree level,
  no depth bound.
* No depth limit exists anywhere else: `btree_search` pushdown
  (`hammer_btree.c:1255` `hammer_cursor_down`) and the parent reload
  (`hammer_cursor.c:490-521 hammer_load_cursor_parent`) are iterative;
  `grep -n depth sys/vfs/hammer/hammer_btree.c` only finds the rebalancer's
  caller-bounded `lcache`/`lock_children` depths.
* Caller thread stack: 16KB — `sys/sys/thread.h:472`
  `#define LWKT_THREAD_STACK (UPAGES * PAGE_SIZE)`, `UPAGES 4`
  (`sys/cpu/x86_64/include/param.h:126`), allocated for user LWPs at
  `sys/kern/kern_fork.c:818` via `kmem_alloc_stack(kernel_map, ...)`
  (`sys/kern/lwkt_thread.c:393-395`) — kernel_map, **no guard page**: overflow
  writes into whatever is mapped below the kstack on production builds.
* Trigger reachability: `hammer_ioctl.c:833` (snaprm), `hammer_prune.c:201`,
  `hammer_mirror.c:754,828`, `hammer_object.c:2236` (nohistory unlink), 
  `hammer_pfs.c:520,525`.

## Why panic and not uid=0 (honest impact ceiling)

The overflowing data are the recursion's own stack frames: saved RBP/RIP and
locals of `btree_remove`/`hammer_cursor_up_locked`/`hammer_load_cursor_parent`/
`hammer_btree_get_parent`/`hammer_get_node`. Content control is coarse (kernel
pointers and small integers influenced by crafted offsets/keys, valid kernel
return addresses). On the INVARIANTS guest the overflow manifests as a double
fault (proof of the primitive). On a production kernel the same overflow writes
below the kstack into adjacent kernel_map allocations — a memory-corruption
primitive — but turning it into a controlled hijack was not attempted within
this run's budget; no leak/groom work was done, and I am reporting DoS-class
impact with a corruption ceiling, not claiming privesc.

## Environmental artifact (NOT part of this finding)

Sub-overflow depths unwind and free the chain nodes; the first
`hammer_blockmap_free` on these dd/newfs-created base images trips
`KKASSERT(layer2->zone == zone)` (hammer_blockmap.c:832). The on-disk freemap
of the fresh image decodes (via vol0_blockmap[4]) to layer1[0].phys = 0x5ba
(garbage) — an inconsistency of the *test image environment* (newfs on sparse
vn images) that affects any btree-node free, unrelated to the recursion bug.
The attack depth (200) always overflows *before* the first free, so the
artifact never masks the finding. (Worth a separate look by the project: the
8-byte offset between the on-disk layer1 table and the blockmap root.)

## Fix validation (single-fix kernel)

* `fix.diff` applied to the guest's /usr/src copy only (audit tree untouched),
  `make nativekernel KERNCONF=X86_64_GENERIC` → build OK (fix_build.log).
* Rebooted into the fixed kernel (uname recorded in fix_run_d200.log).
* Re-ran the exact depth-200 PoC: **no panic** — snaprm returns, mount stays
  healthy, unmount clean (fix_run_d200.log). The capped recursion defers via
  the existing EDEADLK semantics (empty leaf left for the pruner).
* Regression: normal newfs/mount/write/rm/sync/umount cycle on an uncrafted
  image passes on the fixed kernel.

## Reproduce summary

```
build.sh                    # deepforge
sh run.sh 200 0x4000000000000000
# console: DOUBLE FAULT, rsp = kstack floor, panic: double fault
```
