Skip to content

gh-145301: Fix double-free in hashlib and hmac module initialization#145321

Open
krylosov-aa wants to merge 3 commits intopython:mainfrom
krylosov-aa:fix-issue-145301
Open

gh-145301: Fix double-free in hashlib and hmac module initialization#145321
krylosov-aa wants to merge 3 commits intopython:mainfrom
krylosov-aa:fix-issue-145301

Conversation

@krylosov-aa
Copy link

@krylosov-aa krylosov-aa commented Feb 27, 2026

Both hashlib and hmac modules have similar double-free bugs in their hashtable initialization code.

Problem

hashlib

In py_hashentry_table_new(), when _Py_hashtable_set() fails while adding an entry by its py_alias key (after successfully adding it by py_name), the code calls PyMem_Free(entry) before goto error. The error handler then calls _Py_hashtable_destroy() which frees the same entry again via py_hashentry_t_destroy_value().

hmac

In py_hmac_hinfo_ht_new(), the Py_HMAC_HINFO_LINK macro has the same issue. When py_hmac_hinfo_ht_add() fails for hashlib_name key (after successfully adding name), the code calls PyMem_Free(value) while the entry is already in the hashtable with refcnt=1.

Solution

hashlib

Remove the manual PyMem_Free(entry) call since the entry is already tracked by the hashtable under the py_name key and will be properly freed by _Py_hashtable_destroy().

hmac

Add a refcnt check before freeing: only call PyMem_Free(value) if value->refcnt == 0 (meaning it was never added to the table). If refcnt > 0, the entry is already in the table and will be freed by _Py_hashtable_destroy().

Tests

No test added. I couldn't find a practical way to trigger this bug since it requires _Py_hashtable_set() to fail during module initialization. This type of bug is best caught by sanitizers rather than unit tests.

The fix can be verified by running the reproducer provided by the issue author:

Build with ASan:

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

Reproducer script (uaf_asan.py):

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)")

Before fix:

$ ./build-asan/python uaf_asan.py
[*] CRASH confirmed (rc=-6)
[*] python: ../Include/internal/pycore_stackref.h:554: PyStackRef_FromPyObjectSteal: Assertion `obj != NULL' failed.

After fix: No crash.

@python-cla-bot
Copy link

python-cla-bot bot commented Feb 27, 2026

All commit authors signed the Contributor License Agreement.

CLA signed

Comment on lines 1 to 3
Fix a double-free bug in :mod:`_hashlib` module initialization when
``_Py_hashtable_set()`` fails while adding an algorithm alias to the hash
table after the primary name was already added
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too verbose:

Suggested change
Fix a double-free bug in :mod:`_hashlib` module initialization when
``_Py_hashtable_set()`` fails while adding an algorithm alias to the hash
table after the primary name was already added
:mod:`hashlib`: fix a crash when the C extension module initialization fails.

Copy link
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the test repro as a regression test.

@bedevere-app
Copy link

bedevere-app bot commented Feb 27, 2026

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@picnixz picnixz added needs backport to 3.13 bugs and security fixes needs backport to 3.14 bugs and security fixes labels Feb 27, 2026
@picnixz
Copy link
Member

picnixz commented Feb 27, 2026

Can you also check whether _hmac suffers from that? I don't remember whether I actually C/Ced this code or if I used another refcounting approach.

@krylosov-aa
Copy link
Author

Can you also check whether _hmac suffers from that? I don't remember whether I actually C/Ced this code or if I used another refcounting approach.

I checked the HMAC implementation in _hashopenssl.c. It uses the same state->hashtable that is initialized by py_hashentry_table_new(). There's no separate hashtable for HMAC. So this fix covers both hash functions and HMAC.

@picnixz
Copy link
Member

picnixz commented Feb 27, 2026

I'm sorry, I meant the hmacmodule.c

@krylosov-aa
Copy link
Author

krylosov-aa commented Feb 27, 2026

I'm sorry, I meant the hmacmodule.c

Yes, hmacmodule.c has the same issue in py_hmac_hinfo_ht_new().

I can suggest the following solution:

#define Py_HMAC_HINFO_LINK(KEY)                                 \
        do {                                                    \
            int rc = py_hmac_hinfo_ht_add(table, KEY, value);   \
            if (rc < 0) {                                       \
+                if (value->refcnt == 0) {                      \
                    PyMem_Free(value);                          \
+                }                                              \
                goto error;                                     \
            }                                                   \
            else if (rc == 1) {                                 \
                value->refcnt++;                                \
            }                                                   \
        } while (0)
        Py_HMAC_HINFO_LINK(e->name);
        Py_HMAC_HINFO_LINK(e->hashlib_name);
#undef Py_HMAC_HINFO_LINK

Should I include this fix in the same PR or create a separate one?

@picnixz
Copy link
Member

picnixz commented Feb 27, 2026

Yes, please do so, and put a comment as well.

@krylosov-aa
Copy link
Author

krylosov-aa commented Feb 27, 2026

Please add the test repro as a regression test.

I investigated adding a regression test using _testcapi.set_nomemory, but found it's not feasible for this specific bug.

The set_nomemory() approach from the issue report triggers allocation failures globally, and the exact allocation number that hits our specific code path varies by platform/configuration.

Do you have any suggestions for how to reliably test this?

@krylosov-aa krylosov-aa changed the title gh-145301: Fix double-free in _hashlib module initialization gh-145301: Fix double-free in hashlib and hmac module initialization Feb 27, 2026
int rc = py_hmac_hinfo_ht_add(table, KEY, value); \
if (rc < 0) { \
PyMem_Free(value); \
/* entry may already be in ht, will be freed by _Py_hashtable_destroy() */ \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/* entry may already be in ht, will be freed by _Py_hashtable_destroy() */ \
/* entry may already be in ht, will be \
* freed by _Py_hashtable_destroy() */ \
  • correctly align the \. You can augment the column if it does not exceed 80 chars. If it does, properly wrap the comment. Tia.

@picnixz
Copy link
Member

picnixz commented Feb 27, 2026

Do you have any suggestions for how to reliably test this?

You have an ASASN reproducer, why not use it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting changes needs backport to 3.13 bugs and security fixes needs backport to 3.14 bugs and security fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants