CVE-2026-41676
Description
rust-openssl provides OpenSSL bindings for the Rust programming language. From 0.9.27 to before 0.10.78, Deriver::derive (and PkeyCtxRef::derive) sets len = buf.len() and passes it as the in/out length to EVP_PKEY_derive, relying on OpenSSL to honor it. On OpenSSL 1.1.x, X25519, X448, DH and HKDF-extract ignore the incoming *keylen, unconditionally writing the full shared secret (32/56/prime-size bytes). A caller passing a short slice gets a heap/stack overflow from safe code. OpenSSL 3.x providers do check, so this only impacts older OpenSSL. This vulnerability is fixed in 0.10.78.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
opensslcrates.io | >= 0.9.27, < 0.10.78 | 0.10.78 |
Affected products
1- cpe:2.3:a:rust-openssl_project:rust-openssl:*:*:*:*:*:rust:*:*Range: >=0.9.27,<0.10.78
Patches
109b425e5f59aCheck derive output buffer length on OpenSSL 1.1.x (#2606)
2 files changed · +114 −1
openssl/src/derive.rs+40 −0 modified@@ -131,6 +131,26 @@ impl<'a> Deriver<'a> { /// Returns the number of bytes written. #[corresponds(EVP_PKEY_derive)] pub fn derive(&mut self, buf: &mut [u8]) -> Result<usize, ErrorStack> { + // See the matching comment in `PkeyCtxRef::derive`. On 1.1.x some + // pmeths ignore *keylen and write the full natural output + // (X25519/X448), while others (default ECDH) deliberately truncate. + // Derive into a temp buffer when the probed size exceeds the + // caller's buffer to prevent OOB writes while preserving the + // truncation semantics. + #[cfg(any(all(ossl110, not(ossl300)), libressl))] + { + let required = self.len()?; + if required != usize::MAX && buf.len() < required { + let mut temp = vec![0u8; required]; + let mut len = required; + unsafe { + cvt(ffi::EVP_PKEY_derive(self.0, temp.as_mut_ptr(), &mut len))?; + } + let copy_len = buf.len().min(len); + buf[..copy_len].copy_from_slice(&temp[..copy_len]); + return Ok(copy_len); + } + } let mut len = buf.len(); unsafe { cvt(ffi::EVP_PKEY_derive( @@ -195,6 +215,26 @@ mod test { assert!(!shared.is_empty()); } + #[test] + #[cfg(any(ossl111, libressl370))] + fn derive_undersized_buffer() { + // Without the temp-buffer fallback in this crate, X25519 on 1.1.x + // would OOB into a 4-byte buffer because it ignores *keylen. + // On 1.1.x / LibreSSL the fallback kicks in and we return the + // truncated prefix. On 3.0+ the provider rejects undersized + // buffers before any write happens, so the call errors out. + let pkey = PKey::generate_x25519().unwrap(); + let pkey2 = PKey::generate_x25519().unwrap(); + let mut deriver = Deriver::new(&pkey).unwrap(); + deriver.set_peer(&pkey2).unwrap(); + let mut buf = [0u8; 4]; + let result = deriver.derive(&mut buf); + #[cfg(any(all(ossl110, not(ossl300)), libressl))] + assert_eq!(result.unwrap(), 4); + #[cfg(all(ossl300, not(libressl)))] + assert!(result.is_err()); + } + #[test] #[cfg(ossl300)] fn test_ec_key_derive_ex() {
openssl/src/pkey_ctx.rs+74 −1 modified@@ -820,7 +820,57 @@ impl<T> PkeyCtxRef<T> { /// /// If `buf` is set to `None`, an upper bound on the number of bytes required for the buffer will be returned. #[corresponds(EVP_PKEY_derive)] - pub fn derive(&mut self, buf: Option<&mut [u8]>) -> Result<usize, ErrorStack> { + #[allow(unused_mut)] + pub fn derive(&mut self, mut buf: Option<&mut [u8]>) -> Result<usize, ErrorStack> { + // On OpenSSL 1.1.x some pmeths ignore *keylen and unconditionally + // write the full natural output size (X25519, X448, HKDF-extract), + // which can overflow a shorter caller-provided buffer. Others honor + // *keylen by truncating the output (notably the default ECDH + // EVP_PKEY_EC pmeth, where the OpenSSL source explicitly documents + // that *keylen below the natural size "is not an error, the result + // is truncated"). + // + // We can't distinguish those two groups from the probe alone, so + // when the probe reports a natural size larger than the caller's + // buffer, derive into a temporary buffer of the probed size and + // copy the leading bytes out. This prevents the OOB write for the + // ignore-*keylen group and produces the same bytes for the + // honor-*keylen group (ECDH_compute_key copies leading bytes of + // the shared secret either way). + // + // Some pmeths (HKDF extract-and-expand and expand-only on 1.1.x) + // don't support the NULL-out probe and fail it with an empty error + // stack; those honor *keylen during derivation, so clear the + // errors and proceed with the direct path. usize::MAX is a + // sentinel some pmeths use when *keylen is caller-chosen. + // + // 3.0+ providers check the buffer size themselves, so this whole + // dance is cfg-gated to 1.1.x and LibreSSL. + #[cfg(any(all(ossl110, not(ossl300)), libressl))] + { + if let Some(b) = buf.as_deref_mut() { + let mut required = 0; + let probe_ok = unsafe { + ffi::EVP_PKEY_derive(self.as_ptr(), ptr::null_mut(), &mut required) == 1 + }; + if !probe_ok { + let _ = ErrorStack::get(); + } else if required != usize::MAX && b.len() < required { + let mut temp = vec![0u8; required]; + let mut len = required; + unsafe { + cvt(ffi::EVP_PKEY_derive( + self.as_ptr(), + temp.as_mut_ptr(), + &mut len, + ))?; + } + let copy_len = b.len().min(len); + b[..copy_len].copy_from_slice(&temp[..copy_len]); + return Ok(copy_len); + } + } + } let mut len = buf.as_ref().map_or(0, |b| b.len()); unsafe { cvt(ffi::EVP_PKEY_derive( @@ -1042,6 +1092,29 @@ mod test { ctx.derive_to_vec(&mut buf).unwrap(); } + #[test] + #[cfg(any(ossl111, libressl370))] + fn derive_undersized_buffer() { + // Without the temp-buffer fallback in this crate, X25519 on 1.1.x + // would OOB into a 4-byte buffer because it ignores *keylen. + // On 1.1.x / LibreSSL the fallback kicks in and we return the + // truncated prefix. On 3.0+ the provider rejects undersized + // buffers before any write happens, so the call errors out. + let key1 = PKey::generate_x25519().unwrap(); + let key2 = PKey::generate_x25519().unwrap(); + + let mut ctx = PkeyCtx::new(&key1).unwrap(); + ctx.derive_init().unwrap(); + ctx.derive_set_peer(&key2).unwrap(); + + let mut buf = [0u8; 4]; + let result = ctx.derive(Some(&mut buf)); + #[cfg(any(all(ossl110, not(ossl300)), libressl))] + assert_eq!(result.unwrap(), 4); + #[cfg(all(ossl300, not(libressl)))] + assert!(result.is_err()); + } + #[test] #[cfg(not(any(boringssl, awslc)))] fn cmac_keygen() {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-pqf5-4pqq-29f5ghsaADVISORY
- github.com/rust-openssl/rust-openssl/security/advisories/GHSA-pqf5-4pqq-29f5nvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-41676ghsaADVISORY
- github.com/rust-openssl/rust-openssl/commit/09b425e5f59a2466d806e71a83a9a449c914c596ghsaWEB
- github.com/rust-openssl/rust-openssl/pull/2606ghsaWEB
- github.com/rust-openssl/rust-openssl/releases/tag/openssl-v0.10.78ghsaWEB
News mentions
0No linked articles in our index yet.