# DF-0798 — Verdict

**Verdict: REPRODUCED + FIX VALIDATED**

**Status:** reproduced
**Impact:** panic (DoS via crafted HAMMER filesystem image mount; OOB kernel heap read in B-Tree CRC validation)
**Severity:** Low (matches finding)
**Confidence:** certain

---

## Mechanism (confirmed by source trace + live panic)

A crafted HAMMER filesystem image supplies a `vol0_btree_root` whose low 14 bits
(`offset & HAMMER_BUFMASK`, range 0..16383) exceed `HAMMER_BUFSIZE -
sizeof(struct hammer_node_ondisk)` = 16384 − 4096 = **12288**. The kernel never
validates this offset before using it.

The trigger path is the very first B-Tree lookup of any HAMMER mount:

1. `mount_hammer` → `hammer_vfs_mount` (`sys/vfs/hammer/hammer_vfsops.c`) →
   `hammer_vfs_root` (line 743) → `hammer_vfs_vget` → `hammer_get_inode` →
   `hammer_init_cursor` (`sys/vfs/hammer/hammer_cursor.c:160`).
2. `hammer_init_cursor` calls `hammer_get_node(trans,
   volume->ondisk->vol0_btree_root, 0, &error)`. `hammer_get_node` asserts only
   the *zone* bits via `hammer_is_zone_btree()` (`sys/vfs/hammer/hammer_ondisk.c:1214`);
   it never checks the in-buffer offset.
3. `hammer_load_node` (`sys/vfs/hammer/hammer_ondisk.c:1306`) computes:
   ```c
   node->ondisk = (void *)((char *)buffer->ondisk +
                           (node->node_offset & HAMMER_BUFMASK));
   ```
   With `offset & 0x3FFF = 0x3FFC`, `node->ondisk` points 4 bytes before the
   END of the 16384-byte buffer.
4. `hammer_load_node` immediately calls `hammer_crc_test_btree()` (line 1315),
   which at `sys/vfs/hammer/hammer_crc.h:227` does:
   ```c
   hammer_datacrc(vol_version, &node->crc + 1, HAMMER_BTREE_CRCSIZE);
   ```
   `HAMMER_BTREE_CRCSIZE` is `sizeof(struct hammer_node_ondisk) - sizeof(hammer_crc_t)`
   = 4096 − 4 = **4092** (`sys/vfs/hammer/hammer_btree.h:247`). So the CRC engine
   reads 4092 bytes starting at `node->ondisk + 4 = buffer->ondisk + 0x4000` —
   i.e. 4092 bytes ENTIRELY PAST the 16384-byte buffer.

## Live reproduction (unpatched `#0` kernel)

The PoC (`run.sh`) creates a valid 4 GB HAMMER image with `newfs_hammer`, mounts
it briefly RW to allocate the root B-Tree node, unmounts, patches
`vol0_btree_root`'s low 14 bits to `0x3FFC` (via `patch_btree_root.c`), then
mounts the patched image read-only. The mount panics **deterministically**:

```
Fatal trap 12: page fault while in kernel mode
cpuid = 1; lapic id = 1
fault virtual address	= 0xfffff8005bd1a000     <-- KVA past the buffer's mapping
fault code		= supervisor read data, page not present
instruction pointer	= 0x8:0xffffffff809d4e34
kernel: type 12 trap, code=0
Stopped at      calculate_crc32c+0x84:  movzbl  -0x1(%rcx),%r9d
```

`calculate_crc32c` is `hammer_datacrc`. The fault address differs across runs
(`0xfffff800761ba000`, `0xfffff8005434a000`, `0xfffff8005bd1a000`) — i.e. the OOB
read lands in whatever KVA happens to be mapped past the buffer. When it crosses
into an unmapped page, the kernel takes a page fault and panics. The signature
matches the cited line exactly.

Reproduced 3× on the default `X86_64_GENERIC` kernel (INVARIANTS ON,
`6.5-DEVELOPMENT #0`), each run panicking at `calculate_crc32c+0x84`.

## Realistic impact ceiling (no escalation)

This is a pure read. The bytes are never copied to userspace — the CRC engine
just computes a checksum and compares it to `node->crc`. The only feedback to
the attacker is a 1-bit "CRC matched / did not match" oracle (and on this
guest, almost always a panic from crossing an unmapped page). Realistic
outcomes:

