Skip to content

Commit 9703f09

Browse files
benfoglepitrou
authored andcommitted
bpo-31976: Fix race condition when flushing a file is slow. (python#4331)
1 parent 4652bf2 commit 9703f09

File tree

4 files changed

+58
-8
lines changed

4 files changed

+58
-8
lines changed

Lib/_pyio.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,11 +1188,11 @@ def writable(self):
11881188
return self.raw.writable()
11891189

11901190
def write(self, b):
1191-
if self.closed:
1192-
raise ValueError("write to closed file")
11931191
if isinstance(b, str):
11941192
raise TypeError("can't write str to binary stream")
11951193
with self._write_lock:
1194+
if self.closed:
1195+
raise ValueError("write to closed file")
11961196
# XXX we can implement some more tricks to try and avoid
11971197
# partial writes
11981198
if len(self._write_buf) > self.buffer_size:
@@ -1253,6 +1253,21 @@ def seek(self, pos, whence=0):
12531253
self._flush_unlocked()
12541254
return _BufferedIOMixin.seek(self, pos, whence)
12551255

1256+
def close(self):
1257+
with self._write_lock:
1258+
if self.raw is None or self.closed:
1259+
return
1260+
# We have to release the lock and call self.flush() (which will
1261+
# probably just re-take the lock) in case flush has been overridden in
1262+
# a subclass or the user set self.flush to something. This is the same
1263+
# behavior as the C implementation.
1264+
try:
1265+
# may raise BlockingIOError or BrokenPipeError etc
1266+
self.flush()
1267+
finally:
1268+
with self._write_lock:
1269+
self.raw.close()
1270+
12561271

12571272
class BufferedRWPair(BufferedIOBase):
12581273

Lib/test/test_io.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,22 @@ class PyMisbehavedRawIO(MisbehavedRawIO, pyio.RawIOBase):
168168
pass
169169

170170

171+
class SlowFlushRawIO(MockRawIO):
172+
def __init__(self):
173+
super().__init__()
174+
self.in_flush = threading.Event()
175+
176+
def flush(self):
177+
self.in_flush.set()
178+
time.sleep(0.25)
179+
180+
class CSlowFlushRawIO(SlowFlushRawIO, io.RawIOBase):
181+
pass
182+
183+
class PySlowFlushRawIO(SlowFlushRawIO, pyio.RawIOBase):
184+
pass
185+
186+
171187
class CloseFailureIO(MockRawIO):
172188
closed = 0
173189

@@ -1726,6 +1742,18 @@ def bad_write(b):
17261742
self.assertRaises(OSError, b.close) # exception not swallowed
17271743
self.assertTrue(b.closed)
17281744

1745+
def test_slow_close_from_thread(self):
1746+
# Issue #31976
1747+
rawio = self.SlowFlushRawIO()
1748+
bufio = self.tp(rawio, 8)
1749+
t = threading.Thread(target=bufio.close)
1750+
t.start()
1751+
rawio.in_flush.wait()
1752+
self.assertRaises(ValueError, bufio.write, b'spam')
1753+
self.assertTrue(bufio.closed)
1754+
t.join()
1755+
1756+
17291757

17301758
class CBufferedWriterTest(BufferedWriterTest, SizeofTest):
17311759
tp = io.BufferedWriter
@@ -4085,7 +4113,8 @@ def load_tests(*args):
40854113
# Put the namespaces of the IO module we are testing and some useful mock
40864114
# classes in the __dict__ of each test.
40874115
mocks = (MockRawIO, MisbehavedRawIO, MockFileIO, CloseFailureIO,
4088-
MockNonBlockWriterIO, MockUnseekableIO, MockRawIOWithoutRead)
4116+
MockNonBlockWriterIO, MockUnseekableIO, MockRawIOWithoutRead,
4117+
SlowFlushRawIO)
40894118
all_members = io.__all__ + ["IncrementalNewlineDecoder"]
40904119
c_io_ns = {name : getattr(io, name) for name in all_members}
40914120
py_io_ns = {name : getattr(pyio, name) for name in all_members}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix race condition when flushing a file is slow, which can cause a
2+
segfault if closing the file from another thread.

Modules/_io/bufferedio.c

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -347,9 +347,10 @@ _enter_buffered_busy(buffered *self)
347347
}
348348

349349
#define IS_CLOSED(self) \
350+
(!self->buffer || \
350351
(self->fast_closed_checks \
351352
? _PyFileIO_closed(self->raw) \
352-
: buffered_closed(self))
353+
: buffered_closed(self)))
353354

354355
#define CHECK_CLOSED(self, error_msg) \
355356
if (IS_CLOSED(self)) { \
@@ -1971,14 +1972,17 @@ _io_BufferedWriter_write_impl(buffered *self, Py_buffer *buffer)
19711972
Py_off_t offset;
19721973

19731974
CHECK_INITIALIZED(self)
1974-
if (IS_CLOSED(self)) {
1975-
PyErr_SetString(PyExc_ValueError, "write to closed file");
1976-
return NULL;
1977-
}
19781975

19791976
if (!ENTER_BUFFERED(self))
19801977
return NULL;
19811978

1979+
/* Issue #31976: Check for closed file after acquiring the lock. Another
1980+
thread could be holding the lock while closing the file. */
1981+
if (IS_CLOSED(self)) {
1982+
PyErr_SetString(PyExc_ValueError, "write to closed file");
1983+
goto error;
1984+
}
1985+
19821986
/* Fast path: the data to write can be fully buffered. */
19831987
if (!VALID_READ_BUFFER(self) && !VALID_WRITE_BUFFER(self)) {
19841988
self->pos = 0;

0 commit comments

Comments
 (0)