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)
PoC verification
Evidence pack
findings/poc/DF-3187 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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
-
Reachability (unprivileged). The HAMMER ioctl dispatcher (sys/vfs/hammer/hammer_ioctl.c,
case HAMMERIOC_SCAN_PSEUDOFS) assigns the handler's return toerrorunconditionally, discarding thecaps_priv_check(cred, SYSCAP_NOVFS_IOCTL)result computed at function entry. Any local user can therefore invokehammer_ioc_scan_pseudofs()on any mounted HAMMER filesystem (trigger run as uid 1001maxx). -
Sink.
hammer_ioc_scan_pseudofs(hammer_pfs.c:358-366) resolves the record's data and then uses the raw on-diskcursor.leaf->data_lenas thecopyout()length. The underlyinghammer_ip_resolve_dataβhammer_btree_extract(hammer_btree.c:728-771) validates nothing in a production kernel: the only guard is the INVARIANTS-onlyKKASSERT(data_len >= 0 && data_len <= HAMMER_XBUFSIZE)(:736), anddata_off(zone, alignment, containment) is never checked. The record data is then CRC-verified withhammer_crc_test_leafβhammer_datacrc(version, data, leaf->data_len)over a pointer derived from the forgeddata_offset. -
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.
-
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. -
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 unless0 < 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 tosizeof(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=5twice, guest stays up, console showshammer_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
fixedWith 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)
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.
No comments yet.