Skip to content

Commit 9ed24b4

Browse files
committed
gh-143004: Add Counter UAF NEWS entry and benchmark script
1 parent e0036f1 commit 9ed24b4

File tree

2 files changed

+111
-0
lines changed

2 files changed

+111
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
gh-143004: Fix a potential use-after-free in collections.Counter.update() when user code mutates the Counter during an update.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Microbenchmarks for collections.Counter.update() iterable fast path.
2+
3+
This is intended for quick before/after comparisons of small C-level changes.
4+
It avoids third-party deps (e.g. pyperf) and prints simple, stable-enough stats.
5+
6+
Run (from repo root):
7+
PCbuild\\amd64\\python.exe Tools\\scripts\\bench_counter_update.py
8+
9+
You can also override sizes:
10+
... bench_counter_update.py --n-keys 1000 --n-elems 200000
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import argparse
16+
import statistics
17+
import sys
18+
import time
19+
from collections import Counter
20+
21+
22+
def _run_timer(func, *, inner_loops: int, repeats: int) -> dict[str, float]:
23+
# Warmup
24+
for _ in range(5):
25+
func()
26+
27+
samples = []
28+
for _ in range(repeats):
29+
t0 = time.perf_counter()
30+
for _ in range(inner_loops):
31+
func()
32+
t1 = time.perf_counter()
33+
samples.append(t1 - t0)
34+
35+
return {
36+
"min_s": min(samples),
37+
"mean_s": statistics.mean(samples),
38+
"stdev_s": statistics.pstdev(samples) if len(samples) > 1 else 0.0,
39+
"repeats": float(repeats),
40+
"inner_loops": float(inner_loops),
41+
}
42+
43+
44+
def _format_line(name: str, stats: dict[str, float]) -> str:
45+
# Report per-call based on min (least noisy for microbench comparisons).
46+
per_call_ns = (stats["min_s"] / stats["inner_loops"]) * 1e9
47+
return (
48+
f"{name:32s} {per_call_ns:10.1f} ns/call"
49+
f" (min={stats['min_s']:.6f}s, mean={stats['mean_s']:.6f}s, "
50+
f"stdev={stats['stdev_s']:.6f}s, loops={int(stats['inner_loops'])}, reps={int(stats['repeats'])})"
51+
)
52+
53+
54+
def main(argv: list[str]) -> int:
55+
parser = argparse.ArgumentParser()
56+
parser.add_argument("--n-keys", type=int, default=1000)
57+
parser.add_argument("--n-elems", type=int, default=100_000)
58+
parser.add_argument("--repeats", type=int, default=25)
59+
parser.add_argument("--inner-loops", type=int, default=50)
60+
args = parser.parse_args(argv)
61+
62+
n_keys = args.n_keys
63+
n_elems = args.n_elems
64+
65+
# Data sets
66+
keys_unique = list(range(n_keys))
67+
68+
# Many duplicates; all keys are within [0, n_keys)
69+
keys_dupes = [i % n_keys for i in range(n_elems)]
70+
71+
# All elements hit the "oldval != NULL" branch by pre-seeding.
72+
seeded = Counter({k: 1 for k in range(n_keys)})
73+
74+
def bench_unique_from_empty() -> None:
75+
c = Counter()
76+
c.update(keys_unique)
77+
78+
def bench_dupes_from_empty() -> None:
79+
c = Counter()
80+
c.update(keys_dupes)
81+
82+
def bench_dupes_all_preseeded() -> None:
83+
c = seeded.copy()
84+
c.update(keys_dupes)
85+
86+
# A string-like workload (common Counter use): update over a repeated alphabet.
87+
alpha = ("abcdefghijklmnopqrstuvwxyz" * (n_elems // 26 + 1))[:n_elems]
88+
89+
def bench_string_dupes_from_empty() -> None:
90+
c = Counter()
91+
c.update(alpha)
92+
93+
print(sys.version.replace("\n", " "))
94+
print(f"n_keys={n_keys}, n_elems={n_elems}, repeats={args.repeats}, inner_loops={args.inner_loops}")
95+
print()
96+
97+
for name, fn in (
98+
("update(unique) from empty", bench_unique_from_empty),
99+
("update(dupes) from empty", bench_dupes_from_empty),
100+
("update(dupes) preseeded", bench_dupes_all_preseeded),
101+
("update(string dupes) empty", bench_string_dupes_from_empty),
102+
):
103+
stats = _run_timer(fn, inner_loops=args.inner_loops, repeats=args.repeats)
104+
print(_format_line(name, stats))
105+
106+
return 0
107+
108+
109+
if __name__ == "__main__":
110+
raise SystemExit(main(sys.argv[1:]))

0 commit comments

Comments
 (0)