Skip to content

Conversation

@Kaushalt2004
Copy link
Contributor

@Kaushalt2004 Kaushalt2004 commented Dec 21, 2025

Problem
_collections._count_elements has a fast-path for dict that reads oldval via _PyDict_GetItem_KnownHash() (borrowed reference) and then calls PyNumber_Add(oldval, one). PyNumber_Add() can run arbitrary Python code (e.g. add), allowing re-entrant mutation such as Counter.clear(). That can drop the last reference to oldval while the C code still holds a borrowed pointer, resulting in a use-after-free (ASAN).

Reproducer

Fix
In the fast dict path, keep oldval alive across PyNumber_Add() by temporarily owning a reference:

Py_INCREF(oldval); newval = PyNumber_Add(oldval, one); Py_DECREF(oldval);
This prevents oldval from being freed if user code re-enters and clears/mutates the mapping during the add.

Changes

_collectionsmodule.c: INCREF/DECREF oldval around PyNumber_Add() in the fast path.
test_collections.py: add regression test test_update_reentrant_add_clears_counter.
Tests

./python -m test test_collections -k reentrant_add -v

@bedevere-app
Copy link

bedevere-app bot commented Dec 21, 2025

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.

@python-cla-bot
Copy link

python-cla-bot bot commented Dec 21, 2025

All commit authors signed the Contributor License Agreement.

CLA signed

Copy link
Member

@StanFromIreland StanFromIreland left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please create an issue and add a blurb.

@skirpichev skirpichev changed the title Fix UAF in _collections Counter update fast path gh-143004: Fix UAF in _collections Counter update fast path Dec 21, 2025
@skirpichev
Copy link
Contributor

Please create an issue

@StanFromIreland, it's #143004.

@Kaushalt2004, while technically this seems to be correct, this is not something that solves real-world problem. Could you please provide benchmarks to show how this will impact more typical use-cases for Counter()?

@Kaushalt2004
Copy link
Contributor Author

I ran a small microbenchmark to estimate the impact of the Py_INCREF(oldval) / Py_DECREF(oldval) pair added around PyNumber_Add() in the dict fast-path.

Setup

Windows x64
Built both revisions with the same compiler/toolchain to keep the comparison fair.
Note: my VS install is VS 2026; build.bat expects older MSBuild toolset targets (v143). I built both revisions via MSBuild using PlatformToolset=v145 (same for “before” and “after”).
How

Before: origin/main (b8d3fdd)
After: this PR branch (ce88c7e)
Build + run commands (same for both revisions; only git checkout changes)

subst X: "C:\Users\...\New folder (12)"
cd /d X:\cpython
MSBuild pcbuild.proj /t:Build /m /nologo /v:m /clp:summary /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v145 /p:IncludeExternals=true /p:IncludeCTypes=true /p:IncludeSSL=true /p:IncludeTkinter=false
PCbuild\amd64\python.exe Tools\scripts\bench_counter_update.py --repeats 25 --inner-loops 50 --n-keys 1000 --n-elems 200000
Results (ns/call; lower is better)

update(unique) from empty: 26218.0 → 26438.0 (+0.84%)
update(dupes) from empty: 4520476.0 → 4502494.0 (-0.40%)
update(dupes) preseeded (hits the INCREF/DECREF path every time): 4474634.0 → 4499386.0 (+0.55%)
update(string dupes) empty: 6083882.0 → 6028120.0 (-0.92%)
Conclusion

Deltas are within ~±1% on this machine; no meaningful regression observed, including the “preseeded” case that exercises the new INCREF/DECREF on every update.
If you want it even tighter (1–2 paragraphs), tell me whether you prefer “minimal” or “detailed,” and I’ll rewrite it.I ran a small microbenchmark to estimate the impact of the Py_INCREF(oldval) / Py_DECREF(oldval) pair added around PyNumber_Add() in the dict fast-path.

Setup

Windows x64
Built both revisions with the same compiler/toolchain to keep the comparison fair.
Note: my VS install is VS 2026; build.bat expects older MSBuild toolset targets (v143). I built both revisions via MSBuild using PlatformToolset=v145 (same for “before” and “after”).
How

Before: origin/main (b8d3fdd)
After: this PR branch (ce88c7e)
Build + run commands (same for both revisions; only git checkout changes)

