Skip to content

Commit 9747ba6

Browse files
committed
Remove unnecessary extra comments in tests
1 parent 836ac21 commit 9747ba6

2 files changed

Lines changed: 21 additions & 152 deletions

File tree

Lib/test/test_free_threading/test_weakref.py

Lines changed: 17 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,220 +1,91 @@
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
23-
import threading
245
import unittest
256
import weakref
267

278
from test.support import threading_helper
289

2910

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-
5211
@threading_helper.requires_working_threading()
5312
class TestWeakrefRaces(unittest.TestCase):
54-
"""Race-condition tests for subtype_getweakref() (gh-149816 finding #61)."""
5513

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).
5914
ITERATIONS = 5_000
6015

6116
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-
"""
17+
"""obj.__weakref__ racing with concurrent weakref creation/destruction."""
7518
class Target:
7619
pass
7720

7821
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)
8222

8323
def reader():
84-
barrier.wait()
8524
for _ in range(self.ITERATIONS):
86-
# Calls subtype_getweakref() in C — the previously buggy path.
8725
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.
26+
self.assertIn(type(ref), (type(None), weakref.ref))
9527
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-
)
28+
self.assertIn(ref(), (None, obj))
10229

10330
def mutator():
104-
barrier.wait()
10531
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.
11132
weakref.ref(obj, lambda _: None)
11233

113-
run_in_threads([reader, mutator])
34+
threading_helper.run_concurrently([reader, mutator])
11435

11536
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-
"""
37+
"""__weakref__ must return None or a live weakref.ref."""
12138
class Target:
12239
pass
12340

12441
obj = Target()
12542

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-
)
43+
self.assertIsNone(obj.__weakref__)
13244

133-
# State 2: one live weakref exists → must return it and it must
134-
# resolve back to obj.
13545
ref = weakref.ref(obj)
13646
result = obj.__weakref__
137-
self.assertIsNotNone(result, "__weakref__ should not be None with a live ref")
13847
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.
48+
self.assertIs(result(), obj)
49+
14850
del ref, result
14951
gc.collect()
150-
result3 = obj.__weakref__
151-
self.assertIsNone(
152-
result3,
153-
"__weakref__ should be None after all weakrefs are deleted",
154-
)
52+
self.assertIsNone(obj.__weakref__)
15553

15654
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-
"""
55+
"""Each __weakref__ access must not inflate the weakref's refcount."""
16556
class Target:
16657
pass
16758

16859
obj = Target()
16960
ref = weakref.ref(obj)
170-
171-
# Baseline: one ref from `ref`, one transient from getrefcount().
17261
before = sys.getrefcount(ref)
17362

17463
for _ in range(1_000):
17564
r = obj.__weakref__
176-
del r # release the returned reference immediately
65+
del r
17766

17867
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-
)
68+
self.assertEqual(sys.getrefcount(ref), before)
18869

18970
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-
71+
"""Multiple concurrent readers and mutators must not crash."""
19872
class Target:
19973
pass
20074

20175
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
76+
n = 4
77+
iters = self.ITERATIONS // n
20578

20679
def reader():
207-
barrier.wait()
20880
for _ in range(iters):
20981
ref = obj.__weakref__
21082
self.assertIn(type(ref), (type(None), weakref.ref))
21183

21284
def mutator():
213-
barrier.wait()
21485
for _ in range(iters):
21586
weakref.ref(obj, lambda _: None)
21687

217-
run_in_threads([reader] * n + [mutator] * n)
88+
threading_helper.run_concurrently([reader] * n + [mutator] * n)
21889

21990

22091
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)