Skip to content

Commit 52be7f4

Browse files
authored
gh-133931: Introduce _PyObject_XSetRefDelayed to replace Py_XSETREF (gh-134377)
1 parent cb39410 commit 52be7f4

File tree

6 files changed

+104
-21
lines changed

6 files changed

+104
-21
lines changed

Include/internal/pycore_object.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,27 @@ _Py_TryIncref(PyObject *op)
767767
#endif
768768
}
769769

770+
// Enqueue an object to be freed possibly after some delay
771+
#ifdef Py_GIL_DISABLED
772+
PyAPI_FUNC(void) _PyObject_XDecRefDelayed(PyObject *obj);
773+
#else
774+
static inline void _PyObject_XDecRefDelayed(PyObject *obj)
775+
{
776+
Py_XDECREF(obj);
777+
}
778+
#endif
779+
780+
#ifdef Py_GIL_DISABLED
781+
// Same as `Py_XSETREF` but in free-threading, it stores the object atomically
782+
// and queues the old object to be decrefed at a safe point using QSBR.
783+
PyAPI_FUNC(void) _PyObject_XSetRefDelayed(PyObject **p_obj, PyObject *obj);
784+
#else
785+
static inline void _PyObject_XSetRefDelayed(PyObject **p_obj, PyObject *obj)
786+
{
787+
Py_XSETREF(*p_obj, obj);
788+
}
789+
#endif
790+
770791
#ifdef Py_REF_DEBUG
771792
extern void _PyInterpreterState_FinalizeRefTotal(PyInterpreterState *);
772793
extern void _Py_FinalizeRefTotal(_PyRuntimeState *);

Include/internal/pycore_pymem.h

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,6 @@ extern int _PyMem_DebugEnabled(void);
9090
// Enqueue a pointer to be freed possibly after some delay.
9191
extern void _PyMem_FreeDelayed(void *ptr);
9292

93-
// Enqueue an object to be freed possibly after some delay
94-
#ifdef Py_GIL_DISABLED
95-
PyAPI_FUNC(void) _PyObject_XDecRefDelayed(PyObject *obj);
96-
#else
97-
static inline void _PyObject_XDecRefDelayed(PyObject *obj)
98-
{
99-
Py_XDECREF(obj);
100-
}
101-
#endif
102-
10393
// Periodically process delayed free requests.
10494
extern void _PyMem_ProcessDelayed(PyThreadState *tstate);
10595

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import concurrent.futures
2+
import unittest
3+
from threading import Barrier
4+
from unittest import TestCase
5+
import random
6+
import time
7+
8+
from test.support import threading_helper, Py_GIL_DISABLED
9+
10+
threading_helper.requires_working_threading(module=True)
11+
12+
13+
def random_sleep():
14+
delay_us = random.randint(50, 100)
15+
time.sleep(delay_us * 1e-6)
16+
17+
def random_string():
18+
return ''.join(random.choice('0123456789ABCDEF') for _ in range(10))
19+
20+
def set_gen_name(g, b):
21+
b.wait()
22+
random_sleep()
23+
g.__name__ = random_string()
24+
return g.__name__
25+
26+
def set_gen_qualname(g, b):
27+
b.wait()
28+
random_sleep()
29+
g.__qualname__ = random_string()
30+
return g.__qualname__
31+
32+
33+
@unittest.skipUnless(Py_GIL_DISABLED, "Enable only in FT build")
34+
class TestFTGenerators(TestCase):
35+
NUM_THREADS = 4
36+
37+
def concurrent_write_with_func(self, func):
38+
gen = (x for x in range(42))
39+
for j in range(1000):
40+
with concurrent.futures.ThreadPoolExecutor(max_workers=self.NUM_THREADS) as executor:
41+
b = Barrier(self.NUM_THREADS)
42+
futures = {executor.submit(func, gen, b): i for i in range(self.NUM_THREADS)}
43+
for fut in concurrent.futures.as_completed(futures):
44+
gen_name = fut.result()
45+
self.assertEqual(len(gen_name), 10)
46+
47+
def test_concurrent_write(self):
48+
with self.subTest(func=set_gen_name):
49+
self.concurrent_write_with_func(func=set_gen_name)
50+
with self.subTest(func=set_gen_qualname):
51+
self.concurrent_write_with_func(func=set_gen_qualname)

