Repository navigation
Description
Bug report
Bug description:
Description
A decimal.Context object's flags (and traps) attribute returns a signaldict that holds a borrowed reference to the parent Context. When the parent Context is explicitly deleted, its refcount drops to zero and context_dealloc frees it. However, the signaldict object remains alive and accessible. Any subsequent operation that calls signaldict_repr — such as print(), str(), or repr() — reads through the now-freed Context pointer, causing a heap-use-after-free.
Reproducer
import decimal
ctx = decimal.Context(prec=7)
mapping = ctx.flags
del ctx
print(mapping)Expected behavior
Either signaldict holds a strong reference to its parent Context (keeping it alive as long as the signaldict is alive), or accessing a signaldict whose parent has been freed raises a RuntimeError or similar safe error. Under no circumstances should signaldict_repr read through a freed pointer.
Actual behavior
=================================================================
==ERROR: AddressSanitizer: heap-use-after-free on address ... in signaldict_repr
READ of size 4 at ... thread T0
#0 signaldict_repr Include/internal/pycore_moduleobject.h
#1 PyObject_Str Objects/object.c:823
#2 PyFile_WriteObject
#3 builtin_print_impl
...
freed by thread T0 here:
#0 free
#1 context_dealloc Modules/_decimal/_decimal.c:1512
#2 _Py_Dealloc
#3 Py_DECREF / Py_XDECREF
#4 insertdict Objects/dictobject.c:1998
Root cause
signaldict is implemented as a view over its parent Context's internal C struct fields. It stores a raw pointer (or borrowed reference) to the Context but does not increment the Context's refcount. When the user holds the last Python reference to the Context and deletes it, context_dealloc immediately frees the underlying C struct. The signaldict is left with a dangling pointer. Any call to signaldict_repr (or other signaldict methods that dereference the parent pointer) then constitutes a use-after-free.
The fix should ensure signaldict holds a strong (Py_INCREF) reference to its parent Context, releasing it only in signaldict's own dealloc.
CPython versions tested on:
CPython main branch
Operating systems tested on:
No response
Linked PRs
- gh-146011: Fix use-after-free in
signaldict_reprafter deletion #153784 - [3.15] gh-146011: Fix use-after-free in
signaldict_reprafter deletion (GH-153784) #154599 - [3.14] gh-146011: Fix use-after-free in
signaldict_reprafter deletion (GH-153784) #154600 - [3.13] gh-146011: Fix use-after-free in
signaldict_reprafter deletion (GH-153784) #154607 - [3.13] gh-146011: Fix use-after-free in
signaldict_reprafter deletion (#153784) #154615
Activity
brijkapadia commentedon Mar 17, 2026
I'm not entirely sure if
signaldictholding a strong reference is the correct solution. The reason whysignaldictcurrently holds a borrowed reference is because it should live as long as theContextobject. Therefore, I think that the best solution would be something that enforces this lifetime.One potential solution would be to change
context_clearto this:as this would result in a
ValueError(invalid signal dict) if you try to callrepr. Raising an exception seems to be the best strategy as this is a state you would never want to be in.Can open a PR if this is an acceptable fix.
picnixz commentedon Mar 17, 2026
cc @skirpichev (I don't have time to triage this one sorry)
skirpichev commentedon Mar 19, 2026
I think this might be a good solution.
brijkapadia commentedon Mar 19, 2026
One subtle problem I realized is that the C implementation of
decimaland the pure python implementation diverge slightly: in the C version,mappingis stored in asignaldictwhile in the python version, they are stored in a normaldict. This divergence only became apparent when trying to write a test using the reproducer. Wondering if it would be worthwhile to make both of these implementations have the same behavior or if I should just change the C implementation (i.e. what I suggested above).skirpichev commentedon Mar 21, 2026
I don't think, that making behavior compatible is an easy task. There are also a lot of other differences here, e.g.:
You also could check if traps/flags are uniquely referenced (and skip Py_CLEAR, if not).
CC @serhiy-storchaka
brijkapadia commentedon Mar 22, 2026
The main issue I faced was with finding a way to write a test that covers both the C and Python implementations. As you explained, these implementations differ particularly using a
signaldictvs adict. I could write a test that covers just the C implementation; however, I feel like we should make both implementations as similar as possible. But this might end up being a big change and so I don't know where to go from here.Good idea!
gh-146011: Fix use-after-free in `signaldict_repr` after deletion (#1…
[3.15] gh-146011: Fix use-after-free in `signaldict_repr` after delet…
[3.14] gh-146011: Fix use-after-free in `signaldict_repr` after delet…
[3.13] gh-146011: Fix use-after-free in `signaldict_repr` after delet…
pythongh-146011: Fix use-after-free in `signaldict_repr` after deleti…