Skip to content

_hashlib: Use-After-Free + Double Free in _hashopenssl.c py_hashentry_table_new() #145301

@raminfp

Description

@raminfp

Crash report

What happened?

In py_hashentry_table_new(), when _Py_hashtable_set() fails for an alias key (line 270), entry is explicitly freed via PyMem_Free(entry) at line 271. However, this same entry was already successfully inserted into the hashtable under py_name at line 263. The goto error path then calls _Py_hashtable_destroy(ht) (line 280), which invokes the destroy callback py_hashentry_t_destroy_value() on the already-freed entry.

// Modules/_hashopenssl.c - py_hashentry_table_new()

for (const py_hashentry_t *h = py_hashes; h->py_name != NULL; h++) {
    py_hashentry_t *entry = (py_hashentry_t *)PyMem_Malloc(sizeof(py_hashentry_t));
    if (entry == NULL) {
        goto error;
    }
    memcpy(entry, h, sizeof(py_hashentry_t));

    // [line 263] entry inserted into hashtable under py_name - hashtable now owns it
    if (_Py_hashtable_set(ht, (const void*)entry->py_name, (void*)entry) < 0) {
        PyMem_Free(entry);
        goto error;
    }
    entry->refcnt = 1;

    if (h->py_alias != NULL) {
        // [line 270] second insert fails (e.g. OOM)
        if (_Py_hashtable_set(ht, (const void*)entry->py_alias, (void*)entry) < 0) {
            PyMem_Free(entry);   // [line 271] BUG: entry freed, but still in hashtable under py_name
            goto error;          // [line 272] jumps to error path
        }
        entry->refcnt++;
    }
}

return ht;
error:
    _Py_hashtable_destroy(ht);  // [line 280] destroy callback called on already-freed entry
    return NULL;

Build

mkdir build-asan && cd build-asan
../configure --with-pydebug --with-address-sanitizer --without-pymalloc
make -j$(nproc)
import subprocess
import sys

code = (
    "import sys, _testcapi\n"
    "if '_hashlib' in sys.modules:\n"
    "    del sys.modules['_hashlib']\n"
    "_testcapi.set_nomemory(40, 41)\n"
    "try:\n"
    "    import _hashlib\n"
    "except (MemoryError, ImportError):\n"
    "    pass\n"
    "finally:\n"
    "    _testcapi.remove_mem_hooks()\n"
)

result = subprocess.run(
    [sys.executable, '-c', code],
    capture_output=True, text=True, timeout=10
)

if result.returncode != 0:
    print(f"[*] CRASH confirmed (rc={result.returncode})")
    print(f"[*] {result.stderr.strip().split(chr(10))[-1]}")
else:
    print("[*] No crash (try different start values)")
$ ./build-asan/python uaf_asan.py
[*] CRASH confirmed (rc=-6)
[*] python: ../Include/internal/pycore_stackref.h:554: PyStackRef_FromPyObjectSteal: Assertion `obj != NULL' failed.

GDB backtrace

$ gdb -batch -ex run -ex bt --args ./build-asan/python -c "
import sys, _testcapi
if '_hashlib' in sys.modules:
    del sys.modules['_hashlib']
_testcapi.set_nomemory(40, 41)
try:
    import _hashlib
except (MemoryError, ImportError):
    pass
finally:
    _testcapi.remove_mem_hooks()
"
python: ../Include/internal/pycore_stackref.h:554: PyStackRef_FromPyObjectSteal: Assertion `obj != NULL' failed.

Program received signal SIGABRT, Aborted.

#0  __pthread_kill_implementation at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill at ./nptl/pthread_kill.c:89
#3  __GI_raise at ../sysdeps/posix/raise.c:26
#4  __GI_abort at ./stdlib/abort.c:79
#5  __assert_fail_base — Assertion `obj != NULL' failed.
#6  __assert_fail at ./assert/assert.c:105
#7  PyStackRef_FromPyObjectSteal at ../Include/internal/pycore_stackref.h:554
#8  _PyEval_EvalFrameDefault at ../Python/generated_cases.c.h:292
...
#15 import_find_and_load — importing _hashlib
...
#19 _PyEval_EvalFrameDefault at ../Python/generated_cases.c.h:6424

The assertion fires because memory corruption during _hashlib module init (caused by the UAF/double-free) propagates a NULL into the eval loop.

Suggested Fix

Remove PyMem_Free(entry) at line 271. The entry is already owned by the hashtable (under py_name with refcnt=1), so _Py_hashtable_destroy() in the error path will correctly clean it up via the destroy callback.

     if (h->py_alias != NULL) {
         if (_Py_hashtable_set(ht, (const void*)entry->py_alias, (void*)entry) < 0) {
-            PyMem_Free(entry);
             goto error;
         }
         entry->refcnt++;
     }

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.15.0a6+

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    easyextension-modulesC modules in the Modules dirtype-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions