Repository navigation
Crash in HAMT during garbage collection #148891
Description
Crash report
What happened?
We discovered a crash in the https://github.com/datadog/dd-trace-py library we maintain that actually seems to be rooted in CPython, not our own code.
Error UnixSignal: Process terminated with SEGV_MAPERR (SIGSEGV)
#0 0x00007fc92c3dd27f PyObject_GC_UnTrack
#1 0x00007fc92c4e1448 PyContextVar_Set
#2 0x00007fc92c3f3546 _PyEval_EvalFrameDefault
#3 0x00007fc92c3fbce7 PyObject_Vectorcall
#4 0x00007fc92c3f039b _PyEval_EvalFrameDefault
#5 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#6 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#7 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#8 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#9 0x00007fc92c44e25f PyIter_Send
#10 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#11 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#12 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#13 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#14 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#15 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#16 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#17 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#18 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#19 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#20 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#21 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#22 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#23 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#24 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#25 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#26 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#27 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#28 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#29 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#30 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#31 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#32 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#33 0x00007fc92c3f2f38 _PyEval_EvalFrameDefault
#34 0x00007fc8f1f854ff __Pyx_PyObject_Call (/project/uvloop/loop.c:191431:15)
#35 0x00007fc8f204f948 __pyx_f_6uvloop_4loop_6Handle__run (/project/uvloop/loop.c:66901:25)
#36 0x00007fc8f2053f7c __pyx_f_6uvloop_4loop_4Loop__on_idle (/project/uvloop/loop.c:17975:25)
#37 0x00007fc8f204f9a6 __pyx_f_6uvloop_4loop_6Handle__run (/project/uvloop/loop.c:66927:24)
#38 0x00007fc8f2051047 __pyx_f_6uvloop_4loop_cb_idle_callback (/project/uvloop/loop.c:87335:19)
#39 0x00007fc8f20687d1 uv__run_idle (/project/build/libuv-x86_64/src/unix/loop-watcher.c:68:1)
#40 0x00007fc8f2065b07 uv_run (/project/build/libuv-x86_64/src/unix/core.c:439:5)
#41 0x00007fc8f1fb83a0 __pyx_f_6uvloop_4loop_4Loop__Loop__run (/project/uvloop/loop.c:18458:23)
#42 0x00007fc8f1ff612d __pyx_f_6uvloop_4loop_4Loop__run (/project/uvloop/loop.c:18876:18)
#43 0x00007fc8f1ff1e85 __pyx_pf_6uvloop_4loop_4Loop_24run_forever (/project/uvloop/loop.c:31528:18)
#44 0x00007fc8f1ff1e85 __pyx_pw_6uvloop_4loop_4Loop_25run_forever (/project/uvloop/loop.c:31331:13)
#45 0x00007fc92c3ec26e PyObject_VectorcallMethod
#46 0x00007fc92c3f039b _PyEval_EvalFrameDefault
#47 0x00007fc92c47474c PyEval_EvalCode
#48 0x00007fc92c4a24a7 _PyRun_SimpleFileObject
#49 0x00007fc92c4a1b38 _PyRun_AnyFileObject
#50 0x00007fc92c49c3be Py_RunMain
#51 0x00007fc92c46461b Py_BytesMain
#52 0x00007fc92c067d65 __libc_start_main
#53 0x0000559068986071 _start
We fixed the crash on our side with DataDog/dd-trace-py#17407 but figured it might still be worth reporting upstream.
The crash happens in PyObject_GC_UnTrack, called from the dealloc path of a HAMT node that PyContextVar_Set is trying to free via Py_DECREF. If that HAMT node's memory is already unmapped, the PyObject_GC_UnTrack write faults.
We discovered that crash in dd-trace-py accidentally because of the following code:
- Store a fresh
{}dict in the context's HAMT underself._storage - Put the
Tokenreturned byset()back into that same dict
def __enter__(self) -> "BaseWrappingContext":
token: Token = self._storage.set({}) # 1. create new HAMT entry
self._storage.get()["__dd_wrapping_context_token__"] = token # 2. store Token in dict
return selfToken objects holds tok_ctx, which is a strong reference to the PyContext that was active when set was called -- i.e. ts->context.
I unfortunately don't have a proposed fix yet, but I'm looking into it.
CPython versions tested on:
3.12
EDIT: this is happening on 3.12 but the stack I originally reported (and most crashes I see) are coming from Python 3.10 and 3.11.
Operating systems tested on:
Linux
Activity
gvanrossum commentedon Apr 22, 2026
@KowalskiThomas, thank you for your crash report.
Can you provide a self-contained repro? That would go a long way towards helping us understand the bug -- it doesn't seem easy to repro given that HAMT is nearly a decade old and it's only just now being reported.
Also: There's a small amount of C++ and Cython in your package. Have you absolutely ruled out that the cause could be there? (A missed INCREF in user C/C++ code could cause any number of crashes that appear to be coming from inside CPython.)
Finally, can you reproduce the crash on 3.14 or (ideally) 3.15 latest alpha? It would be a shame to spend time investigating this only to find that it's already been fixed in a newer CPython release.
Alternatively, if you can come up with a fix in HAMT (that's not too convoluted), that would also go a long way towards helping us feel it's worth investigating this more.
sergey-miryanov commentedon Apr 22, 2026
It seems that we at least should add checking for this value
PyContextToken *tok = token_new(ctx, var, old_val);.token_newmay return NULL, that not checked before DECREF. And I think we shouldn't set contextvar if new token is NULL.KowalskiThomas commentedon Apr 22, 2026
Thanks for the quick replies!
I unfortunately haven't been able to make one so far, but I haven't tried very hard yet.
We store crash reports from our apps instrumented by
dd-trace-pyand that's how we know it happens, but that's about it... I'll try to though!That's a good point of course -- it's nearly impossible to completely rule it out; however if I can find a repro then I guess that question will be answered! I'll let you know here. (We run on Python 3.12 internally which is why I've reported it as 3.12.)
I was also looking into this but I don't really know it so I don't have anything (yet!)
KowalskiThomas commentedon Apr 23, 2026
Okay so after some more work (I'll admit, lots of it was LLM-assisted...), I do have a reproducer that makes it crash (segmentation fault) almost consistently with pure Python code in 3.10 and 3.11.
I double checked on the crashes we are getting, most of them are coming from Python <= 3.11. Some (more rare) are coming from 3.12 but I can't seem to reproduce them with my reproducer and wasn't able to find one that reproduces consistently.
I'll attach those crashes anyway if that can be any helpful.
Details
According to the LLM analysis, the reason this isn't crashing on 3.12+ is the following -- I have no knowledge at all on the GC so I don't know if it is true...
If I'm not mistaken, though, the latent bug is stil in the code, it's just not being triggered by the current version of the GC. I think the following branch should fix it (increase reference count before the possibly re-entrant call): main...KowalskiThomas:cpython:kowalski/fix-prevent-crash-in-hamt.
It also seems like a similar crash was fixed in the past in #142829 in December last year.
gvanrossum commentedon Apr 23, 2026
KowalskiThomas commentedon Apr 23, 2026
I don't think so. As far as I can tell, Sergey's proposed change addresses a separate concern from the change on my branch:
token_newbefore decref-ing it (since it could be NULL)varsis held during the call to_PyHamt_Assocto make sure it does not get freed.To confirm, I tried running my Python reproducer with my changes and Sergey's separately, after cherry-picking my commit on top of
v3.10.20.v3.10.20crashesv3.10.20+ my changes does not crashv3.10.20+ Sergey's changes crashesv3.10.20+ my changes + Sergey's changes crashes (expectedly)I took the liberty of opening #148928 as a draft since it does seem to make the problem go away. (I do think it would be worth adding Sergey's fix as well since it's extra safety for cheap.)
gvanrossum commentedon Apr 23, 2026
Thanks. I hope someone else can review that code, I'm not closely familiar with HAMT internals. Your approach of using INCREF/DECREF around the offending calls certainly makes sense to me!