Skip to content

gh-144475: Fix use-after-free in functools.partial.__repr__()#145395

Open
Nicolas0315 wants to merge 3 commits intopython:mainfrom
Nicolas0315:fix/partial-repr-use-after-free
Open

gh-144475: Fix use-after-free in functools.partial.__repr__()#145395
Nicolas0315 wants to merge 3 commits intopython:mainfrom
Nicolas0315:fix/partial-repr-use-after-free

Conversation

@Nicolas0315
Copy link

@Nicolas0315 Nicolas0315 commented Mar 1, 2026

Summary

Fix a heap-buffer-overflow (use-after-free) in functools.partial.__repr__() where a user-defined __repr__() on an argument could mutate the partial object via __setstate__(), freeing the args tuple while partial_repr() was still iterating over it.

Root Cause

partial_repr() captured the size of pto->args before the loop and accessed tuple items via borrowed references (PyTuple_GET_ITEM). If a __repr__() called during %R formatting invoked pto.__setstate__() with a new (smaller) args tuple, the original tuple was freed while iteration continued, causing an out-of-bounds read.

Fix

Hold strong references (Py_NewRef) to pto->args, pto->kw, and pto->fn before iterating/using them. This ensures the underlying objects remain alive even if user code mutates the partial via __setstate__() during formatting.

The same pattern is applied to:

  • pto->args: The positional arguments tuple iterated in the loop
  • pto->kw: The keyword arguments dict iterated via PyDict_Next
  • pto->fn: The callable whose __repr__ is invoked via %R

Reproducer (from the issue)

import gc
from functools import partial

g_partial = None

class EvilObject:
    def __init__(self, name, is_trigger=False):
        self.name = name
        self.is_trigger = is_trigger
        self.triggered = False

    def __repr__(self):
        global g_partial
        if self.is_trigger and not self.triggered and g_partial is not None:
            self.triggered = True
            new_state = (lambda x: x, ("replaced",), {}, None)
            g_partial.__setstate__(new_state)
            gc.collect()
        return f"EvilObject({self.name})"

evil1 = EvilObject("trigger", is_trigger=True)
evil2 = EvilObject("victim1")
evil3 = EvilObject("victim2")

p = partial(lambda: None, evil1, evil2, evil3)
g_partial = p
del evil1, evil2, evil3

repr(p)  # heap-buffer-overflow without fix

Fixes #144475.

Hold strong references to pto->args, pto->kw, and pto->fn during
partial_repr() to prevent them from being freed by a user-defined
__repr__() that mutates the partial object via __setstate__().

Previously, partial_repr() iterated over pto->args using a size 'n'
captured before the loop, and accessed tuple items via borrowed
references. If a __repr__() called during formatting invoked
pto.__setstate__() with a new (smaller) args tuple, the original
tuple could be freed while the loop was still iterating, leading to
a heap-buffer-overflow (out-of-bounds read).

The fix takes a new reference (Py_NewRef) to the args tuple, kw dict,
and fn callable before using them, ensuring they stay alive regardless
of any mutations to the partial object during formatting.
@Nicolas0315 Nicolas0315 requested a review from rhettinger as a code owner March 1, 2026 17:27
@python-cla-bot
Copy link

python-cla-bot bot commented Mar 1, 2026

All commit authors signed the Contributor License Agreement.

CLA signed

@bedevere-app
Copy link

bedevere-app bot commented Mar 1, 2026

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

pythongh-144475: Add NEWS entry for functools.partial.__repr__ fix
Use inline code markup instead of :func: and :meth: roles for
partial.__repr__ and __setstate__ to avoid Sphinx reference
resolution failures in the docs CI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

heap-buffer-overflow in functools.partial.__repr__()

1 participant