* **Most common (deterministic on this guest):** panic at `calculate_crc32c`
  when the OOB read crosses an unmapped page. Mount-time DoS only (the admin
  who mounts the crafted image loses the box). NOT reachable by an
  unprivileged user without `vfs.usermount=1` plus a root-created,
  attacker-owned vnode/image.
* **If adjacent KVA happens to be mapped:** silent OOB read into adjacent
  buffer-cache memory; CRC mismatches; mount fails with EIO; no kernel
  message at default `hammer_debug_critical`. No info leak to userspace.
* **Theoretical CRC side-channel:** single-bit oracle, ~1024 crafted-mount
  attempts per byte leaked. Practically useless.

**No escalation path exists.** There is no write, no control-flow hijack, no
attacker-controlled pointer overwrite. This is a textbook Low-severity
mount-time DoS / latent info-leak ceiling. The finding's severity rating is
correct.

## Exploit chain

`none` — non-write primitive. The bug is a bounded OOB kernel heap read in
filesystem image parsing; there is no path from this primitive to privilege
escalation. The chain stops at "panic on mount of crafted image" (DoS).

## PoC changes

The finding was filed without a PoC evidence pack. I built the entire pack
from scratch:

* `patch_btree_root.c` — small C helper that reads the current
  `vol0_btree_root` (8-byte LE at struct offset 240 in the on-disk volume
  header) and ORs its low 14 bits with `0x3FFC`, keeping the high bits
  (zone/volume/buffer-selector) intact so the blockmap lookup still
  succeeds and points to the legitimate root buffer.
* `run.sh` — orchestrates image creation (`newfs_hammer -f`), RW mount +
  populate + unmount, byte-patch, RO mount of the crafted image. Run as
  root (mounting any filesystem requires it; this matches the threat model
  of an admin mounting a malicious image).
* `build.sh` — compiles the helper with `cc -O -o patch_btree_root
  patch_btree_root.c`.

The mask `0x3FFC` was chosen for 4-byte alignment of `node->crc` while still
exceeding the safe maximum (12288), so the entire 4092-byte CRC validation
read happens past the buffer end.

## Fix

`fix.diff` adds a single hunk in `hammer_vfs_mount` (just before
`hammer_flusher_create`, which is before any B-Tree access). The hunk
validates `rootvol->ondisk->vol0_btree_root` against both the zone encoding
and the in-buffer bounds; if malformed it prints
`HAMMER: malformed vol0_btree_root <offset>` and returns `EINVAL`, so the
mount fails cleanly without ever reaching the OOB read.

This is a more user-visible placement than the literal line cited in the
finding (`hammer_ondisk.c:1306`): the cited line is the right *root-cause*
location, but fixing it there in isolation is insufficient because
`hammer_init_cursor` and many of its callers (`hammer_inode.c:499` etc.)
discard the error return and would still crash later in `btree_search` with
a NULL `cursor->node->ondisk`. Validating the offset at mount time — the
single entry point that actually unconditionally fails the mount — closes
the bug end-to-end without touching any caller.

The fix was validated by building a single-fix `#1` kernel and re-running
the identical PoC: the patched kernel prints
`HAMMER: malformed vol0_btree_root 8000000021003ffc`, the mount returns
`EINVAL` (`mount: Invalid argument`), the guest **stays up**, and there is
**no** panic in `boot.log`. Reproduced 3× deterministically.

(An alternative in-place clamp at `hammer_ondisk.c:1306` was also tried and
correctly closes the OOB read, but routes the failure through the existing
CRCBAD/EIO path that the callers don't all handle — exposing pre-existing
latent KKASSERT/NULL-deref issues in `hammer_init_cursor` and
`hammer_inode.c:499`. The mount-time validation sidesteps those entirely
and is the recommended minimal fix.)

## Recommended fix

`findings/poc/DF-0798/fix.diff` — superset of the finding's `## Recommended
fix` proposal: same bounds expression (`> HAMMER_BUFSIZE -
sizeof(hammer_node_ondisk)` → EIO) the finding suggested, but applied at
the mount entry point (`hammer_vfsops.c:738`) instead of in
`hammer_load_node`, so the rejection propagates cleanly through the
existing `goto done` path rather than tripping unrelated caller bugs.
