Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f3b68b3

Browse files
committed
Issue #10478: Reentrant calls inside buffered IO objects (for example by
way of a signal handler) now raise a RuntimeError instead of freezing the current process.
1 parent 38e117d commit f3b68b3

3 files changed

Lines changed: 94 additions & 19 deletions

File tree

Lib/test/test_io.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2653,12 +2653,50 @@ def test_interrupted_write_buffered(self):
26532653
def test_interrupted_write_text(self):
26542654
self.check_interrupted_write("xy", b"xy", mode="w", encoding="ascii")
26552655

2656+
def check_reentrant_write(self, data, **fdopen_kwargs):
2657+
def on_alarm(*args):
2658+
# Will be called reentrantly from the same thread
2659+
wio.write(data)
2660+
1/0
2661+
signal.signal(signal.SIGALRM, on_alarm)
2662+
r, w = os.pipe()
2663+
wio = self.io.open(w, **fdopen_kwargs)
2664+
try:
2665+
signal.alarm(1)
2666+
# Either the reentrant call to wio.write() fails with RuntimeError,
2667+
# or the signal handler raises ZeroDivisionError.
2668+
with self.assertRaises((ZeroDivisionError, RuntimeError)) as cm:
2669+
while 1:
2670+
for i in range(100):
2671+
wio.write(data)
2672+
wio.flush()
2673+
# Make sure the buffer doesn't fill up and block further writes
2674+
os.read(r, len(data) * 100)
2675+
exc = cm.exception
2676+
if isinstance(exc, RuntimeError):
2677+
self.assertTrue(str(exc).startswith("reentrant call"), str(exc))
2678+
finally:
2679+
wio.close()
2680+
os.close(r)
2681+
2682+
def test_reentrant_write_buffered(self):
2683+
self.check_reentrant_write(b"xy", mode="wb")
2684+
2685+
def test_reentrant_write_text(self):
2686+
self.check_reentrant_write("xy", mode="w", encoding="ascii")
2687+
2688+
26562689
class CSignalsTest(SignalsTest):
26572690
io = io
26582691

26592692
class PySignalsTest(SignalsTest):
26602693
io = pyio
26612694

2695+
# Handling reentrancy issues would slow down _pyio even more, so the
2696+
# tests are disabled.
2697+
test_reentrant_write_buffered = None
2698+
test_reentrant_write_text = None
2699+
26622700

26632701
def test_main():
26642702
tests = (CIOTest, PyIOTest,

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ Core and Builtins
3535
Library
3636
-------
3737

38+
- Issue #10478: Reentrant calls inside buffered IO objects (for example by
39+
way of a signal handler) now raise a RuntimeError instead of freezing the
40+
current process.
41+
3842
- logging: Added getLogRecordFactory/setLogRecordFactory with docs and tests.
3943

4044
- Issue #10549: Fix pydoc traceback when text-documenting certain classes.

