Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Lib/test/test_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import gc
import math
import operator
import threading
import unittest
import platform
import struct
Expand Down Expand Up @@ -39,6 +40,24 @@ def bigendian_to_native(value):
else:
return string_reverse(value)

# Helper for test_endian_table_init_subinterpreters
class _Result:
def __init__(self):
self.ret = -1
self.err = None

def _run_in_subinterp_worker(code, barrier, result):
"""
Worker function for a thread. Waits on the barrier, then runs the
code in a subinterpreter.
"""
try:
# Wait until all threads are ready to start simultaneously.
barrier.wait()
result.ret = support.run_in_subinterp(code)
except Exception as e:
result.err = e

class StructTest(ComplexesAreIdenticalMixin, unittest.TestCase):
def test_isbigendian(self):
self.assertEqual((struct.pack('=i', 1)[0] == 0), ISBIGENDIAN)
Expand Down Expand Up @@ -800,6 +819,44 @@ def test_c_complex_round_trip(self):
round_trip = struct.unpack(f, struct.pack(f, z))[0]
self.assertComplexesAreIdentical(z, round_trip)

def test_endian_table_init_subinterpreters(self):
# Verify that struct works correctly in subinterpreters after
# once-only endian table initialization (gh-140260), when
# initialized concurrently.
# Use struct in the main interpreter first.
self.assertEqual(struct.unpack('>i', struct.pack('>i', 1))[0], 1)
self.assertEqual(struct.unpack('<i', struct.pack('<i', 1))[0], 1)

code = (
"import struct\n"
"x = struct.pack('>i', 1)\n"
"assert struct.unpack('>i', x)[0] == 1\n"
"y = struct.pack('<i', 1)\n"
"assert struct.unpack('<i', y)[0] == 1\n"
)

num_threads = 3
barrier = threading.Barrier(num_threads)
results = [_Result() for _ in range(num_threads)]
threads = [
threading.Thread(
target=_run_in_subinterp_worker,
args=(code, barrier, results[i])
)
for i in range(num_threads)
]

for t in threads:
t.start()

for t in threads:
t.join()

for result in results:
if result.err:
raise result.err
self.assertEqual(result.ret, 0)


class UnpackIteratorTest(unittest.TestCase):
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :mod:`struct` data race in endian table initialization with
subinterpreters. Patch by Shamil Abdulaev.
89 changes: 50 additions & 39 deletions Modules/_struct.c
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -2711,45 +2759,8 @@ _structmodule_exec(PyObject *m)
}

/* 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++;
}
if (_PyOnceFlag_CallOnce(&endian_tables_init_once, init_endian_tables, NULL) == -1) {
return -1;
}

/* Add some symbolic constants to the module */
Expand Down
1 change: 1 addition & 0 deletions Tools/c-analyzer/cpython/globals-to-fix.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ Modules/_cursesmodule.c - curses_setupterm_called -
Modules/_cursesmodule.c - curses_start_color_called -
Modules/_cursesmodule.c - curses_screen_encoding -
Modules/_elementtree.c - expat_capi -
Modules/_struct.c - endian_tables_init_once -
Modules/readline.c - libedit_append_replace_history_offset -
Modules/readline.c - using_libedit_emulation -
Modules/readline.c - libedit_history_start -
Expand Down
Loading