FreeRDP: Windows RAIL title is passed to SetWindowTextW without NUL termination
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
Executive Summary
In FreeRDP's Windows RAIL client, a malicious RDP server can send a WINDOW_ORDER_FIELD_TITLE update whose UTF-16 title is full-length and contains no NUL. In a Windows RemoteApp session, the client stores the exact-length buffer produced by rail_read_unicode_string and passes its raw pointer to SetWindowTextW, an API that expects a NUL-terminated string. The demonstrated boundary is a user32 out-of-bounds read past the title allocation; the observed consequences were a failed title update and, in a direct guarded call, STATUS_ACCESS_VIOLATION. This requires a Windows RAIL/RemoteApp session and a server able to send the order. It does not require session theft, and the supplied execution did not demonstrate a complete network attack.
The assessed source is a 3.31.x development revision after the 3.31.1 release. The earliest released tag I inspected, 3.0.0, and every released tag through 3.31.1 contain the same exact-length parser and raw SetWindowTextW call. I found no verified fixed release, so no definitive fixed version is assigned. I reviewed the source, release snapshots, the preserved runtime harness and its logs, and the portable source-only reproducer; the runtime proof used real Windows RAIL code and user32 under Wine, with the wire order injected at wf_rail_window_common, not through a full RDP network transport.
Background
RAIL (Remote Applications Integrated Locally) lets an RDP server describe a remote application's windows to a Windows client. The server controls the title field in a window-state order. A normal title may contain UTF-16 code units but must be represented as a NUL-terminated buffer before it is handed to a Win32 string API.
The relevant parser is rail_read_unicode_string in libfreerdp/core/window.c. It reads the protocol byte length, allocates exactly that many bytes, copies the bytes, and rejects embedded NULs. It therefore intentionally returns a bounded, non-NUL-terminated string for a full-length NUL-free title. The separate Windows client consumer must add a terminator before calling an API with a C-style string contract.
The prerequisite topology is a Windows FreeRDP client with RAIL/RemoteApp enabled and a server that can deliver a window title update. The runtime proof used Wine 9.0 to execute the Windows client and user32 code. It did not establish that the same order was sent over a live RDP connection; the order was supplied directly at the wf_rail_window_common boundary.
Vulnerability Details
For a title consisting of four UTF-16 characters, the parser receives cbString = 8, allocates eight bytes, and copies four code units. The decisive code in the assessed libfreerdp/core/window.c is:
WCHAR* new_str = realloc(unicode_string->string, new_len);
...
unicode_string->length = new_len;
Stream_Read(s, unicode_string->string, unicode_string->length);
const size_t charlen = unicode_string->length / sizeof(WCHAR);
if (_wcsnlen(unicode_string->string, charlen) != charlen)
return FALSE;At client/Windows/wf_rail.c:575-597, wf_rail_window_common selects the same raw pointer and separately performs a length-bounded UTF-8 conversion for its stored title, but then calls the unbounded Windows API with the raw pointer:
const WCHAR* str = (const WCHAR*)windowState->titleInfo.string;
...
title = ConvertWCharNToUtf8Alloc(
str, windowState->titleInfo.length / sizeof(WCHAR), nullptr);
...
SetWindowTextW(railWindow->hWnd, str);The length-bounded conversion does not change str or append a terminator. Consequently, SetWindowTextW scans beyond the new_len allocation looking for a NUL. If the adjacent memory happens to contain one, the title may become garbage or the call may appear to succeed; if the scan reaches inaccessible memory, the client can fault. The supplied guard-page run used the real wf_rail_window_common update path: a four-character title plus a NUL changed the window title to Wind, while the exact-length NUL-free title left the original RdpRailWindow title unchanged. A direct call to SetWindowTextW with the same guarded eight-byte buffer returned 0xC0000005 (STATUS_ACCESS_VIOLATION).
The release inspection found this behavior in the 3.0.0 tag and in every released 3.x tag through 3.31.1 that was checked. The available history does not identify a shipped fix, so the affected range is reported as the inspected releases rather than as an unverified universal range.
Exploitability Analysis
The narrow primitive is a read past a heap allocation in the Windows user32 string consumer. Mallory's input is the RAIL title field; Alice is not involved, and there is no cross-user authorization decision in this path. The title bytes are attacker-controlled, but the bytes returned by the over-read and the final process behavior depend on heap layout, adjacent memory and user32 behavior.
The observed guarded execution establishes that the consumer can read beyond the declared title and can fail with an access violation. It does not establish reliable code execution, memory disclosure to the server, a deterministic client crash in ordinary allocations, or a complete remote network exploit. The candidate's reported low-impact outcome is therefore the appropriate demonstrated boundary: title corruption or client availability impact under favorable layout. A malicious RDP server and an established Windows RAIL session are required; network reachability and delivery through a live RDP transport were not exercised in this proof.
The NUL-terminated control rules out a failure caused merely by window creation or the title value itself: the same four characters update the real window when the extra terminator is present. The direct guarded call isolates the consumer contract from the RAIL bookkeeping. The test does not rule out all user32 implementation differences across Windows versions.
Proof of Concept
The preserved runtime artifacts include harness059.c, the unmodified Windows client implementation compiled into the harness, and separate control, NUL-free and direct-call logs. The harness creates a real RAIL window, invokes wf_rail_window_common for a title-only order, and places the title immediately before a no-access guard page. Its control allocates five WCHAR values (Wind plus NUL); its NUL-free case allocates four (Wind only), matching the parser's exact-length result. The direct mode calls SetWindowTextW on that same shape of buffer.
The runtime boundary must be stated precisely: real Windows RAIL code and real user32 executed under Wine, while the RAIL order was injected at wf_rail_window_common. The RDP/RAIL transport was not a full network end-to-end path. The portable source-only PoC in poc/poc.c reproduces the contract safely with standard C by tracking the declared title length separately from the adjacent sentinel; it does not claim to emulate user32 or to prove a Windows crash.
From the report directory, the portable reproducer can be inspected and built with:
cd poc cc -std=c11 -Wall -Wextra -Wpedantic -O2 poc.c -o poc ./poc
Expected output identifies that the consumer reads five UTF-16 code units while the declared title contains four, followed by the control case reading four. This is a safe contract demonstration, not a successful network exploit. The Wine harness's guard-page execution is the evidence for the actual Windows API fault; its generated Windows binaries are intentionally not included in this source-only disclosure package.
Remediation
Before calling SetWindowTextW, create a temporary NUL-terminated UTF-16 copy sized from the validated byte length, pass that copy to user32, and free it after the call. The stored UTF-8 title and the API buffer should remain separate so that the length-bounded conversion cannot be mistaken for termination. A source-compatible shape for the title branch is:
const size_t chars = windowState->titleInfo.length / sizeof(WCHAR);
WCHAR* terminated = (WCHAR*)calloc(chars + 1, sizeof(WCHAR));
if (!terminated)
return FALSE;
if (chars > 0)
memcpy(terminated, str, chars * sizeof(WCHAR));
SetWindowTextW(railWindow->hWnd, terminated);
free(terminated);The implementation should also retain the existing byte-length and even-byte validation, handle the zero-length title, and check allocation failure. This is a proposed remediation, not a verified shipped fix. Regression coverage should exercise a title-only RAIL update with a full-length NUL-free string, the NUL-terminated control, an empty title, embedded-NUL rejection at the parser, and a title whose final code unit is nonzero. No fixed release was verified.
Summary
FreeRDP's Windows RAIL client accepts a full-length NUL-free title from a remote server, allocates only the declared bytes, and passes the resulting non-NUL-terminated pointer to SetWindowTextW. The earliest released tag inspected was 3.0.0, and the same pattern was present through 3.31.1; no fixed release is verified. Under Wine, real Windows RAIL and user32 code demonstrated the contract violation with a guard page, while the NUL-terminated control succeeded. The demonstrated impact is an out-of-bounds read with failed title handling and conditional client availability impact, not a demonstrated full-network exploit or code execution.
Text from GitHub Advisory Database
Affected versions 1
- FreeRDP, c>= 3.0.0, <= 3.31.1GitHub