Skip to content

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

Closed
Nicolas0315 wants to merge 3 commits into
python:mainfrom
Nicolas0315:fix/partial-repr-use-after-free
Closed

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

Conversation

@Nicolas0315

@Nicolas0315Nicolas0315 commented Mar 1, 2026

Copy link
Copy Markdown

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)

importgcfromfunctoolsimportpartialg_partial=NoneclassEvilObject:
def__init__(self, name, is_trigger=False):
self.name=nameself.is_trigger=is_triggerself.triggered=Falsedef__repr__(self):
globalg_partialifself.is_triggerandnotself.triggeredandg_partialisnotNone:
self.triggered=Truenew_state= (lambdax: x, ("replaced",), {}, None)
g_partial.__setstate__(new_state)
gc.collect()
returnf"EvilObject({self.name})"evil1=EvilObject("trigger", is_trigger=True)
evil2=EvilObject("victim1")
evil3=EvilObject("victim2")
p=partial(lambda: None, evil1, evil2, evil3)
g_partial=pdelevil1, evil2, evil3repr(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.
@python-cla-bot

python-cla-botBot commented Mar 1, 2026

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

@bedevere-app

Copy link
Copy Markdown

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.

gh-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.
@aisk

aisk commented Mar 2, 2026

Copy link
Copy Markdown
Member

There is already an active PR #145362 for this issue, so please don’t open another PR for the same fix.

@encukouencukou closed this Mar 3, 2026
@encukou

Copy link
Copy Markdown
Member

Also, if this is LLM-generated, please review https://devguide.python.org/getting-started/generative-ai/

Sign up for freeto 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__()

3 participants

@Nicolas0315@aisk@encukou