⬢ DragonFlyBSD Kernel Audit
DF-4534 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-4534",
  "status": "not_reproduced",
  "reproduced": 0,
  "impact": "none",
  "confidence": "likely",
  "verdict": "lptclose() frees sc_inbuf/sc_statbuf (lpt.c:598-599) while (a) the lptout callout - which re-arms itself at 1-tick intervals (DF-1680) and calls lpt_intr without the mplock - may be executing on another cpu, callout_stop() not waiting for it, and (b) the lpt interrupt handler is still registered, since BUS_TEARDOWN_INTR only runs inside ppb_release_bus() at lpt.c:605. With an interrupted write pending (lpt.c:585 skips the drain loop), an lpt_intr firing inside the ~1-2us window dereferences *sc->sc_cp++ (lpt.c:856) from the freed 1024-byte M_LPT chunk and strobes the byte to the port: a silent use-after-free read. 300 open/interrupt-write/close cycles on the IRQ-driven QEMU port produced no panic or anomaly - matching the quantified ~5e-6/cycle hit probability and the fact that a hit is externally invisible. Finding retained on structural evidence; recommend the callout_drain + frees-after-teardown ordering in fix.diff regardless (git-apply-clean, validated against the tree).",
  "exploit_chain": "",
  "evidence": "run.log (300-cycle hammer transcript: child writes interrupted at one chunk, dmesg clean, vmstat -m lpt clean, 315 stale W chunks observed draining post-syscall on the wire); VERDICT.md (window math and the refcount argument that rules out write-vs-close overlap); fix.diff",
  "kernel_refs": [
    "sys/dev/misc/lpt/lpt.c:596",
    "sys/dev/misc/lpt/lpt.c:598",
    "sys/dev/misc/lpt/lpt.c:599",
    "sys/dev/misc/lpt/lpt.c:605",
    "sys/dev/misc/lpt/lpt.c:416",
    "sys/dev/misc/lpt/lpt.c:856",
    "sys/bus/ppbus/ppbconf.c:516"
  ],
  "poc_changes": "Trigger written for this finding (no seed). Signals use a caught SIGUSR1 so the IRQ-path write is interrupted (partial-count return) rather than terminated; the close then always skips the drain loop, entering the kfree windows with the callout armed.",
  "attempts": 1,
  "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": 320,
  "guest_dirty": 0,
  "build_cmd": "cc -O -Wall -o close_lifetime close_lifetime.c",
  "run_cmd": "./close_lifetime 300",
  "code_hash": "6c91f15edefbb5651d4291d6119073cb828243d5eabc445b47fde2a9def595ba",
  "notes": "The write-vs-close variant of this race is unreachable: the blocked write syscall holds a file reference, so d_close only runs after write returns. The remaining contexts (callout, pre-teardown IRQ) are covered here. Root-only trigger (device 0600 root:wheel), so severity Low regardless.",
  "recommended_fix": "callout_drain() after callout_stop() and move both kfree()s after lpt_release_ppbus() with NULL-ing and NULL guards (fix.diff).",
  "fix_status": "not_testable",
  "fix_baseline_reproduced": 0,
  "fix_patched_reproduced": 0,
  "fix_verdict": "Fix validation not applicable: the bug has no observable baseline failure (silent UAF read); the correctness of the new ordering (drain callout, teardown intr, then free) is argued in fix.diff comments and VERDICT.md.",
  "fix_evidence": "fix.diff (git apply --check clean against sys/)"
}