Crash report
What happened?
_blake2.blake2b/_blake2.blake2s carry an impl enum that py_blake2_clear (the tp_clear/dealloc path) switches on, with a Py_UNREACHABLE() default. The .copy() method allocates a new object whose impl is left uninitialized, then performs the (fallible) HACL* state allocation; if that allocation fails, the error path deallocates the half-built object without ever setting impl, so py_blake2_clear reads garbage and hits Py_UNREACHABLE():
Fatal Python error: py_blake2_clear: We've reached an unreachable state. ...
On a debug build this is a deterministic fatal error; on a release build Py_UNREACHABLE() is __builtin_unreachable(), so a garbage impl is undefined behaviour (I observed a SIGSEGV on a free-threaded release build; and if the garbage value happens to match a valid enum case, py_blake2_clear calls Hacl_Hash_*_free() on an uninitialized state pointer — a wild free).
Reproducer
The failing allocation is HACL*'s raw malloc() for the copy's hash state, which _testcapi.set_nomemory (a PyMem hook) cannot reach — so the failure is injected at the C malloc layer with a tiny LD_PRELOAD shim. Needs a non-ASan build (ASan owns malloc, bypassing the shim).
/* oomshim.c: cc -shared -fPIC -O2 -o oomshim.so oomshim.c -ldl
* arm_next_malloc() makes the very next malloc() return NULL. */
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <stddef.h>
#include <string.h>
static void *(*real_malloc)(size_t); static void *(*real_calloc)(size_t,size_t);
static void (*real_free)(void*); static int armed = 0;
static char boot[16384]; static size_t boot_off = 0; static int initing = 0;
static int is_boot(const void*p){return (const char*)p>=boot&&(const char*)p<boot+sizeof boot;}
static void init_reals(void){ initing=1; real_malloc=dlsym(RTLD_NEXT,"malloc");
real_calloc=dlsym(RTLD_NEXT,"calloc"); real_free=dlsym(RTLD_NEXT,"free"); initing=0; }
void arm_next_malloc(void){ armed = 1; }
void *malloc(size_t n){ if(!real_malloc){ if(initing){void*p=boot+boot_off;boot_off+=(n+15)&~(size_t)15;return p;} init_reals(); }
if(armed){ armed=0; errno=ENOMEM; return NULL; } return real_malloc(n); }
void *calloc(size_t a,size_t b){ if(!real_calloc){ if(initing){void*p=boot+boot_off;boot_off+=((a*b)+15)&~(size_t)15;memset(p,0,a*b);return p;} init_reals(); }
return real_calloc(a,b); }
void free(void*p){ if(is_boot(p))return; if(!real_free)init_reals(); if(real_free)real_free(p); }
# repro.py — LD_PRELOAD=./oomshim.so ./python repro.py
import ctypes, faulthandler
faulthandler.enable()
import _blake2
lib = ctypes.CDLL(None)
h = _blake2.blake2s(b"data") # (blake2b behaves identically)
lib.arm_next_malloc() # next malloc() returns NULL
h.copy() # -> Fatal Python error: py_blake2_clear ...
Traceback (debug build, gdb)
#8 py_blake2_clear Modules/blake2module.c:997 # default: Py_UNREACHABLE() — reads uninitialized self->impl
#9 py_blake2_dealloc Modules/blake2module.c:1008
#10 _Py_Dealloc Objects/object.c:3319
#11 Py_DECREF Include/refcount.h:359
#12 _blake2_blake2b_copy_impl Modules/blake2module.c:812 # Py_DECREF(cpy) after the copy failed
Root cause
new_Blake2Object (blake2module.c:387) uses PyObject_GC_New (memory not zeroed) and immediately PyObject_GC_Tracks the object, leaving impl and the state pointers uninitialized; each caller is responsible for initializing them. py_blake2_new does so immediately, before any fallible allocation. But blake2_blake2b_copy_unlocked (blake2module.c:749) does the fallible HACL allocation first and only sets cpy->impl = self->impl on success (line 781); its error: path returns -1 with impl still uninitialized, and _blake2_blake2b_copy_impl then Py_DECREFs that object (line 812) → py_blake2_clear reads the garbage discriminant.
Suggested fix
Initialize the discriminant (and NULL the state pointer) before the fallible allocation, mirroring py_blake2_new — e.g. at the top of blake2_blake2b_copy_unlocked:
cpy->impl = self->impl;
/* leave the state pointer NULL until the copy succeeds, so py_blake2_clear is a no-op on error */
(Alternatively, have new_Blake2Object zero-initialize the struct so no caller can leave impl uninitialized.)
Environment
Found via allocation-failure fuzzing with fusil, created by @vstinner.
The investigation was conducted and this report was drafted with the help of Claude Code (Opus 4.8).
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.16.0a0 (heads/main:1b9fe5c7226, Jun 20 2026, 23:56:12) [Clang 21.1.8 (6ubuntu1)]
Crash report
What happened?
_blake2.blake2b/_blake2.blake2scarry animplenum thatpy_blake2_clear(thetp_clear/dealloc path) switches on, with aPy_UNREACHABLE()default. The.copy()method allocates a new object whoseimplis left uninitialized, then performs the (fallible) HACL* state allocation; if that allocation fails, the error path deallocates the half-built object without ever settingimpl, sopy_blake2_clearreads garbage and hitsPy_UNREACHABLE():On a debug build this is a deterministic fatal error; on a release build
Py_UNREACHABLE()is__builtin_unreachable(), so a garbageimplis undefined behaviour (I observed a SIGSEGV on a free-threaded release build; and if the garbage value happens to match a valid enum case,py_blake2_clearcallsHacl_Hash_*_free()on an uninitialized state pointer — a wild free).Reproducer
The failing allocation is HACL*'s raw
malloc()for the copy's hash state, which_testcapi.set_nomemory(a PyMem hook) cannot reach — so the failure is injected at the Cmalloclayer with a tinyLD_PRELOADshim. Needs a non-ASan build (ASan ownsmalloc, bypassing the shim).Traceback (debug build, gdb)
Root cause
new_Blake2Object(blake2module.c:387) usesPyObject_GC_New(memory not zeroed) and immediatelyPyObject_GC_Tracks the object, leavingimpland the state pointers uninitialized; each caller is responsible for initializing them.py_blake2_newdoes so immediately, before any fallible allocation. Butblake2_blake2b_copy_unlocked(blake2module.c:749) does the fallible HACL allocation first and only setscpy->impl = self->implon success (line 781); itserror:path returns-1withimplstill uninitialized, and_blake2_blake2b_copy_implthenPy_DECREFs that object (line 812) →py_blake2_clearreads the garbage discriminant.Suggested fix
Initialize the discriminant (and NULL the state pointer) before the fallible allocation, mirroring
py_blake2_new— e.g. at the top ofblake2_blake2b_copy_unlocked:(Alternatively, have
new_Blake2Objectzero-initialize the struct so no caller can leaveimpluninitialized.)Environment
main(3.16.0a0), commit1b9fe5c; reproduced on debug free-threaded and GIL builds (deterministic fatal) and a release free-threaded build (SIGSEGV). Linux x86-64.325e9b8); most recently reworked in Rewrite & cleanup HACL*-based extension modules #135532 (gh-135532: usedefining_classfor copying BLAKE-2 and SHA-3 objects #135838).Found via allocation-failure fuzzing with fusil, created by @vstinner.
The investigation was conducted and this report was drafted with the help of Claude Code (Opus 4.8).
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.16.0a0 (heads/main:1b9fe5c7226, Jun 20 2026, 23:56:12) [Clang 21.1.8 (6ubuntu1)]