tpm_common_read() clamps the transfer length to priv->response_length but
does not validate the file offset (*off) before using it to index the
fixed TPM_BUFSIZE-byte priv->data_buffer:
ret_size = min_t(ssize_t, size, priv->response_length);
copy_to_user(buf, priv->data_buffer + *off, ret_size);
memset(priv->data_buffer + *off, 0, ret_size);
Sequential read() keeps *off in range, but the fops use the legacy .read
callback and neither tpm_open() nor tpmrm_open() calls nonseekable_open(),
so FMODE_PREAD stays set and pread(2) passes an arbitrary offset straight
into *off. An out-of-range offset then accesses memory beyond data_buffer,
causing an out-of-bounds read through copy_to_user() and, when the copy
succeeds, an out-of-bounds zero-write through memset().
Reject any read whose offset and length leave the response buffer.
Fixes: 9488585b21be ("tpm: add support for partial reads")
Cc: [email protected]
Signed-off-by: Jaewon Yang <[email protected]>
---
Notes for reviewers (not part of the commit):
Reproduced on a KASAN 6.12 build with a swtpm TPM2 device. After a command
leaves a response pending, pread(fd, buf, 16, 0x1400) triggers two
slab-out-of-bounds reports, one for the copy_to_user() read and one for the
memset() write; on that x86-64 build the faulting access was 962 bytes past
a 4344-byte struct tpmrm_priv served from kmalloc-8k. With this patch,
out-of-range preads (offset past the buffer, at the end, or an in-range
offset whose length crosses the end) return -EINVAL with no KASAN report,
while sequential partial reads still return the full response and a normal
read after a rejected pread still works.
Reaching it needs a process that can open the TPM device and send a command.
Access depends on the device-node permissions; the upstream tpm2-tss udev
rules set tpmrm devices to mode 0660 with group tss. My reproduction ran as
root, so I have not shown non-root reach on a specific distribution or built
a privilege-escalation chain.
I searched public archives on 2026-07-10 and found no matching report, which
does not rule out a private, very recent, or unindexed one. Found through
AI-assisted source review; the code path and reproduction were verified by
hand. A reproducer and full logs are available on request.
drivers/char/tpm/tpm-dev-common.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/drivers/char/tpm/tpm-dev-common.c
b/drivers/char/tpm/tpm-dev-common.c
index f942c0c8e..dbf049028 100644
--- a/drivers/char/tpm/tpm-dev-common.c
+++ b/drivers/char/tpm/tpm-dev-common.c
@@ -145,6 +145,16 @@ ssize_t tpm_common_read(struct file *file, char __user
*buf,
goto out;
}
+ /*
+ * Reject reads whose offset and length fall outside the fixed
+ * response buffer.
+ */
+ if (*off < 0 || *off >= TPM_BUFSIZE ||
+ ret_size > TPM_BUFSIZE - *off) {
+ ret_size = -EINVAL;
+ goto out;
+ }
+
rc = copy_to_user(buf, priv->data_buffer + *off, ret_size);
if (rc) {
memset(priv->data_buffer, 0, TPM_BUFSIZE);
--
2.43.0