Skip to content

Commit 63548b3

Browse files
authored
pythongh-140260: fix data race in _struct module initialization with subinterpreters (python#140909)
1 parent 781cc68 commit 63548b3

File tree

4 files changed

+70
-41
lines changed

4 files changed

+70
-41
lines changed

Lib/test/test_struct.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,23 @@ def test_c_complex_round_trip(self):
800800
round_trip = struct.unpack(f, struct.pack(f, z))[0]
801801
self.assertComplexesAreIdentical(z, round_trip)
802802

803+
@unittest.skipIf(
804+
support.is_android or support.is_apple_mobile,
805+
"Subinterpreters are not supported on Android and iOS"
806+
)
807+
def test_endian_table_init_subinterpreters(self):
808+
# Verify that the _struct extension module can be initialized
809+
# concurrently in subinterpreters (gh-140260).
810+
try:
811+
from concurrent.futures import InterpreterPoolExecutor
812+
except ImportError:
813+
raise unittest.SkipTest("InterpreterPoolExecutor not available")
814+
815+
code = "import struct"
816+
with InterpreterPoolExecutor(max_workers=5) as executor:
817+
results = executor.map(exec, [code] * 5)
818+
self.assertListEqual(list(results), [None] * 5)
819+
803820

804821
class UnpackIteratorTest(unittest.TestCase):
805822
"""
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :mod:`struct` data race in endian table initialization with
2+
subinterpreters. Patch by Shamil Abdulaev.

Modules/_struct.c

Lines changed: 50 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include "Python.h"
1111
#include "pycore_bytesobject.h" // _PyBytesWriter
12+
#include "pycore_lock.h" // _PyOnceFlag_CallOnce()
1213
#include "pycore_long.h" // _PyLong_AsByteArray()
1314
#include "pycore_moduleobject.h" // _PyModule_GetState()
1415
#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS()
@@ -1505,6 +1506,53 @@ static formatdef lilendian_table[] = {
15051506
{0}
15061507
};
15071508

1509+
/* Ensure endian table optimization happens exactly once across all interpreters */
1510+
static _PyOnceFlag endian_tables_init_once = {0};
1511+
1512+
static int
1513+
init_endian_tables(void *Py_UNUSED(arg))
1514+
{
1515+
const formatdef *native = native_table;
1516+
formatdef *other, *ptr;
1517+
#if PY_LITTLE_ENDIAN
1518+
other = lilendian_table;
1519+
#else
1520+
other = bigendian_table;
1521+
#endif
1522+
/* Scan through the native table, find a matching
1523+
entry in the endian table and swap in the
1524+
native implementations whenever possible
1525+
(64-bit platforms may not have "standard" sizes) */
1526+
while (native->format != '\0' && other->format != '\0') {
1527+
ptr = other;
1528+
while (ptr->format != '\0') {
1529+
if (ptr->format == native->format) {
1530+
/* Match faster when formats are
1531+
listed in the same order */
1532+
if (ptr == other)
1533+
other++;
1534+
/* Only use the trick if the
1535+
size matches */
1536+
if (ptr->size != native->size)
1537+
break;
1538+
/* Skip float and double, could be
1539+
"unknown" float format */
1540+
if (ptr->format == 'd' || ptr->format == 'f')
1541+
break;
1542+
/* Skip _Bool, semantics are different for standard size */
1543+
if (ptr->format == '?')
1544+
break;
1545+
ptr->pack = native->pack;
1546+
ptr->unpack = native->unpack;
1547+
break;
1548+
}
1549+
ptr++;
1550+
}
1551+
native++;
1552+
}
1553+
return 0;
1554+
}
1555+
15081556

15091557
static const formatdef *
15101558
whichtable(const char **pfmt)
@@ -2710,47 +2758,8 @@ _structmodule_exec(PyObject *m)
27102758
return -1;
27112759
}
27122760

2713-
/* Check endian and swap in faster functions */
2714-
{
2715-
const formatdef *native = native_table;
2716-
formatdef *other, *ptr;
2717-
#if PY_LITTLE_ENDIAN
2718-
other = lilendian_table;
2719-
#else
2720-
other = bigendian_table;
2721-
#endif
2722-
/* Scan through the native table, find a matching
2723-
entry in the endian table and swap in the
2724-
native implementations whenever possible
2725-
(64-bit platforms may not have "standard" sizes) */
2726-
while (native->format != '\0' && other->format != '\0') {
2727-
ptr = other;
2728-
while (ptr->format != '\0') {
2729-
if (ptr->format == native->format) {
2730-
/* Match faster when formats are
2731-
listed in the same order */
2732-
if (ptr == other)
2733-
other++;
2734-
/* Only use the trick if the
2735-
size matches */
2736-
if (ptr->size != native->size)
2737-
break;
2738-
/* Skip float and double, could be
2739-
"unknown" float format */
2740-
if (ptr->format == 'd' || ptr->format == 'f')
2741-
break;
2742-
/* Skip _Bool, semantics are different for standard size */
2743-
if (ptr->format == '?')
2744-
break;
2745-
ptr->pack = native->pack;
2746-
ptr->unpack = native->unpack;
2747-
break;
2748-
}
2749-
ptr++;
2750-
}
2751-
native++;
2752-
}
2753-
}
2761+
/* init cannot fail */
2762+
(void)_PyOnceFlag_CallOnce(&endian_tables_init_once, init_endian_tables, NULL);
27542763

27552764
/* Add some symbolic constants to the module */
27562765
state->StructError = PyErr_NewException("struct.error", NULL, NULL);

Tools/c-analyzer/cpython/ignored.tsv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Modules/posixmodule.c os_dup2_impl dup3_works -
2424

2525
## guards around resource init
2626
Python/thread_pthread.h PyThread__init_thread lib_initialized -
27+
Modules/_struct.c - endian_tables_init_once -
2728

2829
##-----------------------
2930
## other values (not Python-specific)

0 commit comments

Comments
 (0)