Skip to content

Commit 64afa94

Browse files
gpsheadclaude
andauthored
gh-146302: make Py_IsInitialized() thread-safe and reflect true init completion (GH-146303)
## Summary - Move the `runtime->initialized = 1` store from before `site.py` import to the end of `init_interp_main()`, so `Py_IsInitialized()` only returns true after initialization has fully completed - Access `initialized` and `core_initialized` through new inline accessors using acquire/release atomics, to also protect from data race undefined behavior - `PySys_AddAuditHook()` now uses the accessor, so with the flag move it correctly skips audit hook invocation during all init phases (matching the documented "after runtime initialization" behavior) ... We could argue that running these earlier would be good even if the intent was never explicitly expressed, but that'd be its own issue. ## Motivation `Py_IsInitialized()` returned 1 while `Py_InitializeEx()` was still running — specifically, before `site.py` had been imported. See PyO3/pyo3#5900 where a second thread could acquire the GIL and start executing Python with an incomplete `sys.path` because `site.py` hadn't finished. The flag was also a plain `int` with no atomic operations, making concurrent reads a C-standard data race, though unlikely to manifest. ## Regression test: The added test properly fails on `main` with `ERROR: Py_IsInitialized() was true during site import`. --- Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6e2272d commit 64afa94

File tree

10 files changed

+119
-23
lines changed

10 files changed

+119
-23
lines changed

Doc/c-api/interp-lifecycle.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,11 @@ Initializing and finalizing the interpreter
410410
(zero) if not. After :c:func:`Py_FinalizeEx` is called, this returns false until
411411
:c:func:`Py_Initialize` is called again.
412412
413+
.. versionchanged:: next
414+
This function no longer returns true until initialization has fully
415+
completed, including import of the :mod:`site` module. Previously it
416+
could return true while :c:func:`Py_Initialize` was still running.
417+
413418
414419
.. c:function:: int Py_IsFinalizing()
415420

Include/internal/pycore_runtime.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,29 @@ _PyRuntimeState_SetFinalizing(_PyRuntimeState *runtime, PyThreadState *tstate) {
5656
}
5757
}
5858

59+
// Atomic so a thread that reads initialized=1 observes all writes
60+
// from the initialization sequence (gh-146302).
61+
62+
static inline int
63+
_PyRuntimeState_GetCoreInitialized(_PyRuntimeState *runtime) {
64+
return _Py_atomic_load_int(&runtime->core_initialized);
65+
}
66+
67+
static inline void
68+
_PyRuntimeState_SetCoreInitialized(_PyRuntimeState *runtime, int initialized) {
69+
_Py_atomic_store_int(&runtime->core_initialized, initialized);
70+
}
71+
72+
static inline int
73+
_PyRuntimeState_GetInitialized(_PyRuntimeState *runtime) {
74+
return _Py_atomic_load_int(&runtime->initialized);
75+
}
76+
77+
static inline void
78+
_PyRuntimeState_SetInitialized(_PyRuntimeState *runtime, int initialized) {
79+
_Py_atomic_store_int(&runtime->initialized, initialized);
80+
}
81+
5982

6083
#ifdef __cplusplus
6184
}

