Closed Bug 2029771 Opened 5 months ago Closed 5 months ago

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)

RESOLVED FIXED
Tracking Status
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

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

  1. Application initializes NSS (NSS_Initialize / NSS_NoDB_Init); each internal slot's NSSToken has refcount 2 (slot ref + td->tokenList ref).
  2. 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).
  3. 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).
  4. 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.
  5. (Optional) Attacker arranges a concurrent thread that performs heap allocations sized to reclaim a 2048-byte chunk during shutdown.
  6. Application calls NSS_Shutdown → STAN_Shutdown → NSSTrustDomain_Destroy → nssList_Clear(td->tokenList, token_destructor).
  7. token_destructor() calls nssToken_Destroy(tok): refcount 1→0, nssArena_Destroy frees the 2048-byte arena chunk containing tok.
  8. 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

  1. Build NSS with AddressSanitizer (./build.sh --asan).
  2. Add security/nss/gtests/pk11_gtest/pk11_trustdomain_uaf_unittest.cc (provided) to pk11_gtest.gyp sources and rebuild.
  3. Run: GTEST_FILTER='TrustDomainTokenDestructorUAF.ReloadInternalModuleOrphansToken' nss/tests/gtests/gtests.sh (or run pk11_gtest directly with that --gtest_filter).
  4. 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
Attached patch fix.patch — — Splinter Review
Attached file crash_stack.txt —
Group: core-security → crypto-core-security
Assignee: nobody → jschanck
Severity: -- → S3
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Priority: -- → P2
Attached file (secure) —

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

Status: ASSIGNED → RESOLVED
Closed: 5 months ago
Resolution: --- → FIXED
Group: crypto-core-security → core-security-release
status-nss: --- → 3.123
QA Whiteboard: [sec] [qa-triage-done-c152/b151]
Whiteboard: [adv-main151+r][adv-esr115.36+r][adv-esr140.11+r]
Whiteboard: [adv-main151+r][adv-esr115.36+r][adv-esr140.11+r] → [adv-main151+r]
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: