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

No BAR bounds validation on TPM-reported buffer offsets and sizes

  • File: sys/dev/crypto/tpm/tpm_crb.c
  • Lines: 187–196 (register reads), 208–219 (overlap check), 367–397 (use)
  • Severity: Medium
  • CVSS 3.1: CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U:C:N/I:N/A:H
  • CWE: CWE-1284 Improper Validation of Specified Quantity in Input, CWE-787 Out-of-bounds Write (via bus_space)
  • Confidence: likely
  • Status: new

Summary

tpmcrb_attach reads cmd_off, cmd_buf_size, rsp_off, rsp_buf_size directly from TPM CRB registers (lines 187-196) and never validates that each buffer plus its offset fits within the allocated MMIO BAR.

tpmcrb_fix_buff_offsets (lines 124-145) only converts absolute addresses to relative offsets β€” it does not bounds-check.

The subsequent bus_write_region_stream_1 / bus_read_region_stream_1 calls in tpmcrb_transmit (lines 367-370, 385-386, 396-397) pass these unchecked values straight to the bus_space layer, which performs no bounds checking of its own. A buggy or malicious TPM (e.g., a software TPM emulator in a VM, or compromised firmware) can report offsets/sizes that cause MMIO accesses past the mapped resource, producing a kernel panic.

Root cause

tpmcrb_attach (lines 187-196): crb_sc->cmd_off, cmd_buf_size, rsp_off, rsp_buf_size are populated by RD4/RD8 from TPM_CRB_CTRL_CMD_* and TPM_CRB_CTRL_RSP_* registers.

After tpmcrb_fix_buff_offsets (lines 124-145) adjusts them, the only validation is the overlap/size-equality check at lines 208-219 β€” there is no comparison against rman_get_size(sc->mem_res).

In tpmcrb_transmit:

  • bus_write_region_stream_1(sc->mem_res, crb_sc->cmd_off, sc->buf, length) at line 367
  • bus_read_region_stream_1(sc->mem_res, crb_sc->rsp_off + TPM_HEADER_SIZE, &sc->buf[TPM_HEADER_SIZE], bytes_available - TPM_HEADER_SIZE) at line 396

use these unvalidated offsets. bus_write_region_stream_1 is a bare macro (sys/bus.h:595-596) that forwards to bus_space_write_region_stream_1 with no bounds gate.

If cmd_off + length (or rsp_off + bytes_available) exceeds the BAR, the access walks off the end of the kernel virtual mapping established by bus_alloc_resource_any (line 163) β€” on x86 this hits unmapped KVA and panics.

Reachable without user privileges: tpm20_save_state (tpm20.c:292-322) is wired to device_shutdown/device_suspend (lines 412-413 of this file) and calls tpmcrb_transmit at shutdown/suspend time.

Threat model

An attacker who controls the TPM's reported register values β€” a VM hypervisor presenting a malicious CRB emulator (swtpm/QEMU), compromised platform firmware, or even a TPM with a register-implementation bug β€” can crash the guest/host kernel on any buffer access.

Most realistic scenario: a cloud/VM host feeds a guest a CRB device whose CMD/RSP address or size registers report values exceeding the BAR; the guest kernel panics on first transmit (shutdown, suspend, or any root TPM command).

No unprivileged-user path to /dev/tpm0 is needed for the shutdown-triggered variant.

Impact: kernel panic (A:H).

On platforms where bus_space is direct-mapped to RAM, this could additionally corrupt adjacent kernel memory (I:H), though DragonFlyBSD's primary x86 target maps MMIO via pmap_mapdev so the practical result is a reliable panic.

Proof of concept

Setup

A QEMU guest with a custom/swtpm CRB backend whose TPM_CRB_CTRL_CMD_LADDR/ HADDR (offset 0x5C/0x60) or TPM_CRB_CTRL_CMD_SIZE (offset 0x58) return a value placing cmd_off + cmd_buf_size beyond the 0x5000-byte BAR (e.g., report cmd_off=0x6000 while BAR is 0x5000).

