Request an audit

// write-up

Shorter than a signature: an unsigned underflow in CROSS.

A signed message shorter than one signature made the CROSS reference implementation compute a length close to 2^64 and pass it to memcpy, before verification ran. CVE-2026-41509, scored 9.8 Critical.

Published
Reading time
4 min
A cracked digital signature seal pouring a stream of glowing data blocks past the containers meant to hold it, with a counter behind it rolled past zero

Most of a post-quantum signature implementation is hard to read on purpose: matrix arithmetic, syndrome computation, the Fiat-Shamir transcript. The bug behind CVE-2026-41509 sits in none of that. It sits in the few lines that open a signed message before verification starts, the part that looks like plumbing and is therefore rarely read. In the CROSS reference implementation, a signed message shorter than one signature made those lines compute a length close to 264 and hand it to memcpy. No key, no valid signature and no prior state were needed. NVD scored it 9.8 Critical.

The target

CROSS is a code-based post-quantum signature scheme in NIST’s additional-signatures process. Its reference implementation exposes the standard NIST API, including the combined call crypto_sign_open(): it takes a signed message (the signature followed by the message), splits it, verifies the signature and returns the message. The same function is vendored by liboqs into all 36 CROSS variant directories, which is where it was found, during a source-level pass over liboqs 0.15.0. The defect itself is upstream, in Reference_Implementation/lib/sign.c of CROSS-implementation.

Subtract first, check never

Before the fix, crypto_sign_open() derived the message length by subtracting the signature size from the signed-message length:

/* verify returns 1 if signature is ok, 0 otherwise */
*mlen = smlen-(unsigned long long) sizeof(CROSS_sig_t);

Two lines later it copied *mlen bytes from the signed message into the output buffer. Nothing between the function entry and that copy checked that smlen was at least one signature long. The arithmetic is unsigned, so a short input does not produce a negative length: it wraps. Signature verification runs only after the copy, so it cannot stop it.

For the rsdp-128-fast parameter set, sizeof(CROSS_sig_t) is 18,432 bytes. With smlen = 0:

*mlen = 0 - 18432 = 0xFFFFFFFFFFFFB800

That value goes straight into memcpy. In the proof of concept the source was a 16-byte buffer and the destination 64 bytes, so the copy crosses both bounds at once.

Reproducing it

The proof of concept is a standalone C file linked against the audited build. It calls crypto_sign_open() with smlen = 0, and needs no key pair, no signature and no network. Built with -fsanitize=address, it stops at the copy:

  • AddressSanitizer: negative-size-param in memcpy, size=-18432
  • raised from sign.c:86, called from the PoC’s main
  • first recorded on 3 March 2026, identical on a re-run on 22 September 2026

A sanitized build aborts there. An unsanitized build keeps copying and overwrites adjacent heap memory until it faults.

Who could reach it

Presence and reachability are different questions, and the answer differs by caller:

CallerPathReachable
CROSS reference implementation, public NIST APIcrypto_sign_open()Yes
The shipped KAT harnesscrypto_sign_open()Yes
liboqs OQS_SIG_verifycrypto_sign_verify(), detached signature with its own length checkNo

liboqs carried the vulnerable function in every CROSS variant but never called it: its verify path uses the detached-signature function, which takes the signature length separately and checks it. Anything built on the reference implementation’s combined API was exposed.

What it scored, and what was shown

NVD scored CVE-2026-41509 at 9.8 Critical, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. The report keeps a clear line between what was demonstrated and what was not:

  • Shown: an out-of-bounds heap read and write of attacker-influenced length during verification, from a single short buffer, with no key or prior state.
  • Not shown: control over the written data or code execution. That was not attempted.
  • Executed on one variant only (rsdp-128-fast). The other 35 variants and the upstream tree were confirmed by reading the source.

The fix

The upstream fix, commit fc6b7e7 (“add a check on the minimum smlen”), adds the missing guard before the subtraction:

if (smlen < (size_t) sizeof(CROSS_sig_t)) {
   return -1;
}

Four lines, one file. The fix has since reached liboqs as well: on 12 September 2026 all 36 vendored copies on liboqs main (0.16.0-44) carried the guard.

Disclosure

  • 3 March 2026: found and reproduced under AddressSanitizer during a liboqs pass
  • 7 March 2026: reported as liboqs issue #2409
  • 23 March 2026: fixed upstream in fc6b7e7
  • 24 March 2026: published as GHSA-w72c-hgx8-p7cv
  • 8 May 2026: CVE-2026-41509 published, scored 9.8 Critical by NVD
  • 12 September 2026: fix confirmed in all 36 copies on liboqs main

The pattern worth keeping

The guard fixes this instance, not the class. Every NIST-API reference implementation has its own crypto_sign_open(), many of them derive the message length the same way, and the combined open-and-verify call is the one function in the API whose entire input is untrusted. If you ship or vendor a post-quantum reference implementation, that function deserves a read before anything clever does: check that the length is compared to the signature size before it is subtracted from it.

Full report

The complete audit, with the scope, the evidence and a coverage table naming what was examined and what was not, is published whole: XSource Security audit: CROSS signature verification (PDF). The disclosure record is at CVE-2026-41509.

Disclosure record

CVE-2026-41509
CROSS post-quantum signature buffer overflow

Record page →

Same scrutiny, your code

Send the repository or product, the component you care about most, the build under review and the window you have in mind. A written scope and a fixed price come back before any work begins.

Request an audit →

← All write-ups

More write-ups

All write-ups →

Inside the Lenovo SMM escalation chain

Part 1 of 4. The Lenovo WmiBiosSetupSmm disclosure (CVE-2026-10587 to -10590), coordinated with Lenovo PSIRT under case LEN-213632 and published in advisory LEN-220440 on 14 July 2026. Most vulnerability write-ups live in the operating system, the…

Read write-up →

Validating the unvalidated: SMI input handling

Part 2 of 4: the Lenovo WmiBiosSetupSmm disclosure. Start with Part 1 for the architecture and the escalation chain. Part 1 named the target: WmiBiosSetupSmm, software SMI 0xDE, on the Lenovo Legion 5 15IRX10 (BIOS QNCN33WW v1.33, InsydeH2O). This part opens the handler and…

Read write-up →