/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)
PoC verification
Evidence pack
findings/poc/DF-4407 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 ofsc->sc_flags |= TPM_OPENwith 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) usecrit_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 runtpm_tis12_writeconcurrently on different CPUs, interleavingTPM_DATAFIFO writes,TPM_ACCESSlocality requests (tpm.c:412-413) and sharedsc->sc_statupdates (tpm.c:736, 832, 851, 878, 895).- This driver was imported from FreeBSD r365144 (see
$FreeBSD:tag attpm.c:18), which is precisely the commit that addedsxdevice 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.
Recommended fix
Add a lock to struct tpm_softc and hold it across open/close/read/write
bodies (see fix.diff). This mirrors upstream FreeBSD r365144.
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)
- 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.
-
No inter-CPU serialization of transactions.
tpmread(tpm.c:1193-1252) andtpmwrite(tpm.c:1267-1292) bracket the whole command withcrit_enter()/crit_exit(). A DragonFly critical section is per-CPU (interrupt/preemption shielding only β seesys/kern/kern_synch.cwhich 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: - interleavedTPM_DATAFIFO writes fromtpm_tis12_write(tpm.c:821-843) β the TPM receives two merged commands; the response stream is then parsed bytpmread(tpm.c:1208) with a bogus length, typically wedging the chip untilTPM_STS_CMD_READYabort (tpm.c:750) or power cycle; - interleaved locality handoff intpm_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 sharedsc->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, 604vstpm_intrtpm.c:932-942). -
Provenance: the file header carries
$FreeBSD: head/sys/dev/tpm/tpm.c 365144 2020-09-01 mjg $β FreeBSD r365144 is the commit that addedstruct sx dev_lockserialization to tpm(4). The DragonFly import kept the revision tag but the lock (and the softc member) is absent here; thecrit_enter()calls are the OpenBSDspl()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_testablefix.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
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.
No comments yet.