# DF-0838 — VERDICT

**Finding:** Crafted HAMMER1 image triggers kernel panic via unimplemented
directory-hash algorithm (ALG2/ALG3 bits).

**Severity:** Low (local DoS via crafted filesystem image — requires the
victim to mount an attacker-supplied image).

**Verdict:** **REPRODUCED.** Fix validated.

## Mechanism

`hammer_direntry_namekey(dip, name, len, max_iter)` at
`sys/vfs/hammer/hammer_subs.c:944` is the directory-name hash function.  It
switches on the low 2 bits of `dip->ino_data.cap_flags`:

```c
case HAMMER_INODE_CAP_DIRHASH_ALG0: ... break;          /* 0x00 — crc32 */
case HAMMER_INODE_CAP_DIRHASH_ALG1: ... break;          /* 0x01 — domain hash */
case HAMMER_INODE_CAP_DIRHASH_ALG2:
case HAMMER_INODE_CAP_DIRHASH_ALG3:
default:
    key = 0;
    *max_iterationsp = 1;
    hpanic("bad algorithm %p", dip);                    /* <-- hammer_subs.c:1031 */
    break;
```

ALG0 and ALG1 are the only implemented algorithms; ALG2/ALG3 are reserved and
unimplemented. The default case calls `hpanic` (an always-panic wrapper).

`dip->ino_data` is the in-memory copy of the on-disk `struct hammer_inode_data`
(`hammer_disk.h:880`). It is loaded verbatim from disk at
`hammer_inode.c:525`:

```c
ip->ino_data = cursor.data->inode;
```

with no semantic validation of the `cap_flags` byte (offset 65 of the struct).
The only on-disk integrity check is the leaf `data_crc32`
(`hammer_crc.h:265-289`), which the attacker can freely recompute after
modifying `cap_flags`. The CRC is `crc32` for vol_version ≤6, `iscsi_crc32`
for ≥7.

The directory-hash function is reached from every directory-modifying /
lookup VOP — `hammer_vop_nresolve` (path lookup, `hammer_vnops.c:1220`),
`hammer_vop_ncreate` (`hammer_vnops.c:3310`), `hammer_vop_nlink`
(`hammer_vnops.c:2000`), and `hammer_ip_create` (`hammer_object.c:676`) — so
**any filename operation** under the corrupted directory triggers the panic.

## Trigger / PoC

End-to-end reproduction driver is in `repro.sh` (run as root inside the guest).
The image-corruption tool is `image_patcher.c`.

1. Build a HAMMER1 v6 image (`newfs_hammer -V 6`).
2. Populate it (`mkdir testdir; echo hello > testdir/file1`).
3. Run `image_patcher` on the raw image:
   * Locates every directory inode by signature
     (`ver=1, mode=0o755, obj_type=HAMMER_OBJTYPE_DIRECTORY=1`).
   * Sets `cap_flags` bits[1:0] to `0x02` (ALG2) on every directory.
   * Recomputes the per-leaf `data_crc32` (over the inode's first 112 bytes =
     `HAMMER_INODE_CRCSIZE`).
   * Recomputes the parent B-Tree node's own CRC (`hammer_crc.h:227`:
     `crc32(&node->crc+1, sizeof(node)-4)`).
4. Mount the modified image and do **any** filename operation under it.

The patcher modifies three directory inodes by default (root + an
intermediate dir + `testdir`), so even a `ls /mnt/somefile` lookup (which
goes through the root directory's hash) hits the panic — there's no need to
navigate into `testdir` specifically.

## Evidence

**Baseline panic** (unpatched kernel `6.5-DEVELOPMENT #0`, after `ls -la
/mnt/df0838/testdir/file1`):

```
panic: hammer_direntry_namekey: bad algorithm 0xfffff80116ed4640
cpuid = 3
Trace beginning at frame 0xfffff80118b07418
hammer_direntry_namekey() at hammer_direntry_namekey+0x1ae 0xffffffff80951f5e
hammer_direntry_namekey() at hammer_direntry_namekey+0x1ae 0xffffffff80951f5e
hammer_vop_nresolve() at hammer_vop_nresolve+0x20f 0xffffffff8095743f
vop_nresolve() at vop_nresolve+0x53 0xffffffff8070c3c3
cache_resolve() at cache_resolve+0x61e 0xffffffff806e06fe
nlookup() at nlookup+0x8e1 0xffffffff806ef621
Debugger("panic")
Stopped at      Debugger+0x7c:  movb    $0,0xbdaf09(%rip)
db>
```

The stack pins the bug exactly: `hammer_direntry_namekey+0x1ae` is the
`hpanic` call site at `hammer_subs.c:1031`.

**Reproduced twice** from fresh `vm.sh reset with-src` snapshots; deterministic.

## Impact

* **Confidentiality:** none.
* **Integrity:** none (the panic is reached before any state change).
* **Availability:** kernel panic ⇒ full system crash ⇒ **local DoS**.
* **Preconditions:** the victim must `mount_hammer` an attacker-supplied
  HAMMER1 image. Root can do this directly; an unprivileged user can do this
  when `vfs.usermount=1` and the image+mountpoint are owned by them (a normal
  configuration for "mount this USB / image" workflows).

No escalation chain applies — the panic fires synchronously inside the syscall
path before any corruption primitive can be derived. Impact is DoS only,
matching the original Low severity.

## Fix

Authored in `fix.diff`. The change is two-file:

1. **`sys/vfs/hammer/hammer.h`** — add a per-inode flag
   `HAMMER_INODE_BADDIRHASH` (0x10000000) to log the warning exactly once
   per affected directory inode.

2. **`sys/vfs/hammer/hammer_subs.c:1026`** — in the `default` case, instead
   of `hpanic("bad algorithm %p", dip)`, fall back to the ALG0 computation
   (identical code to the ALG0 case) and emit one `hdkprintf` warning.

This is the minimal, root-cause fix: an unimplemented value in
attacker-controlled on-disk metadata should never be a fatal assertion. The
filesystem continues to operate (the ALG0 hash is correct for any directory,
so existing entries — which the attacker has not moved — remain findable).

### Fix validated on a single-fix kernel

* Unpatched kernel `#0` (`6.5-DEVELOPMENT #0`, Thu Jul  2 06:02:54 UTC 2026):
  the PoC panics, signature above.
* Single-fix kernel `#1` (built from `with-src` + `fix.diff`,
  `6.5-DEVELOPMENT #1`, Tue Jul 14 01:05:39 UTC 2026,
  sha256 `b726a337…855602924`):
  `mount_hammer` succeeds, `ls /mnt/df0838/testdir/file1` returns the file,
  `cat` returns `hello`, **no panic**, dmesg logs
  `hammer_direntry_namekey: dir ino … has unknown dirhash alg 2, falling back to ALG0`.
* Repeated 3× to confirm determinism.

The fix supersedes the finding markdown's proposal ("fall back to ALG0 +
kprintf warning") by implementing exactly that, plus a once-per-inode log
guard to avoid log spam.

## Citations

* `sys/vfs/hammer/hammer_subs.c:944`  — `hammer_direntry_namekey` function
* `sys/vfs/hammer/hammer_subs.c:1026-1032` — vulnerable `default` case
* `sys/vfs/hammer/hammer_subs.c:1031` — `hpanic("bad algorithm %p", dip)`
* `sys/vfs/hammer/hammer_inode.c:525` — `cap_flags` loaded verbatim from disk
* `sys/vfs/hammer/hammer_disk.h:880-916` — `struct hammer_inode_data`
* `sys/vfs/hammer/hammer_disk.h:927-931` — DIRHASH_{MASK,ALG0..ALG3} constants
* `sys/vfs/hammer/hammer_vnops.c:1220` — `hammer_vop_nresolve` caller
* `sys/vfs/hammer/hammer_crc.h:71` — `hammer_datacrc` = crc32 for v≤6
* `sys/vfs/hammer/hammer_crc.h:227` — B-Tree node CRC computation
* `sys/vfs/hammer/hammer_crc.h:265-289` — leaf `data_crc` computation
* `sys/vfs/hammer/hammer_btree.h:218-248` — `struct hammer_node_ondisk`
