VYPR
Medium severity5.3NVD Advisory· Published May 29, 2026

CVE-2026-44518

CVE-2026-44518

Description

liboqs is a C-language cryptographic library that provides implementations of post-quantum cryptography algorithms. Prior to 0.16.0, an out-of-bounds read has been identified in the XMSS and XMSS^MT stateful signature verification code. When the verification function is called with a signature buffer shorter than the expected signature size for the given parameter set, the implementation does not validate the caller-supplied length and proceeds to read past the end of the buffer. The out-of-bounds bytes are consumed only as input to an internal hash computation and are not returned to the caller, so no oracle exists to leak their contents to an attacker. The primary observable effect is a possible crash (denial of service) of the verifying process if the read crosses into an unmapped memory page. This vulnerability is fixed in 0.16.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

An out-of-bounds read in liboqs XMSS signature verification can cause denial of service via crafted short signature buffers; fixed in 0.16.0.

Vulnerability

In liboqs versions prior to 0.16.0, the XMSS and XMSS^MT stateful signature verification functions do not validate the caller-supplied signature buffer length against the expected signature size for the given parameter set. The internal function xmssmt_core_sign_open discards the passed length parameter and reads params->sig_bytes bytes from the buffer, leading to an out-of-bounds read when the buffer is shorter than expected. This affects all use of the OQS_SIG_STFL_verify API and per-variant wrappers for XMSS. [1][2]

Exploitation

An attacker can supply a signature buffer shorter than the expected size for the parameter set to the verification function. No special privileges or network position is required beyond the ability to invoke the verification API with crafted input. The read beyond the buffer continues until params->sig_bytes bytes have been consumed, potentially reading into adjacent memory. The out-of-bounds bytes are used only as input to an internal hash computation and do not affect the verification result; the primary risk is a crash if the read crosses into unmapped memory. [2]

Impact

The primary impact is denial of service (crash) of the verifying process. No information disclosure occurs because the out-of-bounds bytes are not returned to the attacker, nor can they influence the signature verification to accept an invalid signature. The attacker does not gain code execution or privilege escalation. [2]

Mitigation

The vulnerability is fixed in liboqs version 0.16.0 (released) and in the main branch on GitHub via commit ef70dea. Users should upgrade to 0.16.0 or later. No workarounds are available for earlier versions. The vulnerability was reported by @zulfff. [1][2]

AI Insight generated on May 29, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

2
077e32a94f39

Merge commit from fork

