Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion cheroot/makefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,38 @@ def _flush_unlocked(self):
del self._write_buf[:n]


class _SocketIO(socket.SocketIO):
"""``socket.SocketIO`` with a resize-safe ``read()``.

``_pyio.RawIOBase.read()`` (the base class' default implementation,
inherited unchanged by ``socket.SocketIO``) allocates a ``bytearray``,
fills it via ``readinto()``, then shrinks it in place with
``del b[n:]``. On PyPy, ``ssl.SSLSocket.recv_into()`` creates a local
``memoryview`` over that same buffer which is not released
deterministically (PyPy keeps buffer exports alive until the next GC
run, unlike CPython's immediate refcounting), so the later in-place
resize can randomly raise ``BufferError: Existing exports of data:
object cannot be re-sized``. Returning a fresh copy via slicing avoids
resizing the original buffer and sidesteps the issue entirely.
"""

def read(self, size=-1):
"""Read and return up to ``size`` bytes, without resizing."""
if size is None or size < 0:
return self.readall()
b = bytearray(size)
n = self.readinto(b)
if n is None:
return None
return bytes(b[:n])


class StreamReader(io.BufferedReader):
"""Socket stream reader."""

def __init__(self, sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE):
"""Initialize socket stream reader."""
super().__init__(socket.SocketIO(sock, mode), bufsize)
super().__init__(_SocketIO(sock, mode), bufsize)
self.bytes_read = 0

def read(self, *args, **kwargs):
Expand Down
26 changes: 25 additions & 1 deletion cheroot/test/test_makefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
class MockSocket:
"""A mock socket."""

def __init__(self):
def __init__(self, keep_buffer_exported=False):
"""Initialize :py:class:`MockSocket`."""
self.messages = []
self.keep_buffer_exported = keep_buffer_exported
self.leaked_view = None

def recv_into(self, buf):
"""Simulate ``recv_into`` for Python 3."""
Expand All @@ -17,6 +19,10 @@ def recv_into(self, buf):
msg = self.messages.pop(0)
for index, byte in enumerate(msg):
buf[index] = byte
if self.keep_buffer_exported:
# Simulate ssl.SSLSocket.recv_into() leaking a memoryview over
# ``buf`` past the call, as happens on PyPy (see GH-#XXX).
self.leaked_view = memoryview(buf)
return len(msg)

def recv(self, size):
Expand Down Expand Up @@ -51,3 +57,21 @@ def test_bytes_written():
wfile = makefile.MakeFile(sock, 'w')
wfile.write(b'bar')
assert wfile.bytes_written == 3


def test_socket_io_read_does_not_resize_buffer():
"""``_SocketIO.read()`` must not shrink its buffer in place.

Regression test for a PyPy-only ``BufferError: Existing exports of
data: object cannot be re-sized``, triggered when the default
``_pyio.RawIOBase.read()`` implementation (``del b[n:]``) resizes a
buffer still referenced by an unreleased ``memoryview``, e.g. one
created internally by ``ssl.SSLSocket.recv_into()``. On PyPy that
memoryview stays alive until the next GC run instead of being
released immediately, unlike CPython's deterministic refcounting.
"""
sock = MockSocket(keep_buffer_exported=True)
sock.messages.append(b'foo')
raw = makefile._SocketIO(sock, 'r')
assert raw.read(3) == b'foo'
assert sock.leaked_view is not None
Loading