β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4407

/dev/tpm open/read/write are not serialized across CPUs: TOCTOU on TPM_OPEN plus interleaved TIS transactions

Summary

tpmopen check-then-sets TPM_OPEN with no lock or atomic (N CPUs can all win the 'exclusive' open); tpmread/tpmwrite use crit_enter/crit_exit as their ONLY serialization - a DF critical section masks interrupts on the current CPU only and provides ZERO inter-CPU exclusion. Concurrent transactions interleave FIFO writes, locality handoff, and shared sc_stat, wedging the chip and corrupting the command stream. The file carries the FreeBSD commit tag that added dev_lock upstream - but the lock is absent from this port. Root-gated (0600). sc_lock fix mirroring upstream.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4407 Β· 5 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 3.6 KB ↓ raw
env.txt β€” 1.9 KB view raw
fix.diff β€” 4.3 KB view raw
verdict.json β€” 2.7 KB view raw

DF-4407 β€” /dev/tpm open/read/write are not serialized across CPUs (TOCTOU on TPM_OPEN + interleaved TIS transactions)

  • File: sys/dev/crypto/tpm/tpm.c (pass 2, GLM 5.3)
  • Class: CWE-362 (race condition) / CWE-667 (improper locking)
  • Severity: Low β€” the device node is created 0600 root:wheel (tpm.c:253-254), so both racers must already be privileged (tcsd runs as root). Impact is TPM state-machine corruption / self-inflicted DoS, not memory corruption.

Build / run

Not runnable on the audit guest: no TPM TIS device is exposed by QEMU here, the driver is optional tpm (not in X86_64_GENERIC) and no tpm module is loaded β€” see env.txt. On real hardware with a TIS 1.2 TPM:

# as root, on an SMP machine, two processes:
A: while :; do dd if=/dev/tpm of=/dev/null bs=64 count=1 &  done
B: while :; do dd if=/dev/zero of=/dev/tpm bs=64 count=1 &   done

Success criterion: with the bug, tpmopen's single-open check is bypassed (two simultaneous opens succeed) and the TIS locality/STS state machine receives interleaved register writes; expect TPM_E_IO / device wedges (tcsd hangs) and tpm_request_locality EBUSY storms that require a power cycle. Expected after fix: second open returns EBUSY; transactions serialize.

Why (source)

  • tpmopen (tpm.c:1146-1161) does a plain check-then-set of sc->sc_flags |= TPM_OPEN with no lock β€” two CPUs can both observe the flag clear and both succeed (classic TOCTOU), defeating the exclusive-open invariant.
  • tpmread/tpmwrite (tpm.c:1181-1292) use crit_enter()/crit_exit() as their only "serialization". In DragonFly a critical section blocks interrupts and preemption on the current CPU only; it provides no inter-CPU mutual exclusion. Two processes that won the open race (or a process racing the interrupt handler / suspend path) can run tpm_tis12_write concurrently on different CPUs, interleaving TPM_DATA FIFO writes, TPM_ACCESS locality requests (tpm.c:412-413) and shared sc->sc_stat updates (tpm.c:736, 832, 851, 878, 895).
  • This driver was imported from FreeBSD r365144 (see $FreeBSD: tag at tpm.c:18), which is precisely the commit that added sx device locking to FreeBSD's tpm(4); the DragonFly port kept the tag but not the lock.
  • tpmclose (tpm.c:1163-1178) has the same unlocked flag clear.

Add a lock to struct tpm_softc and hold it across open/close/read/write bodies (see fix.diff). This mirrors upstream FreeBSD r365144.

VERDICT.md
↓ download raw

DF-4407 VERDICT β€” untested (source-verified; hardware- and privilege-gated)

Reproduced?

No. Status untested: the audit guest exposes no TPM (no /dev/tpm, no tpm module, QEMU image has no TPM TIS device β€” env.txt), and even on hardware both racing processes must be root (make_dev(..., UID_ROOT, GID_WHEEL, 0600) at tpm.c:253-254). No unprivileged trigger exists, so this does not meet the PoC bar of the audit (not Critical/High, no unprivileged local program).

Defect (certain from source)

  1. TOCTOU on the exclusive-open flag. tpmopen (tpm.c:1146-1161):

