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 = 0xFFFFFFFFFFFFB800That 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’smain - 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:
| Caller | Path | Reachable |
|---|---|---|
| CROSS reference implementation, public NIST API | crypto_sign_open() | Yes |
| The shipped KAT harness | crypto_sign_open() | Yes |
liboqs OQS_SIG_verify | crypto_sign_verify(), detached signature with its own length check | No |
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.





