-
-
Notifications
You must be signed in to change notification settings - Fork 34.1k
Description
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+