All vulnerabilities
GHSA-jhv5-m83m-hxhq

FreeRDP: PKCS#11 key enumeration writes past the fixed 64-entry `keys[]` array in `collect_keys()`

FreeRDP
CVSS
n/aMedium
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in3.32.0Fixed versionGitHub
Published28 Sep 2026Updated 28 Sep 2026GitHub

What fixes it

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

This issue was found by Anthropic using Claude to study the security of open-source software, validated manually by me from Ada Logics

Summary

P11EnumKeysState embeds a fixed array NCryptKeyEnum keys[MAX_KEYS] with MAX_KEYS = 64, and the whole structure is allocated as a single calloc in NCryptP11EnumKeys() (ncrypt_pkcs11.c:819, 29720 bytes on x86-64).

collect_keys() fills that array with two independent loops: an outer loop over up to MAX_SLOTS (64) slots and, per slot, an inner loop over up to MAX_KEYS_PER_SLOT (64) objects returned by C_FindObjects. Each discovered object is written through

NCryptKeyEnum* key = &state->keys[state->nKeys];   /* ncrypt_pkcs11.c:525 */

and state->nKeys++ runs at ncrypt_pkcs11.c:568. There is no comparison of state->nKeys against MAX_KEYS anywhere in the function, so the product of the two loop bounds (4096) can be written into a 64-entry array. The four ulValueLen checks after object_load_attributes() bound the length of each attribute inside one entry; they do not bound the index of the entry.

The out-of-range key pointer is passed straight to the PKCS#11 module as the destination of CKA_ID and CKA_LABEL in C_GetAttributeValue, so the first excess object is written by the module's own memcpy at 392 bytes into an entry that already starts at the end of the allocation. state->keyIndex — the cursor NCryptP11EnumKeys() later uses to walk keys[] at ncrypt_pkcs11.c:855 — immediately follows the array and is the first field overwritten.

The path is the ordinary smartcard-logon enumeration: freerdp_smartcard_list() (client/common/smartcard_cli.c:30) → smartcard_enumerateCerts() (libfreerdp/core/smartcardlogon.c:896) → list_provider_keys() (:412) → NCryptEnumKeys() → collect_keys(). WITH_PKCS11 is ON by default on Linux.

Primitive

Linear forward heap write, 456 bytes per excess entry, up to 4032 excess entries (~1.8 MB) past a 29720-byte allocation. Per entry the attacker-supplied bytes are CKA_ID (up to 64, arbitrary), CKA_LABEL (up to 256, arbitrary), CKA_KEY_TYPE (8, constrained), CK_SLOT_INFO (112 bytes of slot description/manufacturer, token/reader-supplied) and the module-assigned slot ID (8). The starting offset is deterministic (keys[64], i.e. the end of the allocation) and the attacker chooses the distance by choosing how many key objects the token set presents. The corruption happens early in client startup, so the set of neighbouring allocations is small and fairly deterministic. No function pointer or vtable is written.

Attacker position and scope caveats

The triggering data is token data, not FreeRDP configuration: the number of CKO_PUBLIC_KEY objects with CKA_VERIFY = TRUE visible across the enumerated slots, and their CKA_ID / CKA_LABEL contents. Enumeration opens a serial session and never calls C_Login, so no PIN is needed. The realistic actor is therefore someone who controls the contents of a token set the victim's client enumerates — a supplied or issued smartcard, a multi-slot token, or a p11-kit configuration aggregating several tokens/modules. That is a physical/local vector, not a malicious RDP server, gateway or .rdp file. The user must be using smartcard logon or listing smartcard certificates. The PKCS#11 module itself is unmodified and legitimate in the reproduction; a malicious module is not required.

Note that a single fully populated card in one reader yields exactly 64 objects, which fills the array without overflowing; two tokens (or one token exposing two slots) is the minimum.

Reproduction

ANT-2026-GP8MQ9VN-e2e.zip

Ubuntu 24.04, clang-20 + AddressSanitizer, FreeRDP built from source at the commit above with the default -DWITH_PKCS11=ON and the X11 client. The token set is the distro SoftHSM2 PKCS#11 module, unmodified, initialised with two tokens of 64 EC key pairs each (128 public-key objects across two slots). FreeRDP sources are unmodified and no FreeRDP code is stubbed or patched.

unzip ANT-2026-GP8MQ9VN-e2e.zip && cd ANT-2026-GP8MQ9VN
docker build -t ant-2026-gp8mq9vn docker-e2e/
docker run --rm ant-2026-gp8mq9vn

No special docker run flags are needed. The container runs the shipped client twice: once as xfreerdp /list:smartcard with no module argument (the module is discovered through p11-kit-proxy, i.e. the stock Linux configuration) and once with /list:smartcard:pkcs11-module:<path>. Both abort.

Reproducer attached as ANT-2026-GP8MQ9VN-e2e.zip.

Observed output

From the first run (xfreerdp /list:smartcard, default configuration):

==13==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7ae5b0be7798 at pc 0x5b28505ab484
WRITE of size 1 at 0x7ae5b0be7798 thread T0
    #0 in memcpy
    #4 0x7c25b1131ce3 in C_GetAttributeValue (/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so+0x26ce3)
    #5 0x7825b0a74338  (/lib/x86_64-linux-gnu/p11-kit-proxy.so+0x3a338)
    #6 0x7c25b4b8b696 in object_load_attributes winpr/libwinpr/ncrypt/ncrypt_pkcs11.c:284:13
    #7 0x7c25b4b8b696 in collect_keys winpr/libwinpr/ncrypt/ncrypt_pkcs11.c:534:10
    #8 0x7c25b4b882df in NCryptP11EnumKeys winpr/libwinpr/ncrypt/ncrypt_pkcs11.c:845:9
    #9 0x7c25b5127048 in list_provider_keys libfreerdp/core/smartcardlogon.c:412:9
   #10 0x7c25b5125b01 in smartcard_hw_enumerateCerts libfreerdp/core/smartcardlogon.c:705:9
   #11 0x7c25b5125b01 in smartcard_enumerateCerts libfreerdp/core/smartcardlogon.c:896:6
   #12 0x7c25b568a51f in freerdp_smartcard_list client/common/smartcard_cli.c:30:7
   #13 0x7c25b5668f80 in evaluate_result client/common/cmdline.c:1885:4
   #15 0x5b28505f0da7 in main client/X11/cli/xfreerdp.c:81:8

0x7ae5b0be7798 is located 384 bytes after 29720-byte region [0x7ae5b0be0200,0x7ae5b0be7618)
allocated by thread T0 here:
    #1 0x7c25b4b88171 in NCryptP11EnumKeys winpr/libwinpr/ncrypt/ncrypt_pkcs11.c:819:30

29712 + 392 = 29720 + 384, i.e. the write is the CKA_ID field of keys[64].

Credit

Found by Anthropic using Claude to study the security of open-source software, validated manually by Ada Logics. Disclosed under Anthropic's coordinated vulnerability disclosure policy (https://www.anthropic.com/coordinated-vulnerability-disclosure, 90 days). Please credit Claude and Ada Logics in any advisory.

Text from GitHub Advisory Database

Affected versions 1

  • FreeRDP, c<= 3.31.1GitHub