GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — heap-use-after-free in signaldict_repr: signaldict outlives its parent Context after explicit del · Issue #146011 · python/cpython

heap-use-after-free in signaldict_repr: signaldict outlives its parent Context after explicit del #146011

New issue
Copy link
New issue
Copy link
Closed
Closed
heap-use-after-free in signaldict_repr: signaldict outlives its parent Context after explicit del#146011
Copy link
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@YuanchengJiang
YuanchengJiang
opened on Mar 16, 2026
Issue body actions

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_repr after deletion #153784
  • [3.15] gh-146011: Fix use-after-free in signaldict_repr after deletion (GH-153784) #154599
  • [3.14] gh-146011: Fix use-after-free in signaldict_repr after deletion (GH-153784) #154600
  • [3.13] gh-146011: Fix use-after-free in signaldict_repr after deletion (GH-153784) #154607
  • [3.13] gh-146011: Fix use-after-free in signaldict_repr after deletion (#153784) #154615

Activity

  1. YuanchengJiang
    added
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    on Mar 16, 2026
  2. skirpichev
    added
    extension-modulesC modules in the Modules dirC modules in the Modules dir
    on Mar 16, 2026
  3. brijkapadia commented on Mar 17, 2026

    @brijkapadia
    brijkapadia
    on Mar 17, 2026
    Contributor
    More actions

    I'm not entirely sure if signaldict holding a strong reference is the correct solution. The reason why signaldict currently holds a borrowed reference is because it should live as long as the Context object. Therefore, I think that the best solution would be something that enforces this lifetime.

    One potential solution would be to change context_clear to this:

    static int
    context_clear(PyObject *op)
    {
        PyDecContextObject *self = _PyDecContextObject_CAST(op);
        /* Start of added code */
        PyDecSignalDictObject *traps = _PyDecSignalDictObject_CAST(self->traps);
        PyDecSignalDictObject *flags = _PyDecSignalDictObject_CAST(self->flags);
        traps->flags = NULL;
        flags->flags = NULL;
        /* End of added code */
        Py_CLEAR(self->traps);
        Py_CLEAR(self->flags);
        return 0;
    }

    as this would result in a ValueError (invalid signal dict) if you try to call repr. 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.

  4. picnixz commented on Mar 17, 2026

    @picnixz
    picnixz
    on Mar 17, 2026
    Member
    More actions

    cc @skirpichev (I don't have time to triage this one sorry)

  5. skirpichev
    assigned
    skirpichev
    and unassigned
    skirpichev
    on Mar 17, 2026
  6. skirpichev commented on Mar 19, 2026

    @skirpichev
    skirpichev
    on Mar 19, 2026
    Member
    More actions

    Can open a PR if this is an acceptable fix.

    I think this might be a good solution.

    Reacted by Brij Kapadia
  7. brijkapadia commented on Mar 19, 2026

    @brijkapadia
    brijkapadia
    on Mar 19, 2026
    Contributor
    More actions

    One subtle problem I realized is that the C implementation of decimal and the pure python implementation diverge slightly: in the C version, mapping is stored in a signaldict while in the python version, they are stored in a normal dict. 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).

  8. skirpichev commented on Mar 21, 2026

    @skirpichev
    skirpichev
    on Mar 21, 2026
    Member
    More actions

    Wondering if it would be worthwhile to make both of these implementations have the same behavior

    I don't think, that making behavior compatible is an easy task. There are also a lot of other differences here, e.g.:

    >>> import decimal
    >>> import _pydecimal
    >>> ctx_py = _pydecimal.Context(prec=7)
    >>> ctx_c = decimal.Context(prec=7)
    >>> del ctx_py.traps[_pydecimal.Clamped]
    >>> del ctx_c.traps[decimal.Clamped]
    Traceback (most recent call last):
      File "<python-input-7>", line 1, in <module>
        del ctx_c.traps[decimal.Clamped]
            ~~~~~~~~~~~^^^^^^^^^^^^^^^^^
    ValueError: signal keys cannot be deleted
    

    or if I should just change the C implementation (i.e. what I suggested above).

    You also could check if traps/flags are uniquely referenced (and skip Py_CLEAR, if not).

    CC @serhiy-storchaka

  9. brijkapadia commented on Mar 22, 2026

    @brijkapadia
    brijkapadia
    on Mar 22, 2026
    Contributor
    More actions

    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 signaldict vs a dict. 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.

    You also could check if traps/flags are uniquely referenced (and skip Py_CLEAR, if not).

    Good idea!

  10. vstinner
    added 2 commits that reference this issue on Jul 24, 2026

    gh-146011: Fix use-after-free in `signaldict_repr` after deletion (#1…

    41a087a

    [3.15] gh-146011: Fix use-after-free in `signaldict_repr` after delet…

    f921edf
  11. vstinner
    closed this as completedon Jul 24, 2026
  12. vstinner
    added 2 commits that reference this issue on Jul 24, 2026

    [3.14] gh-146011: Fix use-after-free in `signaldict_repr` after delet…

    0e8b607

    [3.13] gh-146011: Fix use-after-free in `signaldict_repr` after delet…

    fa16a75
  13. mbeijen
    added a commit that references this issue on Aug 14, 2026

    pythongh-146011: Fix use-after-free in `signaldict_repr` after deleti…

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirC modules in the Modules dirtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.