Skip to content

Commit 47a0745

Browse files
committed
Remove unnecessary extra comments in tests
1 parent 836ac21 commit 47a0745

2 files changed

Lines changed: 21 additions & 151 deletions

File tree

Lib/test/test_free_threading/test_weakref.py

Lines changed: 17 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,4 @@
1-
#!/usr/bin/env python3
2-
"""
3-
Free-threading regression tests for weakref races in subtype_getweakref().
4-
5-
Regression test for https://github.com/python/cpython/issues/149816
6-
finding #61: "Racy weakref head load before incref in Objects/typeobject.c".
7-
8-
subtype_getweakref() is the C implementation of the obj.__weakref__
9-
descriptor getter (Objects/typeobject.c). Before the fix it performed an
10-
unsynchronized load of *weaklistptr followed by Py_NewRef(), creating a
11-
use-after-free window: a concurrent weakref_dealloc() could free the
12-
weakref list head between the load and the incref.
13-
14-
The fix holds LOCK_WEAKREFS(obj) across the read+incref and uses
15-
_Py_TryIncref() instead of Py_NewRef() so that a weakref whose refcount
16-
has already hit zero is handled gracefully (returns None semantics).
17-
LOCK_WEAKREFS / UNLOCK_WEAKREFS are no-ops in GIL builds, so the fix has
18-
zero overhead outside the free-threaded build.
19-
"""
1+
"""Regression tests for weakref races in subtype_getweakref() (gh-149816)."""
202

213
import gc
224
import sys
@@ -27,194 +9,84 @@
279
from test.support import threading_helper
2810

2911

30-
def run_in_threads(targets, *, barrier=None):
31-
"""Start all callables in *targets* in separate threads and join them.
32-
33-
If *barrier* is given it must be a threading.Barrier whose party count
34-
equals len(targets); each target is wrapped so that it waits at the
35-
barrier before doing any work, maximising contention.
36-
"""
37-
if barrier is not None:
38-
def wrap(fn):
39-
def wrapper():
40-
barrier.wait()
41-
fn()
42-
return wrapper
43-
targets = [wrap(t) for t in targets]
44-
45-
threads = [threading.Thread(target=t) for t in targets]
46-
for th in threads:
47-
th.start()
48-
for th in threads:
49-
th.join()
50-
51-
5212
@threading_helper.requires_working_threading()
5313
class TestWeakrefRaces(unittest.TestCase):
54-
"""Race-condition tests for subtype_getweakref() (gh-149816 finding #61)."""
5514

56-
# Number of iterations for the hot-loop race tests. High enough to
57-
# exercise the race window reliably on multi-core machines, low enough
58-
# to keep the test suite fast (no time-based loops).
5915
ITERATIONS = 5_000
6016

6117
def test_getweakref_no_crash(self):
62-
"""obj.__weakref__ racing with concurrent weakref creation/destruction.
63-
64-
Regression test for gh-149816 #61. Without the fix,
65-
subtype_getweakref() could incref a freed PyWeakReference object
66-
(use-after-free), causing a native crash or memory corruption.
67-
68-
The callback on weakref.ref() forces CPython to allocate a fresh
69-
PyWeakReference each time (instead of reusing an existing one),
70-
keeping list-head creation and destruction hot so the race window
71-
is exercised as often as possible.
72-
73-
A crash here means the LOCK_WEAKREFS + _Py_TryIncref fix is broken.
74-
"""
18+
"""obj.__weakref__ racing with concurrent weakref creation/destruction."""
7519
class Target:
7620
pass
7721

7822
obj = Target()
79-
# Barrier ensures all threads enter their hot loops simultaneously,
80-
# maximising the chance of hitting the race window.
81-
barrier = threading.Barrier(2)
8223

8324
def reader():
84-
barrier.wait()
8525
for _ in range(self.ITERATIONS):
86-
# Calls subtype_getweakref() in C — the previously buggy path.
8726
ref = obj.__weakref__
88-
# Must be None or a weakref.ref — never corrupted memory.
89-
self.assertIn(
90-
type(ref),
91-
(type(None), weakref.ref),
92-
f"__weakref__ returned unexpected type {type(ref)!r}",
93-
)
94-
# If it is a live weakref it must still point to obj.
27+
self.assertIn(type(ref), (type(None), weakref.ref))
9528
if ref is not None:
96-
target = ref()
97-
self.assertIn(
98-
target,
99-
(None, obj),
100-
"__weakref__ returned a weakref pointing to wrong object",
101-
)
29+
self.assertIn(ref(), (None, obj))
10230

10331
def mutator():
104-
barrier.wait()
10532
for _ in range(self.ITERATIONS):
106-
# Create a weakref with a callback. Because no variable
107-
# holds the result the refcount drops to 0 immediately,
108-
# triggering weakref_dealloc() — the "freeing" side of
109-
# the race. The callback prevents CPython from reusing
110-
# an existing weakref object, keeping alloc/free hot.
11133
weakref.ref(obj, lambda _: None)
11234

113-
run_in_threads([reader, mutator])
35+
threading_helper.run_concurrently([reader, mutator])
11436

11537
def test_getweakref_return_type(self):
116-
"""__weakref__ must return None or a weakref.ref, never garbage.
117-
118-
Verifies the correctness of subtype_getweakref() return values
119-
across the three possible states of the weakref list.
120-
"""
38+
"""__weakref__ must return None or a live weakref.ref."""
12139
class Target:
12240
pass
12341

