Heap use-after-free in [@ token_destructor] reading tok->pk11slot after nssToken_Destroy frees the token arena
Categories
(NSS :: Libraries, defect, P2)
Tracking
(nss 3.123, firefox-esr115 wontfix, firefox-esr140 wontfix, firefox149 wontfix, firefox150 wontfix, firefox151 fixed)
People
(Reporter: bugmon, Assigned: jschanck)
Details
(5 keywords, Whiteboard: [adv-main151+r])
Attachments
(4 files)
NSS's trust-domain token destructor (security/nss/lib/pki/trustdomain.c:61-73) first calls nssToken_Destroy(tok) and then dereferences tok->pk11slot to call PK11Slot_SetNSSToken(tok->pk11slot, NULL). The author's comment assumes the token "might still have a positive refcount", but when the trust-domain list holds the last reference, nssToken_Destroy() drops the refcount to 0 and calls nssArena_Destroy(tok->base.arena), which frees the arena chunk that contains the NSSToken struct itself. The subsequent read of tok->pk11slot is therefore a heap-use-after-free, and the resulting (potentially attacker-influenced) pointer is passed to PK11Slot_SetNSSToken(), which performs PR_Lock(sl->nssTokenLock), writes sl->nssToken = NULL, and may call nssToken_Destroy(old) on whatever pointer it read — a wild lock, wild 8-byte NULL write, and wild destroy.
A deterministic public-API path to the refcount==1 state exists: SECMOD_LoadUserModule() with a modulespec that has no library=, NSS="flags=internal", and a non-empty parameters= clause routes to softoken, whose C_Initialize returns CKR_CRYPTOKI_ALREADY_INITIALIZED. secmod_ModuleInit takes the reload path and SECMOD_LoadModule returns the existing internal SECMODModule (pk11pars.c:2244-2249). SECMOD_LoadUserModule then unconditionally calls STAN_AddModuleToDefaultTrustDomain(newmod) (pk11pars.c:2415) on that already-registered module. For each slot, STAN_InitTokenForSlotInfo creates a fresh NSSToken and calls PK11Slot_SetNSSToken(slot, newToken), which drops the slot's reference to the original NSSToken (refcount 2→1) while the original remains in td->tokenList. On NSS_Shutdown → STAN_Shutdown → NSSTrustDomain_Destroy → nssList_Clear(td->tokenList, token_destructor), the orphaned token is freed at line 66 and then read at line 72.
The vulnerability is a real memory-safety bug in NSS shutdown logic. Practical exploitability is constrained: the free and the use occur back-to-back on the same thread, so heap reclamation of the 2048-byte arena chunk in that window requires a concurrent allocator thread; and the trigger requires the host application to call SECMOD_LoadUserModule with a spec that aliases the internal softoken (or any other code path that leaves a token in td->tokenList with only the list reference). It is not reachable from web content in Firefox, but is reachable in NSS-embedding applications that allow user-supplied PKCS#11 module specs.
Build Info
- Branch: main
- Revision: 6164ea4bacaeaed1f617c11911df7fc32f2e6ec2
- Timestamp: 2026-04-02T19:36:24+00:00
Affected Code
File: security/nss/lib/pki/trustdomain.c, line 61-73
static void
token_destructor(void *t)
{
NSSToken *tok = (NSSToken *)t;
/* Remove the token list's reference to the token */
(void)nssToken_Destroy(tok);
/* Signal that the slot should not give out any more references to the
* token. The token might still have a positive refcount after this call.
* The token has a reference to the slot, so the slot will not be destroyed
* until after the token's refcount drops to 0. */
PK11Slot_SetNSSToken(tok->pk11slot, NULL);
}
File: security/nss/lib/dev/devtoken.c, line 26-40
NSS_IMPLEMENT PRStatus
nssToken_Destroy(
NSSToken *tok)
{
if (tok) {
if (PR_ATOMIC_DECREMENT(&tok->base.refCount) == 0) {
PK11_FreeSlot(tok->pk11slot);
PR_DestroyLock(tok->base.lock);
nssTokenObjectCache_Destroy(tok->cache);
(void)nssSlot_Destroy(tok->slot);
return nssArena_Destroy(tok->base.arena); /* frees the chunk containing |tok| */
}
}
return PR_SUCCESS;
}
File: security/nss/lib/pk11wrap/pk11pars.c, line 2406-2423
SECMODModule *
SECMOD_LoadUserModule(char *modulespec, SECMODModule *parent, PRBool recurse)
{
SECStatus rv = SECSuccess;
SECMODModule *newmod = SECMOD_LoadModule(modulespec, parent, recurse);
SECMODListLock *moduleLock = SECMOD_GetDefaultModuleListLock();
if (newmod) {
SECMOD_GetReadLock(moduleLock);
rv = STAN_AddModuleToDefaultTrustDomain(newmod); /* called even when newmod is the existing internal module */
SECMOD_ReleaseReadLock(moduleLock);
if (SECSuccess != rv) {
SECMOD_DestroyModule(newmod);
return NULL;
}
}
return newmod;
}
File: security/nss/lib/pki/pki3hack.c, line 62-87
NSS_IMPLEMENT PRStatus
STAN_InitTokenForSlotInfo(NSSTrustDomain *td, PK11SlotInfo *slot)
{
NSSToken *token;
if (!td) {
td = g_default_trust_domain;
...
}
token = nssToken_CreateFromPK11SlotInfo(td, slot);
if (token) {
/* PK11Slot_SetNSSToken increments the refcount on |token| to 2 */
PK11Slot_SetNSSToken(slot, token); /* drops the OLD token's slot ref: 2 -> 1 */
/* we give our reference to |td->tokenList| */
NSSRWLock_LockWrite(td->tokensLock);
nssList_Add(td->tokenList, token); /* old token still in list, now refcount==1 */
NSSRWLock_UnlockWrite(td->tokensLock);
} else {
PK11Slot_SetNSSToken(slot, NULL);
}
return PR_SUCCESS;
}
File: security/nss/lib/pk11wrap/pk11slot.c, line 2771-2787
void
PK11Slot_SetNSSToken(PK11SlotInfo *sl, NSSToken *nsst)
{
NSSToken *old;
if (nsst) {
nsst = nssToken_AddRef(nsst);
}
PR_Lock(sl->nssTokenLock); /* sl is read from freed |tok->pk11slot| */
old = sl->nssToken;
sl->nssToken = nsst; /* wild 8-byte NULL write */
PR_Unlock(sl->nssTokenLock);
if (old) {
(void)nssToken_Destroy(old); /* wild destroy on attacker-influenced pointer */
}
}
token_destructor() releases the list reference before reading tok->pk11slot. When that was the last reference, nssToken_Destroy frees the arena that backs tok, and the subsequent dereference reads freed heap. The orphaned-with-refcount-1 state is reachable via the public SECMOD_LoadUserModule reload path, which re-runs STAN_AddModuleToDefaultTrustDomain on the already-registered internal module.
Exploit Chain
- Application initializes NSS (NSS_Initialize / NSS_NoDB_Init); each internal slot's NSSToken has refcount 2 (slot ref + td->tokenList ref).
- Attacker-influenced code path causes the application to call SECMOD_LoadUserModule() with a modulespec that has no library=, NSS="flags=internal,critical", and a non-empty parameters= clause (e.g. via a user-configurable PKCS#11 module spec).
- Softoken's C_Initialize returns CKR_CRYPTOKI_ALREADY_INITIALIZED; secmod_ModuleInit takes the reload path; SECMOD_LoadModule returns the existing internal SECMODModule (pk11pars.c:2244-2249).
- SECMOD_LoadUserModule unconditionally calls STAN_AddModuleToDefaultTrustDomain on that existing module (pk11pars.c:2415). For each slot, a fresh NSSToken is created and installed via PK11Slot_SetNSSToken, dropping the original token's slot reference to 1 while it remains in td->tokenList.
- (Optional) Attacker arranges a concurrent thread that performs heap allocations sized to reclaim a 2048-byte chunk during shutdown.
- Application calls NSS_Shutdown → STAN_Shutdown → NSSTrustDomain_Destroy → nssList_Clear(td->tokenList, token_destructor).
- token_destructor() calls nssToken_Destroy(tok): refcount 1→0, nssArena_Destroy frees the 2048-byte arena chunk containing tok.
- token_destructor() reads tok->pk11slot from the freed chunk and calls PK11Slot_SetNSSToken(sl, NULL): PR_Lock(sl->nssTokenLock), sl->nssToken = NULL, PR_Unlock, and possibly nssToken_Destroy(old). If the freed chunk was reclaimed, sl is attacker-controlled, yielding a wild lock, an 8-byte NULL write to an attacker-chosen address, and a destroy on an attacker-chosen pointer.
Steps to Reproduce
- Build NSS with AddressSanitizer (./build.sh --asan).
- Add security/nss/gtests/pk11_gtest/pk11_trustdomain_uaf_unittest.cc (provided) to pk11_gtest.gyp sources and rebuild.
- Run: GTEST_FILTER='TrustDomainTokenDestructorUAF.ReloadInternalModuleOrphansToken' nss/tests/gtests/gtests.sh (or run pk11_gtest directly with that --gtest_filter).
- Observe ASAN heap-use-after-free at lib/pki/trustdomain.c:72 during NSS_Shutdown.
Security Impact
- Severity: Moderate
- Attacker capability: Heap use-after-free read of an 8-byte pointer (tok->pk11slot) followed by PR_Lock / 8-byte NULL store / PR_Unlock / nssToken_Destroy through that pointer. If the freed 2048-byte arena chunk is reclaimed with controlled contents before the read (requires a racing allocator thread, since free and use are adjacent on the same thread), this yields an arbitrary-address NULL write and an arbitrary-pointer destroy, which can be leveraged toward memory corruption / code execution inside the NSS-hosting process.
- Preconditions: The host application must call SECMOD_LoadUserModule() with a modulespec that resolves to the already-loaded internal softoken (no library=, flags=internal, non-empty parameters=) — or otherwise reach any state where an NSSToken remains in td->tokenList with only the list reference. This is not reachable from untrusted web content in Firefox; it requires control over PKCS#11 module configuration in an NSS-embedding application. Exploitation additionally requires winning a very tight same-thread free→use race via a concurrent allocating thread.
ASAN Report
==15776==ERROR: AddressSanitizer: heap-use-after-free on address 0x51d000022718 at pc 0x7cd11b865183 bp 0x7ffd61c2b5e0 sp 0x7ffd61c2b5d0
READ of size 8 at 0x51d000022718 thread T0
#0 0x7cd11b865182 in token_destructor ../../lib/pki/trustdomain.c:72
#1 0x7cd11b88029b in nssList_Clear ../../lib/base/list.c:166
#2 0x7cd11b8652c0 in NSSTrustDomain_Destroy ../../lib/pki/trustdomain.c:87
#3 0x7cd11b851b1f in STAN_Shutdown ../../lib/pki/pki3hack.c:211
#4 0x7cd11b72ca79 in nss_Shutdown ../../lib/nss/nssinit.c:1160
#5 0x7cd11b72cc51 in NSS_Shutdown ../../lib/nss/nssinit.c:1221
#6 0x586050b97462 in main ../../gtests/common/gtests.cc:47
#7 0x7cd11ae341c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9)
#8 0x7cd11ae3428a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a)
#9 0x5860500e96d4 in _start (/firefox/security/dist/Debug/bin/pk11_gtest+0xdb6d4)
0x51d000022718 is located 152 bytes inside of 2048-byte region [0x51d000022680,0x51d000022e80)
freed by thread T0 here:
#0 0x7cd11b9f44d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
#1 0x7cd11b3c8839 in PR_Free ../../../../pr/src/malloc/prmem.c:449
#2 0x7cd11b4652b0 in FreeArenaList ../../../lib/ds/plarena.c:201
#3 0x7cd11b46546c in PL_FinishArenaPool ../../../lib/ds/plarena.c:225
#4 0x7cd11b87c00e in nssArena_Destroy ../../lib/base/arena.c:477
#5 0x7cd11b86d480 in nssToken_Destroy ../../lib/dev/devtoken.c:36
#6 0x7cd11b86515d in token_destructor ../../lib/pki/trustdomain.c:66
#7 0x7cd11b88029b in nssList_Clear ../../lib/base/list.c:166
#8 0x7cd11b8652c0 in NSSTrustDomain_Destroy ../../lib/pki/trustdomain.c:87
#9 0x7cd11b851b1f in STAN_Shutdown ../../lib/pki/pki3hack.c:211
#10 0x7cd11b72ca79 in nss_Shutdown ../../lib/nss/nssinit.c:1160
#11 0x7cd11b72cc51 in NSS_Shutdown ../../lib/nss/nssinit.c:1221
#12 0x586050b97462 in main ../../gtests/common/gtests.cc:47
previously allocated by thread T0 here:
#0 0x7cd11b9f59c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x7cd11b3c86e7 in PR_Malloc ../../../../pr/src/malloc/prmem.c:425
#2 0x7cd11b4649cc in PL_ArenaAllocate ../../../lib/ds/plarena.c:132
#3 0x7cd11b87cff2 in nss_zalloc_arena_locked ../../lib/base/arena.c:751
#4 0x7cd11b87d471 in nss_ZAlloc ../../lib/base/arena.c:872
#5 0x7cd11b765334 in nssToken_CreateFromPK11SlotInfo ../../lib/pk11wrap/dev3hack.c:145
#6 0x7cd11b8510d0 in STAN_InitTokenForSlotInfo ../../lib/pki/pki3hack.c:74
#7 0x7cd11b851509 in STAN_LoadDefaultNSS3TrustDomain ../../lib/pki/pki3hack.c:138
#8 0x7cd11b72b982 in nss_Init ../../lib/nss/nssinit.c:729
#9 0x7cd11b72bf6c in NSS_Initialize ../../lib/nss/nssinit.c:889
#10 0x586050b97442 in main ../../gtests/common/gtests.cc:42
SUMMARY: AddressSanitizer: heap-use-after-free ../../lib/pki/trustdomain.c:72 in token_destructor
==15776==ABORTING
| Reporter | ||
Comment 1•5 months ago
|
||
| Reporter | ||
Comment 2•5 months ago
|
||
| Reporter | ||
Comment 3•5 months ago
|
||
Updated•5 months ago
|
| Assignee | ||
Updated•5 months ago
|
| Assignee | ||
Comment 5•5 months ago
|
||
Pushed by jschanck@mozilla.com:
https://hg.mozilla.org/projects/nss/rev/65699665db4a
Heap use-after-free in [@ token_destructor] reading tok->pk11slot after nssToken_Destroy frees the token arena. r=nss-reviewers,rrelyea
Updated•5 months ago
|
| Assignee | ||
Updated•5 months ago
|
| Assignee | ||
Updated•5 months ago
|
Updated•5 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•1 month ago
|
Description
•