DF-2649 / verdict.json
{ "finding_id": "DF-2649", "status": "not_reproduced", "reproduced": 0, "impact": "none", "confidence": "likely", "verdict": "Code-proven NULL dereference not materializable on the QEMU guest. hammer2_bulkfree_scan guards only HAMMER2_ERROR_CHECK (bulkfree.c:135) before unconditionally dereferencing parent->data->ipdata.filename for PFSROOT inodes in its two kprintf()s (bulkfree.c:143-147 and :393-398); hammer2_chain_load_data leaves chain->data == NULL with error = EIO when bread() fails (hammer2_chain.c:1003-1008). Reachability of the crash precondition was demonstrated: a forged PFSROOT INODE bref in the mounted PFS's root-inode blockset drives the unprivileged (ungated, DF-0815) bulkfree scan into hammer2_chain_lock -> hammer2_chain_load_data -> _hammer2_io_bread on the poison chain (panic_getvolume.txt backtrace). But an in-window failing read cannot be produced on a validly mounted volume: hammer2 enforces volu_size <= media_size at mount (hammer2_ondisk.c:283-290; the inflated-volu_size variant is rejected, mount_reject.log), and out-of-volume offsets panic earlier in hammer2_get_volume (already filed as DF-0875). The triggering condition therefore requires real unreadable media (failing disk sector under a PFS-root inode), turning a routine bulkfree into a kernel panic instead of an error report. Not reproduced per the honest-status table; the finding stands on line-proven analysis plus demonstrated reachability up to the exact load site.", "exploit_chain": "", "evidence": [ "panic_getvolume.txt: unprivileged run on craft2649_v1.img -> panic: no volume for offset 0x100000000 with backtrace hammer2_get_volume <- _hammer2_io_getblk <- _hammer2_io_bread <- hammer2_chain_load_data <- hammer2_chain_lock (proves the scan recursed into and locked the forged PFSROOT chain; DF-0875 manifestation)", "mount_reject.log: volu_size-inflated variant rejected at mount ('size 0x8000000 exceeds device size 0x4000000', hammer2_ondisk.c:283-290) - closes the forged-image route to an in-volume EIO", "VERDICT.md: full 3-attempt narrative incl. the key-sorting discovery (hammer2_base_find requires sorted blockset slots)", "fix.diff: parent->data == NULL guard (covers EIO and zero-data_off chains at both kprintf sites)" ], "kernel_refs": [ "sys/vfs/hammer2/hammer2_bulkfree.c:135", "sys/vfs/hammer2/hammer2_bulkfree.c:143-147", "sys/vfs/hammer2/hammer2_bulkfree.c:393-398", "sys/vfs/hammer2/hammer2_chain.c:1003-1008", "sys/vfs/hammer2/hammer2_chain.c:2876-2879" ], "poc_changes": "Seed concept extended: poison bref placed in the mounted PFS's own root-inode blockset (mount never walks it, bulkfree does); key must sort above the dirent key; out-of-volume data_off manifests DF-0875 before the EIO can be observed; volu_size inflation is blocked by the mount-time device-size cross-check (whose DIOCGPART-failure skip is itself an ondisk.c-scope gap, not re-filed here).", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 420, "guest_dirty": 0, "build_cmd": "guest: truncate -s 64M base.img && newfs_hammer2 -L testvol base.img && (mount, f1, sync, umount); host: python3 forge_2649.py base2649.img craft2649_volusize.img eio (+ inline v1 regenerator in build.sh); guest: cc -O -o trigger trigger.c", "run_cmd": "vnconfig -c vn0 /tmp/craft2649_v1.img && mount -t hammer2 /dev/vn0@testvol /mnt/h2x && su -m nobody -c '/tmp/trigger /mnt/h2x/f1'", "code_hash": "323b5e2fdc23e610313b34d03ab4aec2e350a7cec755cba3b453db9dc12aabf9", "notes": "Fix compiled into the validation kernel together with DF-2650's fix (no regression; healthy mounts unaffected), but the triggering EIO cannot be produced on vn-backed media, so fix behavior is unobservable in-guest: on craft2649_v1.img the fixed kernel still panics in hammer2_get_volume (DF-0875, out of this fix's scope).", "recommended_fix": "In hammer2_bulkfree_scan(), skip the chain (goto done with error |= EIO) when parent->data == NULL after the CHECK guard, before the PFSROOT kprintf at :143-147; the same guard protects :393-398.", "fix_status": "not_testable", "fix_kernel_uname": "", "fix_baseline_reproduced": 0, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff compiled and booted in the validation kernel (see DF-2650 fix_build.log / fix_run.log for the shared build); the triggering condition (in-volume bread EIO) cannot be produced on the QEMU guest, so the guard's behavior change is not observable there; no regression on healthy media.", "fix_evidence": ["DF-2650/fix_build.log", "DF-2650/fix_run.log"] } |