Skip to content

Commit 2918926

Browse files
committed
Fix pipe transport double-close of the underlying fd
connect_read_pipe and connect_write_pipe passed the Python file object's fd to libuv, which takes ownership and closes it. Closing the file object later (or GC) then closed the same fd again, which can steal a recycled descriptor. Dup the fd before uv_pipe_open so libuv and the file object each own a distinct descriptor.
1 parent e8efea4 commit 2918926

2 files changed

Lines changed: 107 additions & 2 deletions

File tree

‎tests/test_pipes.py‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,87 @@ def test_write_buffer_full(self):
271271
self.loop.run_until_complete(asyncio.wait_for(proto.done, 1))
272272
self.assertEqual('CLOSED', proto.state)
273273

274+
def _assert_pipe_close_does_not_steal_fd(self, connect, make_pipeobj,
275+
close_other_end):
276+
# Regression test for https://github.com/MagicStack/uvloop/issues/763
277+
# libuv closes the fd given to it; the Python file object must not
278+
# close that same fd again (it may have been reused).
279+
stolen = []
280+
probed = []
281+
282+
class FileIOWithProbe(io.FileIO):
283+
def close(self):
284+
try:
285+
fd = super().fileno()
286+
except (ValueError, OSError):
287+
super().close()
288+
return
289+
try:
290+
os.fstat(fd)
291+
already_closed = False
292+
except OSError:
293+
already_closed = True
294+
probed.append('closed' if already_closed else 'open')
295+
new_r, new_w = os.pipe()
296+
try:
297+
super().close()
298+
finally:
299+
for nfd in (new_r, new_w):
300+
try:
301+
os.fstat(nfd)
302+
except OSError:
303+
stolen.append(nfd)
304+
else:
305+
os.close(nfd)
306+
if already_closed:
307+
stolen.append(fd)
308+
309+
async def main():
310+
pipe_read_fd, pipe_write_fd = os.pipe()
311+
lost = self.loop.create_future()
312+
313+
class Proto(asyncio.Protocol):
314+
def connection_lost(self, exc):
315+
if not lost.done():
316+
lost.set_result(None)
317+
318+
pipeobj = make_pipeobj(FileIOWithProbe, pipe_read_fd,
319+
pipe_write_fd)
320+
transport, _ = await connect(Proto, pipeobj)
321+
transport.close()
322+
await lost
323+
close_other_end(pipe_read_fd, pipe_write_fd)
324+
325+
self.loop.run_until_complete(main())
326+
self.assertIn('open', probed)
327+
self.assertEqual(stolen, [])
328+
329+
def test_read_pipe_close_does_not_steal_fd(self):
330+
def make_pipeobj(fileio_cls, read_fd, write_fd):
331+
return io.BufferedReader(fileio_cls(read_fd, 'rb'))
332+
333+
async def connect(proto_factory, pipeobj):
334+
return await self.loop.connect_read_pipe(proto_factory, pipeobj)
335+
336+
def close_other_end(read_fd, write_fd):
337+
os.close(write_fd)
338+
339+
self._assert_pipe_close_does_not_steal_fd(
340+
connect, make_pipeobj, close_other_end)
341+
342+
def test_write_pipe_close_does_not_steal_fd(self):
343+
def make_pipeobj(fileio_cls, read_fd, write_fd):
344+
return io.BufferedWriter(fileio_cls(write_fd, 'wb'))
345+
346+
async def connect(proto_factory, pipeobj):
347+
return await self.loop.connect_write_pipe(proto_factory, pipeobj)
348+
349+
def close_other_end(read_fd, write_fd):
350+
os.close(read_fd)
351+
352+
self._assert_pipe_close_does_not_steal_fd(
353+
connect, make_pipeobj, close_other_end)
354+
274355

275356
class Test_UV_Pipes(_BasePipeTest, tb.UVTestCase):
276357
pass

‎uvloop/loop.pyx‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2862,18 +2862,31 @@ cdef class Loop:
28622862
ReadTransport interface."""
28632863
cdef:
28642864
ReadUnixTransport transp
2865+
int fd
2866+
bint opened
28652867

28662868
waiter = self._new_future()
28672869
proto = proto_factory()
28682870
transp = ReadUnixTransport.new(self, proto, None, waiter)
28692871
transp._add_extra_info('pipe', pipe)
2872+
# Duplicate the fd so libuv and the Python file object each own a
2873+
# distinct descriptor. uv_close() closes the fd given to libuv;
2874+
# without a dup, fileobj.close() (or GC) would close the same number
2875+
# again and could steal a recycled fd. See issue #763.
2876+
fd = os_dup(pipe.fileno())
2877+
opened = 0
28702878
try:
2871-
transp._open(pipe.fileno())
2879+
transp._open(fd)
2880+
opened = 1
28722881
transp._init_protocol()
28732882
await waiter
28742883
except (KeyboardInterrupt, SystemExit):
2884+
if not opened:
2885+
os_close(fd)
28752886
raise
28762887
except BaseException:
2888+
if not opened:
2889+
os_close(fd)
28772890
transp._close()
28782891
raise
28792892
transp._attach_fileobj(pipe)
@@ -2889,18 +2902,29 @@ cdef class Loop:
28892902
WriteTransport interface."""
28902903
cdef:
28912904
WriteUnixTransport transp
2905+
int fd
2906+
bint opened
28922907

28932908
waiter = self._new_future()
28942909
proto = proto_factory()
28952910
transp = WriteUnixTransport.new(self, proto, None, waiter)
28962911
transp._add_extra_info('pipe', pipe)
2912+
# See connect_read_pipe() — dup so libuv and the file object do not
2913+
# share an fd (issue #763).
2914+
fd = os_dup(pipe.fileno())
2915+
opened = 0
28972916
try:
2898-
transp._open(pipe.fileno())
2917+
transp._open(fd)
2918+
opened = 1
28992919
transp._init_protocol()
29002920
await waiter
29012921
except (KeyboardInterrupt, SystemExit):
2922+
if not opened:
2923+
os_close(fd)
29022924
raise
29032925
except BaseException:
2926+
if not opened:
2927+
os_close(fd)
29042928
transp._close()
29052929
raise
29062930
transp._attach_fileobj(pipe)

0 commit comments

Comments
 (0)