Modules/_io/bufferedio.c

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ typedef struct {
225225

226226
#ifdef WITH_THREAD
227227
PyThread_type_lock lock;
228+
volatile long owner;
228229
#endif
229230

230231
Py_ssize_t buffer_size;
@@ -260,17 +261,34 @@ typedef struct {
260261
/* These macros protect the buffered object against concurrent operations. */
261262

262263
#ifdef WITH_THREAD
263-
#define ENTER_BUFFERED(self) \
264-
if (!PyThread_acquire_lock(self->lock, 0)) { \
265-
Py_BEGIN_ALLOW_THREADS \
266-
PyThread_acquire_lock(self->lock, 1); \
267-
Py_END_ALLOW_THREADS \
264+
265+
static int
266+
_enter_buffered_busy(buffered *self)
267+
{
268+
if (self->owner == PyThread_get_thread_ident()) {
269+
PyErr_Format(PyExc_RuntimeError,
270+
"reentrant call inside %R", self);
271+
return 0;
268272
}
273+
Py_BEGIN_ALLOW_THREADS
274+
PyThread_acquire_lock(self->lock, 1);
275+
Py_END_ALLOW_THREADS
276+
return 1;
277+
}
278+
279+
#define ENTER_BUFFERED(self) \
280+
( (PyThread_acquire_lock(self->lock, 0) ? \
281+
1 : _enter_buffered_busy(self)) \
282+
&& (self->owner = PyThread_get_thread_ident(), 1) )
269283

270284
#define LEAVE_BUFFERED(self) \
271-
PyThread_release_lock(self->lock);
285+
do { \
286+
self->owner = 0; \
287+
PyThread_release_lock(self->lock); \
288+
} while(0);
289+
272290
#else
273-
#define ENTER_BUFFERED(self)
291+
#define ENTER_BUFFERED(self) 1
274292
#define LEAVE_BUFFERED(self)
275293
#endif
276294

@@ -444,7 +462,8 @@ buffered_close(buffered *self, PyObject *args)
444462
int r;
445463

446464
CHECK_INITIALIZED(self)
447-
ENTER_BUFFERED(self)
465+
if (!ENTER_BUFFERED(self))
466+
return NULL;
448467

449468
r = buffered_closed(self);
450469
if (r < 0)
@@ -465,7 +484,8 @@ buffered_close(buffered *self, PyObject *args)
465484
/* flush() will most probably re-take the lock, so drop it first */
466485
LEAVE_BUFFERED(self)
467486
res = PyObject_CallMethodObjArgs((PyObject *)self, _PyIO_str_flush, NULL);
468-
ENTER_BUFFERED(self)
487+
if (!ENTER_BUFFERED(self))
488+
return NULL;
469489
if (res == NULL) {
470490
goto end;
471491
}
@@ -679,6 +699,7 @@ _buffered_init(buffered *self)
679699
PyErr_SetString(PyExc_RuntimeError, "can't allocate read lock");
680700
return -1;
681701
}
702+
self->owner = 0;
682703
#endif
683704
/* Find out whether buffer_size is a power of 2 */
684705
/* XXX is this optimization useful? */
@@ -705,7 +726,8 @@ buffered_flush(buffered *self, PyObject *args)
705726
CHECK_INITIALIZED(self)
706727
CHECK_CLOSED(self, "flush of closed file")
707728

708-
ENTER_BUFFERED(self)
729+
if (!ENTER_BUFFERED(self))
730+
return NULL;
709731
res = _bufferedwriter_flush_unlocked(self, 0);
710732
if (res != NULL && self->readable) {
711733
/* Rewind the raw stream so that its position corresponds to
@@ -732,7 +754,8 @@ buffered_peek(buffered *self, PyObject *args)
732754
return NULL;
733755
}
734756

735-
ENTER_BUFFERED(self)
757+
if (!ENTER_BUFFERED(self))
758+
return NULL;
736759

737760
if (self->writable) {
738761
res = _bufferedwriter_flush_unlocked(self, 1);
@@ -767,15 +790,17 @@ buffered_read(buffered *self, PyObject *args)
767790

768791
if (n == -1) {
769792
/* The number of bytes is unspecified, read until the end of stream */
770-
ENTER_BUFFERED(self)
793+
if (!ENTER_BUFFERED(self))
794+
return NULL;
771795
res = _bufferedreader_read_all(self);
772796
LEAVE_BUFFERED(self)
773797
}
774798
else {
775799
res = _bufferedreader_read_fast(self, n);
776800
if (res == Py_None) {
777801
Py_DECREF(res);
778-
ENTER_BUFFERED(self)
802+
if (!ENTER_BUFFERED(self))
803+
return NULL;
779804
res = _bufferedreader_read_generic(self, n);
780805
LEAVE_BUFFERED(self)
781806
}
@@ -803,7 +828,8 @@ buffered_read1(buffered *self, PyObject *args)
803828
if (n == 0)
804829
return PyBytes_FromStringAndSize(NULL, 0);
805830

806-
ENTER_BUFFERED(self)
831+
if (!ENTER_BUFFERED(self))
832+
return NULL;
807833

808834
if (self->writable) {
809835
res = _bufferedwriter_flush_unlocked(self, 1);
@@ -859,7 +885,8 @@ buffered_readinto(buffered *self, PyObject *args)
859885

860886
/* TODO: use raw.readinto() instead! */
861887
if (self->writable) {
862-
ENTER_BUFFERED(self)
888+
if (!ENTER_BUFFERED(self))
889+
return NULL;
863890
res = _bufferedwriter_flush_unlocked(self, 0);
864891
LEAVE_BUFFERED(self)
865892
if (res == NULL)
@@ -903,7 +930,8 @@ _buffered_readline(buffered *self, Py_ssize_t limit)
903930
goto end_unlocked;
904931
}
905932

906-
ENTER_BUFFERED(self)
933+
if (!ENTER_BUFFERED(self))
934+
goto end_unlocked;
907935

908936
/* Now we try to get some more from the raw stream */
909937
if (self->writable) {
@@ -1053,7 +1081,8 @@ buffered_seek(buffered *self, PyObject *args)
10531081
}
10541082
}
10551083

1056-
ENTER_BUFFERED(self)
1084+
if (!ENTER_BUFFERED(self))
1085+
return NULL;
10571086

10581087
/* Fallback: invoke raw seek() method and clear buffer */
10591088
if (self->writable) {
@@ -1091,7 +1120,8 @@ buffered_truncate(buffered *self, PyObject *args)
10911120
return NULL;
10921121
}
10931122

1094-
ENTER_BUFFERED(self)
1123+
if (!ENTER_BUFFERED(self))
1124+
return NULL;
10951125

10961126
if (self->writable) {
10971127
res = _bufferedwriter_flush_unlocked(self, 0);
@@ -1748,7 +1778,10 @@ bufferedwriter_write(buffered *self, PyObject *args)
17481778
return NULL;
17491779
}
17501780

1751-
ENTER_BUFFERED(self)
1781+
if (!ENTER_BUFFERED(self)) {
1782+
PyBuffer_Release(&buf);
1783+
return NULL;
1784+
}
17521785

17531786
/* Fast path: the data to write can be fully buffered. */
17541787
if (!VALID_READ_BUFFER(self) && !VALID_WRITE_BUFFER(self)) {

0 commit comments

Comments
 (0)