Trigger from inside the guest, no privileges required

# init 0   # shutdown
# OR
# acpiconf -s 3   # suspend

The kernel calls tpm20_save_state β†’ tpmcrb_transmit β†’ bus_write_region_stream_1(mem_res, 0x6000, buf, 12), which writes past the mapped MMIO region and panics.

Alternative (root)

# Issue any TPM command (10-byte TPM2_Startup is sufficient)
dd if=/dev/urandom bs=10 count=1 | dd of=/dev/tpm0 bs=10

Success: kernel panic with a page-fault or "fatal trap" in bus_space_write_region_stream_1 / the MMIO fault handler.

A simpler PoC for a guest with a buggy swtpm: configure swtpm to return a cmd_buf_size of 0xFFFFFFFF (so length <= cmd_buf_size passes at line 317 but the MMIO write of length bytes at a bogus cmd_off faults) and boot the guest.

Add BAR-bounds validation in tpmcrb_attach after the offset adjustment and overlap check, before making the device live via tpm20_init.

Use rman_get_size (sys/rman.h:146) to get the BAR size and reject configurations where the buffer extends past the BAR.

--- a/sys/dev/crypto/tpm/tpm_crb.c
+++ b/sys/dev/crypto/tpm/tpm_crb.c
@@ -219,6 +219,27 @@ tpmcrb_attach(device_t dev)
        }
    }

+   /*
+    * Validate that the command and response buffers actually fit
+    * within the allocated MMIO BAR.  The TPM reports these via CRB
+    * registers; a buggy or malicious implementation (common with
+    * software emulators) can report values that would cause OOB
+    * bus_space accesses in tpmcrb_transmit.
+    */
+   bus_size_t bar_size = rman_get_size(sc->mem_res);
+   if (crb_sc->cmd_off >= bar_size ||
+       crb_sc->cmd_buf_size > bar_size - crb_sc->cmd_off) {
+       device_printf(sc->dev,
+           "Command buffer exceeds BAR (off=0x%jx size=0x%jx bar=0x%jx)\n",
+           (uintmax_t)crb_sc->cmd_off, (uintmax_t)crb_sc->cmd_buf_size,
+           (uintmax_t)bar_size);
+       tpmcrb_detach(dev);
+       return (ENXIO);
+   }
+   if (crb_sc->rsp_off >= bar_size ||
+       crb_sc->rsp_buf_size > bar_size - crb_sc->rsp_off) {
+       device_printf(sc->dev,
+           "Response buffer exceeds BAR (off=0x%jx size=0x%jx bar=0x%jx)\n",
+           (uintmax_t)crb_sc->rsp_off, (uintmax_t)crb_sc->rsp_buf_size,
+           (uintmax_t)bar_size);
+       tpmcrb_detach(dev);
+       return (ENXIO);
+   }
+
    sc->transmit = tpmcrb_transmit;

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1989 Β· 10 files
FileTypeDescriptionSize
tpm_crb_oob.c trigger-source Placeholder PoC: documents the trigger and the audit-guest HW absence (no real in-guest PoC is possible without a TPM CRB device). 1.8 KB view raw
build.sh build-script cc -O2 -Wall -o tpm_crb_oob tpm_crb_oob.c 189 B view raw
run.sh run-script Runs the placeholder; prints the HW-gated message 126 B view raw
build.log build-log Baseline (unpatched) tpm.ko module build, full output, rc=0 1.1 KB view raw
run.log run-log PoC build+run on audit guest: prints HW-gated message, RUN_RC=0 259 B view raw
fix_build.log build-log Patched tpm.ko module build with fix.diff applied, full output, rc=0 1.1 KB view raw
fix.diff suggested-fix Add BAR-bounds validation in tpmcrb_attach using rman_get_size; git-apply-able 1.3 KB view raw
env.txt environment uname, cc version, kern.version, pciconf vga check (no TPM) 399 B view raw
VERDICT.md verdict Full narrative: source-traced mechanism, why HW-gated, fix validation 6.2 KB ↓ raw
README.md readme Evidence-pack slot pointer 317 B ↓ raw
README.md readme Evidence-pack slot pointer
↓ download raw