Objects/genobject.c

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,8 @@ static PyObject *
704704
gen_get_name(PyObject *self, void *Py_UNUSED(ignored))
705705
{
706706
PyGenObject *op = _PyGen_CAST(self);
707-
return Py_NewRef(op->gi_name);
707+
PyObject *name = FT_ATOMIC_LOAD_PTR_ACQUIRE(op->gi_name);
708+
return Py_NewRef(name);
708709
}
709710

710711
static int
@@ -718,15 +719,20 @@ gen_set_name(PyObject *self, PyObject *value, void *Py_UNUSED(ignored))
718719
"__name__ must be set to a string object");
719720
return -1;
720721
}
721-
Py_XSETREF(op->gi_name, Py_NewRef(value));
722+
Py_BEGIN_CRITICAL_SECTION(self);
723+
// gh-133931: To prevent use-after-free from other threads that reference
724+
// the gi_name.
725+
_PyObject_XSetRefDelayed(&op->gi_name, Py_NewRef(value));
726+
Py_END_CRITICAL_SECTION();
722727
return 0;
723728
}
724729

725730
static PyObject *
726731
gen_get_qualname(PyObject *self, void *Py_UNUSED(ignored))
727732
{
728733
PyGenObject *op = _PyGen_CAST(self);
729-
return Py_NewRef(op->gi_qualname);
734+
PyObject *qualname = FT_ATOMIC_LOAD_PTR_ACQUIRE(op->gi_qualname);
735+
return Py_NewRef(qualname);
730736
}
731737

732738
static int
@@ -740,7 +746,11 @@ gen_set_qualname(PyObject *self, PyObject *value, void *Py_UNUSED(ignored))
740746
"__qualname__ must be set to a string object");
741747
return -1;
742748
}
743-
Py_XSETREF(op->gi_qualname, Py_NewRef(value));
749+
Py_BEGIN_CRITICAL_SECTION(self);
750+
// gh-133931: To prevent use-after-free from other threads that reference
751+
// the gi_qualname.
752+
_PyObject_XSetRefDelayed(&op->gi_qualname, Py_NewRef(value));
753+
Py_END_CRITICAL_SECTION();
744754
return 0;
745755
}
746756

Objects/obmalloc.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,21 @@ _PyObject_XDecRefDelayed(PyObject *ptr)
12311231
}
12321232
#endif
12331233

1234+
#ifdef Py_GIL_DISABLED
1235+
void
1236+
_PyObject_XSetRefDelayed(PyObject **ptr, PyObject *value)
1237+
{
1238+
PyObject *old = *ptr;
1239+
FT_ATOMIC_STORE_PTR_RELEASE(*ptr, value);
1240+
if (old == NULL) {
1241+
return;
1242+
}
1243+
if (!_Py_IsImmortal(old)) {
1244+
_PyObject_XDecRefDelayed(old);
1245+
}
1246+
}
1247+
#endif
1248+
12341249
static struct _mem_work_chunk *
12351250
work_queue_first(struct llist_node *head)
12361251
{

Objects/typeobject.c

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3967,13 +3967,9 @@ _PyObject_SetDict(PyObject *obj, PyObject *value)
39673967
return -1;
39683968
}
39693969
Py_BEGIN_CRITICAL_SECTION(obj);
3970-
PyObject *olddict = *dictptr;
3971-
FT_ATOMIC_STORE_PTR_RELEASE(*dictptr, Py_NewRef(value));
3972-
#ifdef Py_GIL_DISABLED
3973-
_PyObject_XDecRefDelayed(olddict);
3974-
#else
3975-
Py_XDECREF(olddict);
3976-
#endif
3970+
// gh-133980: To prevent use-after-free from other threads that reference
3971+
// the __dict__
3972+
_PyObject_XSetRefDelayed(dictptr, Py_NewRef(value));
39773973
Py_END_CRITICAL_SECTION();
39783974
return 0;
39793975
}

0 commit comments

Comments
 (0)