All vulnerabilities
GHSA-grvc-qhhp-7h6m

Server-chosen MS-RDPEVOR video geometry lets a malicious RDP server pick an out-of-range blit destination in the FreeRDP client

FreeRDP
CVSS
n/aMedium
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
Published28 Sep 2026Updated 28 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

This issue was found by Anthropic using Claude to study the security of open-source software, validated manually by me from Ada Logics.

Summary

The MS-RDPEGT geometry dynamic virtual channel lets the server choose where a video overlay is placed. The client copies the server-chosen signed origin straight into the unsigned VideoSurface::x with no range check against the local desktop, and gdiVideoShowSurface() performs its blit clamp in wrapping 32-bit unsigned arithmetic. A server that sends topLevelLeft = -256 makes surface->x become 0xFFFFFF00; the clamp test destinationWidth + surface->x < gdi->width then wraps to 0 < 1024, the clamp is skipped, and the full width is copied to nXDst = 0xFFFFFF00. freerdp_image_scale() takes its equal-size fast path into freerdp_image_copy_no_overlap(), whose row offset is nXDst * bytesPerPixel computed in 64-bit — about 16 GiB — and the decoded H.264 picture rows are written there.

On the upstream-default build (WITH_VERBOSE_WINPR_ASSERT=ON) the WINPR_ASSERTING_INT_CAST in video_PresentationRequest() turns the negative origin into a fatal abort instead, which is still a server-triggered termination of the connecting client.

Affected component: client, /video channel (MS-RDPEVOR) plus the geometry channel (MS-RDPEGT). This requires the user to connect with /video, which is not a default.

Affected code

At master a0114ebd34c71334a028f267acf6da391edbbe24:

libfreerdp/gdi/video.c:89-94 — gdiVideoShowSurface()

const UINT32 width = (destinationWidth + surface->x < (UINT32)gdi->width)
                         ? destinationWidth
                         : (UINT32)gdi->width - surface->x;
const UINT32 height = (destinationHeight + surface->y < (UINT32)gdi->height)
                          ? destinationHeight
                          : (UINT32)gdi->height - surface->y;

nXDst/nYDst are surface->x/surface->y verbatim; the nXDst > INT32_MAX test at video.c:105-107 runs after freerdp_image_scale() at video.c:100 has already copied.

channels/video/client/video_main.c:426-430 and :499-501 — the origin

presentation->surface->x =
    WINPR_ASSERTING_INT_CAST(uint32_t, geometry->topLevelLeft + geometry->left);

topLevelLeft / left are INT32s read verbatim from the wire in geometry_recv_pdu() (channels/geometry/client/geometry_main.c:251-259); only the RGNDATA rectangles are range checked there, not these fields.

The only guard is the assert layer. WINPR_ASSERTING_INT_CAST (winpr/include/winpr/cast.h:179) is two WINPR_ASSERTs plus a plain cast; with WITH_VERBOSE_WINPR_ASSERT=OFF it becomes the libc assert(), which NDEBUG removes in a Release build. FreeRDP's own Android client build sets -DWITH_VERBOSE_WINPR_ASSERT:BOOL=OFF (client/Android/cmake/ExternalFreeRDP.cmake:45), as do the config-qa-static and config-abi CI presets, and the macro degenerates to a plain cast on non-GNU/Clang compilers (winpr/include/winpr/cast.h:189-190). The WINPR_ASSERTs inside freerdp_image_scale() (color.c:1297-1306) and freerdp_image_copy_no_overlap() (color.c:1986-1995) are the same layer.

CWE-190 (integer overflow / wraparound) leading to CWE-787 (out-of-bounds write); CWE-129 (improper validation of array index) for the unchecked geometry origin.

Primitive

Write, attacker-chosen bytes (the decoded picture), width * 4 bytes per row for height rows. The displacement is not tunable: the wraparound that creates the bug requires surface->x >= 0xFFFFFF00, so the write always lands at least ~16 GiB past gdi->primary_buffer. In a normal 64-bit address space that page is unmapped, so the realistic outcome is a crash of the client rather than usable corruption. We are reporting this as a server-triggered client denial of service.

Evidence

Validated end-to-end at release-3.23.0 (b933ae18d9ad2a1d73c610868fcc30eb61654070), assert-disabled Release build, ASan, using FreeRDP's own sfreerdp client against a malicious server built on the public FreeRDP server API:

[server] -> GEOMETRY_UPDATE id=0x1122334455667788 topLevelLeft=-256 left=0 (104 bytes)
[server] -> TSMM_PRESENTATION_REQUEST START id=1 256x256 -> 256x256 mapping=0x1122334455667788
[server] -> TSMM_VIDEO_DATA id=1 sampleNumber=1 cbSample=710

