Bug report
Bug description:
On a free-threaded build, UnicodeDecodeError.__str__ reads the start and end members with plain (non-atomic) loads, while assigning to exc.start / exc.end goes through PyMember_SetOne, which does an atomic store. So calling str(exc) on a shared UnicodeDecodeError concurrently with setting its start/end is a data race on those Py_ssize_t members.
start and end are exposed as Py_T_PYSSIZET members:
|
{"start", Py_T_PYSSIZET, offsetof(PyUnicodeErrorObject, start), 0, |
|
PyDoc_STR("exception start")}, |
|
{"end", Py_T_PYSSIZET, offsetof(PyUnicodeErrorObject, end), 0, |
|
PyDoc_STR("exception end")}, |
Plain read in UnicodeDecodeError_str:
|
Py_ssize_t len = PyBytes_GET_SIZE(exc->object); |
|
Py_ssize_t start = exc->start, end = exc->end; |
|
|
|
if ((start >= 0 && start < len) && (end >= 0 && end <= len) && end == start + 1) { |
|
int badbyte = (int)(PyBytes_AS_STRING(exc->object)[start] & 0xff); |
|
result = PyUnicode_FromFormat( |
|
"'%U' codec can't decode byte 0x%02x in position %zd: %U", |
|
encoding_str, |
|
badbyte, |
|
start, |
|
reason_str); |
|
} |
|
else { |
|
result = PyUnicode_FromFormat( |
|
"'%U' codec can't decode bytes in position %zd-%zd: %U", |
|
encoding_str, |
|
start, |
|
end - 1, |
|
reason_str); |
|
} |
Reproducer:
from threading import Thread
shared_exc = UnicodeDecodeError('utf-8', b'\xff\xfe\xfa', 1, 2, 'invalid start byte')
def chain1_thread():
for i in range(20000):
try:
shared_exc.start = i % 3
except Exception:
pass
def chain2_thread():
for _ in range(20000):
try:
str(shared_exc)
except Exception:
pass
N_C1 = 4
N_C2 = 8
threads = [Thread(target=chain1_thread) for _ in range(N_C1)]
threads += [Thread(target=chain2_thread) for _ in range(N_C2)]
for t in threads: t.start()
for t in threads: t.join()
TSAN Report:
WARNING: ThreadSanitizer: data race (pid=656224)
Read of size 8 at 0x7fffb63734e8 by thread T5:
#0 UnicodeDecodeError_str /cpython/Objects/exceptions.c:3948:29
#1 PyObject_Str /cpython/Objects/object.c:826:11
#2 unicode_vectorcall /cpython/Objects/unicodeobject.c:14279:16
#3 _PyObject_VectorcallTstate /cpython/./Include/internal/pycore_call.h:144:11
#4 PyObject_Vectorcall /cpython/Objects/call.c:327:12
#5 _Py_VectorCallInstrumentation_StackRefSteal /cpython/Python/ceval.c:768:11
#6 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:1906:35
Previous atomic write of size 8 at 0x7fffb63734e8 by thread T3:
#0 PyMember_SetOne /cpython/Python/structmember.c
#1 member_set /cpython/Objects/descrobject.c:239:12
#2 _PyObject_GenericSetAttrWithDict /cpython/Objects/object.c:2049:19
#3 PyObject_GenericSetAttr /cpython/Objects/object.c:2120:12
#4 PyObject_SetAttr /cpython/Objects/object.c:1533:15
#5 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:12057:27
SUMMARY: ThreadSanitizer: data race/cpython/Objects/exceptions.c:3948:29 in UnicodeDecodeError_str
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Bug report
Bug description:
On a free-threaded build,
UnicodeDecodeError.__str__reads thestartandendmembers with plain (non-atomic) loads, while assigning toexc.start/exc.endgoes throughPyMember_SetOne, which does an atomic store. So callingstr(exc)on a sharedUnicodeDecodeErrorconcurrently with setting itsstart/endis a data race on thosePy_ssize_tmembers.startandendare exposed asPy_T_PYSSIZETmembers:cpython/Objects/exceptions.c
Lines 3758 to 3761 in 22a6c51
Plain read in
UnicodeDecodeError_str:cpython/Objects/exceptions.c
Lines 3947 to 3966 in 22a6c51
Reproducer:
TSAN Report:
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux