DF-4929 / verdict.json
{ "finding_id": "DF-4929", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "REPRODUCED 2/2 as a deterministic kernel panic caused by a use-after-free in the tpm detach path: tpmcrb_detach (tpm_crb.c:230-243) calls tpm20_release (tpm20.c:211-226) with no dev_lock and no draining while tpm20_write (tpm20.c:124-143) holds dev_lock across a multi-second transmit; lockuninits KKASSERT passes on a held lock (proven at runtime, lockstatus=8), destroy_dev/devfs_destroy_dev does not drain (devfs_core.c:758), so kldunload tpm frees the softc and sc->buf under the transmitting thread and under the armed discard_buffer_callout. The stock tpm20_discard_buffer then fires ~500ms later on the freed softc (callout context, current process Idle) and faults reading freed memory (device_printf(sc->dev)); the writer thread itself completed silently on the freed softc first (WRITER DONE rc=0). Trigger is root-only (kldunload + /dev/tpm0 0600), so this is a privileged-actor UAF/reliable DoS, not unpriv->root. Fixed and validated: with fix.diff, the identical run serializes (detach blocks behind the writer), drains the callout, and completes with no panic and the guest up.", "exploit_chain": "root: (1) open /dev/tpm0 and issue a slow TPM command (CreatePrimary polls up to 40s under dev_lock); (2) concurrently kldunload tpm -> tpmcrb_detach -> tpm20_release frees sc->buf + softc while the writer sleeps in transmit; (3) heap grooming between the kfree and the 500ms delayed callout reclaims the softc, making tpm20_discard_buffers memset(sc->buf,0,0x1000) and lockmgr(sc->dev_lock) execute with attacker-shaped freed-memory contents (wild kernel write / lock corruption). Demonstrated up to the panic (runs 1-2); grooming-to-controlled-write not needed for the verdict because the trigger itself requires root, capping impact at privileged DoS/memory corruption.", "evidence": "panic.txt (run 1 fatal trap in device_print_prettyname via tpm20_discard_buffer on freed softc), panic.2.txt (run 2: lockstatus=8 during release, lockuninit assert silent, writer completes on freed softc, identical delayed callout panic), run.log/run.1.log/run.2.log (rc=124 ssh hang = guest death), run_fixed.log (patched kernel: no panic, guest up), build.log (stock tpm20.c sha256 provenance), fix_build_tail.log (patched kernel build+install, 0 errors), VERDICT.md (full trace).", "kernel_refs": [ "sys/dev/crypto/tpm/tpm_crb.c:230-243", "sys/dev/crypto/tpm/tpm20.c:100-145", "sys/dev/crypto/tpm/tpm20.c:138-139", "sys/dev/crypto/tpm/tpm20.c:147-166", "sys/dev/crypto/tpm/tpm20.c:211-226", "sys/kern/kern_lock.c:1325-1334", "sys/vfs/devfs/devfs_core.c:758-765", "sys/kern/subr_bus.c:1943-1954" ], "poc_changes": "PoC authored fresh for this finding (orchestrator seeded none): uafsim KLD that #includes the stock guest /usr/src tpm20.c verbatim so all lifetime-relevant code is shipped code; only the MMIO transport is stubbed with tpmcrb_transmits timing profile; detach sysctl replays the exact kldunload teardown sequence. Two iterations: sleep tuning to land the detach mid-transmit (run 2) after run 1 landed post-writer.", "attempts": 3, "guest_uname": "DragonFly dfbsd 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 (guest: cp stock tpm20.c, make -m /usr/share/mk)", "run_cmd": "sh run.sh (guest: writer>/dev/uafsim0 & ; sysctl hw.uafsim_detach=1)", "code_hash": "3f55985bfd393b69117e859b7b04e5307e91f17f6008b86a5081f78622982440", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff (tpm20.c+tpm20.h) applied inside the guest /usr/src, kernel rebuilt with nativekernel (0 errors) and installed; identical reproducer re-run on the patched kernel: same race observed (lockstatus=8), detach serialized behind the writer, callout drained, NO panic, guest stayed up (run_fixed.log). Baseline 2/2 panic vs patched 1/1 clean.", "fix_evidence": "run_fixed.log, fix_build_tail.log (BUILD_INSTALL_OK, install completed Fri Sep 25 05:57:15 UTC 2026), VERDICT.md Fix validation section", "notes": "tpm KMOD not in GENERIC; no TPM hardware on guest (no swtpm backend available), hence the stock-code KLD approach. Shared root cause also affects tpm_tis (same tpm20_release) - same fix applies. Residual narrow window for cv_wait-blocked writers is documented in VERDICT.md; recommended hardening: open-refcount making module unload fail while /dev/tpm0 is open.", "recommended_fix": "Serialize tpm20_release under dev_lock, set a dying flag checked by tpm20_read/tpm20_write/tpm20_discard_buffer, and drain discard_buffer_callout before and after the critical section (see fix.diff)." } |