]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-151179: Fix pidfd leak in asyncio _PidfdChildWatcher (#151186)
authorTimofei <128279579+deadlovelll@users.noreply.github.com>
Thu, 11 Jun 2026 11:43:46 +0000 (14:43 +0300)
committerGitHub <noreply@github.com>
Thu, 11 Jun 2026 11:43:46 +0000 (11:43 +0000)
Lib/asyncio/unix_events.py
Lib/test/test_asyncio/test_unix_events.py
Misc/NEWS.d/next/Library/2026-06-09-19-43-24.gh-issue-151179.hl_6Z0.rst [new file with mode: 0644]

index 93c5802cd44460d16632d518f96c25dc59421a11..676e0b670faa49b192be4cd8168e23e8d710e5d5 100644 (file)
@@ -889,8 +889,8 @@ class _PidfdChildWatcher:
                 pid)
         else:
             returncode = waitstatus_to_exitcode(status)
-
-        os.close(pidfd)
+        finally:
+            os.close(pidfd)
         callback(pid, returncode, *args)
 
 class _ThreadedChildWatcher:
index d2b3de3b9a4cb614954a0c7d7301561c3e21cb74..118cb866997c51c6e4faaa733635cb73de5ee2de 100644 (file)
@@ -1333,5 +1333,45 @@ class TestFork(unittest.TestCase):
 
         self.assertEqual(result.value, 0)
 
+
+@unittest.skipUnless(
+    unix_events.can_use_pidfd(),
+    "operating system does not support pidfd",
+)
+class PidfdChildWatcherTests(test_utils.TestCase):
+
+    def setUp(self):
+        super().setUp()
+        self.loop = asyncio.new_event_loop()
+        self.set_event_loop(self.loop)
+
+    def test_pidfd_closed_when_waitpid_raises(self):
+        # _do_wait() must close the pidfd even when waitpid()
+        # fails with something other than ChildProcessError, otherwise the
+        # pidfd is leaked
+        self.loop.set_exception_handler(lambda loop, context: None)
+
+        async def coro():
+            before = os_helper.fd_count()
+            proc = await asyncio.create_subprocess_exec(
+                sys.executable, '-c', 'import sys; sys.stdin.read()',
+                stdin=asyncio.subprocess.PIPE
+            )
+
+            with mock.patch.object(os, 'waitpid',
+                                   side_effect=OSError('unexpected')) as m:
+                proc.stdin.close()
+                while not m.called:
+                    await asyncio.sleep(0)
+
+            os.waitpid(proc.pid, 0)
+            proc._transport._process_exited(0)
+            await proc.wait()
+
+            self.assertEqual(os_helper.fd_count(), before)
+
+        self.loop.run_until_complete(coro())
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/Misc/NEWS.d/next/Library/2026-06-09-19-43-24.gh-issue-151179.hl_6Z0.rst b/Misc/NEWS.d/next/Library/2026-06-09-19-43-24.gh-issue-151179.hl_6Z0.rst
new file mode 100644 (file)
index 0000000..904edf3
--- /dev/null
@@ -0,0 +1,3 @@
+Fix a pidfd leak in ``_PidfdChildWatcher`` on Linux: the watcher no
+longer leaks the process file descriptor when ``waitpid()`` fails with an
+error other than :exc:`ChildProcessError`.