]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-154836: Fix Popen.wait() with very large timeouts on the pidfd/kqueue wait paths...
authorCalvin Prewitt <calvin@setout.dev>
Wed, 29 Jul 2026 17:45:41 +0000 (12:45 -0500)
committerGitHub <noreply@github.com>
Wed, 29 Jul 2026 17:45:41 +0000 (10:45 -0700)
The event-driven wait introduced by gh-83069 passes the caller's timeout
unclamped to poll() / kqueue.control(), so values that do not fit the C
timestamp conversion (float('inf'), sys.maxsize, 1e10, ...) raise
OverflowError on Linux and, on macOS/BSD, a misleading
"TypeError: timeout must be a real number or None" -- all of which
worked on 3.14 and earlier.

- Lib/subprocess.py: clamp each wait to _MAXIMUM_WAIT_TIMEOUT (24h,
  following asyncio's MAXIMUM_SELECT_TIMEOUT precedent) and loop until
  the real deadline in both _wait_pidfd() and _wait_kqueue().
- Modules/selectmodule.c: only rewrite the kqueue.control() timeout
  conversion failure into TypeError when the original exception IS a
  TypeError, exactly like the select()/poll()/devpoll()/epoll() sites,
  so OverflowError surfaces for out-of-range values.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Lib/subprocess.py
Lib/test/test_kqueue.py
Lib/test/test_subprocess.py
Misc/NEWS.d/next/Library/2026-07-19-20-00-00.gh-issue-154836.kqWait.rst [new file with mode: 0644]
Modules/selectmodule.c

index 6fe2ec98fb4088872849fac97c8fb6832a732333..054860a19c74b6d32960a216dd40eb327454f323 100644 (file)
@@ -917,6 +917,12 @@ def _can_use_kqueue():
 _CAN_USE_PIDFD_OPEN = not _mswindows and _can_use_pidfd_open()
 _CAN_USE_KQUEUE = not _mswindows and _can_use_kqueue()
 
+# Maximum timeout passed to poll() / kqueue.control() by Popen._wait():
+# very large values (e.g. timeout=float('inf')) overflow the C timestamp
+# conversion (gh-154836).  Longer waits are performed in bounded slices
+# until the deadline, like asyncio's MAXIMUM_SELECT_TIMEOUT.
+_MAXIMUM_WAIT_TIMEOUT = 24 * 3600
+
 
 # These are primarily fail-safe knobs for negatives. A True value does not
 # guarantee the given libc/syscall API will be used.
@@ -2228,10 +2234,19 @@ class Popen:
             try:
                 poller = select.poll()
                 poller.register(pidfd, select.POLLIN)
-                events = poller.poll(timeout * 1000)
-                if not events:
-                    raise TimeoutExpired(self.args, timeout)
-                return True
+                endtime = _time() + timeout
+                while True:
+                    # Clamp very large timeouts (e.g. float('inf')):
+                    # they overflow the C timestamp conversion in
+                    # poll().  Wait in bounded slices until the real
+                    # deadline (gh-154836).
+                    delay = min(_deadline_remaining(endtime),
+                                _MAXIMUM_WAIT_TIMEOUT)
+                    events = poller.poll(max(delay, 0) * 1000)
+                    if events:
+                        return True
+                    if _deadline_remaining(endtime) <= 0:
+                        raise TimeoutExpired(self.args, timeout)
             finally:
                 os.close(pidfd)
 
@@ -2252,14 +2267,26 @@ class Popen:
                     flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT,
                     fflags=select.KQ_NOTE_EXIT,
                 )
-                try:
-                    events = kq.control([kev], 1, timeout)  # wait
-                except OSError:
-                    return False
-                else:
-                    if not events:
+                changelist = [kev]
+                endtime = _time() + timeout
+                while True:
+                    # Clamp very large timeouts (e.g. float('inf')):
+                    # they overflow the C timestamp conversion in
+                    # kqueue.control().  Wait in bounded slices until
+                    # the real deadline (gh-154836).
+                    delay = min(_deadline_remaining(endtime),
+                                _MAXIMUM_WAIT_TIMEOUT)
+                    try:
+                        events = kq.control(changelist, 1, max(delay, 0))
+                    except OSError:
+                        return False
+                    if events:
+                        return True
+                    if _deadline_remaining(endtime) <= 0:
                         raise TimeoutExpired(self.args, timeout)
-                    return True
+                    # The kevent was registered by the first control()
+                    # call; don't re-add it on later slices.
+                    changelist = None
             finally:
                 kq.close()
 
index 2cf99be9e2c3baa3eaa7020a59846517772d5523..2649f3a7aee9b96cb015e561c15bf3f89f71cb13 100644 (file)
@@ -23,6 +23,20 @@ class TestKQueue(unittest.TestCase):
         self.assertTrue(kq.closed)
         self.assertRaises(ValueError, kq.fileno)
 
+    def test_control_overflowing_timeout(self):
+        # gh-154836: out-of-range timeouts must raise OverflowError,
+        # not a (misleading) TypeError, like select(), poll() and
+        # epoll() do.
+        kq = select.kqueue()
+        self.addCleanup(kq.close)
+        for timeout in (1e300, float('inf'), 2**200):
+            with self.subTest(timeout=timeout):
+                with self.assertRaises(OverflowError):
+                    kq.control(None, 0, timeout)
+        # Non-numbers still raise TypeError.
+        with self.assertRaises(TypeError):
+            kq.control(None, 0, "0.1")
+
     def test_create_event(self):
         from operator import lt, le, gt, ge
 
index d066ae85dfc51a649ef1be224f99de90cfba0a3c..4fd14d98b0324c0fa17f4f108c53d0a387dbcc31 100644 (file)
@@ -4300,5 +4300,31 @@ class FastWaitTestCase(BaseTestCase):
             self.assertEqual(p.wait(timeout=support.LONG_TIMEOUT), 0)
         self.assertFalse(m.called)
 
+    @unittest.skipIf(mswindows, "requires the POSIX wait implementation")
+    def test_wait_huge_timeout(self):
+        # gh-154836: very large timeout values used to overflow the C
+        # timestamp conversion in poll() / kqueue.control() and raise
+        # OverflowError / TypeError.
+        for timeout in (10**10, sys.maxsize, float('inf')):
+            with self.subTest(timeout=timeout):
+                p = subprocess.Popen(ZERO_RETURN_CMD)
+                self.assertEqual(p.wait(timeout=timeout), 0)
+
+    @unittest.skipIf(mswindows, "requires the POSIX wait implementation")
+    def test_run_huge_timeout(self):
+        # gh-154836: same as test_wait_huge_timeout, via the
+        # subprocess.run() / communicate() code path.
+        cp = subprocess.run(ZERO_RETURN_CMD, timeout=1e10)
+        self.assertEqual(cp.returncode, 0)
+
+    @unittest.skipIf(mswindows, "requires the POSIX wait implementation")
+    def test_wait_slices_do_not_expire_early(self):
+        # A clamped wait slice must not raise TimeoutExpired before the
+        # real deadline: with a tiny slice limit, a process that
+        # outlives many slices must still be waited for successfully.
+        with mock.patch.object(subprocess, "_MAXIMUM_WAIT_TIMEOUT", 0.01):
+            p = subprocess.Popen(self.COMMAND)  # sleeps 0.3s
+            self.assertEqual(p.wait(timeout=support.SHORT_TIMEOUT), 0)
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/Misc/NEWS.d/next/Library/2026-07-19-20-00-00.gh-issue-154836.kqWait.rst b/Misc/NEWS.d/next/Library/2026-07-19-20-00-00.gh-issue-154836.kqWait.rst
new file mode 100644 (file)
index 0000000..3691d60
--- /dev/null
@@ -0,0 +1,5 @@
+Fix :meth:`subprocess.Popen.wait` raising :exc:`TypeError` (macOS and other
+BSDs) or :exc:`OverflowError` (Linux) for very large *timeout* values such
+as ``float('inf')``, a 3.15 regression in the new event-driven wait. Also
+fix :meth:`select.kqueue.control` masking :exc:`OverflowError` for
+out-of-range timeouts as :exc:`TypeError`.
index 2c56dbc6a541f7ab645daa6e8b77aae8cd8c9fa8..b9fb7762e3dacdf695e394452cc191b641d17cc0 100644 (file)
@@ -2365,9 +2365,11 @@ select_kqueue_control_impl(kqueue_queue_Object *self, PyObject *changelist,
     else {
         if (_PyTime_FromSecondsObject(&timeout,
                                       otimeout, _PyTime_ROUND_TIMEOUT) < 0) {
-            PyErr_Format(PyExc_TypeError,
-                         "timeout must be a real number or None, not %T",
-                         otimeout);
+            if (PyErr_ExceptionMatches(PyExc_TypeError)) {
+                PyErr_Format(PyExc_TypeError,
+                             "timeout must be a real number or None, not %T",
+                             otimeout);
+            }
             return NULL;
         }