https://github.com/python/cpython/commit/dd2714d2d7f6c510ddff6be6dfe6efa6c9f6b2e0
commit: dd2714d2d7f6c510ddff6be6dfe6efa6c9f6b2e0
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: gpshead <[email protected]>
date: 2026-07-29T18:16:55Z
summary:
[3.15] gh-154836: Fix Popen.wait() with very large timeouts on the pidfd/kqueue
wait paths (GH-154837) (#154891)
gh-154836: Fix Popen.wait() with very large timeouts on the pidfd/kqueue wait
paths (GH-154837)
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.
(cherry picked from commit 11d0da5b54b1a162e8f2566675007cfb2db797b5)
Co-authored-by: Calvin Prewitt <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
files:
A Misc/NEWS.d/next/Library/2026-07-19-20-00-00.gh-issue-154836.kqWait.rst
M Lib/subprocess.py
M Lib/test/test_kqueue.py
M Lib/test/test_subprocess.py
M Modules/selectmodule.c
diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 38b655f2f7b9d2..5948cd841bc371 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 2cf99be9e2c3ba..2649f3a7aee9b9 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 f944084aaa6d6a..b9d15764c451ab 100644
--- a/Lib/test/test_subprocess.py
+++ b/Lib/test/test_subprocess.py
@@ -4290,5 +4290,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 00000000000000..3691d60413e6a1
--- /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 2c56dbc6a541f7..b9fb7762e3dacd 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;
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]