Page MenuHome GnuPG

Stack-based buffer overflow in TPM2 `PKDECRYPT`
Closed, ResolvedPublic

Description

A stack-based buffer overflow exists in GnuPG’s tpm2daemon when handling the PKDECRYPT command for TPM-backed RSA and ECC keys.

Security Advisory: Stack-based buffer overflow in TPM2 PKDECRYPT for RSA
and ECC due to missing ciphertext length validation
Vulnerability Type: Stack-based Buffer Overflow
Affected Software: GnuPG
Severity: High
Date: 18 Jan 2026
Discoverer: OpenAI Security Research (OutboundDisclosures@openai.com)

Summary
A stack-based buffer overflow exists in GnuPG’s tpm2daemon when handling
the PKDECRYPT command for TPM-backed RSA and ECC keys. A local attacker
who can access the daemon’s Assuan socket can send an oversized ciphertext
and trigger memory corruption, resulting in a crash and potentially
arbitrary code execution. When a user stores private keys inside a TPM,
GnuPG runs a helper process called tpm2daemon to perform cryptographic
operations on their behalf. Other GnuPG components communicate with this
daemon over Assuan, a local IPC protocol. During a PKDECRYPT request,
tpm2daemon copies the attacker-supplied ciphertext into fixed-size TPM
work buffers without validating that the ciphertext fits. If the supplied
ciphertext is larger than the TPM buffer, the copy operation writes past
the end of the stack buffer and corrupts adjacent stack memory. This
affects both supported TPM decrypt paths: RSA (tpm2_rsa_decrypt) and ECC
(tpm2_ecc_decrypt). Because the overflow occurs on the stack and is
attacker-controlled, it is potentially exploitable for code execution
inside the tpm2daemon process.

Affected Versions

  • Software Name: GnuPG
  • Confirmed Version(s): 2.5.17-beta2, commit:

d97e52cc7fc5cc7bd8629643fdf37105891ae201, date: 2025-12-30T17:52:43+01:00

  • Likely Affected: tpm2d
  • Introduced Commit (If relevant): N/A

Vulnerability Details:
Product: GnuPG

Components:

  • tpm2d/command.c handles the PKDECRYPT Assuan command
  • tpm2d/tpm2.c implements the RSA and ECC decrypt paths

Trigger surface:
In tpm2d/command.c, function cmd_pkdecrypt:

  • EXTRA is read with assuan_inquire using MAXLEN_KEYDATA = 4096.
  • The resulting length is passed straight into the decrypt routines.

Code excerpt:

rc = assuan_inquire (ctx, "EXTRA", &crypto, &cryptolen, MAXLEN_KEYDATA);
...
if (type == TPM_ALG_RSA)
  rc = tpm2_rsa_decrypt (ctrl, tssc, key, pin_cb, crypto,
cryptolen, &buf, &buflen);
else if (type == TPM_ALG_ECC)
  rc = tpm2_ecc_decrypt (ctrl, tssc, key, pin_cb, crypto,
cryptolen, &buf, &buflen);

Root cause:
PKDECRYPT handler passes attacker-controlled ciphertext and length into
decrypt routines without enforcing ciphertext length validation. The
decrypt routines copy attacker-controlled ciphertext into fixed-size
buffers using the attacker-controlled length.

RSA path:
In tpm2d/tpm2.c:

  • tpm2_rsa_decrypt copies the ciphertext into a fixed-size buffer using

ciphertext_len as the copy length.

  • No check ensures ciphertext_len is smaller than the destination buffer.
  • The destination buffer has a fixed maximum size, while the Assuan input

limit allows much larger values.

Code excerpt:

/*
 * apparent gcrypt error: occasionally rsa ciphertext will
 * be one byte too long and have a leading zero
 */
if ((ciphertext_len & 1) == 1 && ciphertext[0] == 0)
  {
    ciphertext_len--;
    ciphertext++;
  }
cipherText.size = ciphertext_len;
memcpy (cipherText.buffer, ciphertext, ciphertext_len);

