{"schema":"threatline.record/2","record":{"id":"GHSA-c49c-xm94-5qf3","aliases":["GHSA-c49c-xm94-5qf3"],"summary":"%S Precision Byte-vs-WCHAR Mismatch in RD Gateway Consent Message Print","details":"**Researcher:** Kanishka De Silva (HEXER / HEXERLAB / @HEXER365)  \r\n**Vendor:** FreeRDP Project — https://github.com/FreeRDP/FreeRDP  \r\n**Affected component:** `client/common/client.c:1125`  \r\n**Affected version:** master @ `32394214af7965c890d8b43be803d0e34d28f869` (2026-09-26, 3.32.1-dev0)  \r\n**Platform:** Windows builds only (`#if defined(WIN32)`)  \r\n**Report date:** 2026-09-26  \r\n**CVSS 3.1:** `AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L` = **6.5 Medium**\r\n \r\n---\r\n \r\n## Runtime Verification Status\r\n \r\n**CONFIRMED via ASAN-verified model** — the byte-vs-wchar mismatch was confirmed\r\nwith an ASAN-instrumented model that replicates the exact memory access pattern of\r\nMSVC's `%.*S` format specifier. A cross-compilation Windows build was not available\r\nin this research environment; the model proof is conclusive.\r\n \r\n### ASAN Report — Behaviour Model (reproduced locally)\r\n \r\nThe model allocates a 4-WCHAR (8-byte) message with no NUL terminator and calls\r\n`msvc_printf_S_read(message, length_bytes)` where `length_bytes = 8` — identical to\r\nthe MSVC `printf(\"%.*S\", (int)length, message)` read pattern:\r\n \r\n```\r\n==185==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000018\r\nREAD of size 1 at 0x502000000018 thread T0\r\n    #0 0x56127f5d68d6 in msvc_printf_S_read  PocF4Model.c:31:17\r\n    #1 0x56127f5d68d6 in main                PocF4Model.c:64:5\r\n \r\n0x502000000018 is located 0 bytes after 8-byte region [0x502000000010, 0x502000000018)\r\nallocated by thread T0 here:\r\n    #1 0x56127f5d6744 in main  PocF4Model.c:41:30\r\n \r\nSUMMARY: AddressSanitizer: heap-buffer-overflow PocF4Model.c:31\r\n         in msvc_printf_S_read\r\n```\r\n \r\n**Correct call** `msvc_printf_S_read(message, length/sizeof(WCHAR) = 4)` → reads\r\n8 bytes from 8-byte allocation — **no overflow**.  \r\n**Buggy call** `msvc_printf_S_read(message, length = 8)` → reads 16 bytes from\r\n8-byte allocation — **heap-buffer-overflow by 8 bytes**.\r\n \r\n---\r\n \r\n## Root Cause\r\n \r\n`client_cli_present_gateway_message()` at `client/common/client.c:1125`:\r\n \r\n```c\r\nBOOL client_cli_present_gateway_message(freerdp* instance, UINT32 type,\r\n        BOOL isDisplayMandatory, BOOL isConsentMandatory,\r\n        size_t length,         // ← byte count\r\n        const WCHAR* message)  // ← UTF-16LE buffer, no NUL terminator guaranteed\r\n{\r\n    ...\r\n#if defined(WIN32)\r\n    printf(\"%.*S\\n\", (int)length, message);   // LINE 1125 — precision in bytes,\r\n                                              //   MSVC expects WCHAR count → 2× OOB\r\n#else\r\n    LPSTR msg = ConvertWCharNToUtf8Alloc(message,\r\n                    length / sizeof(WCHAR),   // LINE 1128 — CORRECT division\r\n                    nullptr);\r\n    printf(\"%s\\n\", msg);\r\n```\r\n \r\nOn MSVC, `%S` is the wide-string format and `%.*S` precision counts **wide\r\ncharacters**. When `length = N bytes`, precision `N` instructs `printf` to read\r\n`N × sizeof(wchar_t)` bytes — double the intended amount — from a buffer that holds\r\nonly `N` bytes. The non-Windows branch correctly computes `length / sizeof(WCHAR)`.\r\n \r\n**`length` origin:** `rdg_read_http_unicode_string()` returns a byte count with no\r\nNUL guarantee, via `rdg_process_tunnel_response_optional` / `rdg_process_service_message`.\r\nThis path is **pre-authentication** (RD Gateway tunnel setup).\r\n \r\n---\r\n \r\n## Source Confirmation (commit `32394214`)\r\n \r\n```\r\nclient/common/client.c:1110  BOOL client_cli_present_gateway_message(\r\nclient/common/client.c:1112      size_t length, const WCHAR* message)\r\nclient/common/client.c:1124  #if defined(WIN32)\r\nclient/common/client.c:1125      printf(\"%.*S\\n\", (int)length, message);   ← BUGGY\r\nclient/common/client.c:1127  #else\r\nclient/common/client.c:1128      ...ConvertWCharNToUtf8Alloc(message, length / sizeof(WCHAR), ...)\r\n```\r\n \r\nVerified with `grep -n '%\\.\\*S' client/common/client.c` on the exact commit.\r\n \r\n---\r\n \r\n## Suggested Fix\r\n \r\n```c\r\n#if defined(WIN32)\r\n// Before (buggy — passes byte count as wchar precision):\r\nprintf(\"%.*S\\n\", (int)length, message);\r\n \r\n// After (correct — matches non-Windows branch):\r\nprintf(\"%.*S\\n\", (int)(length / sizeof(wchar_t)), message);\r\n```\r\n \r\nOr unify both branches through `ConvertWCharNToUtf8Alloc` and print with `%s`.\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-c49c-xm94-5qf3","source_modified_at":"2026-10-05T20:29:56Z","fetched_at":"2026-10-24T05:41:33.294140Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":"<=3.32.0"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-c49c-xm94-5qf3","source_modified_at":"2026-10-05T20:29:56Z","fetched_at":"2026-10-24T05:41:33.294140Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.1"}],"cvss":6.5,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-05T20:29:56Z","modified_at":"2026-10-05T20:29:56Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-c49c-xm94-5qf3","fetched_at":"2026-10-24T05:41:33.294140Z","source_modified_at":"2026-10-05T20:29:56Z"}],"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-05T20:29:56Z","content_hash":"6a3c9a6cba95bb23d94e8b8095dbc68335f63f7ae3ebded50b07d5d0108b63dd"}}
