Public issue detail

Runaway CPU investigation for python3.13: busy poll at _PyEval_EvalFrameDefault

python3.13 is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through _PyEval_EvalFrameDefault, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.

highpackage: python3.13-minimalsource: python3.13score: 106reports: 2patch attempt ready

Last seen: 2026-05-10 06:16 UTC. Public JSON: /v1/issues/019dda71-4897-7d62-853d-31f4899d9f8e

Pull-request-ready diff

This is the current best public patch attempt for the issue. The downloadable .patch now includes a short cover letter so it reads like something you could send upstream with git am. If you only want the raw diff, grab the .diff instead.

best patchcreated: 2026-04-30 22:10 UTCvalidation: ready

python3.13 likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.

Suggested subject

subprocess: keep clamped pidfd waits looping

Commit message.

Use pidfd polling for POSIX `Popen.wait(timeout=...)` when available, and keep very large or infinite Python timeouts compatible by clamping each `poll()` call while continuing to recompute the Python deadline.

Problem. python3.13 is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through _PyEval_EvalFrameDefault, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.

How this patch connects to the issue.

The user-visible symptom was a `python3.13` process repeatedly waking in `poll` and `wait4`, with CPU samples in Python frame evaluation. A plausible code-level cause is the POSIX `subprocess.Popen._wait(timeout)` loop, which uses repeated nonblocking `waitpid(..., WNOHANG)` calls with short sleeps.

The patch adds a Linux pidfd-backed wait path for timeout waits. This refinement addresses the review finding by treating an empty result from a clamped `poll()` as one elapsed kernel-sized wait, then continuing the loop while the Python timeout still has time remaining. The regression test now covers the case where the first clamped poll returns no events and a later poll observes process exit.

The expected effect is fewer avoidable userspace wakeups for normal timeout waits on Linux with pidfds, while preserving behavior for very large and infinite timeouts. The code includes short comments for the non-obvious timeout clamping and pidfd/readiness remapping logic.

Files touched

  • Lib/subprocess.py
  • Lib/test/test_subprocess.py

Validation

  • Fixer marked this proposal `ready` on 2026-04-30 22:10 UTC.
  • The underlying issue cluster has 2 report(s) and was last seen 2026-05-10 06:16 UTC.
  • The published diff touches Lib/subprocess.py, Lib/test/test_subprocess.py.
  • `TMPDIR=/tmp/fixer-86731 PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait_timeout_pidfd_clamps_large_timeouts'` passed. `TMPDIR=/tmp/fixer-86731 PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait*'` passed. `git diff --check` passed. `git status --short` shows only `Lib/subprocess.py`, `Lib/test/test_subprocess.py`, and an untracked `.codex` workspace mount. I tried to remove `.codex`, but `unlink` reported `Device or resource busy`; it is not part of the patch.
diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 3a8c743..b41920b 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -2049,6 +2049,74 @@ class Popen:
                 sts = 0
             return (pid, sts)
 
+        def _wait_pidfd(self, endtime, orig_timeout):
+            """Wait for process exit using a pidfd."""
+            if not hasattr(os, "pidfd_open") or not hasattr(select, "poll"):
+                return False
+
+            if self._waitpid_lock.acquire(False):
+                try:
+                    if self.returncode is not None:
+                        return True  # Another thread waited.
+                    (pid, sts) = self._try_wait(os.WNOHANG)
+                    assert pid == self.pid or pid == 0
+                    if pid == self.pid:
+                        self._handle_exitstatus(sts)
+                        return True
+                finally:
+                    self._waitpid_lock.release()
+            else:
+                return False
+
+            try:
+                pidfd = os.pidfd_open(self.pid)
+            except OSError:
+                return False
+
+            try:
+                poller = select.poll()
+                poller.register(pidfd, select.POLLIN)
+                while self.returncode is None:
+                    remaining = self._remaining_time(endtime)
+                    if remaining <= 0:
+                        raise TimeoutExpired(self.args, orig_timeout)
+                    # Preserve the legacy timeout loop's handling of NaN.
+                    if remaining != remaining:
+                        return False
+
+                    # select.poll() accepts a signed int millisecond
+                    # timeout.  Clamp longer waits and recheck the Python
+                    # deadline after each poll returns.
+                    max_timeout = 2_147_483_647
+                    clamped = remaining >= max_timeout / 1000
+                    if clamped:
+                        timeout = max_timeout
+                    else:
+                        timeout = max(1, int(remaining * 1000 + 0.999))
+                    if not poller.poll(timeout):
+                        if clamped:
+                            continue
+                        raise TimeoutExpired(self.args, orig_timeout)
+
+                    if self._waitpid_lock.acquire(False):
+                        try:
+                            if self.returncode is not None:
+                                return True  # Another thread waited.
+                            # A readable pidfd means the same child should now
+                            # be waitable; keep that pidfd/waitpid identity
+                            # check intact.
+                            (pid, sts) = self._try_wait(os.WNOHANG)
+                            assert pid == self.pid or pid == 0
+                            if pid == self.pid:
+                                self._handle_exitstatus(sts)
+                                return True
+                        finally:
+                            self._waitpid_lock.release()
+                    return False
+                return True
+            finally:
+                os.close(pidfd)
+
 
         def _wait(self, timeout):
             """Internal implementation of wait() on POSIX."""
@@ -2057,6 +2125,9 @@ class Popen:
 
             if timeout is not None:
                 endtime = _time() + timeout
+                if self._wait_pidfd(endtime, timeout):
+                    return self.returncode
+
                 # Enter a busy loop if we have a timeout.  This busy loop was
                 # cribbed from Lib/threading.py in Thread.wait() at r71065.
                 delay = 0.0005 # 500 us -> initial delay of 1 ms
diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py
index a9f0ce2..3b9e9e2 100644
--- a/Lib/test/test_subprocess.py
+++ b/Lib/test/test_subprocess.py
@@ -2070,6 +2070,117 @@ class POSIXProcessTestCase(BaseTestCase):
         def __del__(self):
             pass
 
+    def _make_wait_test_popen(self):
+        proc = subprocess.Popen.__new__(subprocess.Popen)
+        proc.args = ["test"]
+        proc.pid = 12345
+        proc.returncode = None
+        proc._waitpid_lock = threading.Lock()
+        return proc
+
+    @unittest.skipUnless(hasattr(select, "poll"), "select.poll required")
+    def test_wait_timeout_uses_pidfd(self):
+        proc = self._make_wait_test_popen()
+        pidfd = 10
+        poller = mock.Mock()
+        poller.poll.return_value = [(pidfd, select.POLLIN)]
+
+        with mock.patch("subprocess.os.pidfd_open", return_value=pidfd,
+                        create=True) as pidfd_open, \
+             mock.patch("subprocess.select.poll",
+                        return_value=poller) as poll, \
+             mock.patch("subprocess.os.waitpid",
+                        side_effect=[(0, 0), (proc.pid, 0)]) as waitpid, \
+             mock.patch("subprocess.os.close") as close:
+            self.assertEqual(proc._wait(timeout=10), 0)
+
+        pidfd_open.assert_called_once_with(proc.pid)
+        poll.assert_called_once_with()
+        poller.register.assert_called_once_with(pidfd, select.POLLIN)
+        self.assertEqual(waitpid.call_args_list,
+                         [mock.call(proc.pid, os.WNOHANG),
+                          mock.call(proc.pid, os.WNOHANG)])
+        close.assert_called_once_with(pidfd)
+
+    @unittest.skipUnless(hasattr(select, "poll"), "select.poll required")
+    def test_wait_timeout_pidfd_clamps_large_timeouts(self):
+        for timeout in (float("inf"), 10**20):
+            with self.subTest(timeout=timeout):
+                proc = self._make_wait_test_popen()
+                pidfd = 10
+                poller = mock.Mock()
+                poller.poll.side_effect = [[], [(pidfd, select.POLLIN)]]
+
+                with mock.patch("subprocess.os.pidfd_open",
+                                return_value=pidfd, create=True), \
+                     mock.patch("subprocess.select.poll",
+                                return_value=poller), \
+                     mock.patch("subprocess.os.waitpid",
+                                side_effect=[(0, 0), (proc.pid, 0)]), \
+                     mock.patch("subprocess._time",
+                                side_effect=[100.0, 100.0, 200.0]), \
+                     mock.patch("subprocess.os.close"):
+                    self.assertEqual(proc._wait(timeout=timeout), 0)
+
+                self.assertEqual(poller.poll.call_args_list,
+                                 [mock.call(2_147_483_647),
+                                  mock.call(2_147_483_647)])
+
+    @unittest.skipUnless(hasattr(select, "poll"), "select.poll required")
+    def test_wait_timeout_pidfd_timeout(self):
+        proc = self._make_wait_test_popen()
+        pidfd = 10
+        poller = mock.Mock()
+        poller.poll.return_value = []
+
+        with mock.patch("subprocess.os.pidfd_open", return_value=pidfd,
+                        create=True), \
+             mock.patch("subprocess.select.poll", return_value=poller), \
+             mock.patch("subprocess.os.waitpid",
+                        return_value=(0, 0)) as waitpid, \
+             mock.patch("subprocess.os.close") as close:
+            with self.assertRaises(subprocess.TimeoutExpired):
+                proc._wait(timeout=10)
+
+        waitpid.assert_called_once_with(proc.pid, os.WNOHANG)
+        close.assert_called_once_with(pidfd)
+
+    def test_wait_timeout_pidfd_open_fallback(self):
+        proc = self._make_wait_test_popen()
+
+        with mock.patch("subprocess.os.pidfd_open",
+                        side_effect=OSError(errno.ENOSYS, "pidfd_open"),
+                        create=True) as pidfd_open, \
+             mock.patch("subprocess.os.waitpid",
+                        side_effect=[(0, 0), (0, 0),
+                                     (proc.pid, 0)]) as waitpid, \
+             mock.patch("subprocess.time.sleep") as sleep:
+            self.assertEqual(proc._wait(timeout=10), 0)
+
+        pidfd_open.assert_called_once_with(proc.pid)
+        self.assertEqual(waitpid.call_args_list,
+                         [mock.call(proc.pid, os.WNOHANG),
+                          mock.call(proc.pid, os.WNOHANG),
+                          mock.call(proc.pid, os.WNOHANG)])
+        sleep.assert_called()
+
+    def test_wait_timeout_does_not_block_on_waitpid_lock(self):
+        proc = self._make_wait_test_popen()
+
+        class BusyWaitpidLock:
+            def acquire(self, blocking=True):
+                if blocking:
+                    raise AssertionError("_wait() must not block on timeout")
+                return False
+
+            def release(self):
+                raise AssertionError("lock was not acquired")
+
+        proc._waitpid_lock = BusyWaitpidLock()
+        with mock.patch("subprocess._time", side_effect=[10.0, 10.2]):
+            with self.assertRaises(subprocess.TimeoutExpired):
+                proc._wait(timeout=0.1)
+
     @mock.patch("subprocess._fork_exec")
     def test_exception_errpipe_normal(self, fork_exec):
         """Test error passing done through errpipe_write in the good case"""

Technical snapshot

This is the clearest retained userspace thread cluster Fixer captured while the process was spinning.

Representative thread backtrace

  • Command: /usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf
  • Why Fixer classified it this way: The trace repeatedly returns to a poll-family syscall without meaningful blocking, which suggests a busy event-loop wakeup.
  • Thread summary: thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>
  • Contention signals: gdb-stderr: gdb: warning: Couldn't determine a path for the index cache directory.
  • Repeated loop: poll -> wait4 -> poll
  • Top syscalls: poll x5, wait4 x5, restart_syscall x1
  • Package: python3.13-minimal 3.13.12-1
  • Kernel: 6.17.10+deb14-amd64
  • Distribution: debian
0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>
0x0000000000bb4cd4 in ?? ()
0x00000000160e950c in ?? ()
0x0000000000a56c88 in _PyRuntime ()
0x00007f1c6e7efead in ?? () from target:<path>
0x0000000000000007 in ?? ()
0x000000000054aeb0 in PyMem_Realloc ()
0x000000000067ca5b in ?? ()

Possible duplicates

These are suggestions based on sanitized trigram similarity plus structured fields like package, subsystem, classification, and wait site. They are not auto-merged.

python3.13 is stuck in a likely busy poll loop: 40.65% of sampled CPU passed through idle_cpu, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.

highpackage: python3.13-minimalsource: python3.13score: 106reports: 2similarity: 99%

Why this looks related: same classification, same package, same source package, same subsystem, same target, same wait site, very similar public summary

Last seen: 2026-05-19 02:35 UTC. Public page: /issues/019dfead-0293-7062-b24e-c9b04c2f1edb. Public JSON: /v1/issues/019dfead-0293-7062-b24e-c9b04c2f1edb

python3.13 is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through unresolved offset, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.

highpackage: python3.13-minimalsource: python3.13score: 106reports: 2similarity: 99%

Why this looks related: same classification, same package, same source package, same subsystem, same target, same wait site, very similar public summary

Last seen: 2026-05-19 01:48 UTC. Public page: /issues/019dc2a0-9ae7-7f42-8f59-415fe22abe51. Public JSON: /v1/issues/019dc2a0-9ae7-7f42-8f59-415fe22abe51

python3.13 is stuck in a likely busy poll loop: 50.00% of sampled CPU passed through read_tsc, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.

highpackage: python3.13-minimalsource: python3.13score: 106reports: 2similarity: 99%

Why this looks related: same classification, same package, same source package, same subsystem, same target, same wait site, very similar public summary

Last seen: 2026-05-19 01:01 UTC. Public page: /issues/019da8df-cd0c-7880-ab1e-6dd908d64437. Public JSON: /v1/issues/019da8df-cd0c-7880-ab1e-6dd908d64437

python3.13 is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through PyObject_GC_Del, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.

highpackage: python3.13-minimalsource: python3.13score: 106reports: 2patch attempt readysimilarity: 99%

Why this looks related: same classification, same package, same source package, same subsystem, same target, same wait site, very similar public summary

Last seen: 2026-05-15 03:34 UTC. Public page: /issues/019dd1e0-1f56-7862-9fd2-d20d576df8c4. Public JSON: /v1/issues/019dd1e0-1f56-7862-9fd2-d20d576df8c4

python3.13 is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through PyUnicode_FromWideChar, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.

highpackage: python3.13-minimalsource: python3.13score: 106reports: 2patch attempt readysimilarity: 99%

Why this looks related: same classification, same package, same source package, same subsystem, same target, same wait site, very similar public summary

Last seen: 2026-05-13 02:59 UTC. Public page: /issues/019e176a-65b7-7d92-a20c-8c276f7ca31a. Public JSON: /v1/issues/019e176a-65b7-7d92-a20c-8c276f7ca31a

python3.13 is stuck in a likely busy poll loop: 17.46% of sampled CPU passed through unresolved offset, with repeated thread backtraces show 2 thread(s) around 0x00007f7052c74ffe in ?? () from target:<path> and 1 thread(s) around 0x00007f70501a4447 in ?? () from target:<path>.

highpackage: python3.13-minimalsource: python3.13score: 106reports: 2successful triagesimilarity: 90%

Why this looks related: same classification, same package, same source package, same subsystem, same target

Last seen: 2026-05-19 04:12 UTC. Public page: /issues/019dd98b-0e1d-76e3-9171-f0f72c689536. Public JSON: /v1/issues/019dd98b-0e1d-76e3-9171-f0f72c689536

Worker outcome summary

This issue has 5 recorded worker attempts. Only ready diffs and ready triage handoffs get dedicated public boards. Diagnosis-only reports and blocked attempts are summarized here so it is easier to see why work stalled.

2 ready patch attempts
1 ready triage handoffs
2 failed patch attempts

No diagnosis-only reports, explained impossible attempts, or other attempt states.

Most common blockers

  • review (2 attempts)
  • stale-best-patch (1 attempt)

Published attempts

ready triage handoff

triage

python3.13 likely remains stuck in a busy-poll loop. Fixer re-reviewed the previous patch, found it stale or incorrect, and reopened the issue for another pass. No replacement patch survived review yet.

state: readycreated: 2026-05-09 07:38 UTCvalidation: review-rejected

Why it stopped

stale-best-patch

Handoff

Likely owner: external dependency or workload outside the current source tree

Reason: stale-best-patch

  • Confirm the hotspot still points at external dependency or workload outside the current source tree with a fresh perf sample before filing the bug.
  • Capture the actual hot backend or child process rather than the parent service wrapper if the issue recurs.
  • Map external dependency or workload outside the current source tree to its owning package or project and file an upstream or distro bug with the summarized evidence.
  • If the owner is still unclear, collect another short strace plus `/proc/<pid>/maps` at the moment of the spike.

Failure details

  • Failure kind: review
  • Review category: findings-persisted
  • Exit status: 0
  • Error: Review Pass 4 still found unresolved issues after 3 refinement pass(es).

stderr excerpt

I could not read `evidence.json`, `refine-3-output.txt`, project docs, the current patch, or the original snapshot diff. I also could not validate whether the author attempted `./configure && make`, whether `## Evidence Confidence` is present and valid, or whether local CPython helpers/conventions were followed.

## Validation

Not run. The command execution wrapper fails before any project command starts, including simple reads such as `ls`.
tokens used
14,819
Published session

Prompt

## Plan Pass

You are planning a fixer patch before any edits happen.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. The original pre-edit snapshot is available at `./source` if you need to inspect it.

Validation expectation: try the project-level build/test entrypoint from the workspace root before reporting only a focused leaf compile. Detected candidate(s): `./configure && make`. If the project-level command fails because dependencies or generated files are missing, include the exact command and failure reason in `## Validation`, then run the narrowest relevant compile/test that is still reproducible from a clean checkout.

Upstream-style expectation: before planning or editing, check for contribution/style docs (`CONTRIBUTING`, `HACKING`, `README-hacking`, `README.md`, `docs/`, `dev-docs/`) and scan the touched subsystem for local helpers. If the project has wrappers for file IO, path-relative IO, process spawning, memory allocation, logging, locking, or platform compatibility, prefer those wrappers over generic libc/std APIs. Do not invent a reproducer or user-visible failure that is not in the evidence bundle; if the evidence is profiler-only or indirect, describe it as a targeted mitigation or stop with a diagnosis instead of presenting a speculative patch as a confirmed bug fix. In the plan and final validation, name any such helper, convention, or evidence limit you found, or say that no relevant local helper was found. Treat this as a `python3.13` upstream patch, not just a Debian-local workaround. Inspect the relevant code, nearby callers, project contribution docs, and local helper/compat APIs, but do not edit files in this pass.

Return a short markdown plan with these exact sections:

## Problem
## Evidence Confidence
## Proposed Subject
## Patch Plan
## Risks
## Validation

Classify `## Evidence Confidence` as exactly one of `reproduced`, `observed`, or `inferred`. The plan must explain how the proposed code change addresses the observed issue evidence, call out any prior Fixer patch that should be improved or replaced, reject awkward control flow such as avoidable `goto` if there is a cleaner bounded alternative, name any local helper APIs or maintainer conventions the patch should follow, and keep the intended maintainer-facing explanation clear enough that someone unfamiliar with the local complaint wording can still follow the fix. In `## Validation`, name the reproducible configure/build/test entrypoint you will try from the workspace root before any focused leaf compile or smoke check.

