{"schema":"threatline.record/2","record":{"id":"GHSA-q56j-jjh6-38jf","aliases":["GHSA-q56j-jjh6-38jf"],"summary":"Allocation of Resources Without Limits or Throttling in FreeRDP","details":"### Summary\r\n\r\nWhen a user connects a FreeRDP client to a malicious or compromised RDP server with drive redirection enabled, the server can open a shared file and send `IRP_MJ_READ` with `Length = 0xffffffff`. `drive_process_irp_read` passes `4ull + Length` to `Stream_EnsureRemainingCapacity` before reading the local file and without applying a per-request maximum.\r\n\r\nOn a 64-bit build, the capacity request is `4294967299` bytes. The stream implementation attempts to grow the output buffer and zeroes the added region, so one small network request can cause roughly 4 GiB of allocation and memory-write pressure. On a 32-bit build, conversion to `size_t` reduces the same capacity argument to three bytes while the file-read count remains `UINT32_MAX`, leaving the following file write under-reserved.\r\n\r\nThe user must initiate the RDP connection and share a drive. The server needs no account or prior access to the client. Once the server has obtained a valid shared-file identifier through the ordinary RDPDR protocol, the malicious length requires no further user interaction.\r\n\r\nThe vulnerable reservation expression is present in every inspected release from 3.0.0 through 3.31.1 and remains present on current `master`. Releases through 3.20.0 used `Length + 4`, which wraps before conversion. Release 3.20.1 changed it to `4ull + Length`; that avoids the pre-conversion wrap on 64-bit builds but causes the oversized 64-bit allocation and still truncates on 32-bit builds. No fixed release is known.\r\n\r\nThe relevant source is `drive_process_irp_read` in `channels/drive/client/drive_main.c`:\r\n\r\n```c\r\nStream_Read_UINT32(irp->input, Length);\r\nStream_Read_UINT64(irp->input, Offset);\r\nfile = drive_get_file_by_id(drive, irp->FileId);\r\n\r\nif (!file)\r\n{\r\n\tirp->IoStatus = STATUS_UNSUCCESSFUL;\r\n\tLength = 0;\r\n}\r\nelse if (!drive_file_seek(file, Offset))\r\n{\r\n\tirp->IoStatus = drive_map_windows_err(GetLastError());\r\n\tLength = 0;\r\n}\r\n\r\nif (!Stream_EnsureRemainingCapacity(irp->output, 4ull + Length))\r\n{\r\n\tWLog_ERR(TAG, \"Stream_EnsureRemainingCapacity failed!\");\r\n\treturn ERROR_INTERNAL_ERROR;\r\n}\r\nelse if (Length == 0)\r\n\tStream_Write_UINT32(irp->output, 0);\r\nelse\r\n{\r\n\tBYTE* buffer = Stream_PointerAs(irp->output, BYTE) + sizeof(UINT32);\r\n\r\n\tif (!drive_file_read(file, buffer, &Length))\r\n\t{\r\n\t\tirp->IoStatus = drive_map_windows_err(GetLastError());\r\n\t\tStream_Write_UINT32(irp->output, 0);\r\n\t}\r\n\telse\r\n\t{\r\n\t\tStream_Write_UINT32(irp->output, Length);\r\n\t\tStream_Seek(irp->output, Length);\r\n\t}\r\n}\r\n```\r\n\r\n`Stream_EnsureCapacity` in `winpr/libwinpr/utils/stream.c` performs the growth and zero fill:\r\n\r\n```c\r\nnew_buf = (BYTE*)realloc(s->buffer, new_capacity);\r\nif (!new_buf)\r\n\treturn FALSE;\r\n\r\ns->buffer = new_buf;\r\ns->capacity = new_capacity;\r\ns->length = new_capacity;\r\nZeroMemory(&s->buffer[old_capacity], s->capacity - old_capacity);\r\n```\r\n\r\n### Reproduction\r\n\r\nSave the following three files in one directory and run `sh ./run.sh`. The PoC safely reproduces the exact native arithmetic and extracts the compiled i386 values. It does not allocate 4 GiB, read a local file, contact an RDP server, or require FreeRDP development libraries.\r\n\r\nRequirements are a POSIX shell, a GCC-compatible C compiler with i386 code-generation support, GNU `objcopy`, and `od`.\r\n\r\nExpected output on a 64-bit host:\r\n\r\n```text\r\npointer_bits=64\r\nnormal_length=4096 normal_reservation=4100\r\nmaximum_length=4294967295 maximum_reservation=4294967299\r\ni386_pocdata=0300000004100000\r\ni386_maximum_reservation=3\r\ni386_normal_reservation=4100\r\n```\r\n\r\n<!-- BEGIN FILE: drive_length_native.c -->\r\n```c\r\n#include <inttypes.h>\r\n#include <stdint.h>\r\n#include <stdio.h>\r\n\r\nstatic uint64_t reservation(uint32_t length)\r\n{\r\n\treturn 4ULL + length;\r\n}\r\n\r\nint main(void)\r\n{\r\n\tconst uint32_t normal_length = 4096U;\r\n\tconst uint32_t maximum_length = UINT32_MAX;\r\n\r\n\tprintf(\"pointer_bits=%zu\\n\", sizeof(size_t) * 8U);\r\n\tprintf(\"normal_length=%\" PRIu32 \" normal_reservation=%\" PRIu64 \"\\n\", normal_length,\r\n\t       reservation(normal_length));\r\n\tprintf(\"maximum_length=%\" PRIu32 \" maximum_reservation=%\" PRIu64 \"\\n\", maximum_length,\r\n\t       reservation(maximum_length));\r\n\r\n\tif ((sizeof(size_t) != 8U) || (reservation(normal_length) != 4100ULL) ||\r\n\t    (reservation(maximum_length) != 4294967299ULL))\r\n\t\treturn 1;\r\n\treturn 0;\r\n}\r\n```\r\n<!-- END FILE: drive_length_native.c -->\r\n\r\n<!-- BEGIN FILE: drive_length_i386.c -->\r\n```c\r\ntypedef __SIZE_TYPE__ size_t;\r\n\r\n__attribute__((used, section(\".pocdata\")))\r\nsize_t reservation_values[2] = {\r\n\t(size_t)(4ULL + 0xffffffffU),\r\n\t(size_t)(4ULL + 4096U),\r\n};\r\n```\r\n<!-- END FILE: drive_length_i386.c -->\r\n\r\n<!-- BEGIN FILE: run.sh -->\r\n```sh\r\n#!/bin/sh\r\nset -eu\r\n\r\nBASE_DIR=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\r\nBUILD_DIR=${BUILD_DIR:-\"$BASE_DIR/.build\"}\r\nCC=${CC:-cc}\r\nOBJCOPY=${OBJCOPY:-objcopy}\r\n\r\nmkdir -p \"$BUILD_DIR\"\r\n\r\n\"$CC\" -std=c11 -Wall -Wextra -Werror -O2 \\\r\n\t\"$BASE_DIR/drive_length_native.c\" -o \"$BUILD_DIR/drive_length_native\"\r\n\"$BUILD_DIR/drive_length_native\"\r\n\r\n\"$CC\" -m32 -ffreestanding -fno-pic -fno-pie -c \\\r\n\t\"$BASE_DIR/drive_length_i386.c\" -o \"$BUILD_DIR/drive_length_i386.o\"\r\n\"$OBJCOPY\" -j .pocdata -O binary \\\r\n\t\"$BUILD_DIR/drive_length_i386.o\" \"$BUILD_DIR/drive_length_i386.bin\"\r\n\r\nI386_BYTES=$(od -An -tx1 -v \"$BUILD_DIR/drive_length_i386.bin\" | tr -d ' \\n')\r\nprintf 'i386_pocdata=%s\\n' \"$I386_BYTES\"\r\ntest \"$I386_BYTES\" = \"0300000004100000\"\r\nprintf 'i386_maximum_reservation=3\\n'\r\nprintf 'i386_normal_reservation=4100\\n'\r\n```\r\n<!-- END FILE: run.sh -->\r\n\r\n### Remediation\r\n\r\nReject redirected-drive read lengths above a documented per-request maximum before calling `Stream_EnsureRemainingCapacity` or `drive_file_read`. After enforcing the limit, compute `sizeof(UINT32) + (size_t)Length` and retain the stream helper's overflow checks as defense in depth.\r\n\r\nAdd regression coverage for zero, the maximum accepted length, one byte above the limit, and `UINT32_MAX` on 32-bit and 64-bit builds. Oversized requests should be rejected before the local file reader is called.\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-q56j-jjh6-38jf","source_modified_at":"2026-09-29T06:38:51Z","fetched_at":"2026-10-23T18:58:35.272684Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":">= 3.0.0"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-q56j-jjh6-38jf","source_modified_at":"2026-09-29T06:38:51Z","fetched_at":"2026-10-23T18:58:35.272684Z","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:38:51Z","modified_at":"2026-09-29T06:38:51Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-q56j-jjh6-38jf","fetched_at":"2026-10-23T18:58:35.272684Z","source_modified_at":"2026-09-29T06:38:51Z"}],"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:38:51Z","content_hash":"85fc67b3c2cc05ecf90a03e67c8502e9eeed5d972ece1ae75a2e4104ed92de52"}}
