Skip to content

Use-after-free with a concurrent stringio_iternext #153296

Description

@Naserume

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

Metadata

Metadata

Assignees

No one assigned

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions