]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-140260: fix data race in `_struct` module initialization with subinterprete...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Thu, 13 Nov 2025 11:53:22 +0000 (12:53 +0100)
committerGitHub <noreply@github.com>
Thu, 13 Nov 2025 11:53:22 +0000 (17:23 +0530)
gh-140260: fix data race in `_struct` module initialization with subinterpreters (GH-140909)
(cherry picked from commit 63548b36998e7f7cd5c7c28b53b348a93f836737)

Co-authored-by: Shamil <ashm.tech@proton.me>
Lib/test/test_struct.py
Misc/NEWS.d/next/Core_and_Builtins/2025-11-02-15-28-33.gh-issue-140260.JNzlGz.rst [new file with mode: 0644]
Modules/_struct.c
Tools/c-analyzer/cpython/ignored.tsv

index 75c76a36ee92f5d025487fe1ad477fc1b1b1e13a..cceecdd526c006f4eb998e0b8fd769a7cb51e3c0 100644 (file)
@@ -800,6 +800,23 @@ class StructTest(ComplexesAreIdenticalMixin, unittest.TestCase):
                     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 (file)
index 0000000..96bf9b5
--- /dev/null
@@ -0,0 +1,2 @@
+Fix :mod:`struct` data race in endian table initialization with
+subinterpreters. Patch by Shamil Abdulaev.
index 3fad35a8c94ee2403eb4642d7ecb3283e06b9438..dc50c167641f5e768f871d3edc30b300d06c16d8 100644 (file)
@@ -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)
@@ -2713,47 +2761,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);
index 687871068e8baebdd1a866cacbad250568475f0c..ba9119a06d57e2b7003c835256efdd872487028a 100644 (file)
@@ -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)