## Patch Pass

You are working on a bounded fixer proposal.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Produce the smallest reasonable patch for the target repository, keep the change upstreamable, prefer the clearest control flow available, and do not keep avoidable `goto` when a simpler structure would read better. Before introducing new file, process, allocation, locking, networking, or platform APIs, inspect nearby code and project contribution docs for existing helpers or compatibility wrappers and use those local patterns unless you can explain why they do not fit. Validate from a reproducible workspace-root entrypoint before falling back to focused leaf commands; if a build or test cannot run, report the exact command, the exact blocker, and any narrower check you ran instead. The final explanation must connect the observed issue evidence to the actual code change, not just paraphrase the diff. Write like a maintainer is going to read the patch mail cold: explain the bug in plain language, define subsystem-specific jargon the first time you need it, and make the causal story obvious. Explicitly classify evidence confidence as `reproduced`, `observed`, or `inferred`: `reproduced` means you reproduced the failure locally; `observed` means Fixer has direct crash/log/trace evidence but you did not independently reproduce it; `inferred` means the patch is based on profiler/strace/indirect signals and must be framed as a cautious mitigation, not as a proven fix. If you introduce non-obvious state translation, index remapping, or backend split logic, add a short source comment that explains the invariant being preserved.

Start by explaining the likely root cause from the collected perf, strace, and /proc evidence. If you cannot land a safe patch, leave a diagnosis that is strong enough for an upstream bug report.

A previous Fixer patch attempt already exists for this issue. It was generated by Fixer `0.127.0`. Review that patch before changing code, improve it instead of starting blind, and clean up anything awkward or underexplained. In particular, remove avoidable `goto`, tighten the explanation of what the patch is doing, and make the resulting diff feel ready for upstream git review.
- Prior patch: `./prior-best.patch`
- Prior published session: `./prior-best-session.md`

Validation expectation: try the project-level build/test entrypoint from the workspace root before reporting only a focused leaf compile. Detected candidate(s): `./configure && make`. If the project-level command fails because dependencies or generated files are missing, include the exact command and failure reason in `## Validation`, then run the narrowest relevant compile/test that is still reproducible from a clean checkout.

Upstream-style expectation: before planning or editing, check for contribution/style docs (`CONTRIBUTING`, `HACKING`, `README-hacking`, `README.md`, `docs/`, `dev-docs/`) and scan the touched subsystem for local helpers. If the project has wrappers for file IO, path-relative IO, process spawning, memory allocation, logging, locking, or platform compatibility, prefer those wrappers over generic libc/std APIs. Do not invent a reproducer or user-visible failure that is not in the evidence bundle; if the evidence is profiler-only or indirect, describe it as a targeted mitigation or stop with a diagnosis instead of presenting a speculative patch as a confirmed bug fix. In the plan and final validation, name any such helper, convention, or evidence limit you found, or say that no relevant local helper was found. Treat this as a `python3.13` upstream patch, not just a Debian-local workaround. 

Keep the change narrowly scoped and summarize validation clearly.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Evidence Confidence
Exactly one word: `reproduced`, `observed`, or `inferred`. Use `reproduced` only when you reproduced the failure locally with a command or test. Use `observed` when Fixer has direct crash/log/trace evidence but you did not independently reproduce it. Use `inferred` for profiler/strace/indirect evidence; inferred source patches are not pull-request-ready until stronger evidence is gathered.

## Issue Connection
Write this as maintainer-facing patch mail, not as local Fixer notes. Cover four things explicitly in readable sentences: the user-visible symptom or the exact collected signal, the code-level cause or the cautious inference from evidence, the specific change you made, and the expected effect. Do not invent a reproducer, command line, crash, or user-visible failure that is not present in the evidence bundle. If the evidence is direct-but-not-reproduced, say it was observed by Fixer and not independently reproduced. If the evidence is indirect, say it is a plausible targeted mitigation rather than a proven root cause. Include an explicit effect sentence such as `The expected effect is ...`, `This should reduce ...`, or `This prevents ...`. If the logic is non-obvious in code, mention that you added a short explanatory comment.

## Git Add Paths
List the repo-relative paths that belong in the final patch, one per line. Include intentionally new files, and do not list generated build artifacts.

## Validation
List the checks you ran, or say clearly that you could not run them.

Before editing, read the plan at `./plan-output.txt` and follow it unless the code proves part of it wrong. If you change course, say so explicitly in the final write-up instead of silently drifting from the plan.

## Review Pass 1

You are reviewing a freshly generated fixer patch.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Review the first patch pass. The original pre-edit snapshot is available at `./source` for diffing.

Upstream-style expectation: before planning or editing, check for contribution/style docs (`CONTRIBUTING`, `HACKING`, `README-hacking`, `README.md`, `docs/`, `dev-docs/`) and scan the touched subsystem for local helpers. If the project has wrappers for file IO, path-relative IO, process spawning, memory allocation, logging, locking, or platform compatibility, prefer those wrappers over generic libc/std APIs. Do not invent a reproducer or user-visible failure that is not in the evidence bundle; if the evidence is profiler-only or indirect, describe it as a targeted mitigation or stop with a diagnosis instead of presenting a speculative patch as a confirmed bug fix. In the plan and final validation, name any such helper, convention, or evidence limit you found, or say that no relevant local helper was found. Treat this as a `python3.13` upstream patch, not just a Debian-local workaround.

Validation expectation: try the project-level build/test entrypoint from the workspace root before reporting only a focused leaf compile. Detected candidate(s): `./configure && make`. If the project-level command fails because dependencies or generated files are missing, include the exact command and failure reason in `## Validation`, then run the narrowest relevant compile/test that is still reproducible from a clean checkout. The latest author response is at `./patch-output.txt`. Inspect the current code and changed paths like a strict code reviewer. Focus on correctness, regressions, maintainability, awkward control flow such as avoidable `goto`, missing validation, weak or non-gittable commit message text, and explanations that fail to connect the observed issue evidence to the code change. Also review the maintainer experience: the patch mail should be easy to accept upstream, the user-visible bug should be explained in plain language, subsystem-specific jargon should be defined when first used, and any non-obvious state translation or index remapping in code should have a short explanatory comment. Reject patches that omit `## Evidence Confidence`, use anything other than `reproduced`, `observed`, or `inferred`, or sound more certain than that evidence level allows. Reject patches that invent a reproducer, command line, crash, or user-visible failure not present in the evidence bundle; indirect profiler or strace evidence may justify a cautious mitigation, but the author must say it is indirect instead of presenting it as a confirmed upstream bug. Reject patches that introduce generic libc/std APIs when nearby code or project docs provide a local compat/helper API for the same job, unless the author explicitly justifies the exception. Reject validation that only reports a leaf object/syntax build when a project-level configure/build/test entrypoint exists and was not attempted; if a full build cannot run, the author must show the exact failed command and blocker.

Do not apply code changes in this pass.

Return a short markdown review report. The first non-empty line must be exactly one of:

RESULT: ok
RESULT: fix-needed

If you choose `RESULT: fix-needed`, add a `## Findings` section with concrete, actionable items.

## Refinement Pass 1

You are refining a fixer patch after an explicit code review.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Read the latest author response at `./patch-output.txt`. Read the review report at `./review-1-output.txt`. This is refinement round 1. The original pre-edit snapshot is available at `./source` if you need to compare the current patch against it. Re-read the planning pass at `./plan-output.txt` before editing.

Upstream-style expectation: before planning or editing, check for contribution/style docs (`CONTRIBUTING`, `HACKING`, `README-hacking`, `README.md`, `docs/`, `dev-docs/`) and scan the touched subsystem for local helpers. If the project has wrappers for file IO, path-relative IO, process spawning, memory allocation, logging, locking, or platform compatibility, prefer those wrappers over generic libc/std APIs. Do not invent a reproducer or user-visible failure that is not in the evidence bundle; if the evidence is profiler-only or indirect, describe it as a targeted mitigation or stop with a diagnosis instead of presenting a speculative patch as a confirmed bug fix. In the plan and final validation, name any such helper, convention, or evidence limit you found, or say that no relevant local helper was found. Treat this as a `python3.13` upstream patch, not just a Debian-local workaround.

Validation expectation: try the project-level build/test entrypoint from the workspace root before reporting only a focused leaf compile. Detected candidate(s): `./configure && make`. If the project-level command fails because dependencies or generated files are missing, include the exact command and failure reason in `## Validation`, then run the narrowest relevant compile/test that is still reproducible from a clean checkout. Address the review findings with the smallest reasonable follow-up changes. If the review identifies a runtime or correctness bug in the changed code, you must update the code itself before answering; a metadata-only response is not sufficient. Keep the patch upstream-friendly, use local project helpers and compat APIs when available, avoid awkward control flow when a simpler structure will do, keep the final response gittable, make the maintainer-facing explanation plain and direct, keep `## Evidence Confidence` honest, add short comments for any non-obvious translation or remapping logic, run relevant project-level build/tests from the workspace root before narrower checks when possible, and summarize which review findings you addressed.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Evidence Confidence
Exactly one word: `reproduced`, `observed`, or `inferred`. Use `reproduced` only when you reproduced the failure locally with a command or test. Use `observed` when Fixer has direct crash/log/trace evidence but you did not independently reproduce it. Use `inferred` for profiler/strace/indirect evidence; inferred source patches are not pull-request-ready until stronger evidence is gathered.

## Issue Connection
Write this as maintainer-facing patch mail, not as local Fixer notes. Cover four things explicitly in readable sentences: the user-visible symptom or the exact collected signal, the code-level cause or the cautious inference from evidence, the specific change you made, and the expected effect. Do not invent a reproducer, command line, crash, or user-visible failure that is not present in the evidence bundle. If the evidence is direct-but-not-reproduced, say it was observed by Fixer and not independently reproduced. If the evidence is indirect, say it is a plausible targeted mitigation rather than a proven root cause. Include an explicit effect sentence such as `The expected effect is ...`, `This should reduce ...`, or `This prevents ...`. If the logic is non-obvious in code, mention that you added a short explanatory comment.

## Git Add Paths
List the repo-relative paths that belong in the final patch, one per line. 

[truncated 19666 bytes]

Response

## Plan Pass

## Problem
Unable to produce a real fixer patch plan yet. Every attempted workspace/evidence read failed before shell execution with:

`bwrap: execvp .../codex: Permission denied`

Because I could not read `evidence.json`, contribution docs, prior patches, or subsystem code, I should not infer a concrete CPython change.

## Evidence Confidence
inferred

## Proposed Subject
Defer patch subject until the evidence bundle and relevant CPython subsystem can be inspected.

## Patch Plan
1. Read `evidence.json`.
2. Check upstream guidance: `CONTRIBUTING`, `README.md`, `docs/`, `dev-docs/`, and subsystem-local conventions.
3. Inspect implicated code and nearby callers.
4. Check for CPython helper APIs/conventions before proposing any libc/std API usage.
5. Inspect any prior Fixer patch and decide whether it should be improved or replaced.
6. Avoid awkward control flow such as avoidable `goto`; prefer a bounded local cleanup path if the code shape permits it.

## Risks
Any patch proposed without reading the evidence would be speculative. No user-visible failure, maintainer-facing subject, local helper API, or prior Fixer patch assessment can be stated reliably from the current blocked environment.

## Validation
Expected project-level entrypoint to try first from workspace root:

`./configure && make`

If that fails due missing dependencies/generated files, record the exact failure, then run the narrowest reproducible relevant compile or smoke check. Current blocker occurred earlier: even `pwd` failed because the sandbox wrapper could not execute its helper binary.

## Patch Pass

Subject: Unable to patch: workspace shell cannot execute

## Commit Message
No patch was produced because every attempt to inspect the evidence bundle or workspace failed before the requested commands could run. The sandbox launcher reported `Permission denied` while trying to execute the Codex helper binary, so I could not review the prior patch, inspect CPython code, or make an upstreamable change.

## Evidence Confidence
inferred

## Issue Connection
I could not read the collected perf, strace, `/proc` evidence, the prior Fixer patch, or the plan file, so I cannot responsibly state the likely CPython root cause or land a safe source patch. The only directly observed signal in this session is the local tooling failure: `bwrap: execvp .../codex: Permission denied`.

The expected effect is no repository change. A maintainer-facing diagnosis for the original Python issue still requires reading the evidence bundle and touched subsystem once the workspace shell can execute.

## Git Add Paths
None

## Validation
Could not run validation.

Attempted from workspace root:
`pwd && sed -n '1,220p' ./evidence.json`

Failure:
`bwrap: execvp /home/kom/.local/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex: Permission denied`

Also attempted:
`pwd` with `/bin/sh`

Same failure. No project-level `./configure && make` or focused compile could run because command execution itself is blocked.

## Review Pass 1

RESULT: fix-needed

## Findings

- Unable to perform the required review because every shell command failed before execution with:
  `bwrap: execvp .../codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex: Permission denied`

I could not read `evidence.json`, `patch-output.txt`, contribution/style docs, touched files, or the diff against `./source`. This needs an environment fix before a meaningful strict patch review can be completed.

## Refinement Pass 1

Subject: Unable to refine patch due sandbox execution failure

## Commit Message
No code changes were made. The local command runner failed before any repository files, evidence, style docs, or review notes could be read.

## Evidence Confidence
inferred

## Issue Connection
I could not inspect the Fixer evidence bundle, prior planning pass, author response, review report, or touched subsystem. Every attempted shell command failed before execution with `bwrap: execvp .../codex: Permission denied`.

Because the code and review findings were not accessible, I did not make a speculative source change. The expected effect is no change to the current patch.

## Git Add Paths
None

## Validation
Could not run validation. Attempted command from the workspace root:

`pwd`

It failed before execution with:

`bwrap: execvp /home/kom/.local/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex: Permission denied`

## Review Pass 2

RESULT: fix-needed

## Findings

- Unable to review the patch: every local inspection command failed before execution with `bwrap: execvp .../codex: Permission denied`. This prevented reading `evidence.json`, `refine-1-output.txt`, contribution/style docs, the changed files, and the diff against `./source`.

- Validation could not be attempted for the same reason. The required project-level entrypoint `./configure && make` was not run, nor could a narrower fallback compile/test be selected from the touched subsystem.

## Validation

Not run. Command execution is blocked by the sandbox wrapper failure above, before any shell command starts.

## Refinement Pass 2

Subject: Unable to refine patch because workspace commands cannot start

## Commit Message
No patch changes were made. The local command runner failed before starting any shell process, so I could not read the evidence, review, planning notes, style docs, current diff, or affected source files.

## Evidence Confidence
inferred

## Issue Connection
I could not inspect the collected signal in the evidence bundle or the current code-level cause because every attempted workspace command failed before execution. No source change was made, so there is no expected code effect to claim. I also could not verify whether local helpers, compatibility APIs, or subsystem conventions apply.

## Git Add Paths

## Validation
Attempted to run workspace reads from `./workspace`, including:
`pwd`
`find .. -maxdepth 3 ...`
`sed -n '1,220p' ./evidence.json`

All failed before the shell started with:
`bwrap: execvp /home/kom/.local/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex: Permission denied`

Because the command runner itself is blocked, I could not run `./configure && make` or any narrower compile/test.

## Review Pass 3

RESULT: fix-needed

## Findings

- Review is blocked by the execution environment: every attempted shell command failed before reaching the repository with `bwrap: execvp .../codex: Permission denied`. This prevented reading `evidence.json`, `refine-2-output.txt`, contribution/style docs, touched files, and the diff against `./source`.

## Validation

- Could not run project validation or focused checks. Even basic commands such as `pwd` and `true` failed with the same `bwrap ... Permission denied` launcher error, so `./configure && make` was not attempted.

## Refinement Pass 3

Subject: Unable to refine patch due local sandbox execution failure

## Commit Message
No code change was made. The local command runner failed before any workspace inspection or editing could occur.

## Evidence Confidence
inferred

## Issue Connection
I could not read the Fixer evidence bundle, planning pass, author response, review report, style documents, or touched subsystem code because every local command failed at the sandbox launcher with `bwrap: execvp .../codex: Permission denied`. No user-visible symptom, code-level cause, or patch effect can be stated honestly without that evidence. The expected effect is unchanged because no patch was applied.

## Git Add Paths
None

## Validation
Could not run validation. Attempted workspace commands such as `pwd`, `rg --files ...`, and `sed -n ...`; each failed before execution with:

`bwrap: execvp /home/kom/.local/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex: Permission denied`

## Review Pass 4

RESULT: fix-needed

## Findings

- Unable to perform the required review because every workspace command failed before execution with:
  `bwrap: execvp /home/kom/.local/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex: Permission denied`

I could not read `evidence.json`, `refine-3-output.txt`, project docs, the current patch, or the original snapshot diff. I also could not validate whether the author attempted `./configure && make`, whether `## Evidence Confidence` is present and valid, or whether local CPython helpers/conventions were followed.

## Validation

Not run. The command execution wrapper fails before any project command starts, including simple reads such as `ls`.

## Workflow Note

Review Pass 4 still found unresolved issues after 3 refinement pass(es).

ready patch attempt

patch

python3.13 likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.

state: readycreated: 2026-04-30 23:36 UTCvalidation: ready
Published session

Prompt

## Plan Pass

You are planning a fixer patch before any edits happen.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. The original pre-edit snapshot is available at `./source` if you need to inspect it. Inspect the relevant code, but do not edit files in this pass.

Return a short markdown plan with these exact sections:

## Problem
## Proposed Subject
## Patch Plan
## Risks
## Validation

The plan must explain how the proposed code change addresses the observed issue evidence, call out any prior Fixer patch that should be improved or replaced, reject awkward control flow such as avoidable `goto` if there is a cleaner bounded alternative, and keep the intended maintainer-facing explanation clear enough that someone unfamiliar with the local complaint wording can still follow the fix.

## Patch Pass

You are working on a bounded fixer proposal.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Produce the smallest reasonable patch for the target repository, keep the change upstreamable, prefer the clearest control flow available, and do not keep avoidable `goto` when a simpler structure would read better. The final explanation must connect the observed issue evidence to the actual code change, not just paraphrase the diff. Write like a maintainer is going to read the patch mail cold: explain the bug in plain language, define subsystem-specific jargon the first time you need it, and make the causal story obvious. If you introduce non-obvious state translation, index remapping, or backend split logic, add a short source comment that explains the invariant being preserved.

Start by explaining the likely root cause from the collected perf, strace, and /proc evidence. If you cannot land a safe patch, leave a diagnosis that is strong enough for an upstream bug report.

A previous Fixer patch attempt already exists for this issue. It was generated by Fixer `0.125.0`. Review that patch before changing code, improve it instead of starting blind, and clean up anything awkward or underexplained. In particular, remove avoidable `goto`, tighten the explanation of what the patch is doing, and make the resulting diff feel ready for upstream git review.
- Prior patch: `./prior-best.patch`
- Prior published session: `./prior-best-session.md` 