Include/internal/pycore_runtime_structs.h

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,18 @@ struct pyruntimestate {
158158
/* Is Python preinitialized? Set to 1 by Py_PreInitialize() */
159159
int preinitialized;
160160

161-
/* Is Python core initialized? Set to 1 by _Py_InitializeCore() */
161+
/* Is Python core initialized? Set to 1 by _Py_InitializeCore().
162+
163+
Use _PyRuntimeState_GetCoreInitialized() and
164+
_PyRuntimeState_SetCoreInitialized() to access it,
165+
don't access it directly. */
162166
int core_initialized;
163167

164-
/* Is Python fully initialized? Set to 1 by Py_Initialize() */
168+
/* Is Python fully initialized? Set to 1 by Py_Initialize().
169+
170+
Use _PyRuntimeState_GetInitialized() and
171+
_PyRuntimeState_SetInitialized() to access it,
172+
don't access it directly. */
165173
int initialized;
166174

167175
/* Set by Py_FinalizeEx(). Only reset to NULL if Py_Initialize()

Lib/test/test_embed.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1930,6 +1930,12 @@ def test_init_in_background_thread(self):
19301930
out, err = self.run_embedded_interpreter("test_init_in_background_thread")
19311931
self.assertEqual(err, "")
19321932

1933+
def test_isinitialized_false_during_site_import(self):
1934+
# gh-146302: Py_IsInitialized() must not return true during site import.
1935+
out, err = self.run_embedded_interpreter(
1936+
"test_isinitialized_false_during_site_import")
1937+
self.assertEqual(err, "")
1938+
19331939

19341940
class AuditingTests(EmbeddingTestsMixin, unittest.TestCase):
19351941
def test_open_code_hook(self):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:c:func:`Py_IsInitialized` no longer returns true until initialization has
2+
fully completed, including import of the :mod:`site` module. The underlying
3+
runtime flags now use atomic operations.

Programs/_testembed.c

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2000,6 +2000,8 @@ static int test_init_main(void)
20002000
config._init_main = 0;
20012001
init_from_config_clear(&config);
20022002

2003+
assert(Py_IsInitialized() == 0);
2004+
20032005
/* sys.stdout don't exist yet: it is created by _Py_InitializeMain() */
20042006
int res = PyRun_SimpleString(
20052007
"import sys; "
@@ -2203,6 +2205,52 @@ static int test_init_in_background_thread(void)
22032205
return PyThread_join_thread(handle);
22042206
}
22052207

2208+
/* gh-146302: Py_IsInitialized() must not return true during site import. */
2209+
static int _initialized_during_site_import = -1; /* -1 = not observed */
2210+
2211+
static int hook_check_initialized_on_site_import(
2212+
const char *event, PyObject *args, void *userData)
2213+
{
2214+
if (strcmp(event, "import") == 0 && args != NULL) {
2215+
PyObject *name = PyTuple_GetItem(args, 0);
2216+
if (name != NULL && PyUnicode_Check(name)
2217+
&& PyUnicode_CompareWithASCIIString(name, "site") == 0
2218+
&& _initialized_during_site_import == -1)
2219+
{
2220+
_initialized_during_site_import = Py_IsInitialized();
2221+
}
2222+
}
2223+
return 0;
2224+
}
2225+
2226+
static int test_isinitialized_false_during_site_import(void)
2227+
{
2228+
_initialized_during_site_import = -1;
2229+
2230+
/* Register audit hook before initialization */
2231+
PySys_AddAuditHook(hook_check_initialized_on_site_import, NULL);
2232+
2233+
_testembed_initialize();
2234+
2235+
if (_initialized_during_site_import == -1) {
2236+
error("audit hook never observed site import");
2237+
Py_Finalize();
2238+
return 1;
2239+
}
2240+
if (_initialized_during_site_import != 0) {
2241+
error("Py_IsInitialized() was true during site import");
2242+
Py_Finalize();
2243+
return 1;
2244+
}
2245+
if (!Py_IsInitialized()) {
2246+
error("Py_IsInitialized() was false after Py_Initialize()");
2247+
return 1;
2248+
}
2249+
2250+
Py_Finalize();
2251+
return 0;
2252+
}
2253+
22062254

22072255
#ifndef MS_WINDOWS
22082256
#include "test_frozenmain.h" // M_test_frozenmain
@@ -2693,6 +2741,7 @@ static struct TestCase TestCases[] = {
26932741
{"test_init_use_frozen_modules", test_init_use_frozen_modules},
26942742
{"test_init_main_interpreter_settings", test_init_main_interpreter_settings},
26952743
{"test_init_in_background_thread", test_init_in_background_thread},
2744+
{"test_isinitialized_false_during_site_import", test_isinitialized_false_during_site_import},
26962745

26972746
// Audit
26982747
{"test_open_code_hook", test_open_code_hook},

Python/preconfig.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -928,7 +928,7 @@ _PyPreConfig_Write(const PyPreConfig *src_config)
928928
return status;
929929
}
930930

931-
if (_PyRuntime.core_initialized) {
931+
if (_Py_IsCoreInitialized()) {
932932
/* bpo-34008: Calling this functions after Py_Initialize() ignores
933933
the new configuration. */
934934
return _PyStatus_OK();

Python/pylifecycle.c

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,13 @@ int (*_PyOS_mystrnicmp_hack)(const char *, const char *, Py_ssize_t) = \
170170
int
171171
_Py_IsCoreInitialized(void)
172172
{
173-
return _PyRuntime.core_initialized;
173+
return _PyRuntimeState_GetCoreInitialized(&_PyRuntime);
174174
}
175175

176176
int
177177
Py_IsInitialized(void)
178178
{
179-
return _PyRuntime.initialized;
179+
return _PyRuntimeState_GetInitialized(&_PyRuntime);
180180
}
181181

182182

@@ -530,7 +530,7 @@ static PyStatus
530530
pycore_init_runtime(_PyRuntimeState *runtime,
531531
const PyConfig *config)
532532
{
533-
if (runtime->initialized) {
533+
if (_PyRuntimeState_GetInitialized(runtime)) {
534534
return _PyStatus_ERR("main interpreter already initialized");
535535
}
536536

@@ -1032,7 +1032,7 @@ pyinit_config(_PyRuntimeState *runtime,
10321032
}
10331033

10341034
/* Only when we get here is the runtime core fully initialized */
1035-
runtime->core_initialized = 1;
1035+
_PyRuntimeState_SetCoreInitialized(runtime, 1);
10361036
return _PyStatus_OK();
10371037
}
10381038

@@ -1359,7 +1359,7 @@ init_interp_main(PyThreadState *tstate)
13591359
* or pure Python code in the standard library won't work.
13601360
*/
13611361
if (is_main_interp) {
1362-
interp->runtime->initialized = 1;
1362+
_PyRuntimeState_SetInitialized(interp->runtime, 1);
13631363
}
13641364
return _PyStatus_OK();
13651365
}
@@ -1471,8 +1471,6 @@ init_interp_main(PyThreadState *tstate)
14711471
Py_XDECREF(warnings_module);
14721472
}
14731473
Py_XDECREF(warnoptions);
1474-
1475-
interp->runtime->initialized = 1;
14761474
}
14771475

14781476
if (config->site_import) {
@@ -1568,6 +1566,10 @@ init_interp_main(PyThreadState *tstate)
15681566

15691567
assert(!_PyErr_Occurred(tstate));
15701568

1569+
if (is_main_interp) {
1570+
_PyRuntimeState_SetInitialized(interp->runtime, 1);
1571+
}
1572+
15711573
return _PyStatus_OK();
15721574
}
15731575

@@ -1587,11 +1589,11 @@ static PyStatus
15871589
pyinit_main(PyThreadState *tstate)
15881590
{
15891591
PyInterpreterState *interp = tstate->interp;
1590-
if (!interp->runtime->core_initialized) {
1592+
if (!_PyRuntimeState_GetCoreInitialized(interp->runtime)) {
15911593
return _PyStatus_ERR("runtime core not initialized");
15921594
}
15931595

1594-
if (interp->runtime->initialized) {
1596+
if (_PyRuntimeState_GetInitialized(interp->runtime)) {
15951597
return pyinit_main_reconfigure(tstate);
15961598
}
15971599

@@ -1645,9 +1647,8 @@ Py_InitializeEx(int install_sigs)
16451647
if (_PyStatus_EXCEPTION(status)) {
16461648
Py_ExitStatusException(status);
16471649
}
1648-
_PyRuntimeState *runtime = &_PyRuntime;
16491650

1650-
if (runtime->initialized) {
1651+
if (Py_IsInitialized()) {
16511652
/* bpo-33932: Calling Py_Initialize() twice does nothing. */
16521653
return;
16531654
}
@@ -2352,7 +2353,7 @@ _Py_Finalize(_PyRuntimeState *runtime)
23522353
int status = 0;
23532354

23542355
/* Bail out early if already finalized (or never initialized). */
2355-
if (!runtime->initialized) {
2356+
if (!_PyRuntimeState_GetInitialized(runtime)) {
23562357
return status;
23572358
}
23582359

@@ -2387,8 +2388,8 @@ _Py_Finalize(_PyRuntimeState *runtime)
23872388
when they attempt to take the GIL (ex: PyEval_RestoreThread()). */
23882389
_PyInterpreterState_SetFinalizing(tstate->interp, tstate);
23892390
_PyRuntimeState_SetFinalizing(runtime, tstate);
2390-
runtime->initialized = 0;
2391-
runtime->core_initialized = 0;
2391+
_PyRuntimeState_SetInitialized(runtime, 0);
2392+
_PyRuntimeState_SetCoreInitialized(runtime, 0);
23922393

23932394
// XXX Call something like _PyImport_Disable() here?
23942395

@@ -2614,7 +2615,7 @@ new_interpreter(PyThreadState **tstate_p,
26142615
}
26152616
_PyRuntimeState *runtime = &_PyRuntime;
26162617

2617-
if (!runtime->initialized) {
2618+
if (!_PyRuntimeState_GetInitialized(runtime)) {
26182619
return _PyStatus_ERR("Py_Initialize must be called first");
26192620
}
26202621

@@ -3454,10 +3455,10 @@ fatal_error_dump_runtime(int fd, _PyRuntimeState *runtime)
34543455
_Py_DumpHexadecimal(fd, (uintptr_t)finalizing, sizeof(finalizing) * 2);
34553456
PUTS(fd, ")");
34563457
}
3457-
else if (runtime->initialized) {
3458+
else if (_PyRuntimeState_GetInitialized(runtime)) {
34583459
PUTS(fd, "initialized");
34593460
}
3460-
else if (runtime->core_initialized) {
3461+
else if (_PyRuntimeState_GetCoreInitialized(runtime)) {
34613462
PUTS(fd, "core initialized");
34623463
}
34633464
else if (runtime->preinitialized) {

Python/pystate.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,8 @@ init_runtime(_PyRuntimeState *runtime,
330330
{
331331
assert(!runtime->preinitializing);
332332
assert(!runtime->preinitialized);
333-
assert(!runtime->core_initialized);
334-
assert(!runtime->initialized);
333+
assert(!_PyRuntimeState_GetCoreInitialized(runtime));
334+
assert(!_PyRuntimeState_GetInitialized(runtime));
335335
assert(!runtime->_initialized);
336336

337337
runtime->open_code_hook = open_code_hook;

Python/sysmodule.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Data members:
3434
#include "pycore_pymem.h" // _PyMem_DefaultRawFree()
3535
#include "pycore_pystate.h" // _PyThreadState_GET()
3636
#include "pycore_pystats.h" // _Py_PrintSpecializationStats()
37+
#include "pycore_runtime.h" // _PyRuntimeState_Get*()
3738
#include "pycore_structseq.h" // _PyStructSequence_InitBuiltinWithFlags()
3839
#include "pycore_sysmodule.h" // export _PySys_GetSizeOf()
3940
#include "pycore_unicodeobject.h" // _PyUnicode_InternImmortal()
@@ -471,7 +472,7 @@ PySys_AddAuditHook(Py_AuditHookFunction hook, void *userData)
471472
PySys_AddAuditHook() can be called before Python is initialized. */
472473
_PyRuntimeState *runtime = &_PyRuntime;
473474
PyThreadState *tstate;
474-
if (runtime->initialized) {
475+
if (_PyRuntimeState_GetInitialized(runtime)) {
475476
tstate = _PyThreadState_GET();
476477
}
477478
else {

0 commit comments

Comments
 (0)