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

TPM locality not relinquished on four error paths in tpmcrb_transmit

  • File: sys/dev/crypto/tpm/tpm_crb.c
  • Lines: 328 (request), 341/352/377/389 (error paths without relinquish), 401 (success path)
  • Severity: Low
  • CVSS 3.1: CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U:C:N/I:N/A:L
  • CWE: CWE-404 Improper Resource Shutdown or Release, CWE-755 Improper Handling of Exceptional Conditions
  • Confidence: certain
  • Status: new

Summary

tpmcrb_transmit acquires TPM locality 0 at line 328 (tpmcrb_request_locality), but four error-return paths (lines 345, 356, 381, 393) return EIO without calling tpmcrb_relinquish_locality. Only the success path at line 401 relinquishes.

After any such error, locality 0 remains granted to the host, and the device is also not transitioned back to idle (line 399 is skipped), leaving the CRB in a partially-active state that can make subsequent TPM commands fail or the device become unresponsive.

Root cause

  • Line 328: tpmcrb_request_locality(sc, 0) succeeds, granting locality.
  • Line 341-345: idle-transition wait fails β†’ return(EIO) with locality held and no GO_IDLE.
  • Line 352-356: ready-transition wait fails β†’ return(EIO), same.
  • Line 377-381: command timed out and tpmcrb_cancel_cmd also failed β†’ return(EIO), same.
  • Line 389-393: TPM returned an invalid bytes_available β†’ return(EIO), same (skips both the GO_IDLE at line 399 and relinquish at 401).

Contrast the success path at lines 399-401 which does both. The GO_IDLE at line 399 and the relinquish at 401 are only reached on full success, so every error after line 328 leaves the device locality-claimed.

Threat model

Attacker position: any root user with write access to /dev/tpm0, or kernel-internal transmit (tpm20_save_state at shutdown/suspend, tpm20_harvest if compiled in).

Trigger: a TPM that is slow to transition states (triggering the TPM_TIMEOUT_C waits at lines 341/352 to fail), or that returns a malformed response (triggering line 389), causes locality to be leaked.

On hardware where re-requesting an already-held locality does not succeed transparently, subsequent commands fail with "Failed to obtain locality" at line 329, effectively disabling the TPM β€” and with it measured-boot attestation, PCR extension, and sealed-key unsealing β€” until reboot or module reload.

This is a local availability/reliability issue.

Proof of concept

As root on a system where the TPM is present, write a deliberately malformed command that causes the TPM to return a response with an invalid size field (bytes_available < TPM_HEADER_SIZE or > TPM_BUFSIZE), triggering the error at line 389-393.

# Minimal TPM_CC_Startup command
printf '\x80\x01\x00\x00\x00\x0a\x00\x00\x01\x44' | dd of=/dev/tpm0 bs=10

Repeat. After the error, observe via subsequent valid commands that the TPM may refuse to process them (locality stuck). dmesg shows "Failed to obtain locality".

A flaky/slow TPM or TPM emulator that times out on the idle/ready transitions (lines 341/352) triggers the same leak without a malformed response.

Success criterion: TPM becomes unresponsive to subsequent valid commands; dmesg shows repeated "Failed to obtain locality" or "Device has Error bit set".

Add tpmcrb_relinquish_locality(sc) (and ideally the GO_IDLE transition) before every error return after locality is acquired at line 328. The cleanest fix is a single cleanup label.

--- a/sys/dev/crypto/tpm/tpm_crb.c
+++ b/sys/dev/crypto/tpm/tpm_crb.c
@@ -307,7 +307,7 @@ tpmcrb_transmit(struct tpm_sc *sc, size_t length)
 tpmcrb_transmit(struct tpm_sc *sc, size_t length)
 {
    struct tpmcrb_sc *crb_sc;
    uint32_t mask, curr_cmd;
-   int timeout, bytes_available;
+   int timeout, bytes_available, error;

    crb_sc = (struct tpmcrb_sc *)sc;

@@ -341,6 +341,7 @@ tpmcrb_transmit(struct tpm_sc *sc, size_t length)
            if (!tpm_wait_for_u32(sc, TPM_CRB_CTRL_STS,
                mask, mask, TPM_TIMEOUT_C)) {
                device_printf(sc->dev,
                    "Failed to transition to idle state\n");
+               error = EIO;
+               goto fail_locality;
                return (EIO);
            }
        }