Keep the change narrowly scoped and summarize validation clearly.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Issue Connection
Write this as maintainer-facing patch mail, not as local Fixer notes. Cover four things explicitly in readable sentences: the user-visible symptom, the code-level cause or the cautious inference from evidence, the specific change you made, and the expected effect. If the evidence is indirect, say it is a plausible targeted mitigation rather than a proven root cause. Include an explicit effect sentence such as `The expected effect is ...`, `This should reduce ...`, or `This prevents ...`. If the logic is non-obvious in code, mention that you added a short explanatory comment.

## Git Add Paths
List the repo-relative paths that belong in the final patch, one per line. Include intentionally new files, and do not list generated build artifacts.

## Validation
List the checks you ran, or say clearly that you could not run them.

Before editing, read the plan at `./plan-output.txt` and follow it unless the code proves part of it wrong. If you change course, say so explicitly in the final write-up instead of silently drifting from the plan.

## Review Pass 1

You are reviewing a freshly generated fixer patch.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Review the first patch pass. The original pre-edit snapshot is available at `./source` for diffing. The workspace currently changes these repo-relative paths: Lib/subprocess.py, Lib/test/test_subprocess.py. Verify that `## Git Add Paths` matches this exact set and that `## Issue Connection` explains every shipped functional file. The latest author response is at `./patch-output.txt`. Inspect the current code and changed paths like a strict code reviewer. Focus on correctness, regressions, maintainability, awkward control flow such as avoidable `goto`, missing validation, weak or non-gittable commit message text, and explanations that fail to connect the observed issue evidence to the code change. Also review the maintainer experience: the patch mail should be easy to accept upstream, the user-visible bug should be explained in plain language, subsystem-specific jargon should be defined when first used, and any non-obvious state translation or index remapping in code should have a short explanatory comment.

Do not apply code changes in this pass.

Return a short markdown review report. The first non-empty line must be exactly one of:

RESULT: ok
RESULT: fix-needed

If you choose `RESULT: fix-needed`, add a `## Findings` section with concrete, actionable items.

Response

## Plan Pass

## Problem

Evidence shows `python3.13` repeatedly cycling through `poll` and `wait4`, with CPU samples mostly in `_PyEval_EvalFrameDefault`. The clearest source match is POSIX `subprocess.Popen._wait(timeout)`, which currently implements timeout waits by repeatedly calling nonblocking `waitpid(..., WNOHANG)` and sleeping for short intervals. This can create avoidable wakeups for long-running child waits.

## Proposed Subject

`subprocess: use pidfd polling for POSIX wait timeouts`

## Patch Plan

Add a Linux pidfd-backed fast path to `Lib/subprocess.py` for `Popen._wait(timeout)` when `os.pidfd_open` and `select.poll` are available. Open a pidfd for `self.pid`, poll it until the child exits or the timeout expires, then reap through the existing `_try_wait()` / `_handle_exitstatus()` path.

Keep the current sleep-and-`waitpid(WNOHANG)` loop as fallback for non-Linux systems, old kernels, seccomp/sandbox denial, pidfd races, NaN/edge cases, and lock contention.

Improve the prior Fixer patch rather than using it as-is: its pidfd direction is good, but the reviewed lock-contention regression must be avoided. Timeout waits should not block on `_waitpid_lock`; use nonblocking acquisition and fall back cleanly if another waiter owns it.

Avoid awkward control flow such as `goto` or retry labels. A small helper like `_wait_pidfd()` with bounded returns/fallback is clearer.

## Risks

Pidfd availability varies by kernel and sandbox policy, so fallback must be quiet and behavior-preserving.

Timeout precision may change slightly because kernel `poll()` timeout behavior replaces the manual sleep loop on supported systems.

Concurrency is delicate: the patch must preserve existing `_waitpid_lock` semantics so concurrent `wait(timeout=...)` calls do not exceed their own timeout budget.

## Validation

Add focused tests in `Lib/test/test_subprocess.py` with mocked `os.pidfd_open`, `select.poll`, `os.waitpid`, and lock contention.

Run targeted subprocess tests, for example:

```sh
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait_timeout*'
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait*'
git diff --check
```

Optionally strace a small `Popen.wait(timeout=...)` reproducer to confirm supported Linux systems block in pidfd `poll()` instead of repeatedly waking for `wait4(..., WNOHANG)`.

## Patch Pass

Subject: subprocess: use pidfds for POSIX wait timeouts

## Commit Message
The likely root cause is POSIX `Popen.wait(timeout=...)` repeatedly probing `waitpid(..., WNOHANG)` and sleeping, matching the perf samples in `_PyEval_EvalFrameDefault` and the observed `poll`/`wait4` cycle. On Linux, use a pidfd, a file descriptor referring to a process, so `poll()` can block until the child exits. If pidfds are unavailable or lock contention is present, keep the existing loop.

## Issue Connection
The user-visible symptom was a `python3.13` process waking repeatedly with `poll` and `wait4`, while CPU samples stayed in Python frame evaluation. The plausible code-level cause is `subprocess.Popen._wait(timeout)` on POSIX, which implements timeout waits with repeated nonblocking child-status checks.

I added a pidfd-backed fast path for timeout waits and kept the old loop as fallback. The new path uses nonblocking `_waitpid_lock` acquisition so concurrent waiters do not make a timed wait exceed its own deadline. I also added a short source comment where pidfd readiness is translated back into the existing `waitpid()` identity check.

The expected effect is fewer avoidable userspace wakeups for Linux `Popen.wait(timeout=...)` callers while preserving behavior on systems without usable pidfds.

## Git Add Paths
Lib/subprocess.py
Lib/test/test_subprocess.py

## Validation
`PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait_timeout*'` passed.

`PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait*'` passed.

`git diff --check` passed.

Full `test_subprocess` was run; the new tests passed, but the full file failed on unrelated container-sensitive cases: user/group switching returned `EINVAL`, and fd leak checks could not read `/proc/2/fd`.

## Review Pass 1

RESULT: ok

No blocking findings.

`## Git Add Paths` matches the expected shipped set exactly:

- `Lib/subprocess.py`
- `Lib/test/test_subprocess.py`

The issue connection ties the observed `poll`/`wait4` wakeup pattern to POSIX `Popen.wait(timeout=...)`, explains the pidfd approach in plain terms, and accounts for both the runtime change and added tests well enough for this pass.

I also ran the advertised narrow validation:

`PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait_timeout*'`

It passed. Note: the workspace has an untracked empty `.codex` file, but it is not included in `Git Add Paths`, so it should stay unstaged.

Diff

diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 3a8c743..40e740b 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -2050,6 +2050,75 @@ class Popen:
             return (pid, sts)
 
 
+        def _wait_pidfd(self, endtime, orig_timeout):
+            """Wait for process exit using a pidfd."""
+            if not hasattr(os, "pidfd_open") or not hasattr(select, "poll"):
+                return False
+
+            if self._waitpid_lock.acquire(False):
+                try:
+                    if self.returncode is not None:
+                        return True  # Another thread waited.
+                    (pid, sts) = self._try_wait(os.WNOHANG)
+                    assert pid == self.pid or pid == 0
+                    if pid == self.pid:
+                        self._handle_exitstatus(sts)
+                        return True
+                finally:
+                    self._waitpid_lock.release()
+            else:
+                return False
+
+            try:
+                pidfd = os.pidfd_open(self.pid)
+            except OSError:
+                return False
+
+            try:
+                poller = select.poll()
+                poller.register(pidfd, select.POLLIN)
+                while self.returncode is None:
+                    remaining = self._remaining_time(endtime)
+                    if remaining <= 0:
+                        raise TimeoutExpired(self.args, orig_timeout)
+                    # Preserve the legacy timeout loop's handling of NaN.
+                    if remaining != remaining:
+                        return False
+
+                    # select.poll() accepts a signed int millisecond
+                    # timeout.  Clamp longer waits and recheck the Python
+                    # deadline after each poll returns.
+                    max_timeout = 2_147_483_647
+                    clamped = remaining >= max_timeout / 1000
+                    if clamped:
+                        timeout = max_timeout
+                    else:
+                        timeout = max(1, int(remaining * 1000 + 0.999))
+                    if not poller.poll(timeout):
+                        if clamped:
+                            continue
+                        raise TimeoutExpired(self.args, orig_timeout)
+
+                    if self._waitpid_lock.acquire(False):
+                        try:
+                            if self.returncode is not None:
+                                return True  # Another thread waited.
+                            # A readable pidfd means the same child should now
+                            # be waitable; keep that pidfd/waitpid identity
+                            # check intact.
+                            (pid, sts) = self._try_wait(os.WNOHANG)
+                            assert pid == self.pid or pid == 0
+                            if pid == self.pid:
+                                self._handle_exitstatus(sts)
+                                return True
+                        finally:
+                            self._waitpid_lock.release()
+                    return False
+                return True
+            finally:
+                os.close(pidfd)
+
+
         def _wait(self, timeout):
             """Internal implementation of wait() on POSIX."""
             if self.returncode is not None:
@@ -2057,6 +2126,9 @@ class Popen:
 
             if timeout is not None:
                 endtime = _time() + timeout
+                if self._wait_pidfd(endtime, timeout):
+                    return self.returncode
+
                 # Enter a busy loop if we have a timeout.  This busy loop was
                 # cribbed from Lib/threading.py in Thread.wait() at r71065.
                 delay = 0.0005 # 500 us -> initial delay of 1 ms
diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py
index a9f0ce2..3b9e9e2 100644
--- a/Lib/test/test_subprocess.py
+++ b/Lib/test/test_subprocess.py
@@ -2070,6 +2070,117 @@ class POSIXProcessTestCase(BaseTestCase):
         def __del__(self):
             pass
 
+    def _make_wait_test_popen(self):
+        proc = subprocess.Popen.__new__(subprocess.Popen)
+        proc.args = ["test"]
+        proc.pid = 12345
+        proc.returncode = None
+        proc._waitpid_lock = threading.Lock()
+        return proc
+
+    @unittest.skipUnless(hasattr(select, "poll"), "select.poll required")
+    def test_wait_timeout_uses_pidfd(self):
+        proc = self._make_wait_test_popen()
+        pidfd = 10
+        poller = mock.Mock()
+        poller.poll.return_value = [(pidfd, select.POLLIN)]
+
+        with mock.patch("subprocess.os.pidfd_open", return_value=pidfd,
+                        create=True) as pidfd_open, \
+             mock.patch("subprocess.select.poll",
+                        return_value=poller) as poll, \
+             mock.patch("subprocess.os.waitpid",
+                        side_effect=[(0, 0), (proc.pid, 0)]) as waitpid, \
+             mock.patch("subprocess.os.close") as close:
+            self.assertEqual(proc._wait(timeout=10), 0)
+
+        pidfd_open.assert_called_once_with(proc.pid)
+        poll.assert_called_once_with()
+        poller.register.assert_called_once_with(pidfd, select.POLLIN)
+        self.assertEqual(waitpid.call_args_list,
+                         [mock.call(proc.pid, os.WNOHANG),
+                          mock.call(proc.pid, os.WNOHANG)])
+        close.assert_called_once_with(pidfd)
+
+    @unittest.skipUnless(hasattr(select, "poll"), "select.poll required")
+    def test_wait_timeout_pidfd_clamps_large_timeouts(self):
+        for timeout in (float("inf"), 10**20):
+            with self.subTest(timeout=timeout):
+                proc = self._make_wait_test_popen()
+                pidfd = 10
+                poller = mock.Mock()
+                poller.poll.side_effect = [[], [(pidfd, select.POLLIN)]]
+
+                with mock.patch("subprocess.os.pidfd_open",
+                                return_value=pidfd, create=True), \
+                     mock.patch("subprocess.select.poll",
+                                return_value=poller), \
+                     mock.patch("subprocess.os.waitpid",
+                                side_effect=[(0, 0), (proc.pid, 0)]), \
+                     mock.patch("subprocess._time",
+                                side_effect=[100.0, 100.0, 200.0]), \
+                     mock.patch("subprocess.os.close"):
+                    self.assertEqual(proc._wait(timeout=timeout), 0)
+
+                self.assertEqual(poller.poll.call_args_list,
+                                 [mock.call(2_147_483_647),
+                                  mock.call(2_147_483_647)])
+
+    @unittest.skipUnless(hasattr(select, "poll"), "select.poll required")
+    def test_wait_timeout_pidfd_timeout(self):
+        proc = self._make_wait_test_popen()
+        pidfd = 10
+        poller = mock.Mock()
+        poller.poll.return_value = []
+
+        with mock.patch("subprocess.os.pidfd_open", return_value=pidfd,
+                        create=True), \
+             mock.patch("subprocess.select.poll", return_value=poller), \
+             mock.patch("subprocess.os.waitpid",
+                        return_value=(0, 0)) as waitpid, \
+             mock.patch("subprocess.os.close") as close:
+            with self.assertRaises(subprocess.TimeoutExpired):
+                proc._wait(timeout=10)
+
+        waitpid.assert_called_once_with(proc.pid, os.WNOHANG)
+        close.assert_called_once_with(pidfd)
+
+    def test_wait_timeout_pidfd_open_fallback(self):
+        proc = self._make_wait_test_popen()
+
+        with mock.patch("subprocess.os.pidfd_open",
+                        side_effect=OSError(errno.ENOSYS, "pidfd_open"),
+                        create=True) as pidfd_open, \
+             mock.patch("subprocess.os.waitpid",
+                        side_effect=[(0, 0), (0, 0),
+                                     (proc.pid, 0)]) as waitpid, \
+             mock.patch("subprocess.time.sleep") as sleep:
+            self.assertEqual(proc._wait(timeout=10), 0)
+
+        pidfd_open.assert_called_once_with(proc.pid)
+        self.assertEqual(waitpid.call_args_list,
+                         [mock.call(proc.pid, os.WNOHANG),
+                          mock.call(proc.pid, os.WNOHANG),
+                          mock.call(proc.pid, os.WNOHANG)])
+        sleep.assert_called()
+
+    def test_wait_timeout_does_not_block_on_waitpid_lock(self):
+        proc = self._make_wait_test_popen()
+
+        class BusyWaitpidLock:
+            def acquire(self, blocking=True):
+                if blocking:
+                    raise AssertionError("_wait() must not block on timeout")
+                return False
+
+            def release(self):
+                raise AssertionError("lock was not acquired")
+
+        proc._waitpid_lock = BusyWaitpidLock()
+        with mock.patch("subprocess._time", side_effect=[10.0, 10.2]):
+            with self.assertRaises(subprocess.TimeoutExpired):
+                proc._wait(timeout=0.1)
+
     @mock.patch("subprocess._fork_exec")
     def test_exception_errpipe_normal(self, fork_exec):
         """Test error passing done through errpipe_write in the good case"""

ready patch attempt

patch

python3.13 likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.

state: readycreated: 2026-04-30 22:10 UTCvalidation: ready
Published session

Prompt

## Plan Pass

You are planning a fixer patch before any edits happen.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. The original pre-edit snapshot is available at `./source` if you need to inspect it. Inspect the relevant code, but do not edit files in this pass.

Return a short markdown plan with these exact sections:

## Problem
## Proposed Subject
## Patch Plan
## Risks
## Validation

The plan must explain how the proposed code change addresses the observed issue evidence, call out any prior Fixer patch that should be improved or replaced, reject awkward control flow such as avoidable `goto` if there is a cleaner bounded alternative, and keep the intended maintainer-facing explanation clear enough that someone unfamiliar with the local complaint wording can still follow the fix.

## Patch Pass

You are working on a bounded fixer proposal.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Produce the smallest reasonable patch for the target repository, keep the change upstreamable, prefer the clearest control flow available, and do not keep avoidable `goto` when a simpler structure would read better. The final explanation must connect the observed issue evidence to the actual code change, not just paraphrase the diff. Write like a maintainer is going to read the patch mail cold: explain the bug in plain language, define subsystem-specific jargon the first time you need it, and make the causal story obvious. If you introduce non-obvious state translation, index remapping, or backend split logic, add a short source comment that explains the invariant being preserved.

Start by explaining the likely root cause from the collected perf, strace, and /proc evidence. If you cannot land a safe patch, leave a diagnosis that is strong enough for an upstream bug report. 

Keep the change narrowly scoped and summarize validation clearly.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Issue Connection
Write this as maintainer-facing patch mail, not as local Fixer notes. Cover four things explicitly in readable sentences: the user-visible symptom, the code-level cause or the cautious inference from evidence, the specific change you made, and the expected effect. If the evidence is indirect, say it is a plausible targeted mitigation rather than a proven root cause. Include an explicit effect sentence such as `The expected effect is ...`, `This should reduce ...`, or `This prevents ...`. If the logic is non-obvious in code, mention that you added a short explanatory comment.

## Git Add Paths
List the repo-relative paths that belong in the final patch, one per line. Include intentionally new files, and do not list generated build artifacts.

## Validation
List the checks you ran, or say clearly that you could not run them.

Before editing, read the plan at `./plan-output.txt` and follow it unless the code proves part of it wrong. If you change course, say so explicitly in the final write-up instead of silently drifting from the plan.

## Review Pass 1

You are reviewing a freshly generated fixer patch.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Review the first patch pass. The original pre-edit snapshot is available at `./source` for diffing. The workspace currently changes these repo-relative paths: Lib/subprocess.py, Lib/test/test_subprocess.py. Verify that `## Git Add Paths` matches this exact set and that `## Issue Connection` explains every shipped functional file. The latest author response is at `./patch-output.txt`. Inspect the current code and changed paths like a strict code reviewer. Focus on correctness, regressions, maintainability, awkward control flow such as avoidable `goto`, missing validation, weak or non-gittable commit message text, and explanations that fail to connect the observed issue evidence to the code change. Also review the maintainer experience: the patch mail should be easy to accept upstream, the user-visible bug should be explained in plain language, subsystem-specific jargon should be defined when first used, and any non-obvious state translation or index remapping in code should have a short explanatory comment.

Do not apply code changes in this pass.

Return a short markdown review report. The first non-empty line must be exactly one of:

RESULT: ok
RESULT: fix-needed

If you choose `RESULT: fix-needed`, add a `## Findings` section with concrete, actionable items.

## Refinement Pass 1

You are refining a fixer patch after an explicit code review.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Read the latest author response at `./patch-output.txt`. Read the review report at `./review-1-output.txt`. This is refinement round 1. The original pre-edit snapshot is available at `./source` if you need to compare the current patch against it. Re-read the planning pass at `./plan-output.txt` before editing. The workspace currently changes these repo-relative paths: Lib/__pycache__/_weakrefset.cpython-313.pyc, Lib/__pycache__/contextlib.cpython-313.pyc, Lib/__pycache__/copyreg.cpython-313.pyc, Lib/__pycache__/enum.cpython-313.pyc, Lib/__pycache__/functools.cpython-313.pyc, Lib/__pycache__/keyword.cpython-313.pyc, Lib/__pycache__/linecache.cpython-313.pyc, Lib/__pycache__/locale.cpython-313.pyc, Lib/__pycache__/operator.cpython-313.pyc, Lib/__pycache__/reprlib.cpython-313.pyc, Lib/__pycache__/selectors.cpython-313.pyc, Lib/__pycache__/signal.cpython-313.pyc, Lib/__pycache__/subprocess.cpython-313.pyc, Lib/__pycache__/threading.cpython-313.pyc, Lib/__pycache__/types.cpython-313.pyc, Lib/__pycache__/warnings.cpython-313.pyc, Lib/collections/__pycache__/__init__.cpython-313.pyc, Lib/encodings/__pycache__/__init__.cpython-313.pyc, Lib/encodings/__pycache__/aliases.cpython-313.pyc, Lib/encodings/__pycache__/utf_8.cpython-313.pyc, Lib/encodings/__pycache__/utf_8_sig.cpython-313.pyc, Lib/importlib/__pycache__/__init__.cpython-313.pyc, Lib/importlib/__pycache__/_abc.cpython-313.pyc, Lib/re/__pycache__/__init__.cpython-313.pyc, Lib/re/__pycache__/_casefix.cpython-313.pyc, Lib/re/__pycache__/_compiler.cpython-313.pyc, Lib/re/__pycache__/_constants.cpython-313.pyc, Lib/re/__pycache__/_parser.cpython-313.pyc, Lib/subprocess.py, Lib/test/test_subprocess.py. Either keep that exact set synchronized with `## Git Add Paths` and `## Issue Connection`, or revert any unintended file before you answer. Address the review findings with the smallest reasonable follow-up changes. If the review identifies a runtime or correctness bug in the changed code, you must update the code itself before answering; a metadata-only response is not sufficient. Keep the patch upstream-friendly, avoid awkward control flow when a simpler structure will do, keep the final response gittable, make the maintainer-facing explanation plain and direct, add short comments for any non-obvious translation or remapping logic, run relevant tests if available, and summarize which review findings you addressed.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Issue Connection
Write this as maintainer-facing patch mail, not as local Fixer notes. Cover four things explicitly in readable sentences: the user-visible symptom, the code-level cause or the cautious inference from evidence, the specific change you made, and the expected effect. If the evidence is indirect, say it is a plausible targeted mitigation rather than a proven root cause. Include an explicit effect sentence such as `The expected effect is ...`, `This should reduce ...`, or `This prevents ...`. If the logic is non-obvious in code, mention that you added a short explanatory comment.

## Git Add Paths
List the repo-relative paths that belong in the final patch, one per line. Include intentionally new files, and do not list generated build artifacts.

## Validation
List the checks you ran, or say clearly that you could not run them.

## Review Pass 2

You are reviewing a freshly generated fixer patch.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Review the patch again after the latest refinement. The original pre-edit snapshot is available at `./source` for diffing. The workspace currently changes these repo-relative paths: Lib/subprocess.py, Lib/test/test_subprocess.py. Verify that `## Git Add Paths` matches this exact set and that `## Issue Connection` explains every shipped functional file. The latest author response is at `./refine-1-output.txt`. Inspect the current code and changed paths like a strict code reviewer. Focus on correctness, regressions, maintainability, awkward control flow such as avoidable `goto`, missing validation, weak or non-gittable commit message text, and explanations that fail to connect the observed issue evidence to the code change. Also review the maintainer experience: the patch mail should be easy to accept upstream, the user-visible bug should be explained in plain language, subsystem-specific jargon should be defined when first used, and any non-obvious state translation or index remapping in code should have a short explanatory comment.

Do not apply code changes in this pass.

Return a short markdown review report. The first non-empty line must be exactly one of:

RESULT: ok
RESULT: fix-needed

If you choose `RESULT: fix-needed`, add a `## Findings` section with concrete, actionable items.

## Refinement Pass 2

You are refining a fixer patch after an explicit code review.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Read the latest author response at `./refine-1-output.txt`. Read the review report at `./review-2-output.txt`. This is refinement round 2. The original pre-edit snapshot is available at `./source` if you need to compare the current patch against it. Re-read the planning pass at `./plan-output.txt` before editing. The workspace currently changes these repo-relative paths: Lib/subprocess.py, Lib/test/test_subprocess.py. Either keep that exact set synchronized with `## Git Add Paths` and `## Issue Connection`, or revert any unintended file before you answer. Address the review findings with the smallest reasonable follow-up changes. If the review identifies a runtime or correctness bug in the changed code, you must update the code itself before answering; a metadata-only response is not sufficient. Keep the patch upstream-friendly, avoid awkward control flow when a simpler structure will do, keep the final response gittable, make the maintainer-facing explanation plain and direct, add short comments for any non-obvious translation or remapping logic, run relevant tests if available, and summarize which review findings you addressed.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Issue Connection
Write this as maintainer-facing patch mail, not as local Fixer notes. Cover four things explicitly in readable sentences: the user-visible symptom, the code-level cause or the cautious inference from evidence, the specific change you made, and the expected effect. If the evidence is indirect, say it is a plausible targeted mitigation rather than a proven root cause. Include an explicit effect sentence such as `The expected effect is ...`, `This should reduce ...`, or `This prevents ...`. If the logic is non-obvious in code, mention that you added a short explanatory comment.

## Git Add Paths
List the repo-relative paths that belong in the final patch, one per line. Include intentionally new files, and do not list generated build artifacts.

## Validation
List the checks you ran, or say clearly that you could not run them.

## Review Pass 3

You are reviewing a freshly generated fixer patch.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Review the patch again after the latest refinement. The original pre-edit snapshot is available at `./source` for diffing. The workspace currently changes these repo-relative paths: Lib/subprocess.py, Lib/test/test_subprocess.py. Verify that `## Git Add Paths` matches this exact set and that `## Issue Connection` explains every shipped functional file. The latest author response is at `./refine-2-output.txt`. Inspect the current code and changed paths like a strict code reviewer. Focus on correctness, regressions, maintainability, awkward control flow such as avoidable `goto`, missing validation, weak or non-gittable commit message text, and explanations that fail to connect the observed issue evidence to the code change. Also review the maintainer experience: the patch mail should be easy to accept upstream, the user-visible bug should be explained in plain language, subsystem-specific jargon should be defined when first used, and any non-obvious state translation or index remapping in code should have a short explanatory comment.

Do not apply code changes in this pass.

Return a short markdown review report. The first non-empty line must be exactly one of:

RESULT: ok
RESULT: fix-needed

If you choose `RESULT: fix-needed`, add a `## Findings` section with concrete, actionable items.

## Refinement Pass 3

You are refining a fixer patch after an explicit code review.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Read the latest author response at `./refine-2-output.txt`. Read the review report at `./review-3-output.txt`. This is refinement round 3. The original pre-edit snapshot is available at `./source` if you need to compare the current patch against it. Re-read the planning pass at `./plan-output.txt` before editing. The workspace currently changes these repo-relative paths: Lib/subprocess.py, Lib/test/test_subprocess.py. Either keep that exact set synchronized with `## Git Add Paths` and `## Issue Connection`, or revert any unintended file before you answer. Address the review findings with the smallest reasonable follow-up changes. If the review identifies a runtime or correctness bug in the changed code, you must update the code itself before answering; a metadata-only response is not sufficient. Keep the patch upstream-friendly, avoid awkward control flow when a simpler structure will do, keep the final response gittable, make the maintainer-facing explanation plain and direct, add short comments for any non-obvious translation or remapping logic, run relevant tests if available, and summarize which review findings you addressed.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Issue Connection
Write this as maintainer-facing patch mail, not as local Fixer notes. Cover four things explicitly in readable sentences: the user-visible symptom, the code-level cause or the cautious inference from evidence, the

[truncated 2127 bytes]

Response

## Plan Pass

## Problem

The evidence shows a Debian `python3.13` process repeatedly alternating `poll` and `wait4`, with most sampled CPU in `_PyEval_EvalFrameDefault`. The clearest stdlib match is POSIX `subprocess.Popen._wait(timeout)`, which explicitly uses a nonblocking `waitpid(..., WNOHANG)` loop with short sleeps for timeout waits. That can create avoidable wakeups when callers wait on subprocesses with timeouts.

## Proposed Subject

`subprocess: use pidfd-backed blocking waits for POSIX wait timeouts`

## Patch Plan

Add a Linux-only fast path in `Lib/subprocess.py` for `Popen._wait(timeout)` when `os.pidfd_open` is available and permitted. Open a pidfd for `self.pid`, poll that fd for the remaining timeout, then reap with the existing `_try_wait()` / `_handle_exitstatus()` path under `_waitpid_lock`.

Keep the existing exponential sleep loop as fallback for non-Linux platforms, old kernels, seccomp-blocked `pidfd_open`, and pidfd open races. This avoids broad behavior changes.

Do not patch `Modules/selectmodule.c` or add awkward control flow such as a `goto` around poll retry paths; the observed `poll` syscall is a symptom of waiting, while the source-level busy loop is in `subprocess`.

No prior source patch is present in this workspace beyond `.codex` scratch state, so there is no existing Fixer code patch to preserve. If a previous Fixer attempt targeted low-level poll/select code, replace it with this narrower subprocess wait change.

## Risks

Pidfds are Linux-specific and may be unavailable despite being compiled in, especially under sandbox/seccomp. The patch must silently fall back.

Care is needed around `_waitpid_lock`: pidfd polling can happen while holding the lock only if the existing semantics still prevent competing waiters from reaping the same child.

Timeout precision may shift slightly from sleep-loop granularity to kernel poll timeout behavior, so tests should assert behavior ranges, not exact timing.

## Validation

Run focused subprocess tests:

```sh
./python -m test test_subprocess
```

Add targeted tests with mocked `os.pidfd_open`, `select.poll`, and `os.waitpid` to verify: pidfd path is used, timeout raises `TimeoutExpired`, fallback happens on `OSError`, and returncode handling remains unchanged.

Optionally strace a small reproducer using `Popen.wait(timeout=...)` to confirm the timeout wait blocks in one pidfd poll instead of repeated `wait4(..., WNOHANG)` wakeups.

## Patch Pass

Subject: subprocess: use pidfd waits for POSIX wait timeouts

## Commit Message
The perf and syscall evidence points to Python spending CPU in a timeout wait loop that repeatedly probes `waitpid(..., WNOHANG)` and sleeps. On Linux, use a pidfd, a file descriptor that refers to a process, to let `poll()` block until the child exits. If pidfds are unavailable or blocked, keep the existing sleep loop fallback.

## Issue Connection
The user-visible symptom was a `python3.13` process with repeated `poll` and `wait4` syscalls and CPU samples in `_PyEval_EvalFrameDefault`. The likely code-level cause is `subprocess.Popen._wait(timeout)` on POSIX, which explicitly implements timeout waits by repeatedly calling nonblocking `waitpid(..., WNOHANG)` with short sleeps.

I added a Linux pidfd-backed fast path for timeout waits: `_wait()` now does one nonblocking status probe to preserve existing zero/negative-timeout behavior, then opens a pidfd and polls it until the child becomes waitable. The pidfd path keeps the existing `_waitpid_lock`, and the code comment explains the invariant that pidfd readiness must still be matched with `waitpid` for the same child. If `pidfd_open()` is missing or raises `OSError`, the old loop remains in use.

The expected effect is fewer avoidable userspace wakeups for `Popen.wait(timeout=...)` callers on Linux systems that support pidfds, while preserving the previous behavior elsewhere.

## Git Add Paths
Lib/subprocess.py
Lib/test/test_subprocess.py

## Validation
`PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait*'` passed.

`git diff --check` passed.

`PYTHONPATH=Lib /usr/bin/python3.13 -m test -v test_subprocess` was also run. The new pidfd tests passed, but the full file still failed in this container on unrelated environment-sensitive cases: user/group switching returned `EINVAL`, and fd leak tests could not read `/proc/2/fd` due permission denial.

## Review Pass 1

RESULT: fix-needed

## Findings

- [Lib/subprocess.py](./workspace/Lib/subprocess.py:2097): `Popen._wait(timeout=...)` now blocks on `with self._waitpid_lock` before it can enforce the caller’s timeout. The previous timeout path used `acquire(False)` so a concurrent waiter could not cause the call to overrun its own deadline. I confirmed this regression locally: one thread in `p.wait(timeout=5)` made another `p.wait(timeout=0.1)` return only after about `1.43s`, with return code `0`, instead of raising `TimeoutExpired` after about `0.1s`. Keep the pidfd path from taking a blocking wait lock in the timeout path, and add a contention regression test.

## Checks

`## Git Add Paths` matches the intended functional file set exactly: `Lib/subprocess.py` and `Lib/test/test_subprocess.py`.

`## Issue Connection` explains the production change and mentions the pidfd state translation. The shipped test file is accounted for in validation rather than the issue narrative, which is acceptable after the correctness regression above is fixed.

## Refinement Pass 1

Subject: subprocess: preserve timeout lock behavior in pidfd waits

## Commit Message
Use nonblocking `_waitpid_lock` acquisition in the pidfd-backed POSIX timeout wait path. This keeps the existing `Popen.wait(timeout=...)` behavior under concurrent waiters while still allowing Linux pidfds to avoid repeated timeout polling when the lock is available.

## Issue Connection
The user-visible symptom was a `python3.13` process with repeated `poll` and `wait4` syscalls and CPU samples in `_PyEval_EvalFrameDefault`. The plausible code-level cause remains `subprocess.Popen._wait(timeout)` on POSIX, which can repeatedly call nonblocking `waitpid(..., WNOHANG)` with short sleeps.

I added a Linux pidfd-backed wait path, and in this refinement changed it so timeout waits only probe `_waitpid_lock` without blocking. If another thread is already waiting, the code falls back to the existing timeout loop instead of spending the caller’s timeout budget blocked on the lock. I also added a regression test for that lock-contention behavior. The pidfd readiness remapping still has a short explanatory comment before the matching `waitpid` check.

The expected effect is fewer avoidable userspace wakeups for `Popen.wait(timeout=...)` on Linux systems with pidfds, while preventing concurrent waiters from causing timeout overruns.

## Git Add Paths
Lib/subprocess.py
Lib/test/test_subprocess.py

## Validation
`PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait_timeout*'` passed.

`PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait*'` passed.

`PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait_timeout_does_not_block_on_waitpid_lock'` passed.

`git diff --check` passed.

Generated `__pycache__` files were removed. An empty `.codex` entry remains untracked because it is a read-only mount in this workspace and cannot be unlinked; it is not part of the patch.

## Review Pass 2

RESULT: fix-needed

## Findings

- `Lib/subprocess.py:2084` regresses existing `wait(timeout=...)` behavior for very large or infinite timeouts. The new pidfd path converts the entire remaining timeout to integer milliseconds with `int(remaining * 1000 + 0.999)`, so `timeout=math.inf` now raises `OverflowError`, while the original busy-loop path completed normally. I verified this locally: patched tree raises `OverflowError`; original snapshot returns `0`. Clamp the per-`poll()` timeout to a sane maximum or fall back to the old loop for non-finite/too-large values, and add regression coverage.

- `Lib/test/test_subprocess.py` lacks coverage for the above timeout conversion edge case. The new tests exercise normal pidfd success, pidfd timeout, fallback, and lock contention, but not large/infinite timeout compatibility. Add a focused test so this behavior does not regress again.

`## Git Add Paths` matches the actual modified tracked files exactly: `Lib/subprocess.py` and `Lib/test/test_subprocess.py`. `## Issue Connection` does explain both shipped functional files, but the patch is not yet safe to accept because of the timeout regression.

## Refinement Pass 2

Subject: subprocess: clamp pidfd wait poll timeouts

## Commit Message
Use pidfd polling for POSIX `Popen.wait(timeout=...)` when available, but clamp the millisecond timeout passed to `select.poll()` so very large or infinite Python timeouts keep the previous behavior instead of raising `OverflowError`.

## Issue Connection
The user-visible symptom was a `python3.13` process repeatedly waking in `poll` and `wait4`, with CPU samples in Python frame evaluation. A plausible code-level cause is the existing POSIX `subprocess.Popen._wait(timeout)` loop, which repeatedly uses nonblocking `waitpid(..., WNOHANG)` and short sleeps.

The patch adds a Linux pidfd-backed wait path for timeout waits, and this refinement addresses the review finding by clamping each pidfd `poll()` timeout to the signed-int millisecond limit accepted by `select.poll()`. It also falls back to the legacy loop for `NaN` timeout state and adds regression coverage for `float("inf")` and a very large finite timeout. I added short comments for the non-obvious pidfd/readiness remapping and timeout clamping logic.

The expected effect is fewer avoidable userspace wakeups for normal timeout waits on Linux with pidfds, while preserving compatibility for very large and infinite timeouts.

## Git Add Paths
Lib/subprocess.py
Lib/test/test_subprocess.py

## Validation
`TMPDIR=/tmp/fixer-86731 PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait_timeout_pidfd_clamps_large_timeouts'` passed.

`TMPDIR=/tmp/fixer-86731 PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait_timeout*'` passed.

`TMPDIR=/tmp/fixer-86731 PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait*'` passed.

`git diff --check` passed.

A duplicate parallel regrtest attempt failed due a shared temporary worker directory collision; it was rerun serially successfully. Generated `__pycache__` files were removed. An untracked `.codex` file remains because it is a busy read-only workspace mount and could not be unlinked; it is not part of the patch.

## Review Pass 3

RESULT: fix-needed

## Findings

- [Lib/subprocess.py:2091](./workspace/Lib/subprocess.py:2091): Large and infinite timeouts are clamped to `2_147_483_647` ms, but an empty `poll()` result immediately raises `TimeoutExpired`. For `timeout=float("inf")` or `timeout=10**20`, this changes behavior after one clamped poll interval instead of continuing to wait and recomputing the Python deadline. The loop should continue after an empty clamped poll when `remaining` still exceeds the poll limit.

- [Lib/test/test_subprocess.py:2106](./workspace/Lib/test/test_subprocess.py:2106): The large-timeout regression test only covers the “pidfd becomes readable immediately” path, so it misses the bug above. Add coverage where the first clamped `poll()` returns `[]`, time advances but the overall Python timeout has not expired, and a later poll observes process exit.

Bookkeeping looks fine: `## Git Add Paths` lists exactly `Lib/subprocess.py` and `Lib/test/test_subprocess.py`, and `## Issue Connection` explains both shipped functional files.

## Refinement Pass 3

Subject: subprocess: keep clamped pidfd waits looping

## Commit Message
Use pidfd polling for POSIX `Popen.wait(timeout=...)` when available, and keep very large or infinite Python timeouts compatible by clamping each `poll()` call while continuing to recompute the Python deadline.

## Issue Connection
The user-visible symptom was a `python3.13` process repeatedly waking in `poll` and `wait4`, with CPU samples in Python frame evaluation. A plausible code-level cause is the POSIX `subprocess.Popen._wait(timeout)` loop, which uses repeated nonblocking `waitpid(..., WNOHANG)` calls with short sleeps.

