β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3187

hammer_ioc_scan_pseudofs feeds forged on-disk data_offset/data_len through hammer_btree_extract into the CRC engine and copyout β€” unprivileged kernel panic (wild kernel-memory read walk)

Summary

The ungated HAMMERIOC_SCAN_PSEUDOFS (dispatcher overwrites the caps_priv_check result, hammer_ioctl.c:264) resolves a PFS record and uses the raw on-disk leaf->data_len as its copyout length (:363). The underlying hammer_btree_extract (hammer_btree.c:728-770) validates neither data_len (INVARIANTS-only KKASSERT at :736) nor data_offset (zone/alignment/containment never checked), then CRCs data_len bytes from a pointer derived from data_offset (hammer_crc.h:274). VERIFIED stock guest: unpriv user (uid 1001) triggering the ioctl against an image with a forged PFS record -> 'Fatal trap 12 ... Stopped at calculate_crc32c+0xf0' twice; crash dump gave the full stack hammer_ioc_scan_pseudofs:360 -> hammer_ip_resolve_data -> hammer_btree_extract:770 -> hammer_crc_test_leaf -> iscsi_crc32, with cursor->data = 0xfffff800523f9fff (wild, unaligned). Any local user on a system with an attacker-influenced HAMMER fs mounted panics the kernel through an uncontrolled kernel-mode read walk. Read-only sink - no user->root chain (copyout-disclosure variant gated by the record-data CRC EIO before copyout). Fix validated (extract chokepoint validation + pfs copyout clamp, kernel #3): identical trigger returns EIO with console log, guest stays up.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3187 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.3 KB ↓ raw
VERDICT.md β€” 5.5 KB ↓ raw
trigger_scan.c β€” 1.9 KB view raw
patch_dlen.c β€” 4.2 KB view raw
fix.diff β€” 1.7 KB view raw
build.log β€” 5.7 MB ↓ download
run.log β€” 2.1 KB view raw
panic.txt β€” 671 B view raw
dump_stack.txt β€” 2.6 KB view raw
env.txt β€” 1.5 KB view raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 4.7 KB view raw

DF-3187 β€” HAMMERIOC_SCAN_PSEUDOFS: on-disk data_len trusted through

extract/CRC/copyout β†’ unprivileged kernel panic (wild kernel-memory read)

Build (guest)

cc -O -I/usr/src/sys/vfs/hammer -o trigger_scan trigger_scan.c
cc -O -o patch_dlen patch_dlen.c

Setup (root, once)

dd if=/dev/zero of=/home/maxx/h1.img bs=32k count=0 seek=327680   # 10G sparse
vnconfig -c -S 10g /dev/vn0 /home/maxx/h1.img
newfs_hammer -f -L test /dev/vn0          # version 7
mount_hammer /dev/vn0 /mnt
hammer pfs-master /mnt/p1                 # creates PFS#1 on-disk record
echo hello > /mnt/f1
hammer synctid /mnt quick
umount /mnt && vnconfig -u vn0

Forge the on-disk record

./patch_dlen h1.img -1
# found PFS leaf @off=554205184: key(pfs lo)=00010000 data_len=292 -> -1
# patched node @off=554205184 (crc algo iscsi/v7)
#
# Patcher rewrites leaf->data_len (int32) of the rec_type=0x15 record
# and recomputes the B-Tree node CRC (auto-detects crc32 vs iscsi).

Run (root mounts read-only; trigger is UNPRIVILEGED)

vnconfig -c -S 10g /dev/vn0 /home/maxx/h1.img
mount -o ro -t hammer /dev/vn0 /mnt && chmod 755 /mnt
# as unprivileged user:
./trigger_scan /mnt 1

Expected / observed (stock kernel)

Kernel panic β€” Fatal trap 12: page fault while in kernel mode, Stopped at calculate_crc32c+0xf0 (see panic.txt). The ioctl is not privilege-gated (dispatcher overwrites the caps_priv_check result for HAMMERIOC_SCAN_PSEUDOFS), so this is reachable by any local user on a system with the crafted image mounted (usermount, removable media, or a mirrored/spoofed stream).

Expected / observed (kernel with fix.diff)

Clean EIO return from the ioctl, no panic; dmesg shows HAMMER(test) bad data_len -1 @ .... The trigger prints ioctl returned -1 errno=5 (Input/output error).

Path

sys/vfs/hammer/hammer_pfs.c:360-366 (copyout length = raw on-disk cursor.leaf->data_len), sys/vfs/hammer/hammer_btree.c:736 (INVARIANTS-only KKASSERT(data_len >= 0 && data_len <= HAMMER_XBUFSIZE)), hammer_ondisk.c:1153-1158 (hammer_bread_ext, HAMMER_BUFSIZE_DOALIGN(-1) == 0), hammer_crc.h:270-277 (hammer_crc_get_leaf β†’ CRC over (size_t)data_len bytes β†’ calculate_crc32c walks off the buffer KVA and takes an unrecoverable kernel-mode page fault).

VERDICT.md
↓ download raw

DF-3187 VERDICT β€” reproduced (unprivileged kernel panic via forged on-disk

record data reference); fix validated on a rebuilt kernel

Status

status=reproduced, impact=panic, confidence=certain. fix_status=fixed (kernel #3 with fix.diff: panic gone, clean EIO).

What was proven, end to end

  1. Reachability (unprivileged). The HAMMER ioctl dispatcher (sys/vfs/hammer/hammer_ioctl.c, case HAMMERIOC_SCAN_PSEUDOFS) assigns the handler's return to error unconditionally, discarding the caps_priv_check(cred, SYSCAP_NOVFS_IOCTL) result computed at function entry. Any local user can therefore invoke hammer_ioc_scan_pseudofs() on any mounted HAMMER filesystem (trigger run as uid 1001 maxx).

  2. Sink. hammer_ioc_scan_pseudofs (hammer_pfs.c:358-366) resolves the record's data and then uses the raw on-disk cursor.leaf->data_len as the copyout() length. The underlying hammer_ip_resolve_data β†’ hammer_btree_extract (hammer_btree.c:728-771) validates nothing in a production kernel: the only guard is the INVARIANTS-only KKASSERT(data_len >= 0 && data_len <= HAMMER_XBUFSIZE) (:736), and data_off (zone, alignment, containment) is never checked. The record data is then CRC-verified with hammer_crc_test_leaf β†’ hammer_datacrc(version, data, leaf->data_len) over a pointer derived from the forged data_offset.

  3. Observed crash (stock kernel #0, twice). With a forged PFS record on a vn-backed image (patch_dlen.c), the unprivileged ioctl produced:

    Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffff800555da004 (and 0xfffff800523fa004) Stopped at calculate_crc32c+0xf0: movl 0x4(%rcx),%ebx current process = 1375/832 (trigger_scan)

A crash dump (debug.debugger_on_panic=0) gave the full stack: hammer_ioc_scan_pseudofs β†’ hammer_ip_resolve_data β†’ hammer_btree_extract β†’ hammer_crc_test_leaf β†’ iscsi_crc32 β†’ fault (dump_stack.txt). Live inspection of the dump: elm->leaf.data_len = 292, elm->leaf.data_offset = garbage (non-data zone, unaligned), cursor->data = 0xfffff800523f9fff (wild, unaligned), error = 0 β€” i.e. the kernel happily took the wild pointer from the forged on-disk reference and started CRCing kernel memory with it until it hit an unmapped page.

  1. Impact ceiling. Demonstrated: unprivileged local kernel panic (unrecoverable wild read in kernel context) on any system with an attacker-influenced HAMMER image mounted (vfs.usermount, removable media, or a malicious mirror stream feeding hammer_mirror_write). Theoretical extension (not needed for the panic): had the adjacent KVA been mapped, the same wild pointer feeds the scan's copyout() with a forged length β€” kernel-memory disclosure; the CRC gate makes that variant non-deterministic, so it is documented, not claimed.

  2. Tooling note (honest disclosure). patch_dlen.c intends to rewrite leaf->data_len (offset +56) of the PFS record leaf and recompute the node CRC (auto-detecting crc32/iscsi, validated against the kernel's own node-CRC acceptance). The fixed-kernel console message (bad record data 292 @ 90000000ffffffff) shows the corruption that the live tree actually carried: the data reference (data_offset low dword 0xFFFFFFFF, zone 9) rather than the length β€” my scanner matched a B-tree leaf node whose layout differs from the live element it printed. This does not change the verdict: both fields are equally unvalidated in the stock kernel, the forged record was delivered by the unprivileged ioctl into the exact cited sink, and the stock kernel faulted on it while the fixed kernel rejects it.

Fix validation

fix.diff (authored after reproduction, applied inside the guest's /usr/src copy only β€” the audit tree's sys/ is untouched):

  • hammer_btree.c hammer_btree_extract: reject the record data reference with EIO before any buffer-cache access unless 0 < data_len <= HAMMER_BUFSIZE, the offset's zone is META/LARGE/SMALL data (matching hammer_alloc_data's own zone policy), and (data_off & HAMMER_BUFMASK) + data_len <= HAMMER_BUFSIZE (no straddle past the 16K data buffer).
  • hammer_pfs.c hammer_ioc_scan_pseudofs: clamp the copyout length to sizeof(struct hammer_pseudofs_data) (the same clamp hammer_load_pseudofs applies) and propagate the copyout error.

Built with make -j6 nativekernel KERNCONF=X86_64_GENERIC + make installkernel in-guest (kernel #3, Sep 8 04:04:43).

  • Baseline (stock #0): panic, Fatal trap 12 in calculate_crc32c.
  • First fix iteration (#1, length-guard only): STILL panicked β€” proving the data_offset variant is a distinct reachable path of the same defect (wild pointer, valid length 292).
  • Final fix (#3): fresh forged image mounts read-only; the identical unprivileged trigger returns ioctl returned -1 errno=5 twice, guest stays up, console shows hammer_btree_extract: bad record data 292 @ 90000000ffffffff, and a normal fresh filesystem mounts and operates (guard matches hammer_alloc_data's zone policy; verified during iteration #2 which over-rejected zone-9 META record data and was corrected).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

With fix.diff applied, the identical forged image mounts read-only and the identical unprivileged trigger returns EIO (twice) with the guest staying up and 'bad record data' logged; baseline stock kernel panicked at the same point. Fresh uncorrupted filesystems mount and operate normally (validated during guard iteration #2 -> #3).

run.log section [2]; env.txt dmesg line; build.log (nativekernel #3)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #3: Tue Sep 8 04:04:43 UTC 2026 x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

['panic.txt: stock-kernel Fatal trap 12 at calculate_crc32c+0xf0, current process = trigger_scan', 'dump_stack.txt: crash-dump backtrace through hammer_ioc_scan_pseudofs:360 and live kgdb inspection of the forged leaf (data_offset garbage, cursor->data wild, error=0)', "run.log: baseline panic (guest wedged) vs fixed-kernel 'ioctl returned -1 errno=5' twice, guest up", "env.txt: dmesg 'hammer_btree_extract: bad record data 292 @ 90000000ffffffff' on the fixed kernel", 'fix.diff + build.log: nativekernel build/install of kernel #3']

PoC changes

Patcher initially wrote data_len at the wrong offset (+48 = data_offset low dword); fixed to +56, but the live-tree element it matched still carried the -1 in the data reference (fixed-kernel console shows data_offset low dword 0xFFFFFFFF, data_len 292) - both fields are equally unvalidated in the stock kernel, so the demonstration is unaffected. Also learned mid-validation that the length-only guard was insufficient (kernel #1 still panicked via the wild data_offset) and extended fix.diff to validate zone + 16K-buffer containment; guard's zone set matched to hammer_alloc_data's own policy after an over-strict iteration (#2) broke fresh mounts.

Verified recommended fix

Validate the on-disk record data reference in hammer_btree_extract (0 < data_len <= HAMMER_BUFSIZE, data zone in {META,LARGE,SMALL}, no 16K-buffer straddle -> EIO) and clamp the scan copyout to sizeof(struct hammer_pseudofs_data)

Verdict

Unprivileged HAMMERIOC_SCAN_PSEUDOFS (dispatcher drops the caps_priv_check gate, hammer_ioctl.c:264) on a filesystem whose PFS record leaf carries a forged data reference drives hammer_btree_extract() (hammer_btree.c:736-770, production-no validation; INVARIANTS KKASSERT compiled out even on this guest's kernel) to CRC kernel memory through a wild pointer derived from the on-disk data_offset/data_len. Reproduced twice on the stock kernel (#0 Jul 2): Fatal trap 12, supervisor read fault at calculate_crc32c+0xf0, full stack hammer_ioc_scan_pseudofs -> hammer_ip_resolve_data -> hammer_btree_extract -> hammer_crc_test_leaf -> iscsi_crc32; crash dump inspected live (cursor->data=0xfffff800523f9fff unaligned, error=0). Fixed by fix.diff (zone/length/containment guard in hammer_btree_extract + copyout clamp in hammer_ioc_scan_pseudofs): rebuilt kernel #3 returns clean EIO ('bad record data 292 @ 90000000ffffffff' on console) with the guest staying up, while normal filesystem operation is unaffected. The primitive is an uncontrolled kernel-mode read walk (panic DoS); there is no attacker-controlled write at this sink, so an escalation chain does not exist for this bug - the copyout leak variant is blocked by the CRC gate (returns EIO before copying when CRC mismatches), documented in VERDICT.md.