DF-2658 / verdict.json
{ "finding_id": "DF-2658", "status": "reproduced", "reproduced": true, "impact": "leak", "confidence": "certain", "verdict": "REPRODUCED on the stock INVARIANTS kernel with a 3-master same-clid hammer2 cluster (dd clones, EBUSY merges): column A/B (quorum) hold F with a 1024-byte data-chain allocation, diverged column C holds the same key with 65536 bytes; Cs sync thread runs hammer2_sync_replace() where nradix=hammer2_getradix(chain->bytes) (the OLD size) makes hammer2_chain_resize() a guaranteed no-op (chain.c:1363-1366), after which bcopy(data, chain->data, 65536) at synchro.c:1041 copies 64KB out of the focuss 1024-byte allocation. On-media proof (F_block_on_C.bin): [1024B focus content][4096B d2 file content = cross-file disclosure][zeros byte-identical to Bs media for exactly 60416 tail bytes, matching OFF_B%65536=4096 geometry][4096 bytes past the end of the 64KB kernel dio buffer - 3423 nonzero, led by a literal ELF64 ET_REL x86-64 header = kernel memory]. setcheck+flush persisted the OOB read to Cs media; Fs logical size silently collapsed 65536->1024. The embedded-dirent NULL-bcopy panic variant remains code-proven only (same-name embed divergence requires forged images). A separate pre-existing NULL-deref (hammer2_inode_chain_and_parent, hammer2_inode.c:446-453, via hammer2_chain_inode_find from xop unlink/lookup) kills the guest within seconds after any successful catch-up pass on stock - it cut every run AFTER the bug executed and was outrun by a dmesg-poll + immediate sync.", "exploit_chain": "Unprivileged-readable trigger surface is media persistence: the OOB tail (other files media blocks + kernel buffer-adjacent memory) is written into the synced files data block and CRCd/flushed to the columns image; reading the tail back through the VFS is size-gated (focus inode wins), so direct exfiltration requires raw-device access (root) or a subsequent consumer of the stale block - realistic impact is cross-file/kernel-memory disclosure persisted to cluster media plus metadata inconsistency (vradix vs allocation), not a direct unpriv readout. Escalation to uid=0 does not apply (CWE-125 read-only primitive).", "evidence": "VERDICT.md (full narrative + fix validation); F_block_on_C.bin [1024 f_ab][4096 d2][zeros==B-media 60416][4096 kernel bytes incl 7fELF]; scanbin cmp output equal_prefix=60416 (run.log); find_B_fab.txt OFF_B=29364224 (OFF_B%65536=4096); panic3.backtrace.txt + panic_fixed_kernel_siblingcrash.txt (sibling defect, out of scope); fix validation: findfix_*.txt + cand_*.bin (equal_prefix=0 everywhere on kernel #1) and run_fixed.log (pass completes through patched code).", "kernel_refs": [ "sys/vfs/hammer2/hammer2_synchro.c:939", "sys/vfs/hammer2/hammer2_synchro.c:941", "sys/vfs/hammer2/hammer2_synchro.c:954", "sys/vfs/hammer2/hammer2_synchro.c:1041", "sys/vfs/hammer2/hammer2_synchro.c:1049", "sys/vfs/hammer2/hammer2_chain.c:97", "sys/vfs/hammer2/hammer2_chain.c:1363", "sys/vfs/hammer2/hammer2_io.c:113", "sys/vfs/hammer2/hammer2.h:1963" ], "poc_changes": "No seed PoC existed (audit-time finding, status was untested). Built the full rig: 3-master same-clid clone cluster via vnconfig+newfs_hammer2+EBUSY merges; size-diverged same-key F (1024B on A/B vs 64KB on C) with inum/key alignment preserved by identical creation order, mtid divergence by a two-stage F write on C (run-2 lesson: identical single-shot sequences on cloned allocators make every mtid equal and the scan is skipped); scanbin.c analysis tool (find/stat/dump/cmp with range args); dmesg-poll + immediate sync to beat the sibling crash to the flush.", "attempts": 4, "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": 9600, "guest_dirty": 0, "build_cmd": "guest: cc -O2 -o /root/poc/df2658/scanbin /root/poc/df2658/scanbin.c", "run_cmd": "guest root: sh /root/poc/df2658/trigger_df2658.sh", "code_hash": "fb6f2cbebc2ce9900aca63adfcd833f661c3704d3a9728525584df5afe25ffa8", "notes": "Verification session also mapped the cluster-rig landscape: 2-master clusters park sync threads forever (DF-2657 h2coll math), 3-master quorum works on stock; sync threads created per column at merge (hammer2_pfsalloc); VFS churn >=80 root-tid/s starves the sync entry ipcluster into 60s h2coll parks; mount(8) rc 64 is ambiguous (EBUSY merge vs usage error) - verify clusters via h2nod census. Sibling crash during verification (chain_and_parent NULL deref via chain_inode_find, xop unlink/lookup vs sync-created partial-cluster in-core inodes) reproduced 5/5 sessions (stock+fixed) - recommend filing as its own defect; it is the prerequisite fix for live DF-2660 verification. Guest left reverted to with-src snapshot, up.", "recommended_fix": "nradix from hammer2_getradix(focus->bytes) rounded up to hold focus->bytes; bound both DATA/DIRENT copies by min(focus->bytes, chain->bytes) with the remainder zeroed; bzero for the embedded-focus dirent transition (fix.diff, validated).", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": false, "fix_verdict": "Kernel #1 (2026-10-15 07:30:39, fix2658+fix2660 applied, BUILD_INSTALL_DONE_RC0): identical trigger runs the full catch-up pass through the patched resize/copy path (replace rec 3.0 mod=0x13 through root final 0x19 in run_fixed.log); on C.img none of the three f_ab blocks carries the stock signature (tail-vs-B-media equal_prefix=0 vs stock 60416, no adjacent d2 content, no kernel-byte window) - the persisted OOB read is gone; Fs chain is a 1024-byte allocation of exactly the focus content. The sibling chain_inode_find NULL-deref still fires post-pass on the fixed kernel (pre-existing, out of scope, unchanged signature).", "fix_evidence": "findings/poc/DF-2658/fix_run.log (fixed-kernel run), findfix_C_fab.txt/findfix_B_fab.txt, cand_29372416.bin/cand_29425664.bin/cand_29491200.bin + Btail.bin (cmp equal_prefix=0 on all), panic_fixed_kernel_siblingcrash.txt, fix_build.log" } |