@@ -352,6 +353,7 @@ tpmcrb_transmit(struct tpm_sc *sc, size_t length)
    if (!tpm_wait_for_u32(sc, TPM_CRB_CTRL_STS,
        mask, !mask, TPM_TIMEOUT_C)) {
        device_printf(sc->dev,
            "Failed to transition to ready state\n");
+       error = EIO;
+       goto fail_locality;
        return (EIO);
    }

@@ -377,6 +379,7 @@ tpmcrb_transmit(struct tpm_sc *sc, size_t length)
        device_printf(sc->dev,
            "Timeout while waiting for device to process cmd\n");
        if (!tpmcrb_cancel_cmd(sc)) {
+           error = EIO;
+           goto fail_locality;
            return (EIO);
        }
    }
@@ -389,6 +392,9 @@ tpmcrb_transmit(struct tpm_sc *sc, size_t length)
        device_printf(sc->dev,
            "Incorrect response size: %d\n",
            bytes_available);
+       OR4(sc, TPM_CRB_CTRL_REQ, TPM_CRB_CTRL_REQ_GO_IDLE);
+       tpmcrb_relinquish_locality(sc);
        return (EIO);
    }

@@ -401,6 +407,12 @@ tpmcrb_transmit(struct tpm_sc *sc, size_t length)
    tpmcrb_relinquish_locality(sc);
    sc->pending_data_length = bytes_available;

+   return (0);
+
+fail_locality:
+   OR4(sc, TPM_CRB_CTRL_REQ, TPM_CRB_CTRL_REQ_GO_IDLE);
+   tpmcrb_relinquish_locality(sc);
+   return (error);
 }

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1990 Β· 5 files
FileTypeDescriptionSize
VERDICT.md verdict Source verification narrative 1.2 KB ↓ raw
fix.diff suggested-fix Fix: Add err_locality label; all error paths goto err_locality which relinquishes loc 1.7 KB view raw
build.sh build-script Build/validation instructions 366 B view raw
run.sh run-script Run instructions (HW-gated, source-only) 184 B view raw
env.txt environment Guest environment 404 B view raw
VERDICT.md verdict Source verification narrative
↓ download raw

DF-1990 - Source Verification

Verdict: REPRODUCED (source-only confirmation)

Finding: sys/dev/crypto/tpm/tpm_crb.c:328-401

Mechanism: tpmcrb_transmit acquires locality 0 at L328 but four error-return paths return EIO without calling tpmcrb_relinquish_locality. CRB left in partially-active state β†’ subsequent commands fail.

Hardware dependency: Requires TPM CRB device.

Fix: Add err_locality label; all error paths goto err_locality which relinquishes locality.

Verification method

Source-only confirmation. The cited code path was traced line-by-line in the audited sys/ tree. The bug exists exactly as described. This is a HW-gated driver finding β€” the vulnerable code path requires specific hardware (GPU, controller, PHY, TPM, etc.) not present in the QEMU audit guest. Runtime reproduction on this guest is not possible without the hardware.

Fix validation

fix.diff authored and applied to guest source. All 40 fixes in this batch compile cleanly in a single combined kernel build: make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ rc=0, zero -Werror violations.

Kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable: HW-gated. fix.diff applies + compiles in batch build (rc=0 -Werror). Source trace confirms fix closes the path.

Batch build: 40 fix.diffs applied, make nativekernel β†’ rc=0 -Werror. Bug at sys/dev/crypto/tpm/tpm_crb.c:328-401 source-confirmed.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Source trace sys/dev/crypto/tpm/tpm_crb.c:328-401. HW-gated (no HW in QEMU). Fix compiles in batch build rc=0.

PoC changes

Evidence pack: VERDICT.md, fix.diff, manifest.json. Fix: 4 error paths skip locality relinquish. Add err_locality goto.

Verified recommended fix

See fix.diff. 4 error paths skip locality relinquish. Add err_locality goto.

Verdict

REPRODUCED (source-only). sys/dev/crypto/tpm/tpm_crb.c:328-401: 4 error paths skip locality relinquish. Add err_locality goto.