Skip to content

Commit 6bf7e17

Browse files
committed
gh-151179: Fix pidfd leak in asyncio _PidfdChildWatcher
1 parent 32104a1 commit 6bf7e17

3 files changed

Lines changed: 56 additions & 13 deletions

File tree

Lib/asyncio/unix_events.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -876,19 +876,20 @@ def _do_wait(self, pid, pidfd, callback, args):
876876
loop = events.get_running_loop()
877877
loop._remove_reader(pidfd)
878878
try:
879-
_, status = os.waitpid(pid, 0)
880-
except ChildProcessError:
881-
# The child process is already reaped
882-
# (may happen if waitpid() is called elsewhere).
883-
returncode = 255
884-
logger.warning(
885-
"child process pid %d exit status already read: "
886-
" will report returncode 255",
887-
pid)
888-
else:
889-
returncode = waitstatus_to_exitcode(status)
890-
891-
os.close(pidfd)
879+
try:
880+
_, status = os.waitpid(pid, 0)
881+
except ChildProcessError:
882+
# The child process is already reaped
883+
# (may happen if waitpid() is called elsewhere).
884+
returncode = 255
885+
logger.warning(
886+
"child process pid %d exit status already read: "
887+
" will report returncode 255",
888+
pid)
889+
else:
890+
returncode = waitstatus_to_exitcode(status)
891+
finally:
892+
os.close(pidfd)
892893
callback(pid, returncode, *args)
893894

894895
class _ThreadedChildWatcher:

Lib/test/test_asyncio/test_unix_events.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,5 +1333,44 @@ async def child_main():
13331333

13341334
self.assertEqual(result.value, 0)
13351335

1336+
1337+
@unittest.skipUnless(
1338+
unix_events.can_use_pidfd(),
1339+
"operating system does not support pidfd",
1340+
)
1341+
class PidfdChildWatcherTests(test_utils.TestCase):
1342+
1343+
def setUp(self):
1344+
super().setUp()
1345+
self.loop = asyncio.new_event_loop()
1346+
self.set_event_loop(self.loop)
1347+
1348+
def test_pidfd_closed_when_waitpid_raises(self):
1349+
# _do_wait() must close the pidfd even when waitpid()
1350+
# fails with something other than ChildProcessError, otherwise the
1351+
# pidfd is leaked
1352+
watcher = unix_events._PidfdChildWatcher()
1353+
pid = os.posix_spawn(sys.executable, [sys.executable, '-c', ''],
1354+
os.environ)
1355+
pidfd = os.pidfd_open(pid)
1356+
try:
1357+
async def coro():
1358+
with mock.patch.object(os, 'waitpid',
1359+
side_effect=OSError('unexpected')):
1360+
with self.assertRaises(OSError):
1361+
watcher._do_wait(pid, pidfd, lambda *a: None, ())
1362+
1363+
self.loop.run_until_complete(coro())
1364+
1365+
with self.assertRaises(OSError):
1366+
os.fstat(pidfd)
1367+
finally:
1368+
try:
1369+
os.close(pidfd)
1370+
except OSError:
1371+
pass
1372+
os.waitpid(pid, 0)
1373+
1374+
13361375
if __name__ == '__main__':
13371376
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a pidfd leak in :class:`_PidfdChildWatcher` on Linux: the watcher no
2+
longer leaks the process file descriptor when ``waitpid()`` fails with an
3+
error other than :exc:`ChildProcessError`.

0 commit comments

Comments
 (0)