DF-5327 / verdict.json
{ "finding_id": "DF-5327", "status": "reproduced", "reproduced": true, "impact": "peripheral refcount desync (deterministic 2->1->0 with the ch device OPEN, then close 0->0xFFFFFFFF silent underflow) plus premature free: cam_periph_invalidate() at refcount==0 runs camperiphfree()/chcleanup() and kfrees softc+periph while the descriptor is still open (dmesg removing device entry + postmortem of the freed chunk) -- same memcorrupt class and privilege gate as DF-5273, but via chopen()s flag-before-hold ordering in a driver DF-5273 did not cover", "confidence": "certain", "verdict": "chopen() sets CH_FLAG_OPEN (or drops a secondary openers acquire) before cam_periph_hold(PCATCH); on a signal-interrupted hold its error path releases one reference too many, undercounting the periph refcount per interrupted open. Reproduced deterministically on the guest: after the interrupted open#2, open#1 completes with refcount==0 while /dev/ch0 is open; device removal then frees periph+softc under the open fd (premature free / UAF-enabling state), and a plain close wraps the count to 0xFFFFFFFF silently (DF-5329). Root-gated (SYSCAP_RESTRICTEDROOT) like the rest of the CAM family; exact-chunk attacker regroom was attempted but the freed chunk stayed parked in a full zalloc magazine (documented miss); no uid0 chase for the same reason as DF-5273 (privileged trigger).", "exploit_chain": "open#1 parked via device stall (holds CAM_PERIPH_LOCKED + acquire) -> open#2 signal-interrupted in cam_periph_hold -> chopen error path drops open#1s backing reference (flag still set) -> open#1 finishes at refcount==0 with fd open -> (a) AC_LOST_DEVICE frees periph+softc under the open descriptor, giving any in-flight chclose/chioctl between cam_extend_get() and cam_periph_lock() a use-after-free of the 144-byte M_CAMPERIPH chunk; or (b) plain close -> 0xFFFFFFFF zombie (silent per DF-5329). Escalation beyond the premature-free state is RESTRICTEDROOT-gated and was not chased.", "evidence": "run.log (stage A+B: [child1] open#1 returned: refcount=0 then after close: refcount=-1; stage B removing device entry with fd open + postmortem dump of freed chunk 0xfffff8008d640ba0), fixedA.log (patched kernel: 2/1/0 -- bug gone), VERDICT.md table, dmesg in run.log", "kernel_refs": [ "sys/bus/cam/scsi/scsi_ch.c:415", "sys/bus/cam/scsi/scsi_ch.c:420", "sys/bus/cam/scsi/scsi_ch.c:432", "sys/bus/cam/cam_periph.c:382", "sys/bus/cam/cam_periph.c:402", "sys/bus/cam/cam_periph.c:573", "sys/bus/cam/cam_periph.c:581" ], "poc_changes": "rig evilcd2.c derived from DF-5271/5273s evilcd: added target-1 fake T_CHANGER (inq type 0x08, MODE SENSE pages 0x1d/0x1f), watch=2 ch-periph refcnt/postmortem, defer (park SCSI CCBs), invalidate (AC_LOST_DEVICE), sprayfake; poc_chopen/poc_chopen2 fork children so the parent controls the defer valve (first draft deadlocked by arming defer after open#1)", "attempts": 5, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Fri Oct 2 21:19:55 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/EVIL x86_64 (GENERIC + device ch; vulnerable + patched builds both verified)", "runtime_sec": 25, "guest_dirty": 1, "build_cmd": "cd /root/df5327 && make && cc -O -o poc_chopen poc_chopen.c && cc -O -o poc_chopen2 poc_chopen2.c && kldload ./evilcd2.ko", "run_cmd": "sysctl -n debug.evilcd.watch=2 >/dev/null; usched :1 ./poc_chopen2; ./poc_chopen", "code_hash": "37b77eace559995826059237af5a79c2849da72b3d18f28292ce87f449308d9a", "notes": "kernel rebuilt in-guest as KERNCONF=EVIL (GENERIC+device ch) because ch(4) is not in X86_64_GENERIC and a kld-loaded scsi_ch never gets chinit() called (periphdriver_register does not run the init; boot does). stage B regroom miss: freed 144B chunk parked in a full zalloc magazine, 16384-alloc + 4096-churn spray could not recycle it (documented).", "recommended_fix": "chopen(): take cam_periph_hold() BEFORE setting CH_FLAG_OPEN / dropping the secondary openers acquire (mirror cdopen/daopen/saopen)", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": false, "fix_verdict": "fix.diff applied in guest /usr/src, kernel rebuilt/reinstalled/rebooted, exact same PoC: interrupted open#2 leaves rc=2 (was 1), open#1 completes at rc=1 with fd open (was 0), close -> 0 (was -1). Desync and underflow eliminated; ordering now matches cdopens proven-safe shape.", "fix_evidence": "fixedA.log (patched run) vs run.log stage A (vulnerable run); fix.diff in pack" } |