Crash report
What happened?
In the free-threaded build, io.StringIO methods are made memory-safe by taking the object's critical section. The Argument Clinic wrappers for write, seek, readline, etc. all run under Py_BEGIN_CRITICAL_SECTION(self) (e.g. _io.StringIO.readline)
|
skip_optional: |
|
Py_BEGIN_CRITICAL_SECTION(self); |
|
return_value = _io_StringIO_readline_impl((stringio *)self, size); |
|
Py_END_CRITICAL_SECTION(); |
But the tp_iternext slot stringio_iternext is not a clinic function and takes no critical section. It advances the stream by calling the internal _stringio_readline helper directly.
|
stringio_iternext(PyObject *op) |
|
{ |
|
PyObject *line; |
|
stringio *self = stringio_CAST(op); |
|
|
|
CHECK_INITIALIZED(self); |
|
CHECK_CLOSED(self); |
|
ENSURE_REALIZED(self); |
|
|
|
if (Py_IS_TYPE(self, self->module_state->PyStringIO_Type)) { |
|
/* Skip method call overhead for speed */ |
|
line = _stringio_readline(self, -1); |
|
} |
Meanwhile write_str (reached from the locked write) reads self->pos/self->string_size and resizes/reallocates self->buf, so next(sio) / for line in sio running concurrently with sio.write() (or seek) reads the buffer and advances self->pos without the lock that the writer holds.
|
if (self->pos + len > self->string_size) { |
|
if (resize_buffer(self, self->pos + len) < 0) |
This fails in two ways: the iterator reads a buffer the writer is reallocating (a use-after-free read, seen in the write_str vs _stringio_readline and mimalloc-heap races below), and the iterator's concurrent bump of self->pos makes write_str compute an out-of-bounds memset (stringio.c:253) that writes past the buffer and segfaults.
Reproducer:
import io
from threading import Thread, Barrier
sio = io.StringIO('line\n' * 200)
b = Barrier(9)
def writer():
b.wait()
for i in range(20000):
try:
sio.write('x' * ((i % 4000) + 1))
except Exception:
pass
def iterer():
b.wait()
for _ in range(20000):
try:
for _ in sio:
break
except Exception:
pass
try:
sio.seek(0)
except Exception:
pass
threads = [Thread(target=writer)]
threads += [Thread(target=iterer) for _ in range(8)]
for t in threads: t.start()
for t in threads: t.join()
TSAN Report: (Segmentation fault)
==================
WARNING: ThreadSanitizer: data race (pid=43515)
Write of size 8 at 0x7fffb63421e8 by thread T1:
#0 write_str /cpython/./Modules/_io/stringio.c:267:15
#1 _io_StringIO_write_impl /cpython/./Modules/_io/stringio.c:575:21
#2 _io_StringIO_write /cpython/./Modules/_io/clinic/stringio.c.h:254:20
#3 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:4453:35
...
Previous read of size 8 at 0x7fffb63421e8 by thread T9:
#0 _stringio_readline /cpython/./Modules/_io/stringio.c:365:15
#1 stringio_iternext /cpython/./Modules/_io/stringio.c:421:16
#2 _PyForIter_VirtualIteratorNext /cpython/Python/ceval.c:3744:22
#3 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:6441:36
...
SUMMARY: ThreadSanitizer: data race /cpython/./Modules/_io/stringio.c:267:15 in write_str
==================
...
==================
WARNING: ThreadSanitizer: data race (pid=43515)
Atomic write of size 8 at 0x7fffb8340000 by thread T1:
#0 mi_block_set_nextx /cpython/./Include/internal/mimalloc/mimalloc/internal.h:652:3
#1 mi_block_set_next /cpython/./Include/internal/mimalloc/mimalloc/internal.h:678:3
#2 mi_page_free_list_extend /cpython/Objects/mimalloc/page.c:612:5
#3 mi_page_extend_free /cpython/Objects/mimalloc/page.c:670:5
#4 mi_page_init /cpython/Objects/mimalloc/page.c:734:3
#5 mi_page_fresh_alloc /cpython/Objects/mimalloc/page.c:299:3
#6 mi_large_huge_page_alloc /cpython/Objects/mimalloc/page.c:874:21
#7 mi_find_page /cpython/Objects/mimalloc/page.c:916:14
#8 _mi_malloc_generic /cpython/Objects/mimalloc/page.c:950:21
#9 _mi_heap_malloc_zero_ex /cpython/Objects/mimalloc/alloc.c:161:21
#10 _mi_heap_malloc_zero /cpython/Objects/mimalloc/alloc.c:179:10
#11 mi_heap_malloc /cpython/Objects/mimalloc/alloc.c:183:10
#12 _mi_heap_realloc_zero /cpython/Objects/mimalloc/alloc.c:732:16
#13 mi_heap_realloc /cpython/Objects/mimalloc/alloc.c:753:10
#14 _PyMem_MiRealloc /cpython/Objects/obmalloc.c:289:12
#15 PyMem_Realloc /cpython/Objects/obmalloc.c:1279:12
#16 resize_buffer /cpython/./Modules/_io/stringio.c:116:26
#17 write_str /cpython/./Modules/_io/stringio.c:239:13
#18 _io_StringIO_write_impl /cpython/./Modules/_io/stringio.c:575:21
#19 _io_StringIO_write /cpython/./Modules/_io/clinic/stringio.c.h:254:20
#20 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:4453:35
...
Previous read of size 4 at 0x7fffb8340004 by thread T6:
#0 ucs4lib_find_max_char /cpython/Objects/stringlib/find_max_char.h:80:31 (python3.16t+0x372ba4)
#1 _PyUnicode_FromUCS4 /cpython/Objects/unicodeobject.c:2216:16
#2 PyUnicode_FromKindAndData /cpython/Objects/unicodeobject.c:2290:16
#3 _stringio_readline /cpython/./Modules/_io/stringio.c:384:12
#4 stringio_iternext /cpython/./Modules/_io/stringio.c:421:16
#5 _PyForIter_VirtualIteratorNext /cpython/Python/ceval.c:3744:22
#6 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:6441:36
...
SUMMARY: ThreadSanitizer: data race /cpython/./Include/internal/mimalloc/mimalloc/internal.h:652:3 in mi_block_set_nextx
==================
...
==================
WARNING: ThreadSanitizer: data race (pid=43515)
Write of size 4 at 0x7fffb8340018 by thread T1:
#0 as_ucs4 /cpython/Objects/unicodeobject.c:2480:9
#1 PyUnicode_AsUCS4 /cpython/Objects/unicodeobject.c:2505:12
#2 write_str /cpython/./Modules/_io/stringio.c:259:10
#3 _io_StringIO_write_impl /cpython/./Modules/_io/stringio.c:575:21
#4 _io_StringIO_write /cpython/./Modules/_io/clinic/stringio.c.h:254:20
#5 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:4453:35
...
Previous read of size 4 at 0x7fffb8340018 by thread T7:
#0 _PyUnicode_FromUCS4 /cpython/Objects/unicodeobject.c:2221:9
#1 PyUnicode_FromKindAndData /cpython/Objects/unicodeobject.c:2290:16
#2 _stringio_readline /cpython/./Modules/_io/stringio.c:384:12
#3 stringio_iternext /cpython/./Modules/_io/stringio.c:421:16
#4 _PyForIter_VirtualIteratorNext /cpython/Python/ceval.c:3744:22
#5 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:6441:36
...
SUMMARY: ThreadSanitizer: data race /cpython/Objects/unicodeobject.c:2480:9 in as_ucs4
==================
...
ThreadSanitizer:DEADLYSIGNAL
==43515==ERROR: ThreadSanitizer: SEGV on unknown address 0x7fffaa100000 (pc 0x7ffff7d8954a bp 0x000000000000 sp 0x7fffb11fc7c8 T43517)
==43515==The signal is caused by a WRITE memory access.
#0 __memset_avx2_unaligned_erms string/../sysdeps/x86_64/multiarch/memset-vec-unaligned-erms.S:328
#1 __tsan_memset <null> (python3.16t+0xf52a6)
#2 write_str /cpython/./Modules/_io/stringio.c:253:9
#3 _io_StringIO_write_impl /cpython/./Modules/_io/stringio.c:575:21
#4 _io_StringIO_write /cpython/./Modules/_io/clinic/stringio.c.h:254:20
#5 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:4453:35
#6 _PyEval_EvalFrame /cpython/./Include/internal/pycore_ceval.h:122:16
#7 _PyEval_Vector /cpython/Python/ceval.c:2141:12
#8 _PyFunction_Vectorcall /cpython/Objects/call.c
#9 _PyObject_VectorcallTstate /cpython/./Include/internal/pycore_call.h:144:11
#10 _PyObject_VectorcallPrepend /cpython/Objects/call.c:855:20
#11 method_vectorcall /cpython/Objects/classobject.c:55:12
#12 _PyObject_VectorcallTstate /cpython/./Include/internal/pycore_call.h:144:11
#13 context_run /cpython/Python/context.c:731:29
#14 method_vectorcall_FASTCALL_KEYWORDS /cpython/Objects/descrobject.c:421:24
#15 _PyObject_VectorcallTstate /cpython/./Include/internal/pycore_call.h:144:11
#16 PyObject_Vectorcall /cpython/Objects/call.c:327:12
#17 _Py_VectorCallInstrumentation_StackRefSteal /cpython/Python/ceval.c:768:11
#18 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:1906:35
#19 _PyEval_EvalFrame /cpython/./Include/internal/pycore_ceval.h:122:16
#20 _PyEval_Vector /cpython/Python/ceval.c:2141:12
#21 _PyFunction_Vectorcall /cpython/Objects/call.c
#22 _PyObject_VectorcallTstate /cpython/./Include/internal/pycore_call.h:144:11
#23 _PyObject_VectorcallPrepend /cpython/Objects/call.c:855:20
#24 method_vectorcall /cpython/Objects/classobject.c:55:12
#25 _PyVectorcall_Call /cpython/Objects/call.c:273:16
#26 _PyObject_Call /cpython/Objects/call.c:348:16
#27 PyObject_Call /cpython/Objects/call.c:373:12
#28 thread_run /cpython/./Modules/_threadmodule.c:388:21
#29 pythread_wrapper /cpython/Python/thread_pthread.h:234:5
#30 __tsan_thread_start_func <null>
#31 start_thread nptl/pthread_create.c:447:8
#32 clone3 misc/../sysdeps/unix/sysv/linux/x86_64/clone3.S:78
ThreadSanitizer can not provide additional info.
SUMMARY: ThreadSanitizer: SEGV string/../sysdeps/x86_64/multiarch/memset-vec-unaligned-erms.S:328 in __memset_avx2_unaligned_erms
==43515==ABORTING
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
No response
Linked PRs
Crash report
What happened?
In the free-threaded build,
io.StringIOmethods are made memory-safe by taking the object's critical section. The Argument Clinic wrappers forwrite,seek,readline, etc. all run underPy_BEGIN_CRITICAL_SECTION(self)(e.g._io.StringIO.readline)cpython/Modules/_io/clinic/stringio.c.h
Lines 129 to 132 in a74280e
But the
tp_iternextslotstringio_iternextis not a clinic function and takes no critical section. It advances the stream by calling the internal_stringio_readlinehelper directly.cpython/Modules/_io/stringio.c
Lines 410 to 422 in a74280e
Meanwhile
write_str(reached from the lockedwrite) readsself->pos/self->string_sizeand resizes/reallocatesself->buf, sonext(sio)/for line in siorunning concurrently withsio.write()(orseek) reads the buffer and advancesself->poswithout the lock that the writer holds.cpython/Modules/_io/stringio.c
Lines 238 to 239 in a74280e
This fails in two ways: the iterator reads a buffer the writer is reallocating (a use-after-free read, seen in the
write_strvs_stringio_readlineand mimalloc-heap races below), and the iterator's concurrent bump ofself->posmakeswrite_strcompute an out-of-boundsmemset(stringio.c:253) that writes past the buffer and segfaults.Reproducer:
TSAN Report: (Segmentation fault)
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
No response
Linked PRs