# DF-3003 — hammer_update_itimes() in-place atime/mtime update writes past the record's 16KB block on crafted HAMMER1 inode records (no data_len validation + CRC-gate 0-return bypass)

Kernel: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), sys/vfs/hammer
File under audit: sys/vfs/hammer/hammer_inode.c (sink: hammer_update_itimes, lines 1410-1474)

## What was verified (all on the QEMU guest, stock INVARIANTS kernel)

Two independent reproductions, both from a crafted HAMMER1 image mounted with
`vnconfig` + `mount_hammer` (root in the PoC; same image reaches any victim
that mounts attacker-supplied media, incl. `vfs.usermount=1` setups):

### Stage A — silent, attacker-valued, persistent write past the record's 16KB block (reproduced)
Craft the file's INODE btree record with `data_len = 32768` (any value != 128
works; see CRC bypass below) and `data_offset = <free 16KB buffer> + 16272`
(so `xoff + 128 > 16384`).  `utimensat(atime=1740000000s, mtime=UTIME_OMIT)`
dirties ONLY `HAMMER_INODE_ATIME` (hammer_vnops.c setattr) so the backend
flush routes to `hammer_update_itimes()`'s ATIME branch
(hammer_inode.c:1454-1463): `hammer_modify_buffer_noundo()` (no UNDO, no
KKASSERT) followed by the in-place store
`cursor->data->inode.atime = ip->sync_ino_data.atime` at
`buffer->ondisk + 16272 + 120 = ondisk + 16392` — 8..136 bytes PAST the end
of the 16KB block the record's data_offset nominally occupies.

Observed: after `sync` + `umount`, the disk image contains exactly
`1740000000 * 10^6 = 0x00062e8551e8c000` at image offset target+16392
(was marker `0x4242424242424242` before).  Chosen value, chosen block,
silent on an INVARIANTS kernel (no panic, no UNDO record; atime is excluded
from the record CRC so nothing detects it).

### Stage B — mtime branch panic with the OOB arithmetic printed (reproduced)
`utimensat(mtime=1740000001s, atime=UTIME_OMIT)` dirties only
`HAMMER_INODE_MTIME` → the MTIME branch calls
`hammer_modify_buffer(trans, data_buffer, &data->inode.mtime, 16)`
(hammer_inode.c:1446) whose `KKASSERT((rel_offset & ~HAMMER_BUFMASK) == 0)`
computes rel_offset = 16384 → panic:

    panic: assertion "(rel_offset & ~(intptr_t)HAMMER_BUFMASK) == 0" failed
           in hammer_modify_buffer at /usr/src/sys/vfs/hammer/hammer_io.c:931
    hammer_modify_buffer() at hammer_modify_buffer+0xc9
    hammer_sync_inode() at hammer_sync_inode+0x778
    hammer_flusher_slave_thread() at hammer_flusher_slave_thread+0x1c2

On non-INVARIANTS kernels the assert is compiled out: the write proceeds and
the UNDO entry is misdirected (undo_offset = zone2 base + 16384, i.e. into
the NEXT 16KB block — recovery would apply the rollback to the wrong block).

### Corollary (small data_len variant)
With `data_len = 1` (16KB buffer fetched), the past-end page is unmapped by
construction on DFly (every buffer-cache buffer has its own MAXBSIZE=64KB kva
reservation, vfs_bio.c:638) → the struct copy in `hammer_get_inode`
(hammer_inode.c:525, DF-0771) page-faults at the buffer-end boundary:
`Fatal trap 12 ... Stopped at hammer_get_inode+0x3f5: movq 0x70(%rax),%rdx`.
The same fault geometry applies to the update_itimes store on the flusher.

## Root cause chain
1. `hammer_crc_get_leaf()` (hammer_crc.h) returns CRC 0 for INODE records
   whose data_len != sizeof(struct hammer_inode_data); a crafted leaf with
   data_crc=0 therefore PASSES `hammer_crc_test_leaf()` (0 == 0).
2. `hammer_btree_extract()` (hammer_btree.c:728-738) never validates
   data_len for the record type nor that (xoff + data_len) stays within the
   record's 16KB block; `cursor->data = buffer->ondisk + xoff` is fully
   attacker-placed.
3. `hammer_update_itimes()` (hammer_inode.c:1410-1474) writes atime/mtime
   IN PLACE at fixed struct offsets (+112/+120) through that pointer with no
   length validation — the only in-place record-data writer in the inode
   engine.

## Reproduce
1. Build a base image (guest, root): dd 4G sparse, vnconfig, newfs_hammer -f,
   mount, `echo hello > /mnt/f`, sync, umount, vnconfig -u.
2. On the host: `python3 craft_image.py h1.img h1_patched2.img`
   (repoints the file's INODE record; data_len=32768, data_crc=0,
   recomputes the btree node CRC32C; prints all offsets; writes a .json).
3. Push to guest; compile `cc -o trig trig.c`;
   Stage A: vnconfig+mount_hammer -o atime, `./trig a /mnt/f`, sync, umount;
   pull image; verify 8 bytes at target+16392 == 0x00062e8551e8c000.
   Stage B: fresh mount, `./trig b /mnt/f`, sync → panic (serial log).

## Fix
`fix.diff` (validated on guest, see VERDICT.md): reject INODE records whose
data_len != sizeof(struct hammer_inode_data) and any record whose data range
crosses out of its aligned buffer block in hammer_btree_extract(); make
hammer_crc_test_leaf() fail such INODE records instead of letting the 0
CRC "match" a stored 0.