ECC path:
In tpm2d/tpm2.c:

  • tpm2_ecc_decrypt splits the ciphertext into x and y coordinate halves

and copies them into fixed-size buffers.

  • No check ensures the coordinate length fits in the x and y destination

buffers.

Code excerpt:

if (ciphertext[0] != 0x04)
  return GPG_ERR_ENCODING_PROBLEM;
if ((ciphertext_len & 0x01) != 1)
  return GPG_ERR_ENCODING_PROBLEM;

len = ciphertext_len >> 1;
memcpy (VAL_2B (inPoint.point.x, buffer), ciphertext + 1, len);
VAL_2B (inPoint.point.x, size) = len;
memcpy (VAL_2B (inPoint.point.y, buffer), ciphertext + 1 + len, len);
VAL_2B (inPoint.point.y, size) = len;

Exploit scenario story:

  1. A user stores a private key in a TPM and uses it through GnuPG, which

causes gpg-agent to launch and communicate with tpm2daemon.

  1. An attacker gains the ability to connect to the user’s tpm2daemon

Assuan socket (for example via a malicious plugin, compromised helper
binary, or any code running as that user).

  1. The attacker sends a PKDECRYPT request with an oversized ciphertext

payload.

  1. tpm2daemon copies the ciphertext into a fixed-size TPM buffer,

overflows the stack, and crashes — and with sufficient exploitation work,
may execute attacker-controlled code.

Notes:

  • The overflow occurs before the PIN prompt and before the TPM operation,

so it can be triggered without interacting with the user once a key is
loadable.

  • Default GnuPG socket permissions usually restrict access to the same

user, so this is primarily a local attack. If the socket is exposed by
misconfiguration, the impact increases.

Proof-of-Concept (PoC)
POC:
https://drive.google.com/file/d/1CHlmAnnxZGpKl-6ZgFOAwbujT1d8izQt/view?usp=sharing

PoC description:

  • Builds and runs tpm2daemon --server with AddressSanitizer enabled,
  • Starts swtpm, a software TPM, and
  • Uses Assuan IMPORT to get valid KEYDATA, then sends PKDECRYPT with

a 4096-byte EXTRA to trigger the overflow.

Files:

  • keydata.b64: base64-encoded canonical RSA private-key S-expression used

as IMPORT KEYDATA.

  • trigger.py: Assuan client that runs IMPORT then triggers the overflow

via PKDECRYPT.

  • Dockerfile: builds an ASan-enabled tpm2daemon and runs the PoC.

Run (Docker)
cd /path/to/poc
docker build -t tpm2-poc .
docker run --rm -it tpm2-poc

ASan or Debug Output
Expected vulnerable result:
On a vulnerable build, tpm2daemon crashes. With AddressSanitizer enabled,
the crash is reported as a stack buffer overflow. Example captured
2026-01-02:

docker run --rm -it bug14-poc
OK GNU Privacy Guard's TPM2 server ready
IMPORT returned 528 bytes of KEYDATA
PKDECRYPT caused tpm2daemon to terminate (expected on vulnerable ASan build)
tpm2daemon exit code: -6

  • tpm2daemon stderr (ASan report) ---

tpm2daemon[18]: handler for fd -1 started
tpm2daemon[18]: DBG: asking for PIN 'Please enter the TPM Authorization
passphrase for the key.'
ERROR:esys:src/tss2-esys/esys_iutil.c:395:iesys_handle_to_tpm_handle()
Error: Esys invalid ESAPI handle (ff).
ERROR:esys:src/tss2-esys/esys_iutil.c:1116:esys_GetResourceObject() Unknown
ESYS handle. ErrorCode (0x0007000b)
ERROR:esys:src/tss2-esys/esys_tr.c:606:Esys_TRSess_SetAttributes() Object

not found ErrorCode (0x0007000b)

18==ERROR: AddressSanitizer: stack-buffer-overflow on address