12442
obj = Target()
12543

126-
# State 1: no weakrefs exist → must return None.
127-
result = obj.__weakref__
128-
self.assertIsNone(
129-
result,
130-
"__weakref__ should be None when no weakrefs exist",
131-
)
44+
self.assertIsNone(obj.__weakref__)
13245

133-
# State 2: one live weakref exists → must return it and it must
134-
# resolve back to obj.
13546
ref = weakref.ref(obj)
13647
result = obj.__weakref__
137-
self.assertIsNotNone(result, "__weakref__ should not be None with a live ref")
13848
self.assertIsInstance(result, weakref.ref)
139-
self.assertIs(
140-
result(),
141-
obj,
142-
"__weakref__ returned a weakref that does not point to obj",
143-
)
144-
145-
# State 3: all strong refs to the weakref object dropped → must
146-
# return None again. Both `ref` AND `result` (from State 2) must
147-
# be deleted — either one alone keeps the weakref alive.
49+
self.assertIs(result(), obj)
50+
14851
del ref, result
14952
gc.collect()
150-
result3 = obj.__weakref__
151-
self.assertIsNone(
152-
result3,
153-
"__weakref__ should be None after all weakrefs are deleted",
154-
)
53+
self.assertIsNone(obj.__weakref__)
15554

15655
def test_getweakref_no_refcount_leak(self):
157-
"""Each __weakref__ access must not inflate the weakref's refcount.
158-
159-
A double-incref bug in subtype_getweakref() would add one extra
160-
reference on every call, preventing the weakref from ever being
161-
freed (reference leak). sys.getrefcount() includes one transient
162-
reference for the function argument, so the count should be stable
163-
across repeated accesses.
164-
"""
56+
"""Each __weakref__ access must not inflate the weakref's refcount."""
16557
class Target:
16658
pass
16759

16860
obj = Target()
16961
ref = weakref.ref(obj)
170-
171-
# Baseline: one ref from `ref`, one transient from getrefcount().
17262
before = sys.getrefcount(ref)
17363

17464
for _ in range(1_000):
17565
r = obj.__weakref__
176-
del r # release the returned reference immediately
66+
del r
17767

17868
gc.collect()
179-
after = sys.getrefcount(ref)
180-
181-
self.assertEqual(
182-
before,
183-
after,
184-
f"Refcount grew from {before} to {after} after 1000 "
185-
f"__weakref__ accesses — possible double-incref in "
186-
f"subtype_getweakref()",
187-
)
69+
self.assertEqual(sys.getrefcount(ref), before)
18870

18971
def test_getweakref_many_readers(self):
190-
"""Multiple concurrent readers of __weakref__ must not crash.
191-
192-
Extends test_getweakref_no_crash to N reader threads + N mutator
193-
threads, matching the repro script's default cpu_count-per-role
194-
configuration, to stress true multi-core parallelism.
195-
"""
196-
import os
197-
72+
"""Multiple concurrent readers and mutators must not crash."""
19873
class Target:
19974
pass
20075

20176
obj = Target()
202-
n = max(2, (os.cpu_count() or 2) // 2) # at least 2 of each role
203-
barrier = threading.Barrier(n * 2)
204-
iters = max(1_000, self.ITERATIONS // n) # scale down per-thread iters
77+
n = 4
78+
iters = self.ITERATIONS // n
20579

20680
def reader():
207-
barrier.wait()
20881
for _ in range(iters):
20982
ref = obj.__weakref__
21083
self.assertIn(type(ref), (type(None), weakref.ref))
21184

21285
def mutator():
213-
barrier.wait()
21486
for _ in range(iters):
21587
weakref.ref(obj, lambda _: None)
21688

217-
run_in_threads([reader] * n + [mutator] * n)
89+
threading_helper.run_concurrently([reader] * n + [mutator] * n)
21890

21991

22092
if __name__ == "__main__":

Objects/typeobject.c

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4076,15 +4076,13 @@ subtype_getweakref(PyObject *obj, void *context)
40764076
((type->tp_weaklistoffset + (Py_ssize_t)sizeof(PyObject *))
40774077
<= type->tp_basicsize));
40784078

4079-
/* In free-threaded builds, the weakref list head can be freed by another
4080-
* thread between our load of *weaklistptr and the Py_NewRef() call
4081-
* (use-after-free / heap corruption). Hold the weakref striped lock across
4082-
* the read+incref to prevent this. LOCK_WEAKREFS/UNLOCK_WEAKREFS are no-ops
4083-
* in GIL builds, so this has zero overhead in the traditional build. */
4079+
/* Synchronize with weakref creation/destruction so the list head cannot
4080+
* be removed and freed between loading it and taking a reference. */
40844081
LOCK_WEAKREFS(obj);
40854082
weaklistptr = (PyObject **)((char *)obj + type->tp_weaklistoffset);
4086-
if (*weaklistptr == NULL)
4083+
if (*weaklistptr == NULL) {
40874084
result = Py_NewRef(Py_None);
4085+
}
40884086
else if (_Py_TryIncref(*weaklistptr)) {
40894087
result = *weaklistptr;
40904088
}

0 commit comments

Comments
 (0)