subst X: "C:\Users\...\New folder (12)"
cd /d X:\cpython
MSBuild pcbuild.proj /t:Build /m /nologo /v:m /clp:summary /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v145 /p:IncludeExternals=true /p:IncludeCTypes=true /p:IncludeSSL=true /p:IncludeTkinter=false
PCbuild\amd64\python.exe Tools\scripts\bench_counter_update.py --repeats 25 --inner-loops 50 --n-keys 1000 --n-elems 200000
Results (ns/call; lower is better)

update(unique) from empty: 26218.0 → 26438.0 (+0.84%)
update(dupes) from empty: 4520476.0 → 4502494.0 (-0.40%)
update(dupes) preseeded (hits the INCREF/DECREF path every time): 4474634.0 → 4499386.0 (+0.55%)
update(string dupes) empty: 6083882.0 → 6028120.0 (-0.92%)
Conclusion

Deltas are within ~±1% on this machine; no meaningful regression observed, including the “preseeded” case that exercises the new INCREF/DECREF on every update.

@bedevere-app
Copy link

bedevere-app bot commented Dec 22, 2025

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.

@skirpichev
Copy link
Contributor

I ran a small microbenchmark

I still see no benchmark code.

PS: @Kaushalt2004, do not click Update branch without a good reason because it notifies everyone watching the PR that there are new changes, when there are not, and it uses up limited CI resources.

@Kaushalt2004
Copy link
Contributor Author

Thanks for the feedback.

I’ve removed the benchmark script from the PR as requested.
I’ve also adjusted the NEWS entry formatting.

Benchmark methodology and results were shared earlier in comments;
they show no meaningful regression (~±1%).

Please let me know if you’d like the benchmark rerun using pyperf
or if further changes are needed.

…af-counter.rst

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
…af-counter.rst

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
…new__ override, rely on __add__ only. Clarify with reviewer note. (python#1 branch)
@Kaushalt2004
Copy link
Contributor Author

Thanks for the clarification. You’re right that Python guarantees subclass construction, and the test does not demonstrate any loss of subclass identity.
The custom new was added defensively, but since this behavior cannot be concretely shown, it is unnecessary.
I will simplify the test by removing the custom constructor and keep it focused on the re-entrant mutation via add.

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Copy link
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. 👍

@serhiy-storchaka serhiy-storchaka added needs backport to 3.13 bugs and security fixes needs backport to 3.14 bugs and security fixes and removed DO-NOT-MERGE labels Dec 25, 2025
@serhiy-storchaka serhiy-storchaka enabled auto-merge (squash) December 25, 2025 09:00
@serhiy-storchaka serhiy-storchaka merged commit 86d9045 into python:main Dec 25, 2025
51 of 52 checks passed
@miss-islington-app
Copy link

Thanks @Kaushalt2004 for the PR, and @serhiy-storchaka for merging it 🌮🎉.. I'm working now to backport this PR to: 3.13, 3.14.
🐍🍒⛏🤖

miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Dec 25, 2025
…pdate() (pythonGH-143044)

This happened when the Counter was mutated when incrementing
the value for an existing key.
(cherry picked from commit 86d904588e8c84c7fccb8faf84b343f03461970d)

Co-authored-by: kaushal trivedi <155625932+Kaushalt2004@users.noreply.github.com>
miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Dec 25, 2025
…pdate() (pythonGH-143044)

This happened when the Counter was mutated when incrementing
the value for an existing key.
(cherry picked from commit 86d9045)

Co-authored-by: kaushal trivedi <155625932+Kaushalt2004@users.noreply.github.com>
@bedevere-app
Copy link

bedevere-app bot commented Dec 25, 2025

GH-143166 is a backport of this pull request to the 3.14 branch.

@bedevere-app bedevere-app bot removed the needs backport to 3.14 bugs and security fixes label Dec 25, 2025
@bedevere-app
Copy link

bedevere-app bot commented Dec 25, 2025

GH-143167 is a backport of this pull request to the 3.13 branch.

@bedevere-app bedevere-app bot removed the needs backport to 3.13 bugs and security fixes label Dec 25, 2025
serhiy-storchaka pushed a commit that referenced this pull request Dec 25, 2025
…update() (GH-143044) (GH-143166)

This happened when the Counter was mutated when incrementing
the value for an existing key.
(cherry picked from commit 86d9045)

Co-authored-by: kaushal trivedi <155625932+Kaushalt2004@users.noreply.github.com>
serhiy-storchaka pushed a commit that referenced this pull request Dec 25, 2025
…update() (GH-143044) (GH-143167)

This happened when the Counter was mutated when incrementing
the value for an existing key.
(cherry picked from commit 86d9045)

Co-authored-by: kaushal trivedi <155625932+Kaushalt2004@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants