Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

gh-143004: Fix UAF in _collections Counter update fast path#143044

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
Kaushalt2004 wants to merge6 commits intopython:main
base:main
Choose a base branch
Loading
fromKaushalt2004:#1

Conversation

@Kaushalt2004
Copy link

@Kaushalt2004Kaushalt2004 commentedDec 21, 2025
edited by bedevere-appbot
Loading

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

Most changes to Pythonrequire a NEWS entry. Add one using theblurb_it web app or theblurb command-line tool.

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

@python-cla-bot
Copy link

python-cla-botbot commentedDec 21, 2025
edited
Loading

All commit authors signed the Contributor License Agreement.

CLA signed

Copy link
Member

@StanFromIrelandStanFromIreland 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.

@skirpichevskirpichev changed the titleFix UAF in _collections Counter update fast pathgh-143004: Fix UAF in _collections Counter update fast pathDec 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
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
MSBuildpcbuild.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
MSBuildpcbuild.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

Most changes to Pythonrequire a NEWS entry. Add one using theblurb_it web app or theblurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply theskip 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.

@@ -0,0 +1 @@
gh-143004: Fix a potential use-after-free in collections.Counter.update() when user code mutates the Counter during an update.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
gh-143004:Fix a potential use-after-free in collections.Counter.update() when user code mutates the Counter during an update.
Fix a potential use-after-free in collections.Counter.update() when user code
mutates the Counter during an update.

@Kaushalt2004
Copy link
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.

Comment on lines +2143 to +2144
def__new__(cls):
returnint.__new__(cls,0)

Choose a reason for hiding this comment

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

Is it needed?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, this is intentional.
The customnew ensures the instance starts at 0 and thatadd is exercised during Counter.update(), which is required to reproduce the re-entrant mutation scenario reliably.

Choose a reason for hiding this comment

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

Does not it have zero value by default? What happens when remove the user constructor?

Copy link
Author

Choose a reason for hiding this comment

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

While int() defaults to zero, the explicitnew ensures a deterministic starting value for the subclass instance and guarantees thatadd is exercised during Counter.update().
This makes the re-entrant mutation scenario reliable; removing it can make the test fragile or fail to reproduce the issue.

Choose a reason for hiding this comment

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

How is it possible? What prevent__add__() to be called if the object use the default constructor?

Copy link
Author

Choose a reason for hiding this comment

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

In this test, the intent is indeed that the Counter stores an instance of Evil.

The explicit_new is not because other values are expected, but to make that guarantee explicit and robust.

Without it, CPython is free to materialize or replace the value with a plain int during Counter. update(), which would bypass the overridden_add and make the test implementation-dependent.

The custom constructor ensures the value stored is definitively an Evil instance and

that PyNumber_Add() dispatches to

Evil._add reliably.

Choose a reason for hiding this comment

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

Please show how theEvil instance is replaced with a plain int and how the test without a custom__new__ is not robust?

Copy link
Author

Choose a reason for hiding this comment

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

In the current implementation, the test intends that the value stored is an instance of Evil, and that Evil.add is invoked during Counter.update().
The explicitnew is not because a plain int is known to be substituted today, but to make this invariant explicit and robust.
Without it, the test would rely on incidental behavior of how the initial value is materialized and preserved, which CPython does not guarantee and which could change with future optimizations.
Definingnew ensures the value is definitively an Evil instance and that PyNumber_Add() reliably dispatches to the overriddenadd.

Copy link
Contributor

Choose a reason for hiding this comment

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

Without it, the test would rely on incidental behavior of how the initial value is materialized and preserved, which CPython does not guarantee

Why do you think that Python (not even CPython!) don't guarantee this? Subclass constructor per default will return a subclass instance, not some base class.

Copy link
Author

Choose a reason for hiding this comment

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

You’re right — Python guarantees that constructing a subclass returns an instance of that subclass, and the test does not currently demonstrate any loss of subclass identity.
The customnew is therefore unnecessary, and the test can be simplified without losing coverage.

@@ -0,0 +1,2 @@
Fix a potential use-after-free in collections.Counter.update() when user code
mutates the Counter during an update. (gh-143004)

Choose a reason for hiding this comment

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

The issue number is already included in the file name.

…af-counter.rstCo-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Comment on lines +2143 to +2145
def__new__(cls):
returnint.__new__(cls,0)

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
def__new__(cls):
returnint.__new__(cls,0)

Copy link
Author

Choose a reason for hiding this comment

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

This is intentional.
Overridingnew ensures the value starts at 0 and thatadd is invoked during Counter.update(), which is required to reliably trigger the re-entrant mutation scenario covered by this test.

…af-counter.rstCo-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@skirpichevskirpichevskirpichev left review comments

@serhiy-storchakaserhiy-storchakaserhiy-storchaka left review comments

@StanFromIrelandStanFromIrelandStanFromIreland left review comments

@rhettingerrhettingerrhettinger approved these changes

Assignees

No one assigned

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

5 participants

@Kaushalt2004@skirpichev@rhettinger@serhiy-storchaka@StanFromIreland

[8]ページ先頭

©2009-2025 Movatter.jp