ITADN

3.15 regression: `Popen.wait(timeout=float("inf"))` raises `TypeError: timeout must be a real number or None` on macOS/BSD (new kqueue wait path)

#154836Opencalvinrp 创建于 23 天前
type-bugstdlibtopic-subprocess
C
calvinrpcommented
## Bug report ### Bug description Since 3.15, `subprocess.Popen.wait(timeout=...)` (and therefore `subprocess.run(..., timeout=...)` and `Popen.communicate(timeout=...)`) fails on macOS with a misleading `TypeError` when the timeout is a large-but-legal number such as `float("inf")`, `sys.maxsize`, or `1e10`. All of these worked in 3.14 and earlier, and `float("inf")` is a common "no practical limit" sentinel. Minimal reproducer: ```python import subprocess p = subprocess.Popen(["sleep", "0.1"]) print("wait returned:", p.wait(timeout=float("inf"))) ``` On 3.15.0b4 (macOS 15, arm64, built from the python.org tarball): ``` Traceback (most recent call last): File "repro.py", line 4, in <module> print("wait returned:", p.wait(timeout=float("inf"))) ~~~~~~^^^^^^^^^^^^^^^^^^^^^^ File ".../lib/python3.15/subprocess.py", line 1439, in wait return self._wait(timeout=timeout) ~~~~~~~~~~^^^^^^^^^^^^^^^^^ File ".../lib/python3.15/subprocess.py", line 2283, in _wait if self._wait_pidfd(timeout) or self._wait_kqueue(timeout): ~~~~~~~~~~~~~~~~~^^^^^^^^^ File ".../lib/python3.15/subprocess.py", line 2256, in _wait_kqueue events = kq.control([kev], 1, timeout) # wait TypeError: timeout must be a real number or None, not float ``` On 3.14.6 the same script prints `wait returned: 0`. Affected values (3.15.0b4, macOS): | timeout | 3.14.6 | 3.15.0b4 | |---|---|---| | `0.5`, `1` | OK | OK | | `1e10` | OK | `TypeError: timeout must be a real number or None, not float` | | `float("inf")` | OK | `TypeError: timeout must be a real number or None, not float` | | `sys.maxsize` | OK | `TypeError: timeout must be a real number or None, not int` | Note the error message is self-contradictory: the rejected value *is* a float (or int). The real error is an `OverflowError` from the timestamp conversion, masked by the `select.kqueue.control` implementation (details below). ("Why not `timeout=None`?" — `None` remains the idiomatic way to block forever and still works. But the regression also rejects large *finite* timeouts (`1e10`, `sys.maxsize`), which `None` cannot express, and `inf` arises naturally from deadline arithmetic — code computing `timeout = min(user_timeout, remaining_budget)` flows `math.inf` through cleanly where `None` would need special-casing at every comparison. All of these values were accepted by every Python version through 3.14.) ### Root cause All references are to current `main` (aeedae8b44558ee5c406c8b31287195912c29107, 2026-07-19), which I verified still has the identical code as 3.15.0b4. 3.15 introduced an event-driven wait for `Popen.wait` (gh-83069, #144047): `_wait` (`Lib/subprocess.py:2283`) now calls the new `_wait_kqueue` (`Lib/subprocess.py:2238`) on macOS/BSD, which passes the caller's timeout **unclamped** to the syscall wrapper: ```python # Lib/subprocess.py:2256 events = kq.control([kev], 1, timeout) # wait ``` `kqueue.control` converts the timeout with `_PyTime_FromSecondsObject`, which raises `OverflowError` for values that don't fit `PyTime_t` — but the kqueue code replaces *any* conversion failure with the TypeError: ```c /* Modules/selectmodule.c:2366-2371 */ if (_PyTime_FromSecondsObject(&timeout, otimeout, _PyTime_ROUND_TIMEOUT) < 0) { PyErr_Format(PyExc_TypeError, "timeout must be a real number or None, not %T", otimeout); return NULL; } ``` This is the only one of the five timeout-conversion sites in `Modules/selectmodule.c` that rewrites unconditionally. The sibling sites for `select` (line 308), `poll` (636), `devpoll` (1009) and `epoll` (1630) all guard the rewrite with `PyErr_ExceptionMatches(PyExc_TypeError)`, so they let the `OverflowError` through: ```c /* Modules/selectmodule.c:633-640 (poll; select/devpoll/epoll identical shape) */ if (_PyTime_FromMillisecondsObject(&timeout, timeout_obj, _PyTime_ROUND_TIMEOUT) < 0) { if (PyErr_ExceptionMatches(PyExc_TypeError)) { PyErr_Format(PyExc_TypeError, "timeout must be a real number or None, not %T", timeout_obj); } ``` Observable contrast on the same 3.15.0b4 build: ```python >>> select.poll().poll(float("inf")) OverflowError: timestamp out of range for C PyTime_t >>> select.select([], [], [], float("inf")) OverflowError: timestamp out of range for C PyTime_t >>> select.kqueue().control(None, 0, float("inf")) TypeError: timeout must be a real number or None, not float # masked ``` Why 3.14 worked: the old POSIX `_wait` never handed the user timeout to a syscall — it busy-waited with `delay = min(delay * 2, remaining, .05)`, so any finite or infinite timeout was fine. **Linux is affected too** (from code inspection; I only have macOS to test): `_wait_pidfd` (`Lib/subprocess.py:2211`) passes `timeout * 1000` unclamped to `poller.poll(...)` at line 2231. Since `poll` correctly propagates the conversion failure (see above — verified on macOS, same C code path), `wait(timeout=float("inf"))` on Linux >= 5.3 should raise `OverflowError` instead — the right exception type, but still a behavior regression from 3.14, where these values were accepted. ### Expected behavior `p.wait(timeout=float("inf"))`, `wait(timeout=sys.maxsize)` etc. behave as in 3.14 and earlier: block until the process exits (or until the — practically unreachable — deadline), rather than raising. ### Suggested fix direction Two independent pieces: 1. **`Lib/subprocess.py`** — clamp the per-syscall wait and retry until the real deadline. There is stdlib precedent in asyncio, which clamps select timeouts to `MAXIMUM_SELECT_TIMEOUT = 24 * 3600` ("Maximum timeout passed to select to avoid OS limitations", `Lib/asyncio/base_events.py:70-71`, applied at 2029-2030) and relies on re-entering the loop. One subtlety: `_wait_pidfd` and `_wait_kqueue` currently raise `TimeoutExpired` themselves (`Lib/subprocess.py:2233` and `:2261`), so a naive clamp inside the helpers would time out early; the clamped wait needs to loop until `endtime` (or return `False` on a clamped-but-not-expired timeout so `_wait` falls back to its existing retry machinery). 2. **`Modules/selectmodule.c`** — make the kqueue error rewrite conditional on `PyErr_ExceptionMatches(PyExc_TypeError)` like the other four sites, so out-of-range timeouts surface as `OverflowError` rather than a false "timeout must be a real number or None". Worth doing regardless of (1): the current message is wrong for any user of `kqueue.control` with a huge timeout. ### CPython versions tested on 3.15.0b4 (regression), 3.14.6 (works); code inspected on current `main` (aeedae8b44558ee5c406c8b31287195912c29107, 2026-07-19) — same code, so `main` is affected as well. ### Operating systems tested on macOS (15, arm64). The failing path is kqueue-only, so all BSD-family platforms should be affected identically; Linux >= 5.3 affected via the pidfd path with `OverflowError` instead (code inspection, untested). ### Output from `python -VV` ``` Python 3.15.0b4 (main, Jul 18 2026, 07:46:30) [Clang 22.1.7 ] ``` <!-- gh-linked-prs --> ### Linked PRs * gh-154837 * gh-154891 <!-- /gh-linked-prs -->
1 条评论