0xffffa5df1382 at pc 0xffffaaa45124 bp 0xffffa6bfd500 sp 0xffffa6bfcce0
WRITE of size 4096 at 0xffffa5df1382 thread T1

#0 0xffffaaa45120 in memcpy

../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115

#1 0xaaaab411d948 in tpm2_rsa_decrypt /src/gnupg/tpm2d/tpm2.c:991
#2 0xaaaab411447c in cmd_pkdecrypt /src/gnupg/tpm2d/command.c:327
#3 0xffffaa76a284  (/lib/aarch64-linux-gnu/libassuan.so.0+0xa284)

(BuildId: 6fb616e77c997cbcf21f487bc05f9a5ac3686073)

#4 0xffffaa76a8dc in assuan_process

(/lib/aarch64-linux-gnu/libassuan.so.0+0xa8dc) (BuildId:
6fb616e77c997cbcf21f487bc05f9a5ac3686073)

#5 0xaaaab4115248 in tpm2d_command_handler

/src/gnupg/tpm2d/command.c:471

#6 0xaaaab4118220 in start_connection_thread

/src/gnupg/tpm2d/tpm2daemon.c:1032

#7 0xffffaa731bf0  (/lib/aarch64-linux-gnu/libnpth.so.0+0x1bf0)

(BuildId: 8a14b3b6ed49610935f78cac8e74a3586086f2f9)

#8 0xffffaa9bf3d0 in asan_thread_start

../../../../src/libsanitizer/asan/asan_interceptors.cpp:234

#9 0xffffaa475958  (/lib/aarch64-linux-gnu/libc.so.6+0x85958) (BuildId:

d6c205bda1b6e91815f8fef45bdf56bc2239c37e)

#10 0xffffaa4db898  (/lib/aarch64-linux-gnu/libc.so.6+0xeb898)

(BuildId: d6c205bda1b6e91815f8fef45bdf56bc2239c37e)

Address 0xffffa5df1382 is located in stack of thread T1 at offset 898 in
frame

  #0 0xaaaab411d7c0 in tpm2_rsa_decrypt /src/gnupg/tpm2d/tpm2.c:971

This frame has 8 object(s):
  [48, 52) 'ah' (line 976)
  [64, 70) 'inScheme' (line 974)
  [96, 104) 'auth' (line 977)
  [128, 136) 'out'
  [160, 226) 'label'
  [272, 338) 'authVal2B'
  [384, 898) 'cipherText' (line 973)
  [1040, 1554) 'message' (line 975) <== Memory access at offset 898

partially underflows this variable
HINT: this may be a false positive if your program uses some custom stack
unwind mechanism, swapcontext or vfork

(longjmp and C++ exceptions *are* supported)

Thread T1 created by T0 here:

#0 0xffffaaa3edfc in pthread_create

../../../../src/libsanitizer/asan/asan_interceptors.cpp:245

#1 0xffffaa731db4 in npth_create

(/lib/aarch64-linux-gnu/libnpth.so.0+0x1db4) (BuildId:
8a14b3b6ed49610935f78cac8e74a3586086f2f9)

#2 0xaaaab4117920 in main /src/gnupg/tpm2d/tpm2daemon.c:643
#3 0xffffaa4184c0  (/lib/aarch64-linux-gnu/libc.so.6+0x284c0) (BuildId:

d6c205bda1b6e91815f8fef45bdf56bc2239c37e)

#4 0xffffaa418594 in __libc_start_main

(/lib/aarch64-linux-gnu/libc.so.6+0x28594) (BuildId:
d6c205bda1b6e91815f8fef45bdf56bc2239c37e)

#5 0xaaaab4113e6c in _start (/poc/tpm2daemon+0x13e6c) (BuildId:

adfa9ccbcf3a7930b6886c49b758f8c62e42f344)

Impact

  • tpm2daemon can be crashed during decrypt operations, breaking

TPM-backed key usage until the daemon is restarted.

  • If exploited, an attacker would gain code execution as the same user

account running tpm2daemon, allowing abuse of that user’s TPM-protected
keys and cryptographic operations.

Suggested fix

  • Enforce strict ciphertext length validation before copying

attacker-controlled data:

  • RSA: Require ciphertext_len == key_size_bytes (after handling

leading-zero padding safely).

  • ECC: Require ciphertext_len == 1 + 2 * coord_size for the curve and

reject oversized coordinates.

  • Query loaded public-key parameters (RSA modulus size, ECC curve) and

validate input sizes against them.

  • Reject invalid sizes before any TPM or memory operations.
  • Never copy attacker-controlled ciphertext directly into fixed-size TPM

buffers without validation.

Timeline

  • January 2026: Discovered
  • January 2026: Reproduced and confirmed
  • January 2026: Advisory drafted

This information is being shared by OpenAI solely for the purpose of
improving security and reducing potential harm. This information is
presented as-is. We make no representations or warranties, express or
implied, as to the completeness, accuracy, or fitness for any particular
purpose of the information. [This includes, without limitation any
suggestions or ideas presented on how to remedy or mitigate an identified
vulnerability, including whether such suggestions or ideas would be
effective and/or could have other negative impacts.]
OpenAI disclaims any liability for direct or indirect damages arising from
the reliance on, or use, misuse, or interpretation of this information. Any
references to third-party systems, services, or entities are included
solely for identification purposes and do not imply endorsement,
responsibility, or attribution.

Revisions and Commits

Event Timeline

Considering the current implementation (tpm2d doesn't support keyinfo like scdaemon), it would be good to check the buffer size.
(If key information is accessible easily, we can check with a specific key.)

diff --git a/tpm2d/tpm2.c b/tpm2d/tpm2.c
index a4677fb98..00da6b2db 100644
--- a/tpm2d/tpm2.c
+++ b/tpm2d/tpm2.c
@@ -951,6 +951,9 @@ tpm2_ecc_decrypt (ctrl_t ctrl, TSS_CONTEXT *tssc, TPM_HANDLE key,
   size_t len;
   int ret;
 
+  if (ciphertext_len > 2*TPM2_MAX_ECC_KEY_BYTES + 1)
+    return GPG_ERR_TOO_LARGE;
+
   /* This isn't really a decryption per se.  The ciphertext actually
    * contains an EC Point which we must multiply by the private key number.
    *
@@ -1010,6 +1013,9 @@ tpm2_rsa_decrypt (ctrl_t ctrl, TSS_CONTEXT *tssc, TPM_HANDLE key,
   TPM_HANDLE ah;
   char *auth;
 
+  if (ciphertext_len > TPM2_MAX_RSA_KEY_BYTES)
+    return GPG_ERR_TOO_LARGE;
+
   inScheme.scheme = TPM_ALG_RSAES;
   /*
    * apparent gcrypt error: occasionally rsa ciphertext will
werner shifted this object from the Restricted Space space to the S1 Public space.Wed, Jan 21, 12:40 PM
werner updated the task description. (Show Details)
werner changed the edit policy from "Custom Policy" to "g10code (Project)".
werner added a project: CVE.
gniibe renamed this task from Security (internal) - Stack-based buffer overflow in TPM2 `PKDECRYPT` to Stack-based buffer overflow in TPM2 `PKDECRYPT`.Thu, Jan 22, 12:33 AM
werner changed the task status from Open to Testing.Sun, Jan 25, 5:02 PM
gniibe mentioned this in Unknown Object (Maniphest Task).Mon, Jan 26, 9:51 AM
ebo moved this task from Backlog to Done on the gnupg26 board.
werner changed the visibility from "g10code (Project)" to "Public (No Login Required)".Tue, Jan 27, 5:12 PM
werner changed the edit policy from "g10code (Project)" to "Contributor (Project)".

CVE-2026-24882 has been assigned to this issue.

This is not 2.5-only, isn't it?

gniibe mentioned this in Unknown Object (Maniphest Task).Mon, Feb 2, 8:25 AM