==31==ERROR: AddressSanitizer: SEGV on unknown address 0x770ab26ad420 (pc 0x7b06c57f6c10 ...)
==31==The signal is caused by a WRITE memory access.
    #0 in FreeRDPWriteColor_int build/./libfreerdp/primitives/../codec/color.h:42:11
    #1 in generic_image_copy_no_overlap_convert build/./libfreerdp/primitives/prim_copy.c:252:4
    #2 in generic_image_copy_no_overlap build/./libfreerdp/primitives/prim_copy.c
    #3 in avx2_image_copy_no_overlap build/./libfreerdp/primitives/sse/prim_copy_avx2.c
    #4 in freerdp_image_copy_no_overlap build/./libfreerdp/codec/color.c:1890:9
    #5 in freerdp_image_scale build/./libfreerdp/codec/color.c:1237:10
    #6 in gdiVideoShowSurface build/./libfreerdp/gdi/video.c:100:8
    #7 in video_VideoData build/./channels/video/client/video_main.c:876:9
    #8 in video_data_on_data_received build/./channels/video/client/video_main.c:999:9
    #9 in drdynvc_process_data build/./channels/drdynvc/client/drdynvc_main.c:1432:11
   #11 in drdynvc_virtual_channel_client_thread build/./channels/drdynvc/client/drdynvc_main.c:1689:17
rsi = 0x00000003fffffc00      <- xDstOffset = 0xFFFFFF00 * 4 = 16 GiB
SUMMARY: AddressSanitizer: SEGV build/./libfreerdp/primitives/../codec/color.h:42:11 in FreeRDPWriteColor_int

On the upstream-default build of the same tree, the same three PDUs abort the client first:

[FATAL][com.freerdp.winpr.assert] - [winpr_int_assert]:
  ... [channels/video/client/video_main.c:video_PresentationRequest:500]
>>> RESULT: client killed by SIGABRT

Status at current master, and a caveat we want to flag

The gdiVideoShowSurface() and video_main.c code is unchanged at a0114ebd34c71334a028f267acf6da391edbbe24. On the upstream-default build the PoC still terminates the client there with the SIGABRT shown above.

The out-of-bounds write itself is currently not reached on master, but not because of any bound on the geometry. video_main.c:884-886 decodes with rect = {0, 0, surface->alignedWidth, surface->alignedHeight}, and VideoClient_CreateCommonContext() (libfreerdp/gdi/video.c:174-175) computes alignedWidth = w + 32 - w % 16, which is always greater than w. log_decompress() (libfreerdp/codec/h264.c:164-167) validates that rect against h264->width/height, which h264_context_reset() set to the presentation's SourceWidth/SourceHeight (= w). The rect can therefore never be valid, avc420_decompress() returns -1016, and every frame is dropped:

[WARN][com.freerdp.codec] - [areRectsValid]: Rectangle 0 {0x0-288x288} outside of bounding frame 256x256

That check arrived with 6482b7a92 (3.24.0), f6380daa9 (3.29.0) and bea10804a (3.31.1). Before 6482b7a92, avc420_decompress() did no rect validation, which is why the write reproduces at 3.23.0. As a side effect it looks like the /video channel decodes nothing at all in 3.24.0 and later; we mention it because it is the only thing currently standing between the unchecked geometry and the copy, and it is not a bound anyone intended.

Reproduction

ANT-2026-0H3M36C1-e2e.zip

Reproducer attached as ANT-2026-0H3M36C1-e2e.zip.

unzip ANT-2026-0H3M36C1-e2e.zip && cd ANT-2026-0H3M36C1

# end-to-end out-of-bounds write (release-3.23.0, the Dockerfile default)
docker build -t ant-0h3m36c1 docker-e2e/ && docker run --rm ant-0h3m36c1

# current master: same PoC, upstream-default build -> SIGABRT in video_PresentationRequest
docker build --build-arg TARGET_COMMIT=a0114ebd34c71334a028f267acf6da391edbbe24 \
    -t ant-0h3m36c1-head docker-e2e/ && docker run --rm ant-0h3m36c1-head

No special docker run flags are needed. The image builds FreeRDP twice from the same unmodified checkout of ${TARGET_COMMIT} (upstream-default assert-on, and assert-off Release matching the Android client configuration), compiles a malicious server against the installed freerdp-server3/freerdp3/winpr3 pkg-config modules, generates a self-signed certificate and a 256x256 H.264 keyframe with ffmpeg, then runs sfreerdp against that server in both configurations. No FreeRDP source is patched, stubbed or reimplemented.

The malicious server opens Microsoft::Windows::RDS::Geometry::v08.01, Microsoft::Windows::RDS::Video::Control::v08.01 and Microsoft::Windows::RDS::Video::Data::v08.01 and sends, in order, a GEOMETRY_UPDATE with topLevelLeft = -256 / left = 0, a TSMM_PRESENTATION_REQUEST (START, H.264, SourceWidth == ScaledWidth == SourceHeight == ScaledHeight == 256, so the equal-size direct-copy path is taken), and one TSMM_VIDEO_DATA.

Credit

Found by Anthropic using Claude to study the security of open-source software, validated manually by Ada Logics. Please credit Claude and Ada Logics in any advisory.

Handled under Anthropic's coordinated vulnerability disclosure policy (https://www.anthropic.com/coordinated-vulnerability-disclosure, 90 days). Reference ANT-2026-0H3M36C1; questions to security-cvd@anthropic.com.

Text from GitHub Advisory Database

Affected versions 1

  • FreeRDP, c<= 3.31.1GitHub