# DF-0798 — Missing bounds check on B-Tree node in-buffer offset (OOB heap read in CRC validation)

**Severity:** Low
**Class:** Heap OOB read in filesystem image parsing (no writes, no control flow hijack)
**File:** `sys/vfs/hammer/hammer_ondisk.c:1306-1315`
**Threat model:** an administrator is tricked into mounting an attacker-crafted HAMMER filesystem image. (Unprivileged users can also do this if `vfs.usermount=1` and a root-created vnode/image is chowned to them — the audit's standard acceptable precondition.)

## How to run

The PoC **must** be run as **root** (mounting any filesystem requires it).
This matches the threat model: a malicious filesystem image being mounted.

```sh
ssh dfbsd                        # root shell on guest
cd /root/poc/DF-0798             # wherever you placed it
./build.sh                       # compile the byte-patching helper
./run.sh                         # build a HAMMER image, patch, mount, observe
```

## What it does

1. Creates a small valid HAMMER filesystem image (`/build/df0798.img`, 4 GB sparse).
2. Mounts it briefly RW and populates it with a couple of files so the B-Tree
   root node is allocated and `vol0_btree_root` is set in the volume header.
3. Unmounts.
4. Reads the current `vol0_btree_root` (8-byte little-endian at struct offset
   240 in the on-disk volume header — see `sys/vfs/hammer/hammer_disk.h:776`)
   and ORs its low 14 bits with `0x3FFC`. This keeps the offset pointing at
   the SAME 16 KiB HAMMER buffer as the legitimate root (the high bits and
   the buffer-select bits are unchanged), so the freemap lookup still passes.
5. Attempts to mount the patched image read-only. The very first B-Tree
   lookup (rooted at `vol0_btree_root` — `sys/vfs/hammer/hammer_cursor.c:160`)
   calls `hammer_get_node()` → `hammer_load_node()`, which executes the
   vulnerable pointer arithmetic and the CRC validation read.

## Vulnerable code path (confirmed by source trace)

`sys/vfs/hammer/hammer_ondisk.c:1306`:
```c
node->ondisk = (void *)((char *)buffer->ondisk +
                        (node->node_offset & HAMMER_BUFMASK));
```
`HAMMER_BUFMASK = 16383` (`hammer_disk.h:72`), so `node_offset & HAMMER_BUFMASK`
can be `0..16383`. There is **no check** that the resulting pointer plus the
node size stays within the 16384-byte buffer.

`sys/vfs/hammer/hammer_ondisk.c:1315` then calls
`hammer_crc_test_btree(hmp->version, node->ondisk)`, which at
`sys/vfs/hammer/hammer_crc.h:227` runs:
```c
hammer_datacrc(vol_version, &node->crc + 1, HAMMER_BTREE_CRCSIZE);
```
`HAMMER_BTREE_CRCSIZE = sizeof(struct hammer_node_ondisk) - sizeof(hammer_crc_t)
= 4096 - 4 = 4092` (`hammer_btree.h:247`).

So with `node_offset & HAMMER_BUFMASK = 0x3FFC`, the CRC engine reads bytes
`[0x3FFC+4 .. 0x3FFC+4+4092) = [0x4000 .. 0x5000-4)` of the buffer's KVA,
i.e. up to 4092 bytes **entirely past the end of the 16384-byte buffer** into
adjacent kernel heap/buffer-cache memory.

## Why it doesn't reproduce visibly (impact ceiling)

`hammer_crc_test_btree` reads the bytes and only compares the resulting CRC
against `node->crc`. The bytes themselves are never copied to userspace.
Two outcomes are possible:

* **Most runs** — the OOB read lands in other valid kernel buffer-cache
  mappings (kernel_map is densely populated around the buffer hash). The CRC
  then almost certainly mismatches → the node is flagged `HAMMER_NODE_CRCBAD`
  → the mount (or lookup) returns `EIO`. No kernel message is printed at the
  default `hammer_debug_critical` level. **No userspace-observable info leak.**
* **Some runs** — if the OOB read happens to cross an unmapped KVA page, the
  kernel takes a protection fault in `hammer_datacrc` → panic. This is a
  deterministic DoS for that specific image, but it is mount-time only (so
  the threat is "the admin who mounts a malicious image loses the box"),
  not a remote or unprivileged-user DoS.

There is **no escalation path** from this primitive: it is a pure read, the
bytes never leave the kernel, and the only feedback channel to userspace is
a 1-bit CRC oracle that requires thousands of crafted-mount attempts to leak
even one byte of adjacent kernel memory. This matches the Low severity
rating in the finding.

## What the fix changes

`fix.diff` adds a bounds check in `hammer_load_node()` right after the
offset is masked, returning `EIO` (consistent with the existing CRC-bad
path) when the masked offset would not leave room for a full
`hammer_node_ondisk` (4096 bytes) inside the 16384-byte buffer:

```c
if ((node->node_offset & HAMMER_BUFMASK) >
    HAMMER_BUFSIZE - sizeof(struct hammer_node_ondisk)) {
    error = EIO;
    goto failed;
}
```
