summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Gross <colesbury@gmail.com>2024-06-03 22:21:32 (GMT)
committerGitHub <noreply@github.com>2024-06-03 22:21:32 (GMT)
commitae705319fcde864b504987dc8e579e3eef68e1e5 (patch)
tree022166446a352d99b784b44a86f163bec179e94d
parentca37034baa2909722df58c02dfd13e1d667252ce (diff)
downloadcpython-ae705319fcde864b504987dc8e579e3eef68e1e5.zip
cpython-ae705319fcde864b504987dc8e579e3eef68e1e5.tar.gz
cpython-ae705319fcde864b504987dc8e579e3eef68e1e5.tar.bz2
[3.13] gh-117657: Fix race involving immortalizing objects (GH-119927) (#120005)
The free-threaded build currently immortalizes objects that use deferred reference counting (see gh-117783). This typically happens once the first non-main thread is created, but the behavior can be suppressed for tests, in subinterpreters, or during a compile() call. This fixes a race condition involving the tracking of whether the behavior is suppressed. (cherry picked from commit 47fb4327b5c405da6df066dcaa01b7c1aefab313)
-rw-r--r--Include/internal/pycore_gc.h14
-rw-r--r--Lib/test/support/__init__.py4
-rw-r--r--Modules/_testinternalcapi.c24
-rw-r--r--Objects/codeobject.c4
-rw-r--r--Objects/object.c2
-rw-r--r--Python/bltinmodule.c6
-rw-r--r--Python/gc_free_threading.c14
-rw-r--r--Python/pystate.c4
-rw-r--r--Tools/tsan/suppressions_free_threading.txt2
9 files changed, 30 insertions, 44 deletions
diff --git a/Include/internal/pycore_gc.h b/Include/internal/pycore_gc.h
index 6058252..ba8b8e1 100644
--- a/Include/internal/pycore_gc.h
+++ b/Include/internal/pycore_gc.h
@@ -347,15 +347,11 @@ struct _gc_runtime_state {
/* gh-117783: Deferred reference counting is not fully implemented yet, so
as a temporary measure we treat objects using deferred referenence
- counting as immortal. */
- struct {
- /* Immortalize objects instead of marking them as using deferred
- reference counting. */
- int enabled;
-
- /* Set enabled=1 when the first background thread is created. */
- int enable_on_thread_created;
- } immortalize;
+ counting as immortal. The value may be zero, one, or a negative number:
+ 0: immortalize deferred RC objects once the first thread is created
+ 1: immortalize all deferred RC objects immediately
+ <0: suppressed; don't immortalize objects */
+ int immortalize;
#endif
};
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index e2a8546..d7a3a54 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -529,11 +529,11 @@ def suppress_immortalization(suppress=True):
yield
return
- old_values = _testinternalcapi.set_immortalize_deferred(False)
+ _testinternalcapi.suppress_immortalization(True)
try:
yield
finally:
- _testinternalcapi.set_immortalize_deferred(*old_values)
+ _testinternalcapi.suppress_immortalization(False)
def skip_if_suppress_immortalization():
try:
diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c
index 129c136..8c65b80 100644
--- a/Modules/_testinternalcapi.c
+++ b/Modules/_testinternalcapi.c
@@ -1965,24 +1965,18 @@ get_py_thread_id(PyObject *self, PyObject *Py_UNUSED(ignored))
#endif
static PyObject *
-set_immortalize_deferred(PyObject *self, PyObject *value)
+suppress_immortalization(PyObject *self, PyObject *value)
{
#ifdef Py_GIL_DISABLED
- PyInterpreterState *interp = PyInterpreterState_Get();
- int old_enabled = interp->gc.immortalize.enabled;
- int old_enabled_on_thread = interp->gc.immortalize.enable_on_thread_created;
- int enabled_on_thread = 0;
- if (!PyArg_ParseTuple(value, "i|i",
- &interp->gc.immortalize.enabled,
- &enabled_on_thread))
- {
+ int suppress = PyObject_IsTrue(value);
+ if (suppress < 0) {
return NULL;
}
- interp->gc.immortalize.enable_on_thread_created = enabled_on_thread;
- return Py_BuildValue("ii", old_enabled, old_enabled_on_thread);
-#else
- return Py_BuildValue("OO", Py_False, Py_False);
+ PyInterpreterState *interp = PyInterpreterState_Get();
+ // Subtract two to suppress immortalization (so that 1 -> -1)
+ _Py_atomic_add_int(&interp->gc.immortalize, suppress ? -2 : 2);
#endif
+ Py_RETURN_NONE;
}
static PyObject *
@@ -1990,7 +1984,7 @@ get_immortalize_deferred(PyObject *self, PyObject *Py_UNUSED(ignored))
{
#ifdef Py_GIL_DISABLED
PyInterpreterState *interp = PyInterpreterState_Get();
- return PyBool_FromLong(interp->gc.immortalize.enable_on_thread_created);
+ return PyBool_FromLong(_Py_atomic_load_int(&interp->gc.immortalize) >= 0);
#else
Py_RETURN_FALSE;
#endif
@@ -2110,7 +2104,7 @@ static PyMethodDef module_functions[] = {
#ifdef Py_GIL_DISABLED
{"py_thread_id", get_py_thread_id, METH_NOARGS},
#endif
- {"set_immortalize_deferred", set_immortalize_deferred, METH_VARARGS},
+ {"suppress_immortalization", suppress_immortalization, METH_O},
{"get_immortalize_deferred", get_immortalize_deferred, METH_NOARGS},
#ifdef _Py_TIER2
{"uop_symbols_test", _Py_uop_symbols_test, METH_NOARGS},
diff --git a/Objects/codeobject.c b/Objects/codeobject.c
index 3d804f7..23fc8a7 100644
--- a/Objects/codeobject.c
+++ b/Objects/codeobject.c
@@ -110,7 +110,7 @@ should_intern_string(PyObject *o)
// unless we've disabled immortalizing objects that use deferred reference
// counting.
PyInterpreterState *interp = _PyInterpreterState_GET();
- if (interp->gc.immortalize.enable_on_thread_created) {
+ if (_Py_atomic_load_int(&interp->gc.immortalize) < 0) {
return 1;
}
#endif
@@ -240,7 +240,7 @@ intern_constants(PyObject *tuple, int *modified)
PyThreadState *tstate = PyThreadState_GET();
if (!_Py_IsImmortal(v) && !PyCode_Check(v) &&
!PyUnicode_CheckExact(v) &&
- tstate->interp->gc.immortalize.enable_on_thread_created)
+ _Py_atomic_load_int(&tstate->interp->gc.immortalize) >= 0)
{
PyObject *interned = intern_one_constant(v);
if (interned == NULL) {
diff --git a/Objects/object.c b/Objects/object.c
index a4c6999..ce3df37 100644
--- a/Objects/object.c
+++ b/Objects/object.c
@@ -2433,7 +2433,7 @@ _PyObject_SetDeferredRefcount(PyObject *op)
assert(op->ob_ref_shared == 0);
_PyObject_SET_GC_BITS(op, _PyGC_BITS_DEFERRED);
PyInterpreterState *interp = _PyInterpreterState_GET();
- if (interp->gc.immortalize.enabled) {
+ if (_Py_atomic_load_int_relaxed(&interp->gc.immortalize) == 1) {
// gh-117696: immortalize objects instead of using deferred reference
// counting for now.
_Py_SetImmortal(op);
diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c
index d192d5b..351fc84 100644
--- a/Python/bltinmodule.c
+++ b/Python/bltinmodule.c
@@ -870,15 +870,15 @@ builtin_compile_impl(PyObject *module, PyObject *source, PyObject *filename,
// gh-118527: Disable immortalization of code constants for explicit
// compile() calls to get consistent frozen outputs between the default
// and free-threaded builds.
+ // Subtract two to suppress immortalization (so that 1 -> -1)
PyInterpreterState *interp = _PyInterpreterState_GET();
- int old_value = interp->gc.immortalize.enable_on_thread_created;
- interp->gc.immortalize.enable_on_thread_created = 0;
+ _Py_atomic_add_int(&interp->gc.immortalize, -2);
#endif
result = Py_CompileStringObject(str, filename, start[compile_mode], &cf, optimize);
#ifdef Py_GIL_DISABLED
- interp->gc.immortalize.enable_on_thread_created = old_value;
+ _Py_atomic_add_int(&interp->gc.immortalize, 2);
#endif
Py_XDECREF(source_copy);
diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c
index e6bd012..d005b79 100644
--- a/Python/gc_free_threading.c
+++ b/Python/gc_free_threading.c
@@ -703,11 +703,9 @@ _PyGC_Init(PyInterpreterState *interp)
{
GCState *gcstate = &interp->gc;
- if (_Py_IsMainInterpreter(interp)) {
- // gh-117783: immortalize objects that would use deferred refcounting
- // once the first non-main thread is created.
- gcstate->immortalize.enable_on_thread_created = 1;
- }
+ // gh-117783: immortalize objects that would use deferred refcounting
+ // once the first non-main thread is created (but not in subinterpreters).
+ gcstate->immortalize = _Py_IsMainInterpreter(interp) ? 0 : -1;
gcstate->garbage = PyList_New(0);
if (gcstate->garbage == NULL) {
@@ -1808,8 +1806,10 @@ _PyGC_ImmortalizeDeferredObjects(PyInterpreterState *interp)
{
struct visitor_args args;
_PyEval_StopTheWorld(interp);
- gc_visit_heaps(interp, &immortalize_visitor, &args);
- interp->gc.immortalize.enabled = 1;
+ if (interp->gc.immortalize == 0) {
+ gc_visit_heaps(interp, &immortalize_visitor, &args);
+ interp->gc.immortalize = 1;
+ }
_PyEval_StartTheWorld(interp);
}
diff --git a/Python/pystate.c b/Python/pystate.c
index 36e4206..d029391 100644
--- a/Python/pystate.c
+++ b/Python/pystate.c
@@ -1583,9 +1583,7 @@ new_threadstate(PyInterpreterState *interp, int whence)
}
else {
#ifdef Py_GIL_DISABLED
- if (interp->gc.immortalize.enable_on_thread_created &&
- !interp->gc.immortalize.enabled)
- {
+ if (_Py_atomic_load_int(&interp->gc.immortalize) == 0) {
// Immortalize objects marked as using deferred reference counting
// the first time a non-main thread is created.
_PyGC_ImmortalizeDeferredObjects(interp);
diff --git a/Tools/tsan/suppressions_free_threading.txt b/Tools/tsan/suppressions_free_threading.txt
index d150e90..0bb0183 100644
--- a/Tools/tsan/suppressions_free_threading.txt
+++ b/Tools/tsan/suppressions_free_threading.txt
@@ -47,7 +47,6 @@ race_top:_PyImport_AcquireLock
race_top:_Py_dict_lookup_threadsafe
race_top:_imp_release_lock
race_top:_multiprocessing_SemLock_acquire_impl
-race_top:builtin_compile_impl
race_top:count_next
race_top:dictiter_new
race_top:dictresize
@@ -56,7 +55,6 @@ race_top:insertdict
race_top:list_get_item_ref
race_top:make_pending_calls
race_top:set_add_entry
-race_top:should_intern_string
race_top:_Py_slot_tp_getattr_hook
race_top:add_threadstate
race_top:dump_traceback