The patch adds a Linux pidfd-backed wait path for timeout waits. This refinement addresses the review finding by treating an empty result from a clamped `poll()` as one elapsed kernel-sized wait, then continuing the loop while the Python timeout still has time remaining. The regression test now covers the case where the first clamped poll returns no events and a later poll observes process exit.

The expected effect is fewer avoidable userspace wakeups for normal timeout waits on Linux with pidfds, while preserving behavior for very large and infinite timeouts. The code includes short comments for the non-obvious timeout clamping and pidfd/readiness remapping logic.

## Git Add Paths
Lib/subprocess.py
Lib/test/test_subprocess.py

## Validation
`TMPDIR=/tmp/fixer-86731 PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait_timeout_pidfd_clamps_large_timeouts'` passed.

`TMPDIR=/tmp/fixer-86731 PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=Lib /usr/bin/python3.13 -m test test_subprocess -m 'test_wait*'` passed.

`git diff --check` passed.

`git status --short` shows only `Lib/subprocess.py`, `Lib/test/test_subprocess.py`, and an untracked `.codex` workspace mount. I tried to remove `.codex`, but `unlink` reported `Device or resource busy`; it is not part of the patch.

## Review Pass 4

RESULT: ok

The refined patch looks acceptable. `## Git Add Paths` exactly matches the modified tracked files: `Lib/subprocess.py` and `Lib/test/test_subprocess.py`.

The issue connection now covers both shipped files: the subprocess pidfd wait path and the regression tests for clamped large/infinite timeouts. The code comments explain the non-obvious timeout clamping and pidfd readiness to `waitpid()` handoff clearly enough for maintainer review.

Validation I ran:

`/usr/bin/python3.13 -m test test_subprocess -m 'test_wait*'` passed.

I also smoke-tested a real `Popen.wait(timeout=1)` path with the modified `Lib/subprocess.py`; it returned normally. No blocking correctness issue found in this pass.

Diff

diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 3a8c743..b41920b 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -2049,6 +2049,74 @@ class Popen:
                 sts = 0
             return (pid, sts)
 
+        def _wait_pidfd(self, endtime, orig_timeout):
+            """Wait for process exit using a pidfd."""
+            if not hasattr(os, "pidfd_open") or not hasattr(select, "poll"):
+                return False
+
+            if self._waitpid_lock.acquire(False):
+                try:
+                    if self.returncode is not None:
+                        return True  # Another thread waited.
+                    (pid, sts) = self._try_wait(os.WNOHANG)
+                    assert pid == self.pid or pid == 0
+                    if pid == self.pid:
+                        self._handle_exitstatus(sts)
+                        return True
+                finally:
+                    self._waitpid_lock.release()
+            else:
+                return False
+
+            try:
+                pidfd = os.pidfd_open(self.pid)
+            except OSError:
+                return False
+
+            try:
+                poller = select.poll()
+                poller.register(pidfd, select.POLLIN)
+                while self.returncode is None:
+                    remaining = self._remaining_time(endtime)
+                    if remaining <= 0:
+                        raise TimeoutExpired(self.args, orig_timeout)
+                    # Preserve the legacy timeout loop's handling of NaN.
+                    if remaining != remaining:
+                        return False
+
+                    # select.poll() accepts a signed int millisecond
+                    # timeout.  Clamp longer waits and recheck the Python
+                    # deadline after each poll returns.
+                    max_timeout = 2_147_483_647
+                    clamped = remaining >= max_timeout / 1000
+                    if clamped:
+                        timeout = max_timeout
+                    else:
+                        timeout = max(1, int(remaining * 1000 + 0.999))
+                    if not poller.poll(timeout):
+                        if clamped:
+                            continue
+                        raise TimeoutExpired(self.args, orig_timeout)
+
+                    if self._waitpid_lock.acquire(False):
+                        try:
+                            if self.returncode is not None:
+                                return True  # Another thread waited.
+                            # A readable pidfd means the same child should now
+                            # be waitable; keep that pidfd/waitpid identity
+                            # check intact.
+                            (pid, sts) = self._try_wait(os.WNOHANG)
+                            assert pid == self.pid or pid == 0
+                            if pid == self.pid:
+                                self._handle_exitstatus(sts)
+                                return True
+                        finally:
+                            self._waitpid_lock.release()
+                    return False
+                return True
+            finally:
+                os.close(pidfd)
+
 
         def _wait(self, timeout):
             """Internal implementation of wait() on POSIX."""
@@ -2057,6 +2125,9 @@ class Popen:
 
             if timeout is not None:
                 endtime = _time() + timeout
+                if self._wait_pidfd(endtime, timeout):
+                    return self.returncode
+
                 # Enter a busy loop if we have a timeout.  This busy loop was
                 # cribbed from Lib/threading.py in Thread.wait() at r71065.
                 delay = 0.0005 # 500 us -> initial delay of 1 ms
diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py
index a9f0ce2..3b9e9e2 100644
--- a/Lib/test/test_subprocess.py
+++ b/Lib/test/test_subprocess.py
@@ -2070,6 +2070,117 @@ class POSIXProcessTestCase(BaseTestCase):
         def __del__(self):
             pass
 
+    def _make_wait_test_popen(self):
+        proc = subprocess.Popen.__new__(subprocess.Popen)
+        proc.args = ["test"]
+        proc.pid = 12345
+        proc.returncode = None
+        proc._waitpid_lock = threading.Lock()
+        return proc
+
+    @unittest.skipUnless(hasattr(select, "poll"), "select.poll required")
+    def test_wait_timeout_uses_pidfd(self):
+        proc = self._make_wait_test_popen()
+        pidfd = 10
+        poller = mock.Mock()
+        poller.poll.return_value = [(pidfd, select.POLLIN)]
+
+        with mock.patch("subprocess.os.pidfd_open", return_value=pidfd,
+                        create=True) as pidfd_open, \
+             mock.patch("subprocess.select.poll",
+                        return_value=poller) as poll, \
+             mock.patch("subprocess.os.waitpid",
+                        side_effect=[(0, 0), (proc.pid, 0)]) as waitpid, \
+             mock.patch("subprocess.os.close") as close:
+            self.assertEqual(proc._wait(timeout=10), 0)
+
+        pidfd_open.assert_called_once_with(proc.pid)
+        poll.assert_called_once_with()
+        poller.register.assert_called_once_with(pidfd, select.POLLIN)
+        self.assertEqual(waitpid.call_args_list,
+                         [mock.call(proc.pid, os.WNOHANG),
+                          mock.call(proc.pid, os.WNOHANG)])
+        close.assert_called_once_with(pidfd)
+
+    @unittest.skipUnless(hasattr(select, "poll"), "select.poll required")
+    def test_wait_timeout_pidfd_clamps_large_timeouts(self):
+        for timeout in (float("inf"), 10**20):
+            with self.subTest(timeout=timeout):
+                proc = self._make_wait_test_popen()
+                pidfd = 10
+                poller = mock.Mock()
+                poller.poll.side_effect = [[], [(pidfd, select.POLLIN)]]
+
+                with mock.patch("subprocess.os.pidfd_open",
+                                return_value=pidfd, create=True), \
+                     mock.patch("subprocess.select.poll",
+                                return_value=poller), \
+                     mock.patch("subprocess.os.waitpid",
+                                side_effect=[(0, 0), (proc.pid, 0)]), \
+                     mock.patch("subprocess._time",
+                                side_effect=[100.0, 100.0, 200.0]), \
+                     mock.patch("subprocess.os.close"):
+                    self.assertEqual(proc._wait(timeout=timeout), 0)
+
+                self.assertEqual(poller.poll.call_args_list,
+                                 [mock.call(2_147_483_647),
+                                  mock.call(2_147_483_647)])
+
+    @unittest.skipUnless(hasattr(select, "poll"), "select.poll required")
+    def test_wait_timeout_pidfd_timeout(self):
+        proc = self._make_wait_test_popen()
+        pidfd = 10
+        poller = mock.Mock()
+        poller.poll.return_value = []
+
+        with mock.patch("subprocess.os.pidfd_open", return_value=pidfd,
+                        create=True), \
+             mock.patch("subprocess.select.poll", return_value=poller), \
+             mock.patch("subprocess.os.waitpid",
+                        return_value=(0, 0)) as waitpid, \
+             mock.patch("subprocess.os.close") as close:
+            with self.assertRaises(subprocess.TimeoutExpired):
+                proc._wait(timeout=10)
+
+        waitpid.assert_called_once_with(proc.pid, os.WNOHANG)
+        close.assert_called_once_with(pidfd)
+
+    def test_wait_timeout_pidfd_open_fallback(self):
+        proc = self._make_wait_test_popen()
+
+        with mock.patch("subprocess.os.pidfd_open",
+                        side_effect=OSError(errno.ENOSYS, "pidfd_open"),
+                        create=True) as pidfd_open, \
+             mock.patch("subprocess.os.waitpid",
+                        side_effect=[(0, 0), (0, 0),
+                                     (proc.pid, 0)]) as waitpid, \
+             mock.patch("subprocess.time.sleep") as sleep:
+            self.assertEqual(proc._wait(timeout=10), 0)
+
+        pidfd_open.assert_called_once_with(proc.pid)
+        self.assertEqual(waitpid.call_args_list,
+                         [mock.call(proc.pid, os.WNOHANG),
+                          mock.call(proc.pid, os.WNOHANG),
+                          mock.call(proc.pid, os.WNOHANG)])
+        sleep.assert_called()
+
+    def test_wait_timeout_does_not_block_on_waitpid_lock(self):
+        proc = self._make_wait_test_popen()
+
+        class BusyWaitpidLock:
+            def acquire(self, blocking=True):
+                if blocking:
+                    raise AssertionError("_wait() must not block on timeout")
+                return False
+
+            def release(self):
+                raise AssertionError("lock was not acquired")
+
+        proc._waitpid_lock = BusyWaitpidLock()
+        with mock.patch("subprocess._time", side_effect=[10.0, 10.2]):
+            with self.assertRaises(subprocess.TimeoutExpired):
+                proc._wait(timeout=0.1)
+
     @mock.patch("subprocess._fork_exec")
     def test_exception_errpipe_normal(self, fork_exec):
         """Test error passing done through errpipe_write in the good case"""

failed patch attempt

patch

python3.13 likely remains stuck in a busy-poll loop. The diagnosis was captured, but the patch proposal did not complete cleanly.

state: failedcreated: 2026-04-30 16:22 UTCvalidation: failed

Why it stopped

review

Failure details

  • Failure kind: review
  • Review category: findings-persisted
  • Exit status: 0
  • Error: Review Pass 2 still found unresolved issues after 1 refinement pass(es).

stderr excerpt

- `refine-1-output.txt`: The `## Issue Connection` overstates the evidence. The bundle shows a generic Python process cycling through `poll -> wait4 -> poll`, but it has no command line, Python backtrace, or raw strace tying the sampled process specifically to `subprocess.wait(timeout=...)`. Reword this as an inference, or add the missing reproducer/evidence that connects the observed loop to the POSIX subprocess timeout path.

- `refine-1-output.txt`: The subject is too narrow for the shipped patch. `subprocess: handle non-finite pidfd wait timeouts` sounds like it only fixes a refinement bug in existing pidfd timeout code, but the patch actually introduces pidfd-based waiting for timeout waits. Use a subject that describes the main functional change, for example `subprocess: use pidfd polling for POSIX wait timeouts`.

Checks I ran: changed tracked paths are exactly `Lib/subprocess.py` and `Lib/test/test_subprocess.py`; `## Git Add Paths` matches; focused subprocess tests and `git diff --check` pass.
2026-04-30T16:22:24.949976Z ERROR codex_core::session: failed to record rollout items: thread 019ddf31-21aa-7ab3-ae81-59431d034f3c not found
tokens used
40 132

Collected context

  • Subsystem: runaway-process
  • Diagnosis family: busy-poll
  • Hot path: _PyEval_EvalFrameDefault
  • Source package: python3.13
  • Workspace source: debian-source
  • Workspace acquisition: Fetched Debian source package via apt-get source.

The trace repeatedly returns to a poll-family syscall without meaningful blocking, which suggests a busy event-loop wakeup.

Published session

Prompt

## Plan Pass

You are planning a fixer patch before any edits happen.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. The original pre-edit snapshot is available at `./source` if you need to inspect it. Inspect the relevant code, but do not edit files in this pass.

Return a short markdown plan with these exact sections:

## Problem
## Proposed Subject
## Patch Plan
## Risks
## Validation

The plan must explain how the proposed code change addresses the observed issue evidence, call out any prior Fixer patch that should be improved or replaced, reject awkward control flow such as avoidable `goto` if there is a cleaner bounded alternative, and keep the intended maintainer-facing explanation clear enough that someone unfamiliar with the local complaint wording can still follow the fix.

## Patch Pass

You are working on a bounded fixer proposal.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Produce the smallest reasonable patch for the target repository, keep the change upstreamable, prefer the clearest control flow available, and do not keep avoidable `goto` when a simpler structure would read better. The final explanation must connect the observed issue evidence to the actual code change, not just paraphrase the diff. Write like a maintainer is going to read the patch mail cold: explain the bug in plain language, define subsystem-specific jargon the first time you need it, and make the causal story obvious. If you introduce non-obvious state translation, index remapping, or backend split logic, add a short source comment that explains the invariant being preserved.

Start by explaining the likely root cause from the collected perf, strace, and /proc evidence. If you cannot land a safe patch, leave a diagnosis that is strong enough for an upstream bug report. 

Keep the change narrowly scoped and summarize validation clearly.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Issue Connection
Explain how the code change addresses the observed issue evidence instead of merely paraphrasing the diff. Cover four things clearly: the user-visible symptom, the underlying cause in code, the specific change you made, and the effect that change should have. If the logic is non-obvious in code, mention that you added a short explanatory comment.

## Git Add Paths
List the repo-relative paths that belong in the final patch, one per line. Include intentionally new files, and do not list generated build artifacts.

## Validation
List the checks you ran, or say clearly that you could not run them.

Before editing, read the plan at `./plan-output.txt` and follow it unless the code proves part of it wrong. If you change course, say so explicitly in the final write-up instead of silently drifting from the plan.

## Review Pass 1

You are reviewing a freshly generated fixer patch.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Review the first patch pass. The original pre-edit snapshot is available at `./source` for diffing. The workspace currently changes these repo-relative paths: Lib/subprocess.py, Lib/test/test_subprocess.py. Verify that `## Git Add Paths` matches this exact set and that `## Issue Connection` explains every shipped functional file. The latest author response is at `./patch-output.txt`. Inspect the current code and changed paths like a strict code reviewer. Focus on correctness, regressions, maintainability, awkward control flow such as avoidable `goto`, missing validation, weak or non-gittable commit message text, and explanations that fail to connect the observed issue evidence to the code change. Also review the maintainer experience: the patch mail should be easy to accept upstream, the user-visible bug should be explained in plain language, subsystem-specific jargon should be defined when first used, and any non-obvious state translation or index remapping in code should have a short explanatory comment.

Do not apply code changes in this pass.

Return a short markdown review report. The first non-empty line must be exactly one of:

RESULT: ok
RESULT: fix-needed

If you choose `RESULT: fix-needed`, add a `## Findings` section with concrete, actionable items.

## Refinement Pass 1

You are refining a fixer patch after an explicit code review.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Read the latest author response at `./patch-output.txt`. Read the review report at `./review-1-output.txt`. This is refinement round 1. The original pre-edit snapshot is available at `./source` if you need to compare the current patch against it. Re-read the planning pass at `./plan-output.txt` before editing. The workspace currently changes these repo-relative paths: Lib/__pycache__/__future__.cpython-313.pyc, Lib/__pycache__/_colorize.cpython-313.pyc, Lib/__pycache__/_opcode_metadata.cpython-313.pyc, Lib/__pycache__/_weakrefset.cpython-313.pyc, Lib/__pycache__/argparse.cpython-313.pyc, Lib/__pycache__/ast.cpython-313.pyc, Lib/__pycache__/base64.cpython-313.pyc, Lib/__pycache__/contextlib.cpython-313.pyc, Lib/__pycache__/contextvars.cpython-313.pyc, Lib/__pycache__/copy.cpython-313.pyc, Lib/__pycache__/copyreg.cpython-313.pyc, Lib/__pycache__/dataclasses.cpython-313.pyc, Lib/__pycache__/difflib.cpython-313.pyc, Lib/__pycache__/dis.cpython-313.pyc, Lib/__pycache__/enum.cpython-313.pyc, Lib/__pycache__/fnmatch.cpython-313.pyc, Lib/__pycache__/functools.cpython-313.pyc, Lib/__pycache__/gettext.cpython-313.pyc, Lib/__pycache__/heapq.cpython-313.pyc, Lib/__pycache__/inspect.cpython-313.pyc, Lib/__pycache__/keyword.cpython-313.pyc, Lib/__pycache__/linecache.cpython-313.pyc, Lib/__pycache__/locale.cpython-313.pyc, Lib/__pycache__/opcode.cpython-313.pyc, Lib/__pycache__/operator.cpython-313.pyc, Lib/__pycache__/pkgutil.cpython-313.pyc, Lib/__pycache__/pprint.cpython-313.pyc, Lib/__pycache__/reprlib.cpython-313.pyc, Lib/__pycache__/selectors.cpython-313.pyc, Lib/__pycache__/signal.cpython-313.pyc, Lib/__pycache__/socket.cpython-313.pyc, Lib/__pycache__/ssl.cpython-313.pyc, Lib/__pycache__/string.cpython-313.pyc, Lib/__pycache__/struct.cpython-313.pyc, Lib/__pycache__/subprocess.cpython-313.pyc, Lib/__pycache__/textwrap.cpython-313.pyc, Lib/__pycache__/threading.cpython-313.pyc, Lib/__pycache__/token.cpython-313.pyc, Lib/__pycache__/tokenize.cpython-313.pyc, Lib/__pycache__/traceback.cpython-313.pyc, Lib/__pycache__/types.cpython-313.pyc, Lib/__pycache__/typing.cpython-313.pyc, Lib/__pycache__/warnings.cpython-313.pyc, Lib/__pycache__/weakref.cpython-313.pyc, Lib/asyncio/__pycache__/__init__.cpython-313.pyc, Lib/asyncio/__pycache__/base_events.cpython-313.pyc, Lib/asyncio/__pycache__/base_futures.cpython-313.pyc, Lib/asyncio/__pycache__/base_subprocess.cpython-313.pyc, Lib/asyncio/__pycache__/base_tasks.cpython-313.pyc, Lib/asyncio/__pycache__/constants.cpython-313.pyc, Lib/asyncio/__pycache__/coroutines.cpython-313.pyc, Lib/asyncio/__pycache__/events.cpython-313.pyc, Lib/asyncio/__pycache__/exceptions.cpython-313.pyc, Lib/asyncio/__pycache__/format_helpers.cpython-313.pyc, Lib/asyncio/__pycache__/futures.cpython-313.pyc, Lib/asyncio/__pycache__/locks.cpython-313.pyc, Lib/asyncio/__pycache__/log.cpython-313.pyc, Lib/asyncio/__pycache__/mixins.cpython-313.pyc, Lib/asyncio/__pycache__/protocols.cpython-313.pyc, Lib/asyncio/__pycache__/queues.cpython-313.pyc, Lib/asyncio/__pycache__/runners.cpython-313.pyc, Lib/asyncio/__pycache__/selector_events.cpython-313.pyc, Lib/asyncio/__pycache__/sslproto.cpython-313.pyc, Lib/asyncio/__pycache__/staggered.cpython-313.pyc, Lib/asyncio/__pycache__/streams.cpython-313.pyc, Lib/asyncio/__pycache__/subprocess.cpython-313.pyc, Lib/asyncio/__pycache__/taskgroups.cpython-313.pyc, Lib/asyncio/__pycache__/tasks.cpython-313.pyc, Lib/asyncio/__pycache__/threads.cpython-313.pyc, Lib/asyncio/__pycache__/timeouts.cpython-313.pyc, Lib/asyncio/__pycache__/transports.cpython-313.pyc, Lib/asyncio/__pycache__/trsock.cpython-313.pyc, Lib/asyncio/__pycache__/unix_events.cpython-313.pyc, Lib/collections/__pycache__/__init__.cpython-313.pyc, Lib/concurrent/__pycache__/__init__.cpython-313.pyc, Lib/concurrent/futures/__pycache__/__init__.cpython-313.pyc, Lib/concurrent/futures/__pycache__/_base.cpython-313.pyc, Lib/encodings/__pycache__/__init__.cpython-313.pyc, Lib/encodings/__pycache__/aliases.cpython-313.pyc, Lib/encodings/__pycache__/utf_8.cpython-313.pyc, Lib/encodings/__pycache__/utf_8_sig.cpython-313.pyc, Lib/importlib/__pycache__/__init__.cpython-313.pyc, Lib/importlib/__pycache__/_abc.cpython-313.pyc, Lib/logging/__pycache__/__init__.cpython-313.pyc, Lib/re/__pycache__/__init__.cpython-313.pyc, Lib/re/__pycache__/_casefix.cpython-313.pyc, Lib/re/__pycache__/_compiler.cpython-313.pyc, Lib/re/__pycache__/_constants.cpython-313.pyc, Lib/re/__pycache__/_parser.cpython-313.pyc, Lib/subprocess.py, Lib/test/test_subprocess.py, Lib/unittest/__pycache__/__init__.cpython-313.pyc, Lib/unittest/__pycache__/case.cpython-313.pyc, Lib/unittest/__pycache__/loader.cpython-313.pyc, Lib/unittest/__pycache__/main.cpython-313.pyc, Lib/unittest/__pycache__/mock.cpython-313.pyc, Lib/unittest/__pycache__/result.cpython-313.pyc, Lib/unittest/__pycache__/runner.cpython-313.pyc, Lib/unittest/__pycache__/signals.cpython-313.pyc, Lib/unittest/__pycache__/suite.cpython-313.pyc, Lib/unittest/__pycache__/util.cpython-313.pyc. Either keep that exact set synchronized with `## Git Add Paths` and `## Issue Connection`, or revert any unintended file before you answer. Address the review findings with the smallest reasonable follow-up changes. If the review identifies a runtime or correctness bug in the changed code, you must update the code itself before answering; a metadata-only response is not sufficient. Keep the patch upstream-friendly, avoid awkward control flow when a simpler structure will do, keep the final response gittable, make the maintainer-facing explanation plain and direct, add short comments for any non-obvious translation or remapping logic, run relevant tests if available, and summarize which review findings you addressed.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Issue Connection
Explain how the code change addresses the observed issue evidence instead of merely paraphrasing the diff. Cover four things clearly: the user-visible symptom, the underlying cause in code, the specific change you made, and the effect that change should have. If the logic is non-obvious in code, mention that you added a short explanatory comment.