https://github.com/open-quantum-safe/liboqsDouglas StebilaMay 15, 2026via github-commit-search
2 files changed · +33 1
  • src/sig_stfl/xmss/sig_stfl_xmss_xmssmt.c+12 0 modified
    @@ -76,6 +76,18 @@ OQS_API OQS_STATUS OQS_SIG_STFL_alg_xmss##xmss_v##_verify(const uint8_t *message
             if (signature_len != OQS_SIG_STFL_alg_xmss##xmss_v##_length_signature) {\
                     return OQS_ERROR;\
             }\
    +        if (public_key == NULL) {\
    +                return OQS_ERROR;\
    +        }\
    +        /* Reject pks whose OID disagrees with the variant the caller invoked. */\
    +        /* Without this, a mutated OID can steer xmss_sign_open into a parameter */\
    +        /* set whose sig_bytes exceeds signature_len, causing an OOB read in */\
    +        /* xmss_commons.c (GHSA-2wxh-55qf-c7wg). */\
    +        uint32_t pk_oid = ((uint32_t)public_key[0] << 24) | ((uint32_t)public_key[1] << 16)\
    +                        | ((uint32_t)public_key[2] <<  8) | ((uint32_t)public_key[3]);\
    +        if (pk_oid != (uint32_t)OQS_SIG_STFL_alg_xmss##xmss_v##_oid) {\
    +                return OQS_ERROR;\
    +        }\
             return OQS_SIG_STFL_alg_xmss##mt##_verify(message, message_len, signature, signature_len, public_key);\
     }\
     \
    
  • tests/test_sig_stfl.c+21 1 modified
    @@ -435,8 +435,28 @@ static OQS_STATUS test_invalid_sig(const char *method_name) {
     	uint8_t message[] = "test";
     	uint8_t malicious_sig[TEST_INVALID_SIG_MALICIOUS_SIG_LEN] = {0};
     
    -	// This triggers the bug via proper API
    +	// Sub-case 1 (GHSA-wf7v-fhxj-73m2): caller-supplied signature_len shorter than the
    +	// declared algorithm's expected size. Must be rejected by the wrapper length check.
     	OQS_STATUS status = OQS_SIG_STFL_verify(sig, message, sizeof(message) - 1, malicious_sig, TEST_INVALID_SIG_MALICIOUS_SIG_LEN, pk);
    +	if (status == OQS_SUCCESS) {
    +		OQS_SIG_STFL_free(sig);
    +		return OQS_ERROR;
    +	}
    +
    +	// Sub-case 2 (GHSA-2wxh-55qf-c7wg): correctly-sized signature buffer for the declared
    +	// algorithm, but the pk's OID bytes reference a different parameter set. Pre-fix, this
    +	// caused xmssmt_core_sign_open to index sig_bytes (derived from the mutated OID) past
    +	// the end of the caller-supplied signature buffer (OOB read, see xmss_commons.c).
    +	// Post-fix, the wrapper rejects the OID mismatch before the OOB read.
    +	uint8_t *full_sig = OQS_MEM_malloc(sig->length_signature);
    +	if (full_sig == NULL) {
    +		OQS_SIG_STFL_free(sig);
    +		return OQS_ERROR;
    +	}
    +	memset(full_sig, 0, sig->length_signature);
    +	pk[TEST_XMSS_OID_LEN - 1] = 0x02; // mirrors the GHSA-2wxh-55qf-c7wg PoC for XMSS-SHA2_10_256
    +	status = OQS_SIG_STFL_verify(sig, message, sizeof(message) - 1, full_sig, sig->length_signature, pk);
    +	OQS_MEM_insecure_free(full_sig);
     	OQS_SIG_STFL_free(sig);
     	if (status == OQS_SUCCESS) {
     		return OQS_ERROR;
    
ef70dea7c85e

Fix Reading beyond buffer end (#2384)

https://github.com/open-quantum-safe/liboqsNorman AshleyApr 23, 2026via nvd-ref
2 files changed · +102 19
  • src/sig_stfl/xmss/sig_stfl_xmss_xmssmt.c+3 0 modified
    @@ -73,6 +73,9 @@ OQS_API OQS_STATUS OQS_SIG_STFL_alg_xmss##xmss_v##_sign(uint8_t *signature, size
     }\
     \
     OQS_API OQS_STATUS OQS_SIG_STFL_alg_xmss##xmss_v##_verify(const uint8_t *message, size_t message_len, const uint8_t *signature, size_t signature_len, const uint8_t *public_key) {\
    +        if (signature_len != OQS_SIG_STFL_alg_xmss##xmss_v##_length_signature) {\
    +                return OQS_ERROR;\
    +        }\
             return OQS_SIG_STFL_alg_xmss##mt##_verify(message, message_len, signature, signature_len, public_key);\
     }\
     \
    
  • tests/test_sig_stfl.c+99 19 modified
    @@ -21,7 +21,7 @@
     #include "system_info.c"
     #include "test_helpers.h"
     
    -#if OQS_USE_PTHREADS_IN_TESTS
    +#if OQS_USE_PTHREADS
     #include <pthread.h>
     static pthread_mutex_t *test_sk_lock = NULL;
     static pthread_mutex_t *sk_lock = NULL;
    @@ -151,7 +151,7 @@ static OQS_STATUS save_secret_key(uint8_t *key_buf, size_t buf_len, void *contex
     	return OQS_ERROR;
     }
     
    -#if OQS_USE_PTHREADS_IN_TESTS
    +#if OQS_USE_PTHREADS
     
     static OQS_SIG_STFL_SECRET_KEY *lock_test_sk = NULL;
     static OQS_SIG_STFL *lock_test_sig_obj = NULL;
    @@ -399,6 +399,52 @@ static char *convert_method_name_to_file_name(const char *method_name) {
     	return strdup(file_store);
     }
     
    +#ifdef OQS_ENABLE_SIG_STFL_XMSS
    +/* test_invalid_sig: n=32 XMSS / XMSSMT pk layout (see sig_stfl_xmss.h): raw key material + OID prefix. */
    +#define TEST_INVALID_SIG_XMSS_PK_RAW_LEN 64
    +#define TEST_XMSS_OID_LEN 4
    +#define TEST_INVALID_SIG_PK_LEN (TEST_INVALID_SIG_XMSS_PK_RAW_LEN + TEST_XMSS_OID_LEN)
    +/* Deliberately short signature buffer to exercise verify bounds (real XMSS sigs are kilobytes). */
    +#define TEST_INVALID_SIG_MALICIOUS_SIG_LEN 10
    +/* XMSS-SHA2_10_256 OID; stored so low byte is at pk[TEST_XMSS_OID_LEN - 1] (see xmss.c). */
    +#define TEST_XMSS_OID_SHA2_10_256 0x01U
    +#endif
    +
    +/*
    + * This function is used to test the invalid signature verification.
    + * @param method_name: The name of the signature algorithm to test.
    + * @return OQS_SUCCESS if the invalid signature verification is fails, OQS_ERROR otherwise.
    + */
    +static OQS_STATUS test_invalid_sig(const char *method_name) {
    +	// Use proper API to create sig object
    +	if (method_name == NULL) {
    +		return OQS_ERROR;
    +	}
    +#ifndef OQS_ENABLE_SIG_STFL_XMSS
    +	(void)method_name;
    +	return OQS_SUCCESS;
    +#else
    +	OQS_SIG_STFL *sig = OQS_SIG_STFL_new(method_name);
    +	if (sig == NULL) {
    +		return OQS_ERROR;
    +	}
    +
    +	uint8_t pk[TEST_INVALID_SIG_PK_LEN] = {0};
    +	pk[TEST_XMSS_OID_LEN - 1] = (uint8_t)TEST_XMSS_OID_SHA2_10_256;
    +
    +	uint8_t message[] = "test";
    +	uint8_t malicious_sig[TEST_INVALID_SIG_MALICIOUS_SIG_LEN] = {0};
    +
    +	// This triggers the bug via proper API
    +	OQS_STATUS status = OQS_SIG_STFL_verify(sig, message, sizeof(message) - 1, malicious_sig, TEST_INVALID_SIG_MALICIOUS_SIG_LEN, pk);
    +	OQS_SIG_STFL_free(sig);
    +	if (status == OQS_SUCCESS) {
    +		return OQS_ERROR;
    +	}
    +	return OQS_SUCCESS;
    +#endif
    +}
    +
     static OQS_STATUS sig_stfl_test_correctness(const char *method_name, const char *katfile, bool bitflips_all[2], size_t bitflips[2]) {
     
     	OQS_SIG_STFL *sig = NULL;
    @@ -468,7 +514,7 @@ static OQS_STATUS sig_stfl_test_correctness(const char *method_name, const char
     	context = strdup(((file_store)));
     	OQS_SIG_STFL_SECRET_KEY_SET_store_cb(secret_key, save_secret_key, (void *)context);
     
    -#if OQS_USE_PTHREADS_IN_TESTS
    +#if OQS_USE_PTHREADS
     	OQS_SIG_STFL_SECRET_KEY_SET_mutex(secret_key, sk_lock);
     #endif
     	public_key = OQS_MEM_malloc(sig->length_public_key + 2 * sizeof(magic_t));
    @@ -745,7 +791,7 @@ static void TEST_SIG_STFL_randombytes(uint8_t *random_array, size_t bytes_to_rea
     }
     #endif
     
    -#if OQS_USE_PTHREADS_IN_TESTS
    +#if OQS_USE_PTHREADS
     static OQS_STATUS sig_stfl_test_query_key(const char *method_name) {
     	OQS_STATUS rc = OQS_SUCCESS;
     	size_t message_len_1 = sizeof(message_1);
    @@ -976,14 +1022,13 @@ static OQS_STATUS sig_stfl_test_secret_key_lock(const char *method_name, const c
     	return OQS_ERROR;
     }
     
    -
     typedef struct thread_data {
     	const char *alg_name;
     	const char *katfile;
     	bool *bitflips_all;
     	size_t *bitflips;
     	OQS_STATUS rc;
    -	// OQS_STATUS rc1;
    +	OQS_STATUS rc2;
     } thread_data_t;
     
     typedef struct lock_test_data {
    @@ -1022,6 +1067,9 @@ void *test_create_keys(void *arg) {
     void *test_correctness_wrapper(void *arg) {
     	struct thread_data *td = arg;
     	td->rc = sig_stfl_test_correctness(td->alg_name, td->katfile, td->bitflips_all, td->bitflips);
    +	if (strstr(td->alg_name, "XMSS") != NULL) {
    +		td->rc2 = test_invalid_sig(td->alg_name);
    +	}
     	OQS_thread_stop();
     	return NULL;
     }
    @@ -1063,7 +1111,7 @@ static OQS_STATUS update_test_result( OQS_STATUS rc, int xmss_or_lms) {
     }
     
     int main(int argc, char **argv) {
    -	OQS_STATUS  rc = OQS_ERROR, rc1 = OQS_ERROR;
    +	OQS_STATUS  rc = OQS_ERROR, rc1 = OQS_ERROR, rc2 = OQS_SUCCESS;
     	OQS_init();
     	rc = oqs_fstore_init();
     	if (rc != OQS_SUCCESS) {
    @@ -1156,7 +1204,7 @@ int main(int argc, char **argv) {
     
     	int exit_status = EXIT_SUCCESS;
     
    -#if OQS_USE_PTHREADS_IN_TESTS
    +#if OQS_USE_PTHREADS
     #define MAX_LEN_SIG_NAME_ 64
     	OQS_STATUS rc_create = OQS_ERROR, rc_sign = OQS_ERROR, rc_query = OQS_ERROR;
     
    @@ -1165,27 +1213,40 @@ int main(int argc, char **argv) {
     	pthread_t sign_key_thread;
     	pthread_t query_key_thread;
     
    -	thread_data_t td = {.alg_name = alg_name, .katfile = katfile, .bitflips_all = bitflips_all, .bitflips = bitflips, .rc = OQS_ERROR};
    -	thread_data_t td_2 = {.alg_name = alg_name, .katfile = katfile, .bitflips_all = bitflips_all, .bitflips = bitflips, .rc = OQS_ERROR};
    +	thread_data_t td = {.alg_name = alg_name, .katfile = katfile, .bitflips_all = bitflips_all, .bitflips = bitflips, .rc = OQS_ERROR, .rc2 = OQS_SUCCESS};
    +	thread_data_t td_2 = {.alg_name = alg_name, .katfile = katfile, .bitflips_all = bitflips_all, .bitflips = bitflips, .rc = OQS_ERROR, .rc2 = OQS_SUCCESS};
     
     	lock_test_data_t td_create = {.alg_name = alg_name, .katfile = katfile, .rc = OQS_ERROR};
     	lock_test_data_t td_sign = {.alg_name = alg_name, .katfile = katfile, .rc = OQS_ERROR};
     	lock_test_data_t td_query = {.alg_name = alg_name, .katfile = katfile, .rc = OQS_ERROR};
     
    -	test_sk_lock = (pthread_mutex_t *)OQS_MEM_malloc(sizeof(pthread_mutex_t));
    -	if (test_sk_lock == NULL) {
    +	pthread_mutex_t *test_sk_lock_local = (pthread_mutex_t *)OQS_MEM_malloc(sizeof(pthread_mutex_t));
    +	if (test_sk_lock_local == NULL) {
     		goto err;
     	}
    -	sk_lock = (pthread_mutex_t *)OQS_MEM_malloc(sizeof(pthread_mutex_t));
    -	if (sk_lock == NULL) {
    +	pthread_mutex_t *sk_lock_local = (pthread_mutex_t *)OQS_MEM_malloc(sizeof(pthread_mutex_t));
    +	if (sk_lock_local == NULL) {
    +		OQS_MEM_insecure_free(test_sk_lock_local);
     		goto err;
     	}
     
    -	if (pthread_mutex_init(test_sk_lock, NULL) || pthread_mutex_init(sk_lock, NULL)) {
    +	if (pthread_mutex_init(test_sk_lock_local, NULL)) {
     		fprintf(stderr, "ERROR: Initializing mutex\n");
    +		OQS_MEM_insecure_free(test_sk_lock_local);
    +		OQS_MEM_insecure_free(sk_lock_local);
     		exit_status = EXIT_FAILURE;
     		goto err;
     	}
    +	if (pthread_mutex_init(sk_lock_local, NULL)) {
    +		fprintf(stderr, "ERROR: Initializing mutex\n");
    +		pthread_mutex_destroy(test_sk_lock_local);
    +		OQS_MEM_insecure_free(test_sk_lock_local);
    +		OQS_MEM_insecure_free(sk_lock_local);
    +		exit_status = EXIT_FAILURE;
    +		goto err;
    +	}
    +	test_sk_lock = test_sk_lock_local;
    +	sk_lock = sk_lock_local;
     
     	if (pthread_create(&thread, NULL, test_correctness_wrapper, &td)) {
     		fprintf(stderr, "ERROR: Creating pthread for test_wrapper\n");
    @@ -1195,6 +1256,7 @@ int main(int argc, char **argv) {
     	pthread_join(thread, NULL);
     	rc = td.rc;
     	rc = update_test_result(rc, is_xmss);
    +	rc2 = td.rc2;
     
     	if (pthread_create(&thread, NULL, test_secret_key_wrapper, &td_2)) {
     		fprintf(stderr, "ERROR: Creating pthread for test_wrapper_2\n");
    @@ -1205,6 +1267,13 @@ int main(int argc, char **argv) {
     	rc1 = td_2.rc;
     	rc1 = update_test_result(rc1, is_xmss);
     
    +	/* The three-thread mutex tests below exercise the lock/unlock paths around
    +	 * real sign operations. When key/sig gen is compiled out, those paths
    +	 * aren't reachable in a meaningful way (and whether setup succeeds or
    +	 * fails depends on whether the alg has a KAT mapping, not on anything we
    +	 * want to assert), so skip them in that case. */
    +#if (defined(OQS_ALLOW_XMSS_KEY_AND_SIG_GEN) || !defined(OQS_ENABLE_SIG_STFL_XMSS)) && \
    +    (defined(OQS_ALLOW_LMS_KEY_AND_SIG_GEN) || !defined(OQS_ENABLE_SIG_STFL_LMS))
     	if (pthread_create(&create_key_thread, NULL, test_create_keys, &td_create)) {
     		fprintf(stderr, "ERROR: Creating pthread for test_create_keys\n");
     		exit_status = EXIT_FAILURE;
    @@ -1231,6 +1300,15 @@ int main(int argc, char **argv) {
     	pthread_join(query_key_thread, NULL);
     	rc_query = td_query.rc;
     	rc_query = update_test_result(rc_query, is_xmss);
    +#else
    +	rc_create = rc_sign = rc_query = OQS_SUCCESS;
    +	(void)td_create;
    +	(void)td_sign;
    +	(void)td_query;
    +	(void)create_key_thread;
    +	(void)sign_key_thread;
    +	(void)query_key_thread;
    +#endif
     
     err:
     	if (test_sk_lock) {
    @@ -1248,11 +1326,11 @@ int main(int argc, char **argv) {
     	OQS_MEM_insecure_free(signature_2);
     
     	OQS_destroy();
    -	if (rc != OQS_SUCCESS || rc1 != OQS_SUCCESS) {
    +	if (rc != OQS_SUCCESS || rc1 != OQS_SUCCESS || rc2 != OQS_SUCCESS) {
     		return EXIT_FAILURE;
     	}
     
    -#if OQS_USE_PTHREADS_IN_TESTS
    +#if OQS_USE_PTHREADS
     	if (rc_create != OQS_SUCCESS || rc_sign != OQS_SUCCESS || rc_query != OQS_SUCCESS) {
     		return EXIT_FAILURE;
     	}
    @@ -1261,13 +1339,15 @@ int main(int argc, char **argv) {
     #else
     	rc = sig_stfl_test_correctness(alg_name, katfile, bitflips_all, bitflips);
     	rc1 = sig_stfl_test_secret_key(alg_name, katfile);
    +	if (is_xmss) {
    +		rc2 = test_invalid_sig(alg_name);
    +	}
     
     	OQS_destroy();
     	rc = update_test_result(rc, is_xmss);
     	rc1 = update_test_result(rc1, is_xmss);
     
    -
    -	if (rc != OQS_SUCCESS || rc1 != OQS_SUCCESS) {
    +	if (rc != OQS_SUCCESS || rc1 != OQS_SUCCESS || rc2 != OQS_SUCCESS) {
     		return EXIT_FAILURE;
     	}
     	return exit_status;
    

Vulnerability mechanics

Root cause

"Missing length validation in XMSS signature verification allows out-of-bounds read when caller-supplied signature buffer is shorter than expected."

Attack vector

An attacker can call the verification function with a signature buffer that is shorter than the expected signature size for the given XMSS parameter set. Because the implementation does not validate the caller-supplied length, it proceeds to read past the end of the buffer. The out-of-bounds bytes are consumed only as input to an internal hash computation and are not returned to the caller, so no oracle exists to leak their contents. The primary observable effect is a possible crash (denial of service) of the verifying process if the read crosses into an unmapped memory page [ref_id=2].

Affected code

The vulnerability resides in the XMSS and XMSS^MT stateful signature verification code. The internal function `xmssmt_core_sign_open` in `src/sig_stfl/xmss/external/xmss_commons.c` discards the caller-supplied signature length parameter (`(void) smlen;`) and instead indexes the signature buffer using the parameter-set-defined `params->sig_bytes`. The public verification API (`OQS_SIG_STFL_verify` and per-variant wrappers) passes the caller-supplied length down without validating it against `params->sig_bytes` at any layer [ref_id=2].

What the fix does

The patch adds explicit length validation at the verification entry points so that signatures shorter than the expected size are rejected before any memory access. It also introduces a new test function `test_invalid_sig` that passes a deliberately short (10-byte) signature buffer to `OQS_SIG_STFL_verify` and asserts that verification fails, confirming the fix prevents the out-of-bounds read [patch_id=3105913].

Preconditions

  • inputThe attacker must be able to invoke the XMSS or XMSS^MT signature verification API with a caller-supplied signature buffer.
  • inputThe signature buffer length must be smaller than the expected signature size for the given parameter set.
  • networkNo authentication is required; the vulnerability is reachable over the network if the verification API is exposed.

Generated on May 29, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.