DF-1989 PoC

See the parent finding markdown at findings/DF-1989-*.md for the full threat model and PoC steps. This directory is the evidence-pack slot for the PoC runner; the runner will populate it with sources, build.sh / run.sh, full untrimmed logs, env.txt, VERDICT.md, and manifest.json after verification.

VERDICT.md verdict Full narrative: source-traced mechanism, why HW-gated, fix validation
↓ download raw

DF-1989 β€” No BAR bounds validation on TPM-reported buffer offsets and sizes

Verdict

REPRODUCED (source-only, HW-gated). The bug is real and the cited data-flow is correct end-to-end, but the audit guest has no TPM CRB device (no /dev/tpm*, no tpmcrb in pciconf -l, no crypto-class PCI device), so the trigger cannot be exercised at runtime. Per the run instructions, source-only confirmation is acceptable for HW-gated findings, so this row is marked status=inconclusive, reproduced=0, impact=none (HW-gated). The recommended fix has been authored, applied to the in-guest source tree, and verified to compile (tpm.ko re-linked with rc=0).

Mechanism (source-traced, every hop cited)

  1. Buffer geometry read directly from CRB MMIO registers. sys/dev/crypto/tpm/tpm_crb.c:187-196: c crb_sc->rsp_off = RD8(sc, TPM_CRB_CTRL_RSP_ADDR); /* or RD4+HADDR */ crb_sc->cmd_off = RD4(sc, TPM_CRB_CTRL_CMD_LADDR); crb_sc->cmd_off |= ((uint64_t) RD4(sc, TPM_CRB_CTRL_CMD_HADDR) << 32); crb_sc->cmd_buf_size = RD4(sc, TPM_CRB_CTRL_CMD_SIZE); crb_sc->rsp_buf_size = RD4(sc, TPM_CRB_CTRL_RSP_SIZE); All four values come from device-controlled registers.

  2. tpmcrb_fix_buff_offsets only rewrites absolute→relative offsets. sys/dev/crypto/tpm/tpm_crb.c:124-145: c if (crb_sc->cmd_off > base_addr && crb_sc->cmd_off < base_addr + length) crb_sc->cmd_off -= base_addr; if (crb_sc->rsp_off > base_addr && crb_sc->rsp_off < base_addr + length) crb_sc->rsp_off -= base_addr; This performs no bounds check against the BAR size.

  3. Only an overlap/size-equality check exists. sys/dev/crypto/tpm/tpm_crb.c:208-219: c if (crb_sc->rsp_off == crb_sc->cmd_off) { if (crb_sc->cmd_buf_size != crb_sc->rsp_buf_size) { ... return ENXIO; } } There is no comparison against rman_get_size(sc->mem_res) anywhere in tpmcrb_attach. (rman_get_size is the canonical BAR-size accessor, sys/sys/rman.h:146.)

  4. Unchecked offsets flow into bus_space. sys/dev/crypto/tpm/tpm_crb.c:367-368: bus_write_region_stream_1(sc->mem_res, crb_sc->cmd_off, sc->buf, length); sys/dev/crypto/tpm/tpm_crb.c:396-397: bus_read_region_stream_1(sc->mem_res, crb_sc->rsp_off + TPM_HEADER_SIZE, &sc->buf[TPM_HEADER_SIZE], bytes_available - TPM_HEADER_SIZE); bus_write_region_stream_1 / bus_read_region_stream_1 are bare macros with no bounds gate (they forward straight to bus_space_*_region_stream_1).

  5. Reachable without /dev/tpm0 access. sys/dev/crypto/tpm/tpm_crb.c:412-413: c DEVMETHOD(device_shutdown, tpm20_shutdown), DEVMETHOD(device_suspend, tpm20_suspend), both of which route through tpm20_save_state β†’ tpmcrb_transmit. A shutdown or suspend initiated by any user (e.g. init 0, acpiconf -s 3) reaches the unchecked MMIO write.

Why it is not reproduced at runtime on this guest

pciconf -l on the guest shows no tpm or crypto-class device:

vgapci0@pci0:0:2:0: class=0x030000 ... chip=0x11111234 ...   (QEMU stdvga)

(no tpmcrb, no TPM TIS, no /dev/tpm0). tpmcrb_attach is never called, so the vulnerable code path never executes. Reproducing at runtime would require either booting the guest with -device tpm-crb-device against a patched swtpm, or running on hardware with a real CRB TPM that reports malformed register values β€” neither of which the audit guest provides.

Exploit chain

Not applicable (this is a panic / DoS-class finding on the primary x86 target; bus_space writes past the MMIO mapping fault on unmapped KVA, not into RAM). The finding markdown notes a theoretical I:H variant on platforms where bus_space is direct-mapped to RAM, but DragonFlyBSD's x86 target uses pmap_mapdev, so the practical impact is reliable kernel panic. There is no privilege-escalation primitive to develop.

PoC changes

There is no in-guest runtime PoC possible without a TPM CRB device. I added a trivial placeholder (tpm_crb_oob.c) that documents the trigger and the HW absence, plus build.sh / run.sh. The substantive confirmation is the source trace above.

Add BAR-bounds validation in tpmcrb_attach, between the existing overlap/size-equality check and the sc->transmit = tpmcrb_transmit assignment. Use rman_get_size(sc->mem_res) (already available via <sys/rman.h> included through tpm20.h) to obtain the BAR size, and reject any configuration where cmd_off + cmd_buf_size or rsp_off + rsp_buf_size exceeds the BAR. The check uses the overflow-safe form off >= bar || size > bar - off to handle off == bar correctly.

The fix in fix.diff matches the structure proposed in the finding markdown and has been verified to compile (see fix_build.log).

Fix validation

The fix was applied to /usr/src in the guest (patch -p1 --forward, PATCH_RC=0), and the tpm KLD module was rebuilt:

cd /usr/src/sys/dev/crypto/tpm
rm -f tpm_crb.o tpm.ko
AWK=awk make KERNCONF=X86_64_GENERIC KMODDIR=/tmp/tpm_test
# rc=0, tpm.ko = 37488 bytes

Full build output is in fix_build.log. The fix compiles cleanly with -Werror. Runtime before/after validation is not_testable because the guest has no TPM CRB device; the patched tpm_crb.c was inspected to confirm the BAR-bounds check is inserted before sc->transmit = tpmcrb_transmit and rejects oversized configurations with ENXIO.

Because the bug is HW-gated on this guest, a runtime before/after kernel boot + PoC re-run is not possible β€” fix_status = "not_testable", with the diff verified to apply + compile and source-traced to close the path.

References

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

VALIDATED module build. Patch applies, tpm.ko rebuilds rc=0 -Werror.

patch -p1 PATCH_RC=0; make rc=0 tpm.ko=37488B.
↓ fix.diffmodule build rc=0 (tpm.ko)

Confirmed kernel references

Detail

Exploit chain

none (panic on unmapped KVA on x86; MMIO-OOB class).

Evidence (decisive lines)

Source-trace only. pciconf: no tpm/crypto device, /dev/tpm* absent.

Verified recommended fix

Add BAR-bounds validation using rman_get_size(sc->mem_res) after overlap check at :219.

Verdict

HW-GATED (no TPM CRB device). Bug CONFIRMED source-trace. tpm_crb.c:187-196 reads cmd_off/cmd_buf_size/rsp_off/rsp_buf_size from CRB MMIO with NO bounds check vs rman_get_size. tpmcrb_fix_buff_offsets only adjusts offsets. Path reachable via device_shutdown/suspend without /dev/tpm0 access.