mirror of
https://github.com/python/cpython
synced 2026-11-15 12:17:15 +03:00
[3.15] gh-157364: Fix use-after-free in io.TextIOWrapper during reentrant detach (#159591) * gh-143008: Fix Null pointer dereferences in TextIOWrapper underlying stream access (#145957) TextIOWrapper keeps its underlying stream in a member called `self->buffer`. That stream can be detached by user code, such as custom `.flush` implementations resulting in `self->buffer` being set to NULL. The implementation often checked at the start of functions if `self->buffer` is in a good state, but did not always recheck after other Python code was called which could modify `self->buffer`. The cases which need to be re-checked are hard to spot so rather than rely on reviewer effort create better safety by making all self->buffer access go through helper functions. Thank you yihong0618 for the test, NEWS and initial implementation in gh-143041. (cherry picked from commitdb4b1948bc) * gh-154523: Fix data-race in `TextIOWrapper.detach()` (#154565) (cherry picked from commit8440a6a465) * gh-157364: Fix use-after-free in io.TextIOWrapper during reentrant detach (#157370) (cherry picked from commite19dc470f4) --------- (cherry picked from commitea1312bdb4) Co-authored-by: Cody Maloney <cmaloney@users.noreply.github.com> Co-authored-by: yihong0618 <zouzou0208@gmail.com> Co-authored-by: Brij Kapadia <97006829+brijkapadia@users.noreply.github.com> Co-authored-by: Shamil <ashm.tech@proton.me>
213 lines
6.5 KiB
Python
213 lines
6.5 KiB
Python
import codecs
|
|
import io
|
|
import threading
|
|
from unittest import TestCase
|
|
from test.support import threading_helper
|
|
from test.support.threading_helper import run_concurrently
|
|
from random import randint
|
|
from io import BytesIO
|
|
from sys import getsizeof
|
|
|
|
threading_helper.requires_working_threading(module=True)
|
|
|
|
|
|
class TestBytesIO(TestCase):
|
|
# Test pretty much everything that can break under free-threading.
|
|
# Non-deterministic, but at least one of these things will fail if
|
|
# BytesIO object is not free-thread safe.
|
|
|
|
def check(self, funcs, *args):
|
|
barrier = threading.Barrier(len(funcs))
|
|
threads = []
|
|
|
|
for func in funcs:
|
|
thread = threading.Thread(target=func, args=(barrier, *args))
|
|
|
|
threads.append(thread)
|
|
|
|
with threading_helper.start_threads(threads):
|
|
pass
|
|
|
|
@threading_helper.requires_working_threading()
|
|
@threading_helper.reap_threads
|
|
def test_free_threading(self):
|
|
"""Test for segfaults and aborts."""
|
|
|
|
def write(barrier, b, *ignore):
|
|
barrier.wait()
|
|
try: b.write(b'0' * randint(100, 1000))
|
|
except ValueError: pass # ignore write fail to closed file
|
|
|
|
def writelines(barrier, b, *ignore):
|
|
barrier.wait()
|
|
b.write(b'0\n' * randint(100, 1000))
|
|
|
|
def truncate(barrier, b, *ignore):
|
|
barrier.wait()
|
|
try: b.truncate(0)
|
|
except BufferError: pass # ignore exported buffer
|
|
|
|
def read(barrier, b, *ignore):
|
|
barrier.wait()
|
|
b.read()
|
|
|
|
def read1(barrier, b, *ignore):
|
|
barrier.wait()
|
|
b.read1()
|
|
|
|
def readline(barrier, b, *ignore):
|
|
barrier.wait()
|
|
b.readline()
|
|
|
|
def readlines(barrier, b, *ignore):
|
|
barrier.wait()
|
|
b.readlines()
|
|
|
|
def readinto(barrier, b, into, *ignore):
|
|
barrier.wait()
|
|
b.readinto(into)
|
|
|
|
def close(barrier, b, *ignore):
|
|
barrier.wait()
|
|
b.close()
|
|
|
|
def getvalue(barrier, b, *ignore):
|
|
barrier.wait()
|
|
b.getvalue()
|
|
|
|
def getbuffer(barrier, b, *ignore):
|
|
barrier.wait()
|
|
b.getbuffer()
|
|
|
|
def iter(barrier, b, *ignore):
|
|
barrier.wait()
|
|
list(b)
|
|
|
|
def getstate(barrier, b, *ignore):
|
|
barrier.wait()
|
|
b.__getstate__()
|
|
|
|
def setstate(barrier, b, st, *ignore):
|
|
barrier.wait()
|
|
b.__setstate__(st)
|
|
|
|
def sizeof(barrier, b, *ignore):
|
|
barrier.wait()
|
|
getsizeof(b)
|
|
|
|
self.check([write] * 10, BytesIO())
|
|
self.check([writelines] * 10, BytesIO())
|
|
self.check([write] * 10 + [truncate] * 10, BytesIO())
|
|
self.check([truncate] + [read] * 10, BytesIO(b'0\n'*204800))
|
|
self.check([truncate] + [read1] * 10, BytesIO(b'0\n'*204800))
|
|
self.check([truncate] + [readline] * 10, BytesIO(b'0\n'*20480))
|
|
self.check([truncate] + [readlines] * 10, BytesIO(b'0\n'*20480))
|
|
self.check([truncate] + [readinto] * 10, BytesIO(b'0\n'*204800), bytearray(b'0\n'*204800))
|
|
self.check([close] + [write] * 10, BytesIO())
|
|
self.check([truncate] + [getvalue] * 10, BytesIO(b'0\n'*204800))
|
|
self.check([truncate] + [getbuffer] * 10, BytesIO(b'0\n'*204800))
|
|
self.check([truncate] + [iter] * 10, BytesIO(b'0\n'*20480))
|
|
self.check([truncate] + [getstate] * 10, BytesIO(b'0\n'*204800))
|
|
self.check([truncate] + [setstate] * 10, BytesIO(b'0\n'*204800), (b'123', 0, None))
|
|
self.check([truncate] + [sizeof] * 10, BytesIO(b'0\n'*204800))
|
|
|
|
# no tests for seek or tell because they don't break anything
|
|
|
|
@threading_helper.requires_working_threading()
|
|
@threading_helper.reap_threads
|
|
def test_concurrent_whole_buffer_read_and_resize(self):
|
|
shared = BytesIO(b"x" * 64)
|
|
writers = 2
|
|
readers = 8
|
|
loops = 2000
|
|
barrier = threading.Barrier(writers + readers)
|
|
|
|
def writer():
|
|
barrier.wait()
|
|
for i in range(loops):
|
|
shared.seek(0)
|
|
shared.write(b"a" * (64 + (i & 63)))
|
|
|
|
def reader():
|
|
barrier.wait()
|
|
for _ in range(loops):
|
|
shared.seek(0)
|
|
shared.read()
|
|
shared.seek(0)
|
|
shared.getvalue()
|
|
|
|
threads = [threading.Thread(target=writer) for _ in range(writers)]
|
|
threads += [threading.Thread(target=reader) for _ in range(readers)]
|
|
with threading_helper.start_threads(threads):
|
|
pass
|
|
|
|
|
|
class IncrementalNewlineDecoderTest(TestCase):
|
|
def make_decoder(self):
|
|
utf8_decoder = codecs.getincrementaldecoder('utf-8')()
|
|
return io.IncrementalNewlineDecoder(utf8_decoder, translate=True)
|
|
|
|
def test_concurrent_reset(self):
|
|
decoder = self.make_decoder()
|
|
|
|
def worker():
|
|
for _ in range(100):
|
|
decoder.reset()
|
|
|
|
run_concurrently(worker_func=worker, nthreads=2)
|
|
|
|
def test_concurrent_decode(self):
|
|
decoder = self.make_decoder()
|
|
|
|
def worker():
|
|
for _ in range(100):
|
|
decoder.decode(b"line\r\n", final=False)
|
|
|
|
run_concurrently(worker_func=worker, nthreads=2)
|
|
|
|
def test_concurrent_getstate_setstate(self):
|
|
decoder = self.make_decoder()
|
|
state = decoder.getstate()
|
|
|
|
def getstate_worker():
|
|
for _ in range(100):
|
|
decoder.getstate()
|
|
|
|
def setstate_worker():
|
|
for _ in range(100):
|
|
decoder.setstate(state)
|
|
|
|
run_concurrently([getstate_worker] * 2 + [setstate_worker] * 2)
|
|
|
|
def test_concurrent_decode_and_reset(self):
|
|
decoder = self.make_decoder()
|
|
|
|
def decode_worker():
|
|
for _ in range(100):
|
|
decoder.decode(b"line\r\n", final=False)
|
|
|
|
def reset_worker():
|
|
for _ in range(100):
|
|
decoder.reset()
|
|
|
|
run_concurrently([decode_worker] * 2 + [reset_worker] * 2)
|
|
|
|
|
|
class TextIOWrapperTest(TestCase):
|
|
def test_buffer_detach_race(self):
|
|
make = lambda: io.TextIOWrapper(io.BytesIO())
|
|
slot = [make()]
|
|
|
|
def reader():
|
|
for _ in range(1000):
|
|
try:
|
|
slot[0].buffer
|
|
except ValueError:
|
|
pass
|
|
|
|
def detacher():
|
|
for _ in range(1000):
|
|
slot[0] = make()
|
|
slot[0].detach()
|
|
|
|
run_concurrently([reader, detacher])
|