Repository navigation
_hashopenssl.c: Missing negative return check in EVP_get_block_size and EVP_get_digest_size #146287
Copy link
Copy link
Closed
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirpendingThe issue will be closed if no feedback is providedThe issue will be closed if no feedback is providedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Description
Bug report
Bug description:
EVP_get_block_size and EVP_get_digest_size pass the return value of
EVP_MD_CTX_block_size() / EVP_MD_CTX_size() directly to PyLong_FromLong()
without checking for a negative error return. When OpenSSL signals an error by
returning -1, Python silently exposes that value to the caller instead of
raising a ValueError.
Vulnerable Code
// Modules/_hashopenssl.c
static PyObject *
EVP_get_block_size(EVPobject *self, void *closure)
{
long block_size;
block_size = EVP_MD_CTX_block_size(self->ctx);
return PyLong_FromLong(block_size); // no check for negative value
}
static PyObject *
EVP_get_digest_size(EVPobject *self, void *closure)
{
long size;
size = EVP_MD_CTX_size(self->ctx);
return PyLong_FromLong(size); // no check for negative value
}Root Cause
In OpenSSL 3.x, both macros are defined in <openssl/evp.h> as:
#define EVP_MD_CTX_get_size(e) EVP_MD_get_size(EVP_MD_CTX_get0_md(e))
#define EVP_MD_CTX_size EVP_MD_CTX_get_size
#define EVP_MD_CTX_get_block_size(e) EVP_MD_get_block_size(EVP_MD_CTX_get0_md(e))
#define EVP_MD_CTX_block_size EVP_MD_CTX_get_block_sizeBoth expand to EVP_MD_get_size(EVP_MD_CTX_get0_md(ctx)).
When the context has no digest set, EVP_MD_CTX_get0_md() returns NULL,
and EVP_MD_get_size(NULL) returns -1 an error signal that Python never checks.
Proof of Concept
import ctypes, ctypes.util, ssl
print(f"OpenSSL: {ssl.OPENSSL_VERSION}\n")
lib = ctypes.CDLL(ctypes.util.find_library('crypto'))
lib.EVP_MD_CTX_new.restype = ctypes.c_void_p
lib.EVP_MD_CTX_free.restype = None
lib.EVP_MD_CTX_free.argtypes = [ctypes.c_void_p]
lib.EVP_MD_CTX_get0_md.restype = ctypes.c_void_p
lib.EVP_MD_CTX_get0_md.argtypes = [ctypes.c_void_p]
lib.EVP_MD_get_size.restype = ctypes.c_int
lib.EVP_MD_get_size.argtypes = [ctypes.c_void_p]
lib.EVP_MD_get_block_size.restype = ctypes.c_int
lib.EVP_MD_get_block_size.argtypes = [ctypes.c_void_p]
# context without EVP_DigestInit_ex => get0_md() returns NULL
ctx = lib.EVP_MD_CTX_new()
md = lib.EVP_MD_CTX_get0_md(ctx)
print(f"EVP_MD_CTX_get0_md() = {md or 'NULL'}")
print(f"EVP_MD_get_size(NULL) = {lib.EVP_MD_get_size(md)}")
print(f"EVP_MD_get_block_size() = {lib.EVP_MD_get_block_size(md)}\n")
digest_size = lib.EVP_MD_get_size(md)
block_size = lib.EVP_MD_get_block_size(md)
print(f"hash.digest_size => {digest_size} (no ValueError raised)")
print(f"hash.block_size => {block_size} (no ValueError raised)")
lib.EVP_MD_CTX_free(ctx)Output:
OpenSSL: OpenSSL 3.3.1 4 Jun 2024
EVP_MD_CTX_get0_md() = NULL
EVP_MD_get_size(NULL) = -1
EVP_MD_get_block_size() = -1
hash.digest_size => -1 (no ValueError raised)
hash.block_size => -1 (no ValueError raised)
Suggested Fix
static PyObject *
EVP_get_block_size(EVPobject *self, void *closure)
{
long block_size = EVP_MD_CTX_block_size(self->ctx);
if (block_size <= 0) {
PyErr_SetString(PyExc_ValueError, "block size unavailable");
return NULL;
}
return PyLong_FromLong(block_size);
}
static PyObject *
EVP_get_digest_size(EVPobject *self, void *closure)
{
long size = EVP_MD_CTX_size(self->ctx);
if (size <= 0) {
PyErr_SetString(PyExc_ValueError, "digest size unavailable");
return NULL;
}
return PyLong_FromLong(size);
}CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Linked PRs
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirpendingThe issue will be closed if no feedback is providedThe issue will be closed if no feedback is providedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Activity
picnixz commentedon Mar 22, 2026
It should never happen in practice. Did you hit this without messing up with ctypes?
picnixz commentedon Mar 22, 2026
Note: I was aware of this but since ctx should always exist when hitting those paths thia should never happen. However we can add an assert.
raminfp commentedon Mar 22, 2026
The
assertapproach is insufficient: CPython release builds compile with-DNDEBUG, so the assert is removed entirely:In
_hashlib_hmac_digest_size, with-DNDEBUG:Also, OpenSSL 3.x documents
EVP_MD_get_size()returning-2explicitly:A
if (size <= 0)check handles both cases in debug and release.picnixz commentedon Mar 22, 2026
Again I don't think it is necessary. I know that an assert will only be present in debug builds but when does it matter in production?
picnixz commentedon Mar 22, 2026
If the size is not available we have another problem. Having a null context should never happen. Messing up with ctypes is not a valid reproducer in general.
picnixz commentedon Mar 22, 2026
I would rather prefer raising something else than a ValueError here.
picnixz commentedon Mar 22, 2026
As for _hashlib_hmac_digest_size, changing the unsigned long to long is howeber right but again I do not think it should be necessary because we should never get a -1.
raminfp commentedon Mar 22, 2026
Thanks for the clarification. I agree the
EVP_get_block_size/EVP_get_digest_sizecases cannot be triggered through the public Python API.picnixz commentedon Mar 22, 2026
I am not against an explicit assert() for debug builds or a comment and if we want a real check, it should raise a SystemError and not a ValueError.
OTOH, I am ok with changing rhe unsigned long value to long one and add the assert as well. However we need to keep the compatibility with OpenSSL 1.1.1 where the size fields are directly accessed and so there should be no checks behind the scenes IIRC.
raminfp commentedon Mar 23, 2026
Thanks for the clarification. Based on your feedback, I'd like to submit a focused PR with the following minimal changes to
_hashlib_hmac_digest_size:unsigned longtolongBAD_DIGEST_SIZEsentinel from0to-1and adjust callers accordinglyassert(digest_size > 0)in the legacy (OpenSSL 1.1.1) path for debug buildsNo runtime checks in the legacy path, and no changes to
EVP_get_block_size/EVP_get_digest_size. If a runtime guard is ever needed, I'll useSystemErroras you suggested.Does this scope look good to you?
pythongh-146287: use signed type for HMAC digest size to prevent unsi…
gpshead commentedon Apr 11, 2026
I put up a PR going with a variant of that plan. it raises a SystemError and uses signed types internally to detect the issue.
gh-146287: Fix signed/unsigned mismatch in _hashlib_hmac_digest_size (G…
pythongh-146287: Fix signed/unsigned mismatch in _hashlib_hmac_digest…