c if (sc->sc_flags & TPM_OPEN) /* check */ return EBUSY; sc->sc_flags |= TPM_OPEN; /* set β€” not atomic w.r.t. the check */

No lock, no atomic op. On SMP, N concurrent open("/dev/tpm") calls can all observe the flag clear. tpmclose (tpm.c:1172-1175) clears it the same racy way.

  1. No inter-CPU serialization of transactions. tpmread (tpm.c:1193-1252) and tpmwrite (tpm.c:1267-1292) bracket the whole command with crit_enter()/crit_exit(). A DragonFly critical section is per-CPU (interrupt/preemption shielding only β€” see sys/kern/kern_synch.c which freely combines crit sections with sleeping, e.g. tstop()); it does not exclude another CPU from executing the same driver entry simultaneously. Consequences of two concurrent transactions: - interleaved TPM_DATA FIFO writes from tpm_tis12_write (tpm.c:821-843) β€” the TPM receives two merged commands; the response stream is then parsed by tpmread (tpm.c:1208) with a bogus length, typically wedging the chip until TPM_STS_CMD_READY abort (tpm.c:750) or power cycle; - interleaved locality handoff in tpm_request_locality (tpm.c:412-427) β€” one thread releases locality (tpm_tis12_end, tpm.c:891-892) while the other still assumes it; - data races on shared sc->sc_stat (tpm.c:736, 832, 851, 878, 895) and on the tsleep/wakeup channels (function pointers used as idents, tpm.c:420, 463, 548, 604 vs tpm_intr tpm.c:932-942).

  2. Provenance: the file header carries $FreeBSD: head/sys/dev/tpm/tpm.c 365144 2020-09-01 mjg $ β€” FreeBSD r365144 is the commit that added struct sx dev_lock serialization to tpm(4). The DragonFly import kept the revision tag but the lock (and the softc member) is absent here; the crit_enter() calls are the OpenBSD spl() heritage, which never provided SMP exclusion.

Impact ceiling

Privileged-only DoS of the TPM subsystem (chip wedge / tcsd hang / forced reboot to recover). No kernel memory corruption: all copies stay within buf[TPM_BUFSIZ] regardless of interleaving, because every length used against the buffer is MIN(sizeof(buf), …) (tpm.c:1273) or bounded by the response length check (tpm.c:1212, len > uio->uio_resid with uio_resid being size_t, which also rejects negative lengths β€” verified separately as a killed candidate).

Fix

fix.diff β€” add struct lock sc_lock to struct tpm_softc, init in tpm_attach, uninit at the end of tpm_detach, and take it exclusively in tpmopen/tpmclose/tpmread/tpmwrite. Mirrors FreeBSD r365144. Unvalidated on hardware (fix_status: not_testable) for the same gating reasons as above.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored from source analysis (lockmgr serialization); not buildable/validatable on the audit guest - no TPM device, driver not compiled into X86_64_GENERIC (optional tpm).

findings/poc/DF-4407/fix.diff; env.txt
↓ fix.diffper-fix-DF-4407

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4407/VERDICT.md (full path:line trace), README.md (would-be PoC on hardware), env.txt (live guest proof: no /dev/tpm, no tpm module), fix.diff

Verified recommended fix

Add a lockmgr lock to struct tpm_softc, take it LK_EXCLUSIVE across tpmopen/tpmclose/tpmread/tpmwrite (mirror FreeBSD r365144); see fix.diff.

Verdict

Source-verified missing SMP serialization of /dev/tpm: tpmopen (tpm.c:1146-1161) check-then-sets sc_flags|=TPM_OPEN with no lock (TOCTOU lets N CPUs all win the exclusive open), and tpmread/tpmwrite (tpm.c:1193-1252, 1267-1292) rely on crit_enter(), which in DragonFly only masks interrupts/preemption on the CURRENT CPU and provides no inter-CPU mutual exclusion, so concurrent transactions interleave TPM_DATA FIFO writes (tpm.c:821-843), locality handoff (tpm.c:412-427, 891-892) and shared sc->sc_stat. FreeBSD import tag is r365144 (tpm.c:18) - the exact commit that added dev_lock upstream - but the lock is absent here. Untested at runtime: the audit guest has no TPM device/module (env.txt) and both racers must already be root (make_dev 0600 root:wheel, tpm.c:253-254), so impact ceiling is privileged-only TPM wedge/DoS, not memory corruption.