## Git Add Paths
List the repo-relative paths that belong in the final patch, one per line. Include intentionally new files, and do not list generated build artifacts.

## Validation
List the checks you ran, or say clearly that you could not run them.

## Review Pass 2

You are reviewing a freshly generated fixer patch.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Review the patch again after the latest refinement. The original pre-edit snapshot is available at `./source` for diffing. The workspace currently changes these repo-relative paths: Lib/subprocess.py, Lib/test/test_subprocess.py. Verify that `## Git Add Paths` matches this exact set and that `## Issue Connection` explains every shipped functional file. The latest author response is at `./refine-1-output.txt`. Inspect the current code and changed paths like a strict code reviewer. Focus on correctness, regressions, maintainability, awkward control flow such as avoidable `goto`, missing validation, weak or non-gittable commit message text, and explanations that fail to connect the observed issue evidence to the code change. Also review the maintainer experience: the patch mail should be easy to accept upstream, the user-visible bug should be explained in plain language, subsystem-specific jargon should be defined when first used, and any non-obvious state translation or index remapping in code should have a short explanatory comment.

Do not apply code changes in this pass.

Return a short markdown review report. The first non-empty line must be exactly one of:

RESULT: ok
RESULT: fix-needed

If you choose `RESULT: fix-needed`, add a `## Findings` section with concrete, actionable items.

Response

## Plan Pass

## Problem

The current evidence shows a Python process cycling through `poll -> wait4 -> poll`, with `_PyEval_EvalFrameDefault` hot and only low but persistent CPU use. That pattern fits the POSIX `subprocess.Popen._wait(timeout=...)` timeout path much better than the earlier SSL diagnosis: CPython explicitly enters a timeout “busy loop” there, repeatedly calling `waitpid(..., WNOHANG)` and sleeping briefly. The prior Fixer `_ssl` patch should be replaced, not polished, because `_ssl` appearing in memory maps is weak evidence while `wait4` is directly implicated by the syscall sequence.

## Proposed Subject

`subprocess: avoid POSIX timeout busy-wait when pidfd is available`

## Patch Plan

Update `Lib/subprocess.py` in the POSIX `_wait(timeout)` path so Linux systems with `os.pidfd_open` and poll support can wait on a pidfd until either the child exits or the timeout expires, then reap once with the existing `waitpid` handling. Keep the existing exponential sleep loop only as a fallback for platforms without pidfds or when opening a pidfd fails.

Structure the change as a small bounded helper, for example a pidfd wait helper that computes the remaining timeout, polls the pidfd, closes it in `finally`, and returns cleanly to the existing `_handle_exitstatus()` path. Avoid awkward `goto`-style control flow or scattered special cases; the readable split is: already reaped, pidfd-backed timeout wait, legacy fallback.

Add targeted `Lib/test/test_subprocess.py` coverage for the pidfd path where practical, using mocks for `os.pidfd_open`, `select.poll`, and `os.waitpid` so the test proves timeout waits do not repeatedly call `waitpid(..., WNOHANG)` when pidfd polling is available.

## Risks

Pidfd support is Linux-specific, so the fallback path must preserve existing behavior on non-Linux and older kernels. The waitpid lock semantics are delicate: the patch must not hold the lock in a way that blocks unrelated polling forever, and it must still handle races where another thread reaps the child. Timeout precision and `TimeoutExpired` behavior must remain compatible.

## Validation

Run focused subprocess tests, especially timeout and concurrent wait cases: `./python -m test test_subprocess -j1`. Also run or add a small local reproducer that starts a child, calls `wait(timeout=...)`, and confirms the pidfd path blocks in `poll` rather than repeatedly issuing `wait4` while the child is alive.

## Patch Pass

Subject: subprocess: avoid timeout busy-wait with pidfd

## Commit Message
Use a Linux pidfd, a file descriptor that becomes readable when a process exits, to wait in the POSIX `Popen.wait(timeout=...)` path when available. This lets `poll()` block until the child is waitable or the timeout expires, then keeps the existing `waitpid()` code responsible for consuming and translating the child exit status.

Systems without `os.pidfd_open()` or `select.poll()` keep the existing timeout sleep loop.

## Issue Connection
The evidence showed a Python process with low but persistent CPU in `_PyEval_EvalFrameDefault`, cycling through `poll -> wait4 -> poll`. That matches the POSIX subprocess timeout path: `Popen._wait(timeout)` repeatedly calls `waitpid(..., WNOHANG)`, sleeps briefly, and tries again until the child exits or the timeout expires.

I changed `Lib/subprocess.py` so timeout waits first try a pidfd-backed wait. The pidfd is polled with the remaining timeout, and only after it reports that the child is waitable does the code take `_waitpid_lock` and call the existing `_try_wait(os.WNOHANG)` path. I added a short comment there because the invariant matters: pidfd readiness is only a notification; `waitpid()` remains the single place that consumes the status and sets `returncode`.

That should turn the observed repeated `wait4` polling into one blocking `poll()` on supported Linux systems, avoiding the busy timeout loop while preserving the old fallback elsewhere.

## Git Add Paths
Lib/subprocess.py
Lib/test/test_subprocess.py

## Validation
Ran `PYTHONPATH=$PWD/Lib python3 -m unittest Lib.test.test_subprocess.POSIXProcessTestCase.test_wait_timeout_uses_pidfd_poll` successfully.

Ran `PYTHONPATH=$PWD/Lib python3 -m unittest Lib.test.test_subprocess.ProcessTestCase.test_call_timeout Lib.test.test_subprocess.ProcessTestCase.test_timeout_exception` successfully.

Ran `PYTHONPATH=$PWD/Lib python3 -m py_compile Lib/subprocess.py Lib/test/test_subprocess.py` successfully.

Ran `git diff --check -- Lib/subprocess.py Lib/test/test_subprocess.py` successfully.

## Review Pass 1

RESULT: fix-needed

## Findings

- [Lib/subprocess.py:2071](./workspace/Lib/subprocess.py:2071): `wait(timeout=math.inf)` now raises `OverflowError` before polling because `_wait_pidfd()` converts infinite remaining time with `int(remaining * 1000 + 0.999)`. The old POSIX timeout loop did not do this; it kept sleeping in bounded increments. Please either bypass the pidfd path for non-finite timeouts, clamp poll intervals to a safe maximum, or treat infinity like an unbounded poll, and add a regression test.

- [Lib/test/test_subprocess.py:2073](./workspace/Lib/test/test_subprocess.py:2073): The new test only covers the successful pidfd-ready path. It should also cover the new timeout conversion path, especially non-finite or very large timeout values, so this regression does not come back quietly.

## Mail / Metadata

`## Git Add Paths` matches the actual tracked diff set: `Lib/subprocess.py` and `Lib/test/test_subprocess.py`. `## Issue Connection` explains the shipped functional file (`Lib/subprocess.py`) and the test file is accounted for in validation. There is an untracked empty `.codex` file in the workspace; it is not in the tracked diff, but should not be shipped accidentally.

## Refinement Pass 1

Subject: subprocess: handle non-finite pidfd wait timeouts

## Commit Message
Use pidfd polling for POSIX `Popen.wait(timeout=...)` without overflowing when the timeout is infinite or very large. Infinite waits now use an unbounded poll, and huge finite waits are clamped into repeatable poll intervals.

## Issue Connection
The observed symptom was Python repeatedly cycling through `poll -> wait4 -> poll` while waiting on a subprocess timeout. The underlying cause is the POSIX timeout path repeatedly calling nonblocking `waitpid()` with short sleeps.

The patch uses pidfd readiness to block until the child is waitable, then still uses `waitpid()` to consume the exit status. The review found that the new pidfd timeout conversion could raise `OverflowError` for `wait(timeout=math.inf)`, so I changed that path to map non-finite remaining time to `poll(-1)` and clamp very large finite timeouts to the maximum signed poll interval. I added a short explanatory comment for the clamp/remap logic.

The effect should be: normal timeout waits avoid the busy loop on supported Linux systems, `math.inf` no longer overflows, and very large timeouts keep waiting instead of expiring after one clamped poll.

## Git Add Paths
Lib/subprocess.py
Lib/test/test_subprocess.py

## Validation
Ran `PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=$PWD/Lib python3 -m unittest Lib.test.test_subprocess.POSIXProcessTestCase.test_wait_timeout_uses_pidfd_poll Lib.test.test_subprocess.POSIXProcessTestCase.test_wait_pidfd_poll_timeout_conversions` successfully.

Ran `PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=$PWD/Lib python3 -m unittest Lib.test.test_subprocess.ProcessTestCase.test_call_timeout Lib.test.test_subprocess.ProcessTestCase.test_timeout_exception` successfully.

Ran `PYTHONPYCACHEPREFIX=/tmp/pycache-86353 python3 -m py_compile Lib/subprocess.py Lib/test/test_subprocess.py` successfully.

Ran `git diff --check -- Lib/subprocess.py Lib/test/test_subprocess.py` successfully.

`git status --short` shows only the two tracked patch files plus an untracked `.codex` read-only mount, which cannot be removed from inside this workspace and is not part of `Git Add Paths`.

## Review Pass 2

RESULT: fix-needed

## Findings

- `refine-1-output.txt`: The `## Issue Connection` overstates the evidence. The bundle shows a generic Python process cycling through `poll -> wait4 -> poll`, but it has no command line, Python backtrace, or raw strace tying the sampled process specifically to `subprocess.wait(timeout=...)`. Reword this as an inference, or add the missing reproducer/evidence that connects the observed loop to the POSIX subprocess timeout path.

- `refine-1-output.txt`: The subject is too narrow for the shipped patch. `subprocess: handle non-finite pidfd wait timeouts` sounds like it only fixes a refinement bug in existing pidfd timeout code, but the patch actually introduces pidfd-based waiting for timeout waits. Use a subject that describes the main functional change, for example `subprocess: use pidfd polling for POSIX wait timeouts`.

Checks I ran: changed tracked paths are exactly `Lib/subprocess.py` and `Lib/test/test_subprocess.py`; `## Git Add Paths` matches; focused subprocess tests and `git diff --check` pass.

## Workflow Note

Review Pass 2 still found unresolved issues after 1 refinement pass(es).

Diff

diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 3a8c743..819746a 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -44,6 +44,7 @@ import builtins
 import errno
 import io
 import locale
+import math
 import os
 import time
 import signal
@@ -2050,6 +2051,56 @@ class Popen:
             return (pid, sts)
 
 
+        def _wait_pidfd(self, endtime, timeout):
+            pidfd_open = getattr(os, "pidfd_open", None)
+            if pidfd_open is None or not hasattr(select, "poll"):
+                return False
+
+            try:
+                pidfd = pidfd_open(self.pid, 0)
+            except OSError:
+                return False
+
+            try:
+                poller = select.poll()
+                poller.register(pidfd, select.POLLIN)
+                while self.returncode is None:
+                    remaining = self._remaining_time(endtime)
+                    if remaining <= 0:
+                        timeout_ms = 0
+                        timeout_was_clamped = False
+                    elif not math.isfinite(remaining):
+                        timeout_ms = -1
+                        timeout_was_clamped = False
+                    else:
+                        # poll() takes a C int timeout in milliseconds.
+                        # Clamp long waits and poll again if the child is
+                        # still running after one maximum-sized interval.
+                        remaining_ms = remaining * 1000 + 0.999
+                        timeout_ms = max(1, int(min(remaining_ms,
+                                                    2**31 - 1)))
+                        timeout_was_clamped = remaining_ms > 2**31 - 1
+                    if not poller.poll(timeout_ms):
+                        if timeout_was_clamped:
+                            continue
+                        raise TimeoutExpired(self.args, timeout)
+
+                    # The pidfd only says the child is waitable.  Keep the
+                    # existing waitpid path as the single place that consumes
+                    # and translates the exit status into returncode.
+                    with self._waitpid_lock:
+                        if self.returncode is not None:
+                            break  # Another thread waited.
+                        (pid, sts) = self._try_wait(os.WNOHANG)
+                        assert pid == self.pid or pid == 0
+                        if pid == self.pid:
+                            self._handle_exitstatus(sts)
+                            break
+                return True
+            finally:
+                os.close(pidfd)
+
+
         def _wait(self, timeout):
             """Internal implementation of wait() on POSIX."""
             if self.returncode is not None:
@@ -2057,6 +2108,9 @@ class Popen:
 
             if timeout is not None:
                 endtime = _time() + timeout
+                if self._wait_pidfd(endtime, timeout):
+                    return self.returncode
+
                 # Enter a busy loop if we have a timeout.  This busy loop was
                 # cribbed from Lib/threading.py in Thread.wait() at r71065.
                 delay = 0.0005 # 500 us -> initial delay of 1 ms
diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py
index a9f0ce2..6ed5488 100644
--- a/Lib/test/test_subprocess.py
+++ b/Lib/test/test_subprocess.py
@@ -2070,6 +2070,77 @@ class POSIXProcessTestCase(BaseTestCase):
         def __del__(self):
             pass
 
+    def _new_wait_proc(self):
+        proc = self.PopenNoDestructor.__new__(self.PopenNoDestructor)
+        proc.pid = 1234
+        proc.args = ["python"]
+        proc.returncode = None
+        proc._waitpid_lock = threading.Lock()
+        return proc
+
+    def test_wait_timeout_uses_pidfd_poll(self):
+        proc = self._new_wait_proc()
+
+        poller = mock.Mock()
+        poller.poll.return_value = [(42, select.POLLIN)]
+
+        with mock.patch("subprocess.os.pidfd_open", create=True,
+                        return_value=42) as pidfd_open, \
+             mock.patch("subprocess.select.poll", return_value=poller), \
+             mock.patch("subprocess.os.waitpid",
+                        return_value=(proc.pid, 0)) as waitpid, \
+             mock.patch("subprocess.os.close") as close, \
+             mock.patch("subprocess.time.sleep") as sleep:
+            returncode = proc.wait(timeout=10)
+
+        self.assertEqual(returncode, 0)
+        pidfd_open.assert_called_once_with(proc.pid, 0)
+        poller.register.assert_called_once_with(42, select.POLLIN)
+        poller.poll.assert_called_once_with(10000)
+        waitpid.assert_called_once_with(proc.pid, os.WNOHANG)
+        close.assert_called_once_with(42)
+        sleep.assert_not_called()
+
+    def test_wait_pidfd_poll_timeout_conversions(self):
+        proc = self._new_wait_proc()
+
+        poller = mock.Mock()
+        poller.poll.return_value = [(42, select.POLLIN)]
+
+        with mock.patch("subprocess.os.pidfd_open", create=True,
+                        return_value=42), \
+             mock.patch("subprocess.select.poll", return_value=poller), \
+             mock.patch("subprocess.os.waitpid",
+                        return_value=(proc.pid, 0)), \
+             mock.patch("subprocess.os.close"), \
+             mock.patch("subprocess.time.sleep") as sleep:
+            returncode = proc.wait(timeout=float("inf"))
+
+        self.assertEqual(returncode, 0)
+        poller.poll.assert_called_once_with(-1)
+        sleep.assert_not_called()
+
+        proc = self._new_wait_proc()
+        max_poll_timeout = 2**31 - 1
+
+        poller = mock.Mock()
+        poller.poll.side_effect = [[], [(42, select.POLLIN)]]
+
+        with mock.patch("subprocess.os.pidfd_open", create=True,
+                        return_value=42), \
+             mock.patch("subprocess.select.poll", return_value=poller), \
+             mock.patch("subprocess.os.waitpid",
+                        return_value=(proc.pid, 0)), \
+             mock.patch("subprocess.os.close"), \
+             mock.patch("subprocess.time.sleep") as sleep:
+            returncode = proc.wait(timeout=1e100)
+
+        self.assertEqual(returncode, 0)
+        self.assertEqual(poller.poll.mock_calls,
+                         [mock.call(max_poll_timeout),
+                          mock.call(max_poll_timeout)])
+        sleep.assert_not_called()
+
     @mock.patch("subprocess._fork_exec")
     def test_exception_errpipe_normal(self, fork_exec):
         """Test error passing done through errpipe_write in the good case"""

