https://github.com/python/cpython/commit/63548b36998e7f7cd5c7c28b53b348a93f836737
commit: 63548b36998e7f7cd5c7c28b53b348a93f836737
branch: main
author: Shamil <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2025-11-13T16:31:31+05:30
summary:
gh-140260: fix data race in `_struct` module initialization with
subinterpreters (#140909)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2025-11-02-15-28-33.gh-issue-140260.JNzlGz.rst
M Lib/test/test_struct.py
M Modules/_struct.c
M Tools/c-analyzer/cpython/ignored.tsv
diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py
index 75c76a36ee92f5..cceecdd526c006 100644
--- a/Lib/test/test_struct.py
+++ b/Lib/test/test_struct.py
@@ -800,6 +800,23 @@ def test_c_complex_round_trip(self):
round_trip = struct.unpack(f, struct.pack(f, z))[0]
self.assertComplexesAreIdentical(z, round_trip)
+ @unittest.skipIf(
+ support.is_android or support.is_apple_mobile,
+ "Subinterpreters are not supported on Android and iOS"
+ )
+ def test_endian_table_init_subinterpreters(self):
+ # Verify that the _struct extension module can be initialized
+ # concurrently in subinterpreters (gh-140260).
+ try:
+ from concurrent.futures import InterpreterPoolExecutor
+ except ImportError:
+ raise unittest.SkipTest("InterpreterPoolExecutor not available")
+
+ code = "import struct"
+ with InterpreterPoolExecutor(max_workers=5) as executor:
+ results = executor.map(exec, [code] * 5)
+ self.assertListEqual(list(results), [None] * 5)
+
class UnpackIteratorTest(unittest.TestCase):
"""
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-02-15-28-33.gh-issue-140260.JNzlGz.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-02-15-28-33.gh-issue-140260.JNzlGz.rst
new file mode 100644
index 00000000000000..96bf9b51e4862c
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-02-15-28-33.gh-issue-140260.JNzlGz.rst
@@ -0,0 +1,2 @@
+Fix :mod:`struct` data race in endian table initialization with
+subinterpreters. Patch by Shamil Abdulaev.
diff --git a/Modules/_struct.c b/Modules/_struct.c
index f09252e82c3915..2acb3df3a30395 100644
--- a/Modules/_struct.c
+++ b/Modules/_struct.c
@@ -9,6 +9,7 @@
#include "Python.h"
#include "pycore_bytesobject.h" // _PyBytesWriter
+#include "pycore_lock.h" // _PyOnceFlag_CallOnce()
#include "pycore_long.h" // _PyLong_AsByteArray()
#include "pycore_moduleobject.h" // _PyModule_GetState()
#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS()
@@ -1505,6 +1506,53 @@ static formatdef lilendian_table[] = {
{0}
};
+/* Ensure endian table optimization happens exactly once across all
interpreters */
+static _PyOnceFlag endian_tables_init_once = {0};
+
+static int
+init_endian_tables(void *Py_UNUSED(arg))
+{
+ const formatdef *native = native_table;
+ formatdef *other, *ptr;
+#if PY_LITTLE_ENDIAN
+ other = lilendian_table;
+#else
+ other = bigendian_table;
+#endif
+ /* Scan through the native table, find a matching
+ entry in the endian table and swap in the
+ native implementations whenever possible
+ (64-bit platforms may not have "standard" sizes) */
+ while (native->format != '\0' && other->format != '\0') {
+ ptr = other;
+ while (ptr->format != '\0') {
+ if (ptr->format == native->format) {
+ /* Match faster when formats are
+ listed in the same order */
+ if (ptr == other)
+ other++;
+ /* Only use the trick if the
+ size matches */
+ if (ptr->size != native->size)
+ break;
+ /* Skip float and double, could be
+ "unknown" float format */
+ if (ptr->format == 'd' || ptr->format == 'f')
+ break;
+ /* Skip _Bool, semantics are different for standard size */
+ if (ptr->format == '?')
+ break;
+ ptr->pack = native->pack;
+ ptr->unpack = native->unpack;
+ break;
+ }
+ ptr++;
+ }
+ native++;
+ }
+ return 0;
+}
+
static const formatdef *
whichtable(const char **pfmt)
@@ -2710,47 +2758,8 @@ _structmodule_exec(PyObject *m)
return -1;
}
- /* Check endian and swap in faster functions */
- {
- const formatdef *native = native_table;
- formatdef *other, *ptr;
-#if PY_LITTLE_ENDIAN
- other = lilendian_table;
-#else
- other = bigendian_table;
-#endif
- /* Scan through the native table, find a matching
- entry in the endian table and swap in the
- native implementations whenever possible
- (64-bit platforms may not have "standard" sizes) */
- while (native->format != '\0' && other->format != '\0') {
- ptr = other;
- while (ptr->format != '\0') {
- if (ptr->format == native->format) {
- /* Match faster when formats are
- listed in the same order */
- if (ptr == other)
- other++;
- /* Only use the trick if the
- size matches */
- if (ptr->size != native->size)
- break;
- /* Skip float and double, could be
- "unknown" float format */
- if (ptr->format == 'd' || ptr->format == 'f')
- break;
- /* Skip _Bool, semantics are different for standard size */
- if (ptr->format == '?')
- break;
- ptr->pack = native->pack;
- ptr->unpack = native->unpack;
- break;
- }
- ptr++;
- }
- native++;
- }
- }
+ /* init cannot fail */
+ (void)_PyOnceFlag_CallOnce(&endian_tables_init_once, init_endian_tables,
NULL);
/* Add some symbolic constants to the module */
state->StructError = PyErr_NewException("struct.error", NULL, NULL);
diff --git a/Tools/c-analyzer/cpython/ignored.tsv
b/Tools/c-analyzer/cpython/ignored.tsv
index 11a3cd794ff4d7..4621ad250f4633 100644
--- a/Tools/c-analyzer/cpython/ignored.tsv
+++ b/Tools/c-analyzer/cpython/ignored.tsv
@@ -24,6 +24,7 @@ Modules/posixmodule.c os_dup2_impl dup3_works -
## guards around resource init
Python/thread_pthread.h PyThread__init_thread lib_initialized -
+Modules/_struct.c - endian_tables_init_once -
##-----------------------
## other values (not Python-specific)
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]