DF-3068 / verdict.json
{ "finding_id": "DF-3068", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "REPRODUCED twice on the stock INVARIANTS kernel (second run after a fresh vm.sh reset with-src, clean sequence): mounting a crafted version-3 HAMMER image whose vol0_blockmap[3] names an 8-byte nominal undo range (first=UNDO|0x1000, next=UNDO|0x1008) containing a fake tail {0xC74F,0x0041,24} at 0x1000 that anchors a CRC-valid 24-byte DUMMY head at 0xff0 makes hammer_recover_stage1's undo loop consume 24 bytes where 8 remain (bytes -= hdr_size, hammer_recover.c:431) and KKASSERT(error || bytes == 0) at :460 panics: 'panic: assertion \"error || bytes == 0\" failed in hammer_recover_stage1 at hammer_recover.c:460', trace hammer_recover_stage1 <- hammer_vfs_mount <- sys_mount, guest wedged at db>. Root cause: hammer_check_tail_signature positions the head via the TAIL's size (:1023-1026) while hammer_recover_scan_rev steps via the HEAD's hdr_size (:815), and _hammer_check_signature never requires tail/head size agreement for PAD/DUMMY-style short records (:901-924 skip, :948-961 re-derive) - version<4 mounts take next_offset verbatim (:225-226). On production (non-INVARIANTS) kernels the assert is absent: the mount silently succeeds, executes a record from outside the nominal undo range, and rewrites+flushes the volume-header blockmap (:481-488) - silent mis-recovery, no memory unsafety (undo copies clamped at :1053-1076), hence Low severity. FIXED and validated: fix.diff adds hdr_size>bytes -> EIO guards in stage1's undo loop and stage2's fwd loop; on the rebuilt kernel #1 the identical image yields 'HAMMER(uabase) Corrupt UNDO record size 0018 at 3000000000000ff0' + 'mount: Input/output error', no panic, guest up; stock regressions (mount/RW cycle, hard-crash dirty recovery) all pass.", "exploit_chain": "", "evidence": [ "panic.txt (clean run): 'HAMMER(uabase) recovery undo 3000000000001000-3000000000001008 (8 bytes)(RW)' then 'panic: assertion \"error || bytes == 0\" failed in hammer_recover_stage1 at /usr/src/sys/vfs/hammer/hammer_recover.c:460' with trace hammer_recover_stage1 <- hammer_vfs_mount <- sys_mount <- syscall2, guest at db>", "panic_console_full.txt: first (accidental) run - identical panic, proving determinism", "run.log: padflood desync craft output - 'DUMMY+fake-tail written at phys 0000000001088000 + ff0', 'undo AFTER first=3000000000001000 next=3000000000001008', 'vol_version=3 mode=desync crafted OK'", "fix_run.log (kernel #1 + fix.diff): 'Corrupt UNDO record size 0018 at 3000000000000ff0' + 'mount: Input/output error' MOUNT_RC=1, no panic, guest up; plus stock regressions", "fix_build.log: make nativekernel KERNCONF=X86_64_GENERIC completed (kernel #1)" ], "kernel_refs": [ "sys/vfs/hammer/hammer_recover.c:225-226", "sys/vfs/hammer/hammer_recover.c:393-431", "sys/vfs/hammer/hammer_recover.c:460", "sys/vfs/hammer/hammer_recover.c:814-815", "sys/vfs/hammer/hammer_recover.c:901-924", "sys/vfs/hammer/hammer_recover.c:948-961", "sys/vfs/hammer/hammer_recover.c:1023-1026" ], "poc_changes": "Pass-2 author wrote the PoC from scratch (padflood desync mode). The DUMMY's tail bytes double as the next fake tail so one 24-byte record satisfies both the real-tail and fake-tail decodes; CRC computed as crc32(head[0..12)) ^ crc32(bytes[16..24)) per hammer_crc_get_fifo_head. Version patched 7->3 so the nominal range comes straight from the crafted blockmap; no vol_crc fixup needed (DF-3042).", "attempts": 2, "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": 1500, "guest_dirty": 0, "build_cmd": "sh build.sh (cc -O -o padflood padflood.c -I/usr/src/sys/vfs/hammer; truncate -s 12G + newfs_hammer base image)", "run_cmd": "sh run3068.sh", "code_hash": "9dee8b15160f8fa2963263bf24c926c06bbf765cd9c89cc33cda337119e6f35f", "notes": "Deterministic INVARIANTS-only panic (dos bucket); production kernels silently mis-recover instead - no memory corruption, all undo copies bounds-checked. Sibling asserts reachable via the same desync: :394, stage2 :698/:716, and the rterm KKASSERT at :1286. Same trust boundary as the rest of the hammer crafted-image family (root mount or vfs.usermount=1). Guest reset (with-src) after validation.", "recommended_fix": "Reject records whose hdr_size exceeds the remaining recovery bytes (EIO) in stage1's undo loop and stage2's forward loop before executing them; see fix.diff", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Sep 5 22:10:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Identical PoC on the patched kernel: the new guard fires at exactly the crafted record ('Corrupt UNDO record size 0018 at 3000000000000ff0'), mount fails with I/O error, no panic, guest stays up (vm.sh status: up). Stock regressions pass incl. hard-crash dirty recovery.", "fix_evidence": "fix_run.log, fix_build.log (kernel #1 build), fix.diff" } |