Skip to content

Fix PyPy BufferError on SSL reads in StreamReader - #855

Closed
deronnax wants to merge 1 commit into
cherrypy:mainfrom
deronnax:fix/pypy-ssl-buffererror
Closed

deronnax wants to merge 1 commit into
cherrypy:mainfrom
deronnax:fix/pypy-ssl-buffererror

Conversation

@deronnax

@deronnax deronnax commented Oct 10, 2026 •

Copy link
Copy Markdown

Problem

On PyPy, StreamReader.read() (used for socket/SSL reads) can intermittently raise:

BufferError: Existing exports of data: object cannot be re-sized

Root cause

StreamReader wraps the socket with stdlib's socket.SocketIO, whose inherited _pyio.RawIOBase.read() implementation allocates a bytearray, fills it via readinto(), then shrinks it in place with del b[n:].

For SSL sockets, readinto() goes through ssl.SSLSocket.recv_into(), which creates a local memoryview over that same buffer. On CPython this view is released immediately via deterministic refcounting, so the following resize is safe. On PyPy, buffer exports stay alive until the next GC run instead of being released deterministically (documented PyPy/CPython difference), so the in-place resize can randomly fail while that view is still considered alive.

This surfaces as a flaky PyPy-only CI failure for any project reading from SSL sockets through cheroot (e.g. Kozea/Radicale#2270, built on cheroot, hits it in pypy-3.10/pypy-3.11 CI).

Why _pyio can't just be swapped for the accelerated io

makefile.py has imported _pyio instead of io since 2015 (originally in cherrypy/wsgiserver3.py), with only the comment "prefer slower Python-based io module" and no written rationale. Digging through history: BufferedWriter.write()/_flush_unlocked() and StreamReader.has_data() directly access private attributes (_write_buf, _write_lock, _read_buf, _read_pos) that only exist on the pure-Python _pyio implementation; CPython's C-accelerated io module keeps equivalent state in C, not exposed to subclasses. So swapping to io isn't an option as long as those overrides exist.

I also verified on a real PyPy 3.12 install that forcing io instead of _pyio does not avoid the bug anyway: PyPy's io module is itself pure-Python (no separate C accelerator), so it hits the exact same code path.

Fix

Add _SocketIO, a thin socket.SocketIO subclass used by StreamReader, whose read() returns a sliced copy (bytes(b[:n])) instead of resizing the buffer in place. This avoids the unsafe code path entirely, with no behavior change on CPython.

Testing

  • Added a regression test (test_socket_io_read_does_not_resize_buffer) that keeps a memoryview alive across the read() call (simulating the PyPy situation) and asserts no BufferError is raised. Verified it fails against the previous implementation and passes with the fix, on CPython.
  • Additionally reproduced the original bug and verified the fix on a real local PyPy 3.12 install (not just simulated on CPython), confirming both the root cause and the fix hold on the actual affected interpreter.
  • flake8 clean.

Bug, PR text body and solution done by Claude Sonnet 5.5 Medium, all reviewed by me (deronnax)

StreamReader wraps the socket with stdlib's socket.SocketIO, whose
inherited _pyio.RawIOBase.read() allocates a bytearray, fills it via
readinto(), then shrinks it in place with 'del b[n:]'.

For SSL sockets, readinto() goes through ssl.SSLSocket.recv_into(),
which creates a local memoryview over that same buffer. On CPython
this view is released immediately via deterministic refcounting, but
on PyPy buffer exports stay alive until the next GC run, so the
subsequent in-place resize can randomly raise:

BufferError: Existing exports of data: object cannot be re-sized

Add _SocketIO, a thin socket.SocketIO subclass whose read() returns a
sliced copy instead of resizing the buffer in place, avoiding the
unsafe code path entirely.

Opened by Claude, in a Cursor session, on behalf of @deronnax.
@agentscanapp

agentscanapp Bot commented Oct 10, 2026

Copy link
Copy Markdown

Mixed activity

Activity patterns show a mix of organic and automated signals.

View full analysis →

Evidence
  • Rapid PRs to repository: 6 PRs opened to Pylons/waitress within 8s intervals

Last 5 PRs:

This is an automated analysis by AgentScan

@codecov

codecov Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
240 1 239 17
View the full list of 1 ❄️ flaky test(s)
cheroot\test\test_ssl.py::test::test_ssl::test_tls_client_auth[2-False-localhost-builtin]

Flake rate in main: 7.69% (Passed 12 times, Failed 1 times)

Stack Traces | 0.156s run time
http_request_timeout = 5.0
mocker = <pytest_mock.plugin.MockerFixture object at 0x000001AFE6EB7BA0>
tls_http_server = functools.partial(<function make_tls_http_server at 0x000001AFE7A38040>, request=<SubRequest 'tls_http_server' for <Function test_tls_client_auth[2-False-localhost-builtin]>>)
adapter_type = 'builtin', ca = <trustme.CA object at 0x000001AFE817FA70>
tls_certificate = <trustme.LeafCert object at 0x000001AFE7B722D0>
tls_certificate_chain_pem_path = 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\tmp05inv7qg.pem'
tls_certificate_private_key_pem_path = 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\tmp9gh0pl1z.pem'
tls_ca_certificate_pem_path = 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\tmpic6vkc9w.pem'
is_trusted_cert = False, tls_client_identity = 'localhost'
tls_verify_mode = <VerifyMode.CERT_REQUIRED: 2>

    #x1B[0m#x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(  #x1B[90m# noqa: C901  # FIXME#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[33m'#x1B[39;49;00m#x1B[33madapter_type#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
        (#x1B[90m#x1B[39;49;00m
            #x1B[33m'#x1B[39;49;00m#x1B[33mbuiltin#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
            #x1B[33m'#x1B[39;49;00m#x1B[33mpyopenssl#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
        ),#x1B[90m#x1B[39;49;00m
    )#x1B[90m#x1B[39;49;00m
    #x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[90m#x1B[39;49;00m
        (#x1B[33m'#x1B[39;49;00m#x1B[33mis_trusted_cert#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m, #x1B[33m'#x1B[39;49;00m#x1B[33mtls_client_identity#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
        (#x1B[90m#x1B[39;49;00m
            (#x1B[94mTrue#x1B[39;49;00m, #x1B[33m'#x1B[39;49;00m#x1B[33mlocalhost#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
            (#x1B[94mTrue#x1B[39;49;00m, #x1B[33m'#x1B[39;49;00m#x1B[33m127.0.0.1#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
            (#x1B[94mTrue#x1B[39;49;00m, #x1B[33m'#x1B[39;49;00m#x1B[33m*.localhost#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
            (#x1B[94mTrue#x1B[39;49;00m, #x1B[33m'#x1B[39;49;00m#x1B[33mnot_localhost#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
            (#x1B[94mFalse#x1B[39;49;00m, #x1B[33m'#x1B[39;49;00m#x1B[33mlocalhost#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m),#x1B[90m#x1B[39;49;00m
        ),#x1B[90m#x1B[39;49;00m
    )#x1B[90m#x1B[39;49;00m
    #x1B[37m@pytest#x1B[39;49;00m.mark.parametrize(#x1B[90m#x1B[39;49;00m
        #x1B[33m'#x1B[39;49;00m#x1B[33mtls_verify_mode#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
        (#x1B[90m#x1B[39;49;00m
            ssl.CERT_NONE,  #x1B[90m# server shouldn't validate client cert#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            ssl.CERT_OPTIONAL,  #x1B[90m# same as CERT_REQUIRED in client mode, don't use#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            ssl.CERT_REQUIRED,  #x1B[90m# server should validate if client cert CA is OK#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        ),#x1B[90m#x1B[39;49;00m
    )#x1B[90m#x1B[39;49;00m
    #x1B[37m@pytest#x1B[39;49;00m.mark.xfail(#x1B[90m#x1B[39;49;00m
        IS_PYPY #x1B[95mand#x1B[39;49;00m IS_CI,#x1B[90m#x1B[39;49;00m
        reason=#x1B[33m'#x1B[39;49;00m#x1B[33mFails under PyPy in CI for unknown reason#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
        strict=#x1B[94mFalse#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
    )#x1B[90m#x1B[39;49;00m
    #x1B[90m# pylint: disable-next=too-many-positional-arguments#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mtest_tls_client_auth#x1B[39;49;00m(  #x1B[90m# noqa: C901, WPS213  # FIXME#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        #x1B[90m# FIXME: remove twisted logic, separate tests#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        http_request_timeout,#x1B[90m#x1B[39;49;00m
        mocker,#x1B[90m#x1B[39;49;00m
        tls_http_server,#x1B[90m#x1B[39;49;00m
        adapter_type,#x1B[90m#x1B[39;49;00m
        ca,#x1B[90m#x1B[39;49;00m
        tls_certificate,#x1B[90m#x1B[39;49;00m
        tls_certificate_chain_pem_path,#x1B[90m#x1B[39;49;00m
        tls_certificate_private_key_pem_path,#x1B[90m#x1B[39;49;00m
        tls_ca_certificate_pem_path,#x1B[90m#x1B[39;49;00m
        is_trusted_cert,#x1B[90m#x1B[39;49;00m
        tls_client_identity,#x1B[90m#x1B[39;49;00m
        tls_verify_mode,#x1B[90m#x1B[39;49;00m
    ):#x1B[90m#x1B[39;49;00m
    #x1B[90m    #x1B[39;49;00m#x1B[33m"""Verify that client TLS certificate auth works correctly."""#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        test_cert_rejection = (#x1B[90m#x1B[39;49;00m
            tls_verify_mode != ssl.CERT_NONE #x1B[95mand#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m is_trusted_cert#x1B[90m#x1B[39;49;00m
        )#x1B[90m#x1B[39;49;00m
        interface, _host, port = _get_conn_data(ANY_INTERFACE_IPV4)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        client_cert_root_ca = ca #x1B[94mif#x1B[39;49;00m is_trusted_cert #x1B[94melse#x1B[39;49;00m trustme.CA()#x1B[90m#x1B[39;49;00m
        #x1B[94mwith#x1B[39;49;00m mocker.mock_module.patch(#x1B[90m#x1B[39;49;00m
            #x1B[33m'#x1B[39;49;00m#x1B[33midna.core.ulabel#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
            return_value=ntob(tls_client_identity),#x1B[90m#x1B[39;49;00m
        ):#x1B[90m#x1B[39;49;00m
            client_cert = client_cert_root_ca.issue_cert(#x1B[90m#x1B[39;49;00m
                ntou(tls_client_identity),#x1B[90m#x1B[39;49;00m
            )#x1B[90m#x1B[39;49;00m
            #x1B[94mdel#x1B[39;49;00m client_cert_root_ca#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
        #x1B[94mwith#x1B[39;49;00m client_cert.private_key_and_cert_chain_pem.tempfile() #x1B[94mas#x1B[39;49;00m cl_pem:#x1B[90m#x1B[39;49;00m
            tls_adapter_cls = get_ssl_adapter_class(name=adapter_type)#x1B[90m#x1B[39;49;00m
            tls_adapter = tls_adapter_cls(#x1B[90m#x1B[39;49;00m
                tls_certificate_chain_pem_path,#x1B[90m#x1B[39;49;00m
                tls_certificate_private_key_pem_path,#x1B[90m#x1B[39;49;00m
            )#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m adapter_type == #x1B[33m'#x1B[39;49;00m#x1B[33mpyopenssl#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                tls_adapter.context = tls_adapter.get_context()#x1B[90m#x1B[39;49;00m
                tls_adapter.context.set_verify(#x1B[90m#x1B[39;49;00m
                    _stdlib_to_openssl_verify[tls_verify_mode],#x1B[90m#x1B[39;49;00m
                    #x1B[94mlambda#x1B[39;49;00m conn, cert, errno, depth, preverify_ok: preverify_ok,#x1B[90m#x1B[39;49;00m
                )#x1B[90m#x1B[39;49;00m
            #x1B[94melse#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                tls_adapter.context.verify_mode = tls_verify_mode#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            ca.configure_trust(tls_adapter.context)#x1B[90m#x1B[39;49;00m
            tls_certificate.configure_cert(tls_adapter.context)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            tlshttpserver = tls_http_server((interface, port), tls_adapter)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            interface, _host, port = _get_conn_data(tlshttpserver.bind_addr)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            make_https_request = functools.partial(#x1B[90m#x1B[39;49;00m
                requests.get,#x1B[90m#x1B[39;49;00m
                #x1B[33mf#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mhttps://#x1B[39;49;00m#x1B[33m{#x1B[39;49;00minterface#x1B[33m!s}#x1B[39;49;00m#x1B[33m:#x1B[39;49;00m#x1B[33m{#x1B[39;49;00mport#x1B[33m!s}#x1B[39;49;00m#x1B[33m/#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                #x1B[90m# Don't wait for the first byte forever:#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                timeout=http_request_timeout,#x1B[90m#x1B[39;49;00m
                #x1B[90m# Server TLS certificate verification:#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                verify=tls_ca_certificate_pem_path,#x1B[90m#x1B[39;49;00m
                #x1B[90m# Client TLS certificate verification:#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                cert=cl_pem,#x1B[90m#x1B[39;49;00m
            )#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m test_cert_rejection:#x1B[90m#x1B[39;49;00m
                resp = make_https_request()#x1B[90m#x1B[39;49;00m
                is_req_successful = resp.status_code == #x1B[94m200#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                #x1B[94mif#x1B[39;49;00m (#x1B[90m#x1B[39;49;00m
                    #x1B[95mnot#x1B[39;49;00m is_req_successful#x1B[90m#x1B[39;49;00m
                    #x1B[95mand#x1B[39;49;00m IS_PYOPENSSL_SSL_VERSION_1_0#x1B[90m#x1B[39;49;00m
                    #x1B[95mand#x1B[39;49;00m adapter_type == #x1B[33m'#x1B[39;49;00m#x1B[33mbuiltin#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                    #x1B[95mand#x1B[39;49;00m tls_verify_mode == ssl.CERT_REQUIRED#x1B[90m#x1B[39;49;00m
                    #x1B[95mand#x1B[39;49;00m tls_client_identity == #x1B[33m'#x1B[39;49;00m#x1B[33mlocalhost#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                    #x1B[95mand#x1B[39;49;00m is_trusted_cert#x1B[90m#x1B[39;49;00m
                ):#x1B[90m#x1B[39;49;00m
                    pytest.xfail(#x1B[90m#x1B[39;49;00m
                        #x1B[33m'#x1B[39;49;00m#x1B[33mOpenSSL 1.0 has problems with verifying client certs#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                    )#x1B[90m#x1B[39;49;00m
                #x1B[94massert#x1B[39;49;00m is_req_successful#x1B[90m#x1B[39;49;00m
                #x1B[94massert#x1B[39;49;00m resp.text == #x1B[33m'#x1B[39;49;00m#x1B[33mHello world!#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                resp.close()#x1B[90m#x1B[39;49;00m
                #x1B[94mreturn#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            #x1B[90m# xfail some flaky tests#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            #x1B[90m# https://github..../cheroot/issues/237#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            issue_237 = (#x1B[90m#x1B[39;49;00m
                IS_MACOS#x1B[90m#x1B[39;49;00m
                #x1B[95mand#x1B[39;49;00m adapter_type == #x1B[33m'#x1B[39;49;00m#x1B[33mbuiltin#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                #x1B[95mand#x1B[39;49;00m tls_verify_mode != ssl.CERT_NONE#x1B[90m#x1B[39;49;00m
            )#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m issue_237:#x1B[90m#x1B[39;49;00m
                pytest.xfail(#x1B[33m'#x1B[39;49;00m#x1B[33mTest sometimes fails#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            expected_ssl_errors = (requests.exceptions.SSLError,)#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m IS_WINDOWS #x1B[95mor#x1B[39;49;00m IS_GITHUB_ACTIONS_WORKFLOW:#x1B[90m#x1B[39;49;00m
                expected_ssl_errors += (requests.exceptions.ConnectionError,)#x1B[90m#x1B[39;49;00m
            #x1B[94mwith#x1B[39;49;00m pytest.raises(expected_ssl_errors) #x1B[94mas#x1B[39;49;00m ssl_err:#x1B[90m#x1B[39;49;00m
                make_https_request().close()#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            #x1B[94mtry#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                err_text = ssl_err.value.args[#x1B[94m0#x1B[39;49;00m].reason.args[#x1B[94m0#x1B[39;49;00m].args[#x1B[94m0#x1B[39;49;00m]#x1B[90m#x1B[39;49;00m
            #x1B[94mexcept#x1B[39;49;00m #x1B[96mAttributeError#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                #x1B[94mif#x1B[39;49;00m IS_WINDOWS #x1B[95mor#x1B[39;49;00m IS_GITHUB_ACTIONS_WORKFLOW:#x1B[90m#x1B[39;49;00m
                    err_text = #x1B[96mstr#x1B[39;49;00m(ssl_err.value)#x1B[90m#x1B[39;49;00m
                #x1B[94melse#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                    #x1B[94mraise#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m #x1B[96misinstance#x1B[39;49;00m(err_text, #x1B[96mint#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
                err_text = #x1B[96mstr#x1B[39;49;00m(ssl_err.value)#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            expected_substrings = (#x1B[90m#x1B[39;49;00m
                #x1B[33m'#x1B[39;49;00m#x1B[33msslv3 alert bad certificate#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                #x1B[94mif#x1B[39;49;00m IS_LIBRESSL_BACKEND#x1B[90m#x1B[39;49;00m
                #x1B[94melse#x1B[39;49;00m #x1B[33m'#x1B[39;49;00m#x1B[33mtlsv1 alert unknown ca#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
            )#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m IS_MACOS #x1B[95mand#x1B[39;49;00m IS_PYPY #x1B[95mand#x1B[39;49;00m adapter_type == #x1B[33m'#x1B[39;49;00m#x1B[33mpyopenssl#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
                expected_substrings = (#x1B[33m'#x1B[39;49;00m#x1B[33mtlsv1 alert unknown ca#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m,)#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m (#x1B[90m#x1B[39;49;00m
                tls_verify_mode#x1B[90m#x1B[39;49;00m
                #x1B[95min#x1B[39;49;00m {#x1B[90m#x1B[39;49;00m
                    ssl.CERT_REQUIRED,#x1B[90m#x1B[39;49;00m
                    ssl.CERT_OPTIONAL,#x1B[90m#x1B[39;49;00m
                }#x1B[90m#x1B[39;49;00m
                #x1B[95mand#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m is_trusted_cert#x1B[90m#x1B[39;49;00m
                #x1B[95mand#x1B[39;49;00m tls_client_identity == #x1B[33m'#x1B[39;49;00m#x1B[33mlocalhost#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
            ):#x1B[90m#x1B[39;49;00m
                expected_substrings += (#x1B[90m#x1B[39;49;00m
                    (#x1B[90m#x1B[39;49;00m
                        #x1B[33m"#x1B[39;49;00m#x1B[33mbad handshake: SysCallError(10054, #x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mWSAECONNRESET#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m)#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                        (#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33m(#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection aborted.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m, #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m'#x1B[39;49;00m#x1B[33mOSError(#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33m(10054, #x1B[39;49;00m#x1B[33m\'#x1B[39;49;00m#x1B[33mWSAECONNRESET#x1B[39;49;00m#x1B[33m\'#x1B[39;49;00m#x1B[33m)#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33m))#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                        ),#x1B[90m#x1B[39;49;00m
                        (#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33m(#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection aborted.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m, #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m'#x1B[39;49;00m#x1B[33mOSError(#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33m(10054, #x1B[39;49;00m#x1B[33m\'#x1B[39;49;00m#x1B[33mWSAECONNRESET#x1B[39;49;00m#x1B[33m\'#x1B[39;49;00m#x1B[33m)#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33m,))#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                        ),#x1B[90m#x1B[39;49;00m
                        (#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33m(#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection aborted.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m, #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m'#x1B[39;49;00m#x1B[33merror(#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33m(10054, #x1B[39;49;00m#x1B[33m\'#x1B[39;49;00m#x1B[33mWSAECONNRESET#x1B[39;49;00m#x1B[33m\'#x1B[39;49;00m#x1B[33m)#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33m,))#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                        ),#x1B[90m#x1B[39;49;00m
                        (#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33m(#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection aborted.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m, #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m'#x1B[39;49;00m#x1B[33mConnectionResetError(10054, #x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mAn existing connection was forcibly closed #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33mby the remote host#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m, None, 10054, None))#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                        ),#x1B[90m#x1B[39;49;00m
                        (#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33m(#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection aborted.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m, #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m'#x1B[39;49;00m#x1B[33merror(10054, #x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mAn existing connection was forcibly closed #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33mby the remote host#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m))#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                        ),#x1B[90m#x1B[39;49;00m
                    )#x1B[90m#x1B[39;49;00m
                    #x1B[94mif#x1B[39;49;00m IS_WINDOWS#x1B[90m#x1B[39;49;00m
                    #x1B[94melse#x1B[39;49;00m (#x1B[90m#x1B[39;49;00m
                        (#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33m(#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection aborted.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m, #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m'#x1B[39;49;00m#x1B[33mOSError(#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33m(104, #x1B[39;49;00m#x1B[33m\'#x1B[39;49;00m#x1B[33mECONNRESET#x1B[39;49;00m#x1B[33m\'#x1B[39;49;00m#x1B[33m)#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33m))#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                        ),#x1B[90m#x1B[39;49;00m
                        (#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33m(#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection aborted.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m, #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m'#x1B[39;49;00m#x1B[33mOSError(#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33m(104, #x1B[39;49;00m#x1B[33m\'#x1B[39;49;00m#x1B[33mECONNRESET#x1B[39;49;00m#x1B[33m\'#x1B[39;49;00m#x1B[33m)#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[33m,))#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                        ),#x1B[90m#x1B[39;49;00m
                        #x1B[33m"#x1B[39;49;00m#x1B[33m(#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection aborted.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m, error(#x1B[39;49;00m#x1B[33m\"#x1B[39;49;00m#x1B[33m(104, #x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mECONNRESET#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m)#x1B[39;49;00m#x1B[33m\"#x1B[39;49;00m#x1B[33m,))#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m,#x1B[90m#x1B[39;49;00m
                        (#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33m(#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection aborted.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m, #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33mConnectionResetError(104, #x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection reset by peer#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m))#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                        ),#x1B[90m#x1B[39;49;00m
                        (#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33m(#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection aborted.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m, #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33merror(104, #x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection reset by peer#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m))#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                        ),#x1B[90m#x1B[39;49;00m
                    )#x1B[90m#x1B[39;49;00m
                    #x1B[94mif#x1B[39;49;00m (IS_GITHUB_ACTIONS_WORKFLOW #x1B[95mand#x1B[39;49;00m IS_LINUX)#x1B[90m#x1B[39;49;00m
                    #x1B[94melse#x1B[39;49;00m (#x1B[90m#x1B[39;49;00m
                        (#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33m(#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection aborted.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m, #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                            #x1B[33m"#x1B[39;49;00m#x1B[33mBrokenPipeError(32, #x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mBroken pipe#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m))#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                        ),#x1B[90m#x1B[39;49;00m
                    )#x1B[90m#x1B[39;49;00m
                )#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m PY310_PLUS:#x1B[90m#x1B[39;49;00m
                #x1B[90m# FIXME: Figure out what's happening and correct the problem#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                expected_substrings += (#x1B[90m#x1B[39;49;00m
                    (#x1B[90m#x1B[39;49;00m
                        #x1B[33m'#x1B[39;49;00m#x1B[33mSSLError(SSLEOFError(8, #x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                        #x1B[33m"#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mEOF occurred in violation of protocol (_ssl.c:#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                    ),#x1B[90m#x1B[39;49;00m
                )#x1B[90m#x1B[39;49;00m
            #x1B[94mif#x1B[39;49;00m IS_GITHUB_ACTIONS_WORKFLOW #x1B[95mand#x1B[39;49;00m IS_WINDOWS #x1B[95mand#x1B[39;49;00m PY310_PLUS:#x1B[90m#x1B[39;49;00m
                expected_substrings += (#x1B[90m#x1B[39;49;00m
                    (#x1B[90m#x1B[39;49;00m
                        #x1B[33m"#x1B[39;49;00m#x1B[33m(#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mConnection aborted.#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m, #x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                        #x1B[33m'#x1B[39;49;00m#x1B[33mRemoteDisconnected(#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                        #x1B[33m"#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33mRemote end closed connection without response#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[33m))#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
                    ),#x1B[90m#x1B[39;49;00m
                )#x1B[90m#x1B[39;49;00m
    #x1B[90m#x1B[39;49;00m
>           #x1B[94massert#x1B[39;49;00m #x1B[96many#x1B[39;49;00m(e #x1B[95min#x1B[39;49;00m err_text #x1B[94mfor#x1B[39;49;00m e #x1B[95min#x1B[39;49;00m expected_substrings)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE           assert False#x1B[0m
#x1B[1m#x1B[31mE            +  where False = any(<generator object test_tls_client_auth.<locals>.<genexpr> at 0x000001AFE79243C0>)#x1B[0m

_host      = '127.0.0.1'
adapter_type = 'builtin'
ca         = <trustme.CA object at 0x000001AFE817FA70>
cl_pem     = 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\tmpw9suuud5.pem'
client_cert = <trustme.LeafCert object at 0x000001AFE79022B0>
err_text   = "('Connection aborted.', ConnectionResetError(10054, 'An existing connection was forcibly closed by the remote host'))"
expected_ssl_errors = (<class 'requests.exceptions.SSLError'>, <class 'requests.exceptions.ConnectionError'>)
expected_substrings = ('tlsv1 alert unknown ca', "bad handshake: SysCallError(10054, 'WSAECONNRESET')", '(\'Connection aborted.\', OSError("...ConnectionResetError(10054, 'An existing connection was forcibly closed by the remote host', None, 10054, None))", ...)
http_request_timeout = 5.0
interface  = '127.0.0.1'
is_trusted_cert = False
issue_237  = False
make_https_request = functools.partial(<function get at 0x000001AFE6F98EB0>, 'https://127.0.0.1:63173/', timeout=5.0, verify='C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\tmpic6vkc9w.pem', cert='C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\tmpw9suuud5.pem')
mocker     = <pytest_mock.plugin.MockerFixture object at 0x000001AFE6EB7BA0>
port       = 63173
ssl_err    = <ExceptionInfo ConnectionError(ProtocolError('Connection aborted.', ConnectionResetError(10054, 'An existing connection was forcibly closed by the remote host'))) tblen=6>
test_cert_rejection = True
tls_adapter = <cheroot.ssl.builtin.BuiltinSSLAdapter object at 0x000001AFE7F75E00>
tls_adapter_cls = <class 'cheroot.ssl.builtin.BuiltinSSLAdapter'>
tls_ca_certificate_pem_path = 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\tmpic6vkc9w.pem'
tls_certificate = <trustme.LeafCert object at 0x000001AFE7B722D0>
tls_certificate_chain_pem_path = 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\tmp05inv7qg.pem'
tls_certificate_private_key_pem_path = 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\tmp9gh0pl1z.pem'
tls_client_identity = 'localhost'
tls_http_server = functools.partial(<function make_tls_http_server at 0x000001AFE7A38040>, request=<SubRequest 'tls_http_server' for <Function test_tls_client_auth[2-False-localhost-builtin]>>)
tls_verify_mode = <VerifyMode.CERT_REQUIRED: 2>
tlshttpserver = <cheroot.server.HTTPServer object at 0x000001AFE6CB2850>

#x1B[1m#x1B[31mcheroot\test\test_ssl.py#x1B[0m:554: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@deronnax

deronnax commented Oct 10, 2026 •

Copy link
Copy Markdown
Author

Hmmm, actually I don't like this fix (it dupplicates code). Claude keeps telling me that's the only way, but I think I can maybe come up with something better, wait a bit.

@webknjaz

Copy link
Copy Markdown
Member

While in it, would you mind also not disregarding the PR template?

@deronnax

deronnax commented Oct 11, 2026 •

Copy link
Copy Markdown
Author

Closing this in favor of fixing it at the actual source: this was a PyPy bug, not something cheroot needed to work around. I reported it upstream (pypy/pypy#5634), and the PyPy maintainer (mattip) confirmed _pyio/_io's RawIOBase.read() used an unsafe del b[n:] in-place resize instead of CPython's actual bytes(b[:n]) copy pattern, and already fixed it in PyPy's _io (commit 27be2a6b3ec). The PyPy-only BufferError we hit here was a side effect of that bug, triggered via ssl.SSLSocket.recv_into(). No change needed on the cheroot side once that fix ships in a PyPy release; happy to reopen if older PyPy versions still need a workaround. But given the inelegance of the fix, I would not mind.

Really sorry for the PR template, I will pay attention in the future, should any further contributions be made 😬

@deronnax deronnax closed this Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants