Skip to content

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

Closed

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

Activity

  1. added
    type-crashA hard crash of the interpreter, possibly with a core dump
    on Feb 27, 2026
  2. changed the title [-]Use-After-Free + Double Free in `_hashopenssl.c` `py_hashentry_table_new()`[/-] [+]`_hashlib`: Use-After-Free + Double Free in `_hashopenssl.c` `py_hashentry_table_new()`[/+] on Feb 27, 2026
  3. sergey-miryanov commented on Feb 27, 2026

    @sergey-miryanov
    Contributor

    Would you like to make a PR?

  4. raminfp commented on Feb 27, 2026

    @raminfp
    ContributorAuthor

    Thanks for offering. If possible, I’d appreciate it if you could create the PR. Thank you!

  5. sergey-miryanov commented on Feb 27, 2026

    @sergey-miryanov
    Contributor

    Thanks! It looks like an easy task. I will add such label.

  6. added
    3.13only security fixes
    3.14bugs and security fixes
    3.15bugs and security fixes
    on Feb 27, 2026
  7. added a commit that references this issue on Mar 5, 2026
  8. added a commit that references this issue on Mar 5, 2026
  9. added a commit that references this issue on Mar 5, 2026
  10. sergey-miryanov commented on Mar 5, 2026

    @sergey-miryanov
    Contributor

    @raminfp Thanks for the report and @krylosov-aa for the fix!

  11. sergey-miryanov commented on Mar 5, 2026

    @sergey-miryanov
    Contributor

    Reopen as 3.13 not backported.

  12. added a commit that references this issue on Mar 10, 2026
  13. sergey-miryanov commented on Mar 10, 2026

    @sergey-miryanov
    Contributor

    Thanks all, now closing.

  14. 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

    3.13only security fixes3.14bugs and security fixes3.15bugs and security fixeseasyextension-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