diff --git a/Lib/subprocess.py b/Lib/subprocess.py index 6fe2ec98fb40888..054860a19c74b6d 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -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 @@ def _wait_pidfd(self, timeout): 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 @@ def _wait_kqueue(self, timeout): 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() diff --git a/Lib/test/test_kqueue.py b/Lib/test/test_kqueue.py index 2cf99be9e2c3baa..2649f3a7aee9b96 100644 --- a/Lib/test/test_kqueue.py +++ b/Lib/test/test_kqueue.py @@ -23,6 +23,20 @@ def test_create_queue(self): 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 diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index d066ae85dfc51a6..4fd14d98b0324c0 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -4300,5 +4300,31 @@ def test_fast_path_avoid_busy_loop(self): 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 index 000000000000000..3691d60413e6a1f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-19-20-00-00.gh-issue-154836.kqWait.rst @@ -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`. diff --git a/Modules/selectmodule.c b/Modules/selectmodule.c index 2c56dbc6a541f7a..b9fb7762e3dacdf 100644 --- a/Modules/selectmodule.c +++ b/Modules/selectmodule.c @@ -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; }