CVE-2026-40033
Description
FreeRDP before 3.26.0 contains a heap-buffer-overflow vulnerability in gdi_CacheToSurface that allows remote attackers to write out-of-bounds heap memory. The vulnerability occurs because rectangle validation clamps coordinates to UINT16_MAX but performs copy operations using unclamped cache entry dimensions, enabling malicious RDP servers to trigger large out-of-bounds writes and potentially achieve remote code execution or client crash.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
FreeRDP before 3.26.0 contains a heap-buffer-overflow in gdi_CacheToSurface where rectangle validation clamps coordinates but uses unclamped dimensions, enabling remote code execution via malicious RDP server.
Vulnerability
FreeRDP versions before 3.26.0 contain a heap-buffer-overflow vulnerability in the gdi_CacheToSurface function. The bug arises because rectangle validation clamps coordinates to UINT16_MAX, but the subsequent copy operation uses the original unclamped cacheEntry->width and cacheEntry->height. This allows a malicious RDP server to craft RDPGFX PDUs that bypass bounds checks and write out-of-bounds heap memory. The vulnerability is reachable when the client has RDPGFX enabled [1][3].
Exploitation
An attacker operating a malicious RDP server can trigger the vulnerability by sending crafted RDPGFX messages. Specifically, the server creates a surface with dimensions (65535,1) and a cache entry with the same dimensions, then sends a CacheToSurface PDU with a destination point (65534,15). The rectangle validation clamps the right/bottom edges to UINT16_MAX, passing the check, but the copy uses the full width/height, causing a large out-of-bounds write on the heap. No authentication or user interaction beyond connecting to the malicious server is required [1].
Impact
Successful exploitation allows the attacker to write out-of-bounds heap memory, potentially leading to remote code execution in the context of the FreeRDP client process or a client crash. The CVSS v3 score is 8.8 (High) [3]. The attacker gains the ability to execute arbitrary code on the client system.
Mitigation
The vulnerability is fixed in FreeRDP version 3.26.0. The fix is included in commit 23b36cd00ebf0ccd97750fcdbc9aa2f362352da7 [2]. Users should upgrade to FreeRDP 3.26.0 or later. As a workaround, users can disable RDPGFX support in the client configuration if upgrading is not immediately possible [1]. The vulnerability is not listed in CISA's Known Exploited Vulnerabilities catalog as of the publication date.
AI Insight generated on May 26, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
123b36cd00ebfMerge pull request #12635 from morgan9e/fix-window-size
5 files changed · +50 −14
client/SDL/SDL3/sdl_config.hpp.in+1 −1 modified@@ -27,4 +27,4 @@ static const char SDL_CLIENT_VENDOR[] = "@VENDOR@"; static const char SDL_CLIENT_UUID[] = "@SDL_CLIENT_UUID@"; static const char SDL_CLIENT_COPYRIGHT[] = "FreeRDP project"; static const char SDL_CLIENT_URL[] = "@PROJECT_URL@"; -static const char SDL_CLIENT_TYPE[] = "application"; +static const char SDL_CLIENT_TYPE[] = "application";
client/SDL/SDL3/sdl_context.cpp+15 −0 modified@@ -1117,6 +1117,8 @@ bool SdlContext::handleEvent(const SDL_WindowEvent& ev) case SDL_EVENT_WINDOW_MOUSE_ENTER: return restoreCursor(); case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: + if (!resizeToScale(window)) + return false; if (isConnected()) { if (!window->fill()) @@ -1128,6 +1130,8 @@ bool SdlContext::handleEvent(const SDL_WindowEvent& ev) } break; case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + if (!resizeToScale(window)) + return false; if (!window->fill()) return false; if (!drawToWindow(*window)) @@ -1454,6 +1458,17 @@ int SdlContext::argumentHandler(const COMMAND_LINE_ARGUMENT_A* arg, void* custom return 0; } +bool SdlContext::resizeToScale(SdlWindow* window) +{ + if (freerdp_settings_get_bool(context()->settings, FreeRDP_SmartSizing)) + return true; + if (!useLocalScale()) + return true; + if (!window) + return false; + return window->resizeToScale(); +} + bool SdlContext::useLocalScale() const { const auto ssize = freerdp_settings_get_bool(context()->settings, FreeRDP_SmartSizing);
client/SDL/SDL3/sdl_context.hpp+1 −0 modified@@ -150,6 +150,7 @@ class SdlContext [[nodiscard]] static int argumentHandler(const COMMAND_LINE_ARGUMENT_A* arg, void* custom); private: + [[nodiscard]] bool resizeToScale(SdlWindow* window); [[nodiscard]] bool useLocalScale() const; [[nodiscard]] static BOOL preConnect(freerdp* instance);
client/SDL/SDL3/sdl_window.cpp+30 −12 modified@@ -28,14 +28,15 @@ SdlWindow::SdlWindow(SDL_DisplayID id, const std::string& title, const SDL_Rect& rect, [[maybe_unused]] Uint32 flags) - : _displayID(id) + : _initialW(rect.w), _initialH(rect.h), _displayID(id) { auto props = SDL_CreateProperties(); SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, title.c_str()); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, rect.x); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, rect.y); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, rect.w); SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, rect.h); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true); if (flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true); @@ -48,21 +49,22 @@ SdlWindow::SdlWindow(SDL_DisplayID id, const std::string& title, const SDL_Rect& _window = SDL_CreateWindowWithProperties(props); SDL_DestroyProperties(props); - - std::ignore = resizeToPixelSize({ rect.w, rect.h }); SDL_SetHint(SDL_HINT_APP_NAME, ""); std::ignore = SDL_SyncWindow(_window); _renderer = SDL_CreateRenderer(_window, nullptr); + std::ignore = resizeToScale(); + _monitor = query(_window, id, true); } SdlWindow::SdlWindow(SdlWindow&& other) noexcept : _window(other._window), _renderer(other._renderer), _renderTarget(other._renderTarget), _gdiTexture(other._gdiTexture), _gdiTextureW(other._gdiTextureW), - _gdiTextureH(other._gdiTextureH), _displayID(other._displayID), _offset_x(other._offset_x), - _offset_y(other._offset_y), _monitor(other._monitor) + _gdiTextureH(other._gdiTextureH), _initialW(other._initialW), _initialH(other._initialH), + _displayID(other._displayID), _offset_x(other._offset_x), _offset_y(other._offset_y), + _monitor(other._monitor) { other._window = nullptr; other._renderer = nullptr; @@ -228,13 +230,29 @@ void SdlWindow::minimize() std::ignore = SDL_SyncWindow(_window); } -bool SdlWindow::resizeToPixelSize(const SDL_Point& size) +bool SdlWindow::resizeToScale() { - auto sc = scale(); - const int iscale = static_cast<int>(sc * 100.0f); - auto w = 100 * size.x / iscale; - auto h = 100 * size.y / iscale; - return resize({ w, h }); + if (!_window || _initialW <= 0 || _initialH <= 0) + return false; + if ((SDL_GetWindowFlags(_window) & SDL_WINDOW_FULLSCREEN) != 0) + return true; + + float pd = SDL_GetWindowPixelDensity(_window); + if (pd <= 0.0f) + pd = 1.0f; + + const int targetW = static_cast<int>(std::ceil(static_cast<float>(_initialW) / pd)); + const int targetH = static_cast<int>(std::ceil(static_cast<float>(_initialH) / pd)); + + int curW = 0; + int curH = 0; + if (!SDL_GetWindowSize(_window, &curW, &curH)) + return false; + + if (curW == targetW && curH == targetH) + return true; + + return resize({ targetW, targetH }); } bool SdlWindow::resize(const SDL_Point& size) @@ -551,7 +569,7 @@ SdlWindow SdlWindow::create(SDL_DisplayID id, const std::string& title, Uint32 f SdlWindow window{ id, title, rect, flags }; - if ((flags & (SDL_WINDOW_FULLSCREEN)) != 0) + if ((flags & SDL_WINDOW_FULLSCREEN) != 0) { window.setOffsetX(rect.x); window.setOffsetY(rect.y);
client/SDL/SDL3/sdl_window.hpp+3 −1 modified@@ -67,7 +67,7 @@ class SdlWindow void fullscreen(bool enter, bool forceOriginalDisplay); void minimize(); - [[nodiscard]] bool resizeToPixelSize(const SDL_Point& size); + [[nodiscard]] bool resizeToScale(); [[nodiscard]] bool resize(const SDL_Point& size); [[nodiscard]] bool drawRect(SDL_Surface* surface, SDL_Point offset, const SDL_Rect& srcRect); @@ -114,6 +114,8 @@ class SdlWindow SDL_Texture* _gdiTexture = nullptr; int _gdiTextureW = 0; int _gdiTextureH = 0; + int _initialW = 0; + int _initialH = 0; SDL_DisplayID _displayID = 0; Sint32 _offset_x = 0; Sint32 _offset_y = 0;
Vulnerability mechanics
Root cause
"Missing bounds check in gdi_CacheToSurface: rectangle validation clamps coordinates to UINT16_MAX but the subsequent copy uses the unclamped cache entry width/height, allowing out-of-bounds heap writes."
Attack vector
A malicious RDP server sends crafted RDPGFX PDUs to the FreeRDP client. The server creates a surface with dimensions (65535,1) which get aligned to (65536,16), then creates a cache entry with width=65535 and height=1, and sends a CacheToSurface PDU with destination point (65534,15) [ref_id=1]. The rectangle validation in gdi_CacheToSurface clamps the right/bottom edges to UINT16_MAX, so the rectangle passes is_rect_valid, but the subsequent freerdp_image_copy_no_overlap call uses the original unclamped cacheEntry->width (65535) and cacheEntry->height (1), writing far past the allocated surface bounds [ref_id=1]. The attacker controls the size and content of the out-of-bounds write via server-supplied pixel data, enabling potential remote code execution or client crash [ref_id=1].
Affected code
The vulnerable function is gdi_CacheToSurface in libfreerdp/gdi/gfx.c [ref_id=1]. The function validates a destination rectangle using is_rect_valid, which clamps right/bottom to UINT16_MAX, but then passes the original unclamped cacheEntry->width and cacheEntry->height to freerdp_image_copy_no_overlap [ref_id=1].
What the fix does
The advisory's suggested fix adds two bounds checks in gdi_CacheToSurface before calling freerdp_image_copy_no_overlap: if ((UINT32)destPt->x + cacheEntry->width > surface->width) goto fail; and similarly for y + height vs surface->height [ref_id=1]. These checks use the real (unclamped) copy dimensions against the actual allocated surface size, preventing the out-of-bounds write. The patch referenced in the advisory (commit 23b36cd) is unrelated — it fixes an SDL3 HiDPI window sizing issue and does not address this vulnerability [patch_id=2562552]. No fix for the gdi_CacheToSurface bug has been merged into the FreeRDP repository at the time of this advisory [ref_id=1].
Preconditions
- configClient must have RDPGFX enabled (default in many configurations)
- networkAttacker must control a malicious RDP server that the client connects to
- authNo authentication required beyond standard RDP connection negotiation
- inputAttacker sends crafted RDPGFX CreateSurface, CreateCacheEntry, and CacheToSurface PDUs
Reproduction
The advisory includes a complete C PoC (poc_505.c) that reproduces the crash without a live RDP server [ref_id=1]. Build FreeRDP at commit 23b36cd with ASAN/UBSAN, compile the PoC against the FreeRDP libraries, and run it — ASAN reports a heap-buffer-overflow WRITE of size 262140 [ref_id=1]. The PoC simulates the exact bounds check and calls freerdp_image_copy_no_overlap with the same parameters gdi_CacheToSurface would pass [ref_id=1].
Generated on May 26, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.