Heap-use-after-free of sslSessionID in [@ ssl_FreeLockedSID] via dangling ss->sec.ci.sid left by ssl_BeginClientHandshake on OOM
Categories
(NSS :: Libraries, defect, P3)
Tracking
(nss 3.125, firefox-esr115 wontfix, firefox-esr140 affected, firefox151 wontfix, firefox152 wontfix, firefox153 fixed)
People
(Reporter: bugmon, Assigned: jschanck)
Details
(5 keywords, Whiteboard: [adv-main153+r])
Attachments
(8 files)
|
3.95 KB,
application/octet-stream
|
Details | |
|
224 bytes,
patch
|
Details | Diff | Splinter Review | |
|
211 bytes,
patch
|
Details | Diff | Splinter Review | |
|
84 bytes,
application/octet-stream
|
Details | |
|
1.13 KB,
application/octet-stream
|
Details | |
|
746 bytes,
patch
|
Details | Diff | Splinter Review | |
|
9.92 KB,
text/plain
|
Details | |
|
48 bytes,
text/x-phabricator-request
|
Details | Review |
In NSS libssl, ssl_BeginClientHandshake() (security/nss/lib/ssl/sslcon.c) mishandles an externally-cached session ID when its TLS version no longer falls within the socket's enabled range. When a client has installed a resumption token via the public SSL_SetResumptionToken() API, ss->sec.ci.sid holds the only reference (refcount 1, cached == in_external_cache). ssl_BeginClientHandshake() aliases that pointer into a local without adding a reference, and on version mismatch calls ssl_FreeSID() at line 167, which destroys and frees the heap object — but ss->sec.ci.sid is never cleared.
Immediately afterwards the function calls ssl3_NewSessionID(). If that allocation fails (PORT_ZAlloc returning NULL under memory pressure), control jumps to the loser: label and the function returns SECFailure with ss->sec.ci.sid still pointing into freed heap. When the application subsequently closes the socket (or calls SSL_ResetHandshake), ssl_DestroySecurityInfo() → ssl_ResetSecurityInfo() calls ssl_FreeSID(sec->ci.sid) on the dangling pointer, reading and decrementing sid->references in freed memory. If the freed slot has been reallocated, this becomes an arbitrary-object decrement followed by a potential second ssl_DestroySID/PORT_ZFree — i.e., a double-free of an attacker-influenceable 416-byte chunk.
The bug is reachable through documented public NSS client APIs in a plausible sequence (set external resumption token → narrow SSL_VersionRangeSet → SSL_ForceHandshake → PR_Close). It is gated on a single small-allocation failure during the handshake, which an attacker can only influence indirectly (e.g., by driving the client toward heap exhaustion). The fix is trivial: set ss->sec.ci.sid = NULL immediately after the ssl_FreeSID() at sslcon.c:167.
Build Info
- Branch: main
- Revision: b1c5a092fe1916975580c6ed95c51cbe78728a62
- Timestamp: 2026-04-05T14:37:35+00:00
Affected Code
File: security/nss/lib/ssl/sslcon.c, line 151-179
/* If there's an sid set from an external cache, use it. */
if (ss->sec.ci.sid && ss->sec.ci.sid->cached == in_external_cache) {
sid = ss->sec.ci.sid; /* alias only; refcount stays 1 */
SSL_TRC(3, ("%d: SSL[%d]: using external token", SSL_GETPID(), ss->fd));
} else if (!ss->opt.noCache) {
sid = ssl_LookupSID(ssl_Time(ss), &ss->sec.ci.peer,
ss->sec.ci.port, ss->peerID, ss->url);
}
if (sid) {
if (sid->version >= ss->vrange.min && sid->version <= ss->vrange.max) {
PORT_Assert(!ss->sec.localCert);
ss->sec.localCert = CERT_DupCertificate(sid->localCert);
} else {
ssl_UncacheSessionID(ss);
ssl_FreeSID(sid); /* refcount 1->0 -> ssl_DestroySID frees heap.
* BUG: ss->sec.ci.sid is NOT cleared and now dangles. */
sid = NULL;
}
}
if (!sid) {
sid = ssl3_NewSessionID(ss, PR_FALSE);
if (!sid) {
goto loser; /* returns SECFailure with ss->sec.ci.sid dangling */
}
sid->u.ssl3.keys.resumable = PR_FALSE;
}
ss->sec.ci.sid = sid; /* only reached on success */
File: security/nss/lib/ssl/sslsecur.c, line 665-668
/* cleanup the ci */
if (sec->ci.sid != NULL) {
ssl_FreeSID(sec->ci.sid); /* UAF: sec->ci.sid points to freed sslSessionID */
}
File: security/nss/lib/ssl/sslnonce.c, line 216-222
static void
ssl_FreeLockedSID(sslSessionID *sid)
{
PORT_Assert(sid->references >= 1); /* <-- ASAN: READ of size 4 in freed heap */
if (--sid->references == 0) {
ssl_DestroySID(sid, PR_TRUE); /* potential double-free */
}
}
File: security/nss/lib/ssl/sslsock.c, line 4613-4643
sid = ssl3_NewSessionID(ss, PR_FALSE); /* refcount = 1 */
...
/* Use the sid->cached as marker that this is from an external cache and
* we don't have to look up anything in the NSS internal cache. */
sid->cached = in_external_cache;
sid->lastAccessTime = ssl_Time(ss);
ss->sec.ci.sid = sid; /* sole owner; no extra ref */
SSLExp_SetResumptionToken stores a refcount-1 sslSessionID with cached==in_external_cache into ss->sec.ci.sid. ssl_BeginClientHandshake aliases it without adding a reference; on version-range mismatch it frees the only reference (line 167) but leaves ss->sec.ci.sid pointing at freed memory. The subsequent ssl3_NewSessionID failure path (goto loser) returns without overwriting ss->sec.ci.sid, so ssl_ResetSecurityInfo later dereferences and re-frees the dangling pointer.
Exploit Chain
- Application creates an NSS TLS client socket and installs an externally-cached resumption token via SSL_SetResumptionToken(); this sets ss->sec.ci.sid (refcount 1, cached=in_external_cache) with the token's negotiated TLS version (e.g., TLS 1.2).
- Application narrows the socket's enabled versions via SSL_VersionRangeSet() so that the stored sid->version is outside [vrange.min, vrange.max] (e.g., restricts to TLS 1.3 only after a configuration update).
- Application initiates the handshake (SSL_ForceHandshake / first read/write). ssl_Do1stHandshake → ssl_BeginClientHandshake takes the in_external_cache branch, detects the version mismatch, and calls ssl_FreeSID(sid) at sslcon.c:167. The 416-byte sslSessionID is destroyed and freed; ss->sec.ci.sid is left dangling.
- Under memory pressure, the immediately following ssl3_NewSessionID() → PORT_ZAlloc(sizeof(sslSessionID)) fails and returns NULL. ssl_BeginClientHandshake jumps to loser: and returns SECFailure without touching ss->sec.ci.sid.
- (Optional) Attacker-influenced heap activity reallocates the freed 416-byte slot with controlled contents.
- Application closes the socket (PR_Close) or calls SSL_ResetHandshake. ssl_DestroySecurityInfo → ssl_ResetSecurityInfo calls ssl_FreeSID(sec->ci.sid) on the dangling pointer.
- ssl_FreeLockedSID reads and decrements sid->references in freed/reallocated memory (use-after-free). If the field reads as 1, ssl_DestroySID runs again on the reclaimed object → double-free / arbitrary-pointer frees of fields inside the reclaimed chunk, providing a heap-corruption primitive.
Steps to Reproduce
- Apply the test-only allocation-fault helper to gtests/ssl_gtest/libssl_internals.c (SSLInt_FailNextZAllocOfSize / SSLInt_SizeofSessionID and a one-shot PORT_ZAlloc_Util shadow) and add gtests/ssl_gtest/ssl_sslcon_unittest.cc with TlsConnectTest.BeginClientHandshakeExternalSidVersionMismatchUAF; register both in ssl_gtest.gyp and manifest.mn.
- Build NSS with ASAN: cd /firefox/security/nss && ./build.sh --asan (or use the clauditor build_nss helper with firefox_dir=/firefox).
- Run: /firefox/security/dist/Debug/bin/ssl_gtest --gtest_filter=TlsConnectTest.BeginClientHandshakeExternalSidVersionMismatchUAF -d /firefox/security/tests_results/security/localhost/ssl_gtests
- Observe AddressSanitizer: heap-use-after-free in ssl_FreeLockedSID (sslnonce.c:219), freed at ssl_BeginClientHandshake (sslcon.c:167), allocated at SSLExp_SetResumptionToken (sslsock.c:4613).
Security Impact
- Severity: Moderate
- Attacker capability: Heap use-after-free of a 416-byte sslSessionID followed by a refcount decrement and potential double-free / ssl_DestroySID on a reclaimed object. If an attacker can groom the freed slot before PR_Close, this yields a write-what-where-lite (decrement at fixed offset) and a second free of attacker-chosen sub-pointers, which can be leveraged toward memory corruption / code execution in the embedding process.
- Preconditions: Embedding application must use the NSS external resumption-token client API (SSL_SetResumptionToken) and subsequently narrow SSL_VersionRangeSet so the stored token's version is excluded before the handshake; AND a single ~416-byte heap allocation (PORT_ZAlloc in ssl3_NewSessionID) must fail at that moment. The allocation failure is not directly attacker-controlled, only indirectly influenceable via memory pressure, hence OOM-gated.
ASAN Report
==25463==ERROR: AddressSanitizer: heap-use-after-free on address 0x5140010f484c at pc 0x5b610aeed673 bp 0x7ffd3dd2cf90 sp 0x7ffd3dd2cf80
READ of size 4 at 0x5140010f484c thread T0
#0 0x5b610aeed672 in ssl_FreeLockedSID ../../lib/ssl/sslnonce.c:219
#1 0x5b610aeed72e in ssl_FreeSID ../../lib/ssl/sslnonce.c:236
#2 0x5b610aef9919 in ssl_ResetSecurityInfo ../../lib/ssl/sslsecur.c:667
#3 0x5b610aef99d9 in ssl_DestroySecurityInfo ../../lib/ssl/sslsecur.c:683
#4 0x5b610af0a064 in ssl_DestroySocketContents ../../lib/ssl/sslsock.c:478
#5 0x5b610af0a4a7 in ssl_FreeSocket ../../lib/ssl/sslsock.c:535
#6 0x5b610aee6a16 in ssl_DefClose ../../lib/ssl/ssldef.c:221
#7 0x5b610aef9fd3 in ssl_SecureClose ../../lib/ssl/sslsecur.c:748
#8 0x5b610af1617a in ssl_Close ../../lib/ssl/sslsock.c:3229
#9 0x7eae42c84043 in PR_Close ../../../../pr/src/io/priometh.c:95
...
#20 0x5b610aa92af3 in nss_test::TlsConnectTest_BeginClientHandshakeExternalSidVersionMismatchUAF_Test::TestBody() ../../gtests/ssl_gtest/ssl_sslcon_unittest.cc:88
0x5140010f484c is located 12 bytes inside of 416-byte region [0x5140010f4840,0x5140010f49e0)
freed by thread T0 here:
#0 0x7eae431564d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
#1 0x7eae42ca2839 in PR_Free ../../../../pr/src/malloc/prmem.c:449
#2 0x7eae43004e87 in PORT_ZFree_Util ../../lib/util/secport.c:182
#3 0x5b610aeed624 in ssl_DestroySID ../../lib/ssl/sslnonce.c:205
#4 0x5b610aeed703 in ssl_FreeLockedSID ../../lib/ssl/sslnonce.c:221
#5 0x5b610aeed72e in ssl_FreeSID ../../lib/ssl/sslnonce.c:236
#6 0x5b610af8356f in ssl_BeginClientHandshake ../../lib/ssl/sslcon.c:167
#7 0x5b610aef692d in ssl_Do1stHandshake ../../lib/ssl/sslsecur.c:43
#8 0x5b610aef88e9 in SSL_ForceHandshake ../../lib/ssl/sslsecur.c:433
#9 0x5b610aa9289c in nss_test::TlsConnectTest_BeginClientHandshakeExternalSidVersionMismatchUAF_Test::TestBody() ../../gtests/ssl_gtest/ssl_sslcon_unittest.cc:82
previously allocated by thread T0 here:
#0 0x7eae43157340 in calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:77
#1 0x7eae42ca2773 in PR_Calloc ../../../../pr/src/malloc/prmem.c:434
#2 0x5b610a33c76f in PORT_ZAlloc_Util ../../gtests/ssl_gtest/libssl_internals.c:644
#3 0x5b610aea67db in ssl3_NewSessionID ../../lib/ssl/ssl3con.c:8644
#4 0x5b610af1ceb3 in SSLExp_SetResumptionToken ../../lib/ssl/sslsock.c:4613
#5 0x5b610aa92135 in nss_test::TlsConnectTest_BeginClientHandshakeExternalSidVersionMismatchUAF_Test::TestBody() ../../gtests/ssl_gtest/ssl_sslcon_unittest.cc:66
SUMMARY: AddressSanitizer: heap-use-after-free ../../lib/ssl/sslnonce.c:219 in ssl_FreeLockedSID
==25463==ABORTING
| Reporter | ||
Comment 1•5 months ago
|
||
| Reporter | ||
Comment 2•5 months ago
|
||
| Reporter | ||
Comment 3•5 months ago
|
||
| Reporter | ||
Comment 4•5 months ago
|
||
| Reporter | ||
Comment 5•5 months ago
|
||
| Reporter | ||
Comment 6•5 months ago
|
||
| Reporter | ||
Comment 7•5 months ago
|
||
Updated•5 months ago
|
| Assignee | ||
Updated•5 months ago
|
| Assignee | ||
Comment 9•4 months ago
|
||
| Assignee | ||
Updated•4 months ago
|
Comment 10•4 months ago
|
||
Pushed by jschanck@mozilla.com:
https://hg.mozilla.org/projects/nss/rev/a2b4ae220df6
avoid leaving a dangling ss->sec.ci.sid on allocation failure. r=nss-reviewers,keeler
Updated•4 months ago
|
Updated•2 months ago
|
Updated•13 days ago
|
Description
•