python3.13 likely remains stuck in a busy-poll loop. The diagnosis was captured, but the patch proposal did not complete cleanly.
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