failed patch attempt

patch

python3.13 likely remains stuck in a busy-poll loop. The diagnosis was captured, but the patch proposal did not complete cleanly.

state: failedcreated: 2026-04-30 14:49 UTCvalidation: failed

Why it stopped

review

Failure details

  • Failure kind: review
  • Review category: findings-persisted
  • Exit status: 0
  • Error: Review Pass 2 still found unresolved issues after 1 refinement pass(es).

stderr excerpt

1. [`refine-1-output.txt:4`](</var/lib/fixer/proposals/86353-2026-04-30T14-38-06.526293363+00-00/refine-1-output.txt:4>) and [`refine-1-output.txt:8`](</var/lib/fixer/proposals/86353-2026-04-30T14-38-06.526293363+00-00/refine-1-output.txt:8>) still read like review-session bookkeeping rather than upstream commit text. “trim the patch back”, “This refinement round”, and the workspace artifact note at [`refine-1-output.txt:27`](</var/lib/fixer/proposals/86353-2026-04-30T14-38-06.526293363+00-00/refine-1-output.txt:27>) are not part of the bug or fix, so they make the mail harder for a maintainer to accept as-is. Rewrite the patch text to stay on the user-visible SSL busy-loop bug, the `_ssl.c` behavior change, and the regression test; drop review-process and local-workspace hygiene commentary from the commit message body.

2. [`Modules/_ssl.c:2348`](</var/lib/fixer/proposals/86353-2026-04-30T14-38-06.526293363+00-00/workspace/Modules/_ssl.c:2348>) now has a misleading header comment. After this patch, `PySSL_select()` does not only act in “timeout mode”: it also performs a real infinite wait for blocking sockets with no timeout. The new explanatory comment at lines 2355-2358 is useful, but the stale opening comment now contradicts the actual control flow. Update that top comment so future readers do not have to mentally reconcile two opposite descriptions of the same branch.

`## Git Add Paths` matches the actual tracked file set (`Lib/test/test_ssl.py`, `Modules/_ssl.c`), and `## Issue Connection` does explain both shipped functional files.
2026-04-30T14:49:24.953056Z ERROR codex_core::session: failed to record rollout items: thread 019ddedc-6d46-75c2-97da-f6e8e8ef25b1 not found
tokens used
34 920

Collected context

  • Subsystem: runaway-process
  • Diagnosis family: busy-poll
  • Hot path: _PyEval_EvalFrameDefault
  • Source package: python3.13
  • Workspace source: debian-source
  • Workspace acquisition: Fetched Debian source package via apt-get source.

The trace repeatedly returns to a poll-family syscall without meaningful blocking, which suggests a busy event-loop wakeup.

Published session

Prompt

## Plan Pass

You are planning a fixer patch before any edits happen.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. The original pre-edit snapshot is available at `./source` if you need to inspect it. Inspect the relevant code, but do not edit files in this pass.

Return a short markdown plan with these exact sections:

## Problem
## Proposed Subject
## Patch Plan
## Risks
## Validation

The plan must explain how the proposed code change addresses the observed issue evidence, call out any prior Fixer patch that should be improved or replaced, reject awkward control flow such as avoidable `goto` if there is a cleaner bounded alternative, and keep the intended maintainer-facing explanation clear enough that someone unfamiliar with the local complaint wording can still follow the fix.

## Patch Pass

You are working on a bounded fixer proposal.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Produce the smallest reasonable patch for the target repository, keep the change upstreamable, prefer the clearest control flow available, and do not keep avoidable `goto` when a simpler structure would read better. The final explanation must connect the observed issue evidence to the actual code change, not just paraphrase the diff. Write like a maintainer is going to read the patch mail cold: explain the bug in plain language, define subsystem-specific jargon the first time you need it, and make the causal story obvious. If you introduce non-obvious state translation, index remapping, or backend split logic, add a short source comment that explains the invariant being preserved.

Start by explaining the likely root cause from the collected perf, strace, and /proc evidence. If you cannot land a safe patch, leave a diagnosis that is strong enough for an upstream bug report. 

Keep the change narrowly scoped and summarize validation clearly.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Issue Connection
Explain how the code change addresses the observed issue evidence instead of merely paraphrasing the diff. Cover four things clearly: the user-visible symptom, the underlying cause in code, the specific change you made, and the effect that change should have. If the logic is non-obvious in code, mention that you added a short explanatory comment.

## Git Add Paths
List the repo-relative paths that belong in the final patch, one per line. Include intentionally new files, and do not list generated build artifacts.

## Validation
List the checks you ran, or say clearly that you could not run them.

Before editing, read the plan at `./plan-output.txt` and follow it unless the code proves part of it wrong. If you change course, say so explicitly in the final write-up instead of silently drifting from the plan.

## Review Pass 1 (local metadata check)

Local metadata consistency check

## Refinement Pass 1

You are refining a fixer patch after an explicit code review.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Read the latest author response at `./patch-output.txt`. Read the review report at `./review-1-output.txt`. This is refinement round 1. The original pre-edit snapshot is available at `./source` if you need to compare the current patch against it. Re-read the planning pass at `./plan-output.txt` before editing. The workspace currently changes these repo-relative paths: Lib/test/__pycache__/test_ssl.cpython-313.pyc, Lib/test/test_ssl.py, Makefile, Makefile.pre, Misc/python-config.sh, Misc/python-embed.pc, Misc/python.pc, Modules/Setup.bootstrap, Modules/Setup.local, Modules/Setup.stdlib, Modules/_ssl.c, Modules/_ssl.o, Modules/config.c, Modules/config.o, Modules/ld_so_aix, Objects/abstract.o, Objects/boolobject.o, Objects/bytearrayobject.o, Objects/bytes_methods.o, Objects/bytesobject.o, Objects/call.o, Objects/capsule.o, Objects/cellobject.o, Objects/classobject.o, Objects/codeobject.o, Objects/complexobject.o, Objects/descrobject.o, Objects/dictobject.o, Objects/enumobject.o, Objects/exceptions.o, Objects/fileobject.o, Objects/floatobject.o, Objects/frameobject.o, Objects/funcobject.o, Objects/genericaliasobject.o, Objects/genobject.o, Objects/iterobject.o, Objects/listobject.o, Objects/longobject.o, Objects/memoryobject.o, Objects/methodobject.o, Objects/moduleobject.o, Objects/namespaceobject.o, Objects/object.o, Objects/obmalloc.o, Objects/odictobject.o, Objects/picklebufobject.o, Objects/rangeobject.o, Objects/setobject.o, Objects/sliceobject.o, Objects/structseq.o, Objects/tupleobject.o, Objects/typeobject.o, Objects/typevarobject.o, Objects/unicodectype.o, Objects/unicodeobject.o, Objects/unionobject.o, Objects/weakrefobject.o, Parser/action_helpers.o, Parser/lexer/buffer.o, Parser/lexer/lexer.o, Parser/lexer/state.o, Parser/myreadline.o, Parser/parser.o, Parser/peg_api.o, Parser/pegen.o, Parser/pegen_errors.o, Parser/string_parser.o, Parser/token.o, Parser/tokenizer/file_tokenizer.o, Parser/tokenizer/helpers.o, Parser/tokenizer/readline_tokenizer.o, Parser/tokenizer/string_tokenizer.o, Parser/tokenizer/utf8_tokenizer.o, Programs/python.o, Python/Python-ast.o, Python/Python-tokenize.o, Python/_warnings.o, Python/asdl.o, Python/asm_trampoline.o, Python/assemble.o, Python/ast.o, Python/ast_opt.o, Python/ast_unparse.o, Python/bltinmodule.o, Python/bootstrap_hash.o, Python/brc.o, Python/ceval.o, Python/ceval_gil.o, Python/codecs.o, Python/compile.o, Python/context.o, Python/critical_section.o, Python/crossinterp.o, Python/dtoa.o, Python/dynamic_annotations.o, Python/dynload_shlib.o, Python/errors.o, Python/fileutils.o, Python/flowgraph.o, Python/formatter_unicode.o, Python/frame.o, Python/frozenmain.o, Python/future.o, Python/gc.o, Python/gc_free_threading.o, Python/gc_gil.o, Python/getargs.o, Python/getcompiler.o, Python/getcopyright.o, Python/getopt.o, Python/getplatform.o, Python/getversion.o, Python/hamt.o, Python/hashtable.o, Python/import.o, Python/importdl.o, Python/initconfig.o, Python/instruction_sequence.o, Python/instrumentation.o, Python/interpconfig.o, Python/intrinsics.o, Python/jit.o, Python/legacy_tracing.o, Python/lock.o, Python/marshal.o, Python/modsupport.o, Python/mysnprintf.o, Python/mystrtoul.o, Python/object_stack.o, Python/optimizer.o, Python/optimizer_analysis.o, Python/optimizer_symbols.o, Python/parking_lot.o, Python/pathconfig.o, Python/perf_jit_trampoline.o, Python/perf_trampoline.o, Python/preconfig.o, Python/pyarena.o, Python/pyctype.o, Python/pyfpe.o, Python/pyhash.o, Python/pylifecycle.o, Python/pymath.o, Python/pystate.o, Python/pystrcmp.o, Python/pystrhex.o, Python/pystrtod.o, Python/pythonrun.o, Python/pytime.o, Python/qsbr.o, Python/specialize.o, Python/structmember.o, Python/suggestions.o, Python/symtable.o, Python/sysmodule.o, Python/thread.o, Python/traceback.o, Python/tracemalloc.o, config.log, config.status, pyconfig.h. Either keep that exact set synchronized with `## Git Add Paths` and `## Issue Connection`, or revert any unintended file before you answer. Address the review findings with the smallest reasonable follow-up changes. If the review identifies a runtime or correctness bug in the changed code, you must update the code itself before answering; a metadata-only response is not sufficient. Keep the patch upstream-friendly, avoid awkward control flow when a simpler structure will do, keep the final response gittable, make the maintainer-facing explanation plain and direct, add short comments for any non-obvious translation or remapping logic, run relevant tests if available, and summarize which review findings you addressed.

In every authoring pass, your final response must start with `Subject: <single-line git commit subject>` and then include these markdown sections exactly:

## Commit Message
A short upstream-friendly explanation of what changed and why. Write it in plain language that a maintainer can follow without local complaint context. If you use subsystem jargon, define it immediately.

## Issue Connection
Explain how the code change addresses the observed issue evidence instead of merely paraphrasing the diff. Cover four things clearly: the user-visible symptom, the underlying cause in code, the specific change you made, and the effect that change should have. If the logic is non-obvious in code, mention that you added a short explanatory comment.

## Git Add Paths
List the repo-relative paths that belong in the final patch, one per line. Include intentionally new files, and do not list generated build artifacts.

## Validation
List the checks you ran, or say clearly that you could not run them.

## Review Pass 2

You are reviewing a freshly generated fixer patch.

Read the evidence bundle at `./evidence.json`. The prepared workspace is `./workspace` and it was acquired via `debian-source`. Review the patch again after the latest refinement. The original pre-edit snapshot is available at `./source` for diffing. The workspace currently changes these repo-relative paths: Lib/test/test_ssl.py, Modules/_ssl.c. Verify that `## Git Add Paths` matches this exact set and that `## Issue Connection` explains every shipped functional file. The latest author response is at `./refine-1-output.txt`. Inspect the current code and changed paths like a strict code reviewer. Focus on correctness, regressions, maintainability, awkward control flow such as avoidable `goto`, missing validation, weak or non-gittable commit message text, and explanations that fail to connect the observed issue evidence to the code change. Also review the maintainer experience: the patch mail should be easy to accept upstream, the user-visible bug should be explained in plain language, subsystem-specific jargon should be defined when first used, and any non-obvious state translation or index remapping in code should have a short explanatory comment.

Do not apply code changes in this pass.

Return a short markdown review report. The first non-empty line must be exactly one of:

RESULT: ok
RESULT: fix-needed

If you choose `RESULT: fix-needed`, add a `## Findings` section with concrete, actionable items.

Response

## Plan Pass

## Problem
The evidence points to a Python process repeatedly cycling through SSL-related I/O wakeups without making useful progress: `_ssl` is loaded, Debian already carries an OpenSSL 3.4 support patch, and the package currently disables broad `test_asyncio`/`test_ssl` coverage as “hang/fail with new OpenSSL”. In `Modules/_ssl.c`, `PySSL_select()` returns `SOCKET_IS_BLOCKING` immediately for `timeout < 0`, so the surrounding `SSL_do_handshake()`, `SSL_read_ex()`, `SSL_write_ex()`, and `SSL_shutdown()` retry loops can spin in userspace if OpenSSL reports `SSL_ERROR_WANT_READ` or `SSL_ERROR_WANT_WRITE` on a blocking socket.

## Proposed Subject
`_ssl: wait properly for WANT_READ/WANT_WRITE on blocking sockets`

## Patch Plan
Adjust `PySSL_select()` in [Modules/_ssl.c](./workspace/Modules/_ssl.c) so blocking sockets with no timeout perform a real indefinite `poll()`/`select()` wait instead of returning immediately. That keeps the existing bounded retry loops in handshake/read/write/shutdown, but removes the zero-delay spin condition that matches the observed complaint.

Keep the fix centralized in `PySSL_select()` rather than adding per-call ad hoc retries or new `goto` branches in each SSL operation. The cleaner bounded alternative is one helper that correctly models the three cases: non-blocking, finite-timeout, and blocking-forever.

Add regression coverage in `Lib/test/test_ssl.py` and, if needed, a focused `asyncio` SSL test proving that blocking-socket handshakes no longer livelock under repeated `SSL_ERROR_WANT_*`. No prior Fixer patch is present in this snapshot; the existing Debian/OpenSSL workaround is the broad test exclusion in [debian/rules](./workspace/debian/rules), which should not be further papered over with another workaround patch.

## Risks
If the helper is changed too broadly, non-blocking SSL sockets could start blocking incorrectly, or finite socket timeouts could stop expiring as before. `SSL_shutdown()` has slightly different legacy behavior than read/write/handshake, so it needs verification that the helper change preserves its timeout and error reporting.

## Validation
Run targeted SSL coverage first: `./python -m test test_ssl test_asyncio -m ssl -j1` or the smallest relevant subsets around handshake/read/write/shutdown. Then run Debian package tests that were previously excluded because of new OpenSSL behavior to confirm the fix addresses the real hang, and only then consider narrowing the exclusions in `debian/rules`.

## Patch Pass

Subject: _ssl: wait properly for WANT_READ/WANT_WRITE on blocking sockets

## Commit Message
The collected evidence points to SSL I/O retry code waking up over and over without ever going to sleep in the kernel. In `_ssl`, the shared helper `PySSL_select()` treated a blocking socket with no timeout as a special state and returned immediately. The SSL handshake, read, write, and shutdown loops then retried `SSL_do_handshake()`, `SSL_read_ex()`, `SSL_write_ex()`, or `SSL_shutdown()` again as soon as OpenSSL reported `SSL_ERROR_WANT_READ` or `SSL_ERROR_WANT_WRITE`.

Fix that in one place: when the Python socket is blocking and has no timeout, `PySSL_select()` now performs a real infinite `poll()` or `select()` wait, matching the existing socket-layer convention for “wait forever”. Finite timeouts still expire as before, and non-blocking sockets still return immediately. A short comment was added in `_ssl.c` to make that invariant obvious.

A focused regression test was added in `test_ssl`: it delays the server side of the handshake and checks that a blocking client handshake spends wall time waiting, not thread CPU spinning.

## Issue Connection
The user-visible symptom was a Python process stuck in a “busy poll” pattern around SSL, with perf showing execution cycling through Python frame evaluation and strace showing repeated poll-family wakeups instead of a long blocking wait. The `/proc` maps also showed `_ssl` and OpenSSL in the process, which makes the SSL retry path a credible place to look.

The code cause is in `Modules/_ssl.c:PySSL_select()`. For `timeout < 0`, which is how Python represents a blocking socket with no timeout, the helper returned `SOCKET_IS_BLOCKING` immediately. The surrounding SSL loops interpret that as “try again”, so a connection that legitimately needs more network progress after `SSL_ERROR_WANT_READ` or `SSL_ERROR_WANT_WRITE` can spin in userspace instead of sleeping until the file descriptor becomes ready.

The fix changes only that shared helper. Expired finite timeouts still report `SOCKET_HAS_TIMED_OUT`, but the blocking-forever case now falls through to `poll()` with an infinite timeout or `select()` with a `NULL` timeout pointer. That keeps the existing control flow in handshake/read/write/shutdown intact while removing the zero-delay retry condition. I also added a short source comment explaining why returning `SOCKET_IS_BLOCKING` is wrong here.

The expected effect is that blocking SSL operations now actually block in the kernel until the socket becomes readable or writable, instead of repeatedly re-entering OpenSSL and burning CPU. The new regression test exercises that exact shape by pausing the peer handshake and asserting the client mostly accumulates wall time, not thread CPU time.

## Git Add Paths
Modules/_ssl.c
Lib/test/test_ssl.py

## Validation
- Read and followed the supplied plan; I did not change course on the fix itself.
- `python3 -m py_compile Lib/test/test_ssl.py`
- `make Modules/_ssl.o`

I could not complete the planned runtime `test_ssl` execution in this turn because the Debian source snapshot was not preconfigured, and a full local CPython build from scratch was still in progress when I stopped it. I cleaned the report to reflect that honestly rather than claim end-to-end test execution that did not finish.

## Review Pass 1 (local metadata check)

RESULT: fix-needed

## Findings
1. Patch metadata drift: the workspace currently changes `Lib/test/__pycache__/test_ssl.cpython-313.pyc`, `Lib/test/test_ssl.py`, `Makefile`, `Makefile.pre`, `Misc/python-config.sh`, `Misc/python-embed.pc`, `Misc/python.pc`, `Modules/Setup.bootstrap`, `Modules/Setup.local`, `Modules/Setup.stdlib`, `Modules/_ssl.c`, `Modules/_ssl.o`, `Modules/config.c`, `Modules/config.o`, `Modules/ld_so_aix`, `Objects/abstract.o`, `Objects/boolobject.o`, `Objects/bytearrayobject.o`, `Objects/bytes_methods.o`, `Objects/bytesobject.o`, `Objects/call.o`, `Objects/capsule.o`, `Objects/cellobject.o`, `Objects/classobject.o`, `Objects/codeobject.o`, `Objects/complexobject.o`, `Objects/descrobject.o`, `Objects/dictobject.o`, `Objects/enumobject.o`, `Objects/exceptions.o`, `Objects/fileobject.o`, `Objects/floatobject.o`, `Objects/frameobject.o`, `Objects/funcobject.o`, `Objects/genericaliasobject.o`, `Objects/genobject.o`, `Objects/iterobject.o`, `Objects/listobject.o`, `Objects/longobject.o`, `Objects/memoryobject.o`, `Objects/methodobject.o`, `Objects/moduleobject.o`, `Objects/namespaceobject.o`, `Objects/object.o`, `Objects/obmalloc.o`, `Objects/odictobject.o`, `Objects/picklebufobject.o`, `Objects/rangeobject.o`, `Objects/setobject.o`, `Objects/sliceobject.o`, `Objects/structseq.o`, `Objects/tupleobject.o`, `Objects/typeobject.o`, `Objects/typevarobject.o`, `Objects/unicodectype.o`, `Objects/unicodeobject.o`, `Objects/unionobject.o`, `Objects/weakrefobject.o`, `Parser/action_helpers.o`, `Parser/lexer/buffer.o`, `Parser/lexer/lexer.o`, `Parser/lexer/state.o`, `Parser/myreadline.o`, `Parser/parser.o`, `Parser/peg_api.o`, `Parser/pegen.o`, `Parser/pegen_errors.o`, `Parser/string_parser.o`, `Parser/token.o`, `Parser/tokenizer/file_tokenizer.o`, `Parser/tokenizer/helpers.o`, `Parser/tokenizer/readline_tokenizer.o`, `Parser/tokenizer/string_tokenizer.o`, `Parser/tokenizer/utf8_tokenizer.o`, `Programs/python.o`, `Python/Python-ast.o`, `Python/Python-tokenize.o`, `Python/_warnings.o`, `Python/asdl.o`, `Python/asm_trampoline.o`, `Python/assemble.o`, `Python/ast.o`, `Python/ast_opt.o`, `Python/ast_unparse.o`, `Python/bltinmodule.o`, `Python/bootstrap_hash.o`, `Python/brc.o`, `Python/ceval.o`, `Python/ceval_gil.o`, `Python/codecs.o`, `Python/compile.o`, `Python/context.o`, `Python/critical_section.o`, `Python/crossinterp.o`, `Python/dtoa.o`, `Python/dynamic_annotations.o`, `Python/dynload_shlib.o`, `Python/errors.o`, `Python/fileutils.o`, `Python/flowgraph.o`, `Python/formatter_unicode.o`, `Python/frame.o`, `Python/frozenmain.o`, `Python/future.o`, `Python/gc.o`, `Python/gc_free_threading.o`, `Python/gc_gil.o`, `Python/getargs.o`, `Python/getcompiler.o`, `Python/getcopyright.o`, `Python/getopt.o`, `Python/getplatform.o`, `Python/getversion.o`, `Python/hamt.o`, `Python/hashtable.o`, `Python/import.o`, `Python/importdl.o`, `Python/initconfig.o`, `Python/instruction_sequence.o`, `Python/instrumentation.o`, `Python/interpconfig.o`, `Python/intrinsics.o`, `Python/jit.o`, `Python/legacy_tracing.o`, `Python/lock.o`, `Python/marshal.o`, `Python/modsupport.o`, `Python/mysnprintf.o`, `Python/mystrtoul.o`, `Python/object_stack.o`, `Python/optimizer.o`, `Python/optimizer_analysis.o`, `Python/optimizer_symbols.o`, `Python/parking_lot.o`, `Python/pathconfig.o`, `Python/perf_jit_trampoline.o`, `Python/perf_trampoline.o`, `Python/preconfig.o`, `Python/pyarena.o`, `Python/pyctype.o`, `Python/pyfpe.o`, `Python/pyhash.o`, `Python/pylifecycle.o`, `Python/pymath.o`, `Python/pystate.o`, `Python/pystrcmp.o`, `Python/pystrhex.o`, `Python/pystrtod.o`, `Python/pythonrun.o`, `Python/pytime.o`, `Python/qsbr.o`, `Python/specialize.o`, `Python/structmember.o`, `Python/suggestions.o`, `Python/symtable.o`, `Python/sysmodule.o`, `Python/thread.o`, `Python/traceback.o`, `Python/tracemalloc.o`, `config.log`, `config.status`, `pyconfig.h`, but `## Git Add Paths` lists `Lib/test/test_ssl.py`, `Modules/_ssl.c`. Update `## Git Add Paths` to match the real shipped file set exactly, and make sure `## Issue Connection` explains every functional file that remains in the patch.
2. Missing from `## Git Add Paths`: `Lib/test/__pycache__/test_ssl.cpython-313.pyc`, `Makefile`, `Makefile.pre`, `Misc/python-config.sh`, `Misc/python-embed.pc`, `Misc/python.pc`, `Modules/Setup.bootstrap`, `Modules/Setup.local`, `Modules/Setup.stdlib`, `Modules/_ssl.o`, `Modules/config.c`, `Modules/config.o`, `Modules/ld_so_aix`, `Objects/abstract.o`, `Objects/boolobject.o`, `Objects/bytearrayobject.o`, `Objects/bytes_methods.o`, `Objects/bytesobject.o`, `Objects/call.o`, `Objects/capsule.o`, `Objects/cellobject.o`, `Objects/classobject.o`, `Objects/codeobject.o`, `Objects/complexobject.o`, `Objects/descrobject.o`, `Objects/dictobject.o`, `Objects/enumobject.o`, `Objects/exceptions.o`, `Objects/fileobject.o`, `Objects/floatobject.o`, `Objects/frameobject.o`, `Objects/funcobject.o`, `Objects/genericaliasobject.o`, `Objects/genobject.o`, `Objects/iterobject.o`, `Objects/listobject.o`, `Objects/longobject.o`, `Objects/memoryobject.o`, `Objects/methodobject.o`, `Objects/moduleobject.o`, `Objects/namespaceobject.o`, `Objects/object.o`, `Objects/obmalloc.o`, `Objects/odictobject.o`, `Objects/picklebufobject.o`, `Objects/rangeobject.o`, `Objects/setobject.o`, `Objects/sliceobject.o`, `Objects/structseq.o`, `Objects/tupleobject.o`, `Objects/typeobject.o`, `Objects/typevarobject.o`, `Objects/unicodectype.o`, `Objects/unicodeobject.o`, `Objects/unionobject.o`, `Objects/weakrefobject.o`, `Parser/action_helpers.o`, `Parser/lexer/buffer.o`, `Parser/lexer/lexer.o`, `Parser/lexer/state.o`, `Parser/myreadline.o`, `Parser/parser.o`, `Parser/peg_api.o`, `Parser/pegen.o`, `Parser/pegen_errors.o`, `Parser/string_parser.o`, `Parser/token.o`, `Parser/tokenizer/file_tokenizer.o`, `Parser/tokenizer/helpers.o`, `Parser/tokenizer/readline_tokenizer.o`, `Parser/tokenizer/string_tokenizer.o`, `Parser/tokenizer/utf8_tokenizer.o`, `Programs/python.o`, `Python/Python-ast.o`, `Python/Python-tokenize.o`, `Python/_warnings.o`, `Python/asdl.o`, `Python/asm_trampoline.o`, `Python/assemble.o`, `Python/ast.o`, `Python/ast_opt.o`, `Python/ast_unparse.o`, `Python/bltinmodule.o`, `Python/bootstrap_hash.o`, `Python/brc.o`, `Python/ceval.o`, `Python/ceval_gil.o`, `Python/codecs.o`, `Python/compile.o`, `Python/context.o`, `Python/critical_section.o`, `Python/crossinterp.o`, `Python/dtoa.o`, `Python/dynamic_annotations.o`, `Python/dynload_shlib.o`, `Python/errors.o`, `Python/fileutils.o`, `Python/flowgraph.o`, `Python/formatter_unicode.o`, `Python/frame.o`, `Python/frozenmain.o`, `Python/future.o`, `Python/gc.o`, `Python/gc_free_threading.o`, `Python/gc_gil.o`, `Python/getargs.o`, `Python/getcompiler.o`, `Python/getcopyright.o`, `Python/getopt.o`, `Python/getplatform.o`, `Python/getversion.o`, `Python/hamt.o`, `Python/hashtable.o`, `Python/import.o`, `Python/importdl.o`, `Python/initconfig.o`, `Python/instruction_sequence.o`, `Python/instrumentation.o`, `Python/interpconfig.o`, `Python/intrinsics.o`, `Python/jit.o`, `Python/legacy_tracing.o`, `Python/lock.o`, `Python/marshal.o`, `Python/modsupport.o`, `Python/mysnprintf.o`, `Python/mystrtoul.o`, `Python/object_stack.o`, `Python/optimizer.o`, `Python/optimizer_analysis.o`, `Python/optimizer_symbols.o`, `Python/parking_lot.o`, `Python/pathconfig.o`, `Python/perf_jit_trampoline.o`, `Python/perf_trampoline.o`, `Python/preconfig.o`, `Python/pyarena.o`, `Python/pyctype.o`, `Python/pyfpe.o`, `Python/pyhash.o`, `Python/pylifecycle.o`, `Python/pymath.o`, `Python/pystate.o`, `Python/pystrcmp.o`, `Python/pystrhex.o`, `Python/pystrtod.o`, `Python/pythonrun.o`, `Python/pytime.o`, `Python/qsbr.o`, `Python/specialize.o`, `Python/structmember.o`, `Python/suggestions.o`, `Python/symtable.o`, `Python/sysmodule.o`, `Python/thread.o`, `Python/traceback.o`, `Python/tracemalloc.o`, `config.log`, `config.status`, `pyconfig.h`.

## Refinement Pass 1

Subject: _ssl: wait properly for WANT_READ/WANT_WRITE on blocking sockets

## Commit Message
Keep the `_ssl` fix and trim the patch back to the real upstreamable changes.

`PySSL_select()` now treats a blocking socket with no timeout as a true wait-forever case, so SSL handshake, read, write, and shutdown retries sleep in `poll()` or `select()` instead of immediately looping in userspace after `SSL_ERROR_WANT_READ` or `SSL_ERROR_WANT_WRITE`. The regression test in `test_ssl` keeps coverage focused on that behavior.

This refinement round addressed the review findings by removing generated build products from the workspace so the patch again contains only the functional source changes.

## Issue Connection
The visible problem was a Python process burning CPU while stuck in SSL I/O retry paths instead of blocking until the socket became ready. The cause was `_ssl.c` returning `SOCKET_IS_BLOCKING` immediately for a blocking socket with no timeout, which let the surrounding OpenSSL retry loops re-enter right away after `SSL_ERROR_WANT_READ` or `SSL_ERROR_WANT_WRITE`.

The code change fixes that shared helper in one place. For the blocking-forever case, `PySSL_select()` now falls through to an actual infinite `poll()` or `select()` wait, while non-blocking and finite-timeout behavior stays separate. I kept the short explanatory comment in `_ssl.c` because this remapping from Python’s negative timeout to the OS “wait forever” convention is not obvious at first glance.

The test change delays peer progress during the handshake and checks that a blocking client spends wall time waiting rather than consuming comparable thread CPU time. Together, those changes should turn the observed busy loop into normal kernel-blocked waiting.

## Git Add Paths
Lib/test/test_ssl.py
Modules/_ssl.c

## Validation
- `git diff --check`
- `python3 -m py_compile Lib/test/test_ssl.py`

I also retried the new regression test with `PYTHONPATH=Lib python3 -m unittest -v test.test_ssl.ThreadedTests.test_blocking_handshake_waits_instead_of_spinning`, but this sandbox denied socket creation with `PermissionError: [Errno 1] Operation not permitted`, so I could not complete runtime SSL validation here.

The review’s metadata-drift findings were addressed by removing the generated build outputs from the workspace. One untracked zero-byte `.codex` harness artifact remains visible in `git status`; the environment reports it as busy and the local git exclude file is read-only, so it could not be removed from this session.

## Review Pass 2

RESULT: fix-needed

## Findings
1. [`refine-1-output.txt:4`](<./refine-1-output.txt:4>) and [`refine-1-output.txt:8`](<./refine-1-output.txt:8>) still read like review-session bookkeeping rather than upstream commit text. “trim the patch back”, “This refinement round”, and the workspace artifact note at [`refine-1-output.txt:27`](<./refine-1-output.txt:27>) are not part of the bug or fix, so they make the mail harder for a maintainer to accept as-is. Rewrite the patch text to stay on the user-visible SSL busy-loop bug, the `_ssl.c` behavior change, and the regression test; drop review-process and local-workspace hygiene commentary from the commit message body.

2. [`Modules/_ssl.c:2348`](<./workspace/Modules/_ssl.c:2348>) now has a misleading header comment. After this patch, `PySSL_select()` does not only act in “timeout mode”: it also performs a real infinite wait for blocking sockets with no timeout. The new explanatory comment at lines 2355-2358 is useful, but the stale opening comment now contradicts the actual control flow. Update that top comment so future readers do not have to mentally reconcile two opposite descriptions of the same branch.

`## Git Add Paths` matches the actual tracked file set (`Lib/test/test_ssl.py`, `Modules/_ssl.c`), and `## Issue Connection` does explain both shipped functional files.

## Workflow Note

Review Pass 2 still found unresolved issues after 1 refinement pass(es).

Diff

diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
index 61c5730..b6acb12 100644
--- a/Lib/test/test_ssl.py
+++ b/Lib/test/test_ssl.py
@@ -3828,6 +3828,65 @@ class ThreadedTests(unittest.TestCase):
             t.join()
             server.close()
 
+    @threading_helper.requires_working_threading()
+    @unittest.skipUnless(hasattr(time, "thread_time"),
+                         "needs per-thread CPU clock")
+    def test_blocking_handshake_waits_instead_of_spinning(self):
+        client_ctx, server_ctx, hostname = testing_context()
+        server = socket.socket(socket.AF_INET)
+        host = "127.0.0.1"
+        port = socket_helper.bind_port(server)
+        server.listen()
+        accepted = threading.Event()
+        release_handshake = threading.Event()
+        server_error = None
+        delay = 0.3
+
+        def serve():
+            nonlocal server_error
+            try:
+                conn, _addr = server.accept()
+                with conn:
+                    accepted.set()
+                    release_handshake.wait()
+                    with server_ctx.wrap_socket(conn, server_side=True):
+                        pass
+            except Exception as exc:
+                server_error = exc
+
+        t = threading.Thread(target=serve)
+        t.start()
+
+        try:
+            with socket.create_connection((host, port)) as raw_client:
+                self.assertTrue(accepted.wait(support.SHORT_TIMEOUT))
+
+                timer = threading.Timer(delay, release_handshake.set)
+                timer.start()
+                try:
+                    start_wall = time.monotonic()
+                    start_cpu = time.thread_time()
+                    with client_ctx.wrap_socket(raw_client,
+                                                server_hostname=hostname):
+                        pass
+                    wall_elapsed = time.monotonic() - start_wall
+                    cpu_elapsed = time.thread_time() - start_cpu
+                finally:
+                    timer.cancel()
+                    release_handshake.set()
+
+            t.join(support.SHORT_TIMEOUT)
+            self.assertFalse(t.is_alive())
+            if server_error is not None:
+                raise server_error
+
+            self.assertGreaterEqual(wall_elapsed, delay * 0.8)
+            self.assertLess(cpu_elapsed, delay / 3)
+        finally:
+            release_handshake.set()
+            t.join(support.SHORT_TIMEOUT)
+            server.close()
+
     def test_server_accept(self):
         # Issue #16357: accept() on a SSLSocket created through
         # SSLContext.wrap_socket().
diff --git a/Modules/_ssl.c b/Modules/_ssl.c
index 8d0d8fc..5152ad7 100644
--- a/Modules/_ssl.c
+++ b/Modules/_ssl.c
@@ -2342,6 +2342,7 @@ PySSL_select(PySocketSockObject *s, int writing, PyTime_t timeout)
     int nfds;
     fd_set fds;
     struct timeval tv;
+    struct timeval *tvp;
 #endif
 
     /* Nothing to do unless we're in timeout mode (not non-blocking) */
@@ -2350,8 +2351,11 @@ PySSL_select(PySocketSockObject *s, int writing, PyTime_t timeout)
     else if (timeout < 0) {
         if (s->sock_timeout > 0)
             return SOCKET_HAS_TIMED_OUT;
-        else
-            return SOCKET_IS_BLOCKING;
+
+        /* A blocking socket with no timeout still has to wait for the fd.
+         * Returning SOCKET_IS_BLOCKING here makes the SSL WANT_READ /
+         * WANT_WRITE retry loops spin in userspace instead of sleeping in
+         * poll()/select(). */
     }
 
     /* Guard against closed socket */
@@ -2364,9 +2368,19 @@ PySSL_select(PySocketSockObject *s, int writing, PyTime_t timeout)
     pollfd.fd = s->sock_fd;
     pollfd.events = writing ? POLLOUT : POLLIN;
 
-    /* timeout is in seconds, poll() uses milliseconds */
-    ms = (int)_PyTime_AsMilliseconds(timeout, _PyTime_ROUND_CEILING);
-    assert(ms <= INT_MAX);
+    if (timeout >= 0) {
+        /* timeout is in seconds, poll() uses milliseconds */
+        ms = _PyTime_AsMilliseconds(timeout, _PyTime_ROUND_CEILING);
+        assert(ms <= INT_MAX);
+    }
+    else {
+        /* Match socketmodule.c: a negative interval means wait forever. */
+#ifdef INFTIM
+        ms = INFTIM;
+#else
+        ms = -1;
+#endif
+    }
 
     Py_BEGIN_ALLOW_THREADS
     rc = poll(&pollfd, 1, (int)ms);
@@ -2377,7 +2391,13 @@ PySSL_select(PySocketSockObject *s, int writing, PyTime_t timeout)
     if (!_PyIsSelectable_fd(s->sock_fd))
         return SOCKET_TOO_LARGE_FOR_SELECT;
 
-    _PyTime_AsTimeval_clamp(timeout, &tv, _PyTime_ROUND_CEILING);
+    if (timeout >= 0) {
+        _PyTime_AsTimeval_clamp(timeout, &tv, _PyTime_ROUND_CEILING);
+        tvp = &tv;
+    }
+    else {
+        tvp = NULL;
+    }
 
     FD_ZERO(&fds);
     FD_SET(s->sock_fd, &fds);
@@ -2386,9 +2406,9 @@ PySSL_select(PySocketSockObject *s, int writing, PyTime_t timeout)
     Py_BEGIN_ALLOW_THREADS
     nfds = Py_SAFE_DOWNCAST(s->sock_fd+1, SOCKET_T, int);
     if (writing)
-        rc = select(nfds, NULL, &fds, NULL, &tv);
+        rc = select(nfds, NULL, &fds, NULL, tvp);
     else
-        rc = select(nfds, &fds, NULL, NULL, &tv);
+        rc = select(nfds, &fds, NULL, NULL, tvp);
     Py_END_ALLOW_THREADS
     _PySSL_FIX_ERRNO;
 #endif