Skip to content

_hashopenssl.c‍: Missing negative return check in EVP_get_block_size and EVP_get_digest_size #146287

Closed

Description

@raminfp

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_size

Both 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

Activity

  1. picnixz commented on Mar 22, 2026

    @picnixz
    Member

    It should never happen in practice. Did you hit this without messing up with ctypes?

  2. added
    pendingThe issue will be closed if no feedback is provided
    on Mar 22, 2026
  3. picnixz commented on Mar 22, 2026

    @picnixz
    Member

    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.

  4. raminfp commented on Mar 22, 2026

    @raminfp
    ContributorAuthor

    The assert approach is insufficient: CPython release builds compile with -DNDEBUG, so the assert is removed entirely:

    $ python3 -c "import sysconfig; print(sysconfig.get_config_var('PY_CFLAGS'))" | grep NDEBUG
    -DNDEBUG
    

    In _hashlib_hmac_digest_size, with -DNDEBUG:

    unsigned int digest_size = EVP_MD_size(md);  // -1 wraps to 4294967295
    assert(...);                                  // compiled out
    if (digest_size == 0) { ... }                // bypassed
    return digest_size;                          // 4294967295 returned

    Also, OpenSSL 3.x documents EVP_MD_get_size() returning -2 explicitly:

    "Returns -2 if the parameter is not supported" (man EVP_MD_get_size)

    A if (size <= 0) check handles both cases in debug and release.

  5. picnixz commented on Mar 22, 2026

    @picnixz
    Member

    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?

  6. picnixz commented on Mar 22, 2026

    @picnixz
    Member

    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.

  7. picnixz commented on Mar 22, 2026

    @picnixz
    Member

    I would rather prefer raising something else than a ValueError here.

  8. picnixz commented on Mar 22, 2026

    @picnixz
    Member

    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.

  9. raminfp commented on Mar 22, 2026

    @raminfp
    ContributorAuthor

    Thanks for the clarification. I agree the EVP_get_block_size / EVP_get_digest_size cases cannot be triggered through the public Python API.

  10. picnixz commented on Mar 22, 2026

    @picnixz
    Member

    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.

  11. raminfp commented on Mar 23, 2026

    @raminfp
    ContributorAuthor

    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:

    1. Change the return type from unsigned long to long
    2. Update BAD_DIGEST_SIZE sentinel from 0 to -1 and adjust callers accordingly
    3. Add assert(digest_size > 0) in the legacy (OpenSSL 1.1.1) path for debug builds

    No 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 use SystemError as you suggested.

    Does this scope look good to you?

  12. added a commit that references this issue on Apr 11, 2026
  13. gpshead commented on Apr 11, 2026

    @gpshead
    Member

    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.

  14. added a commit that references this issue on Apr 12, 2026
  15. added a commit that references this issue on Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirpendingThe issue will be closed if no feedback is providedtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions