From b786a5ed5aa86de6842cbbfc438a0faf9432c6f9 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Sat, 8 Aug 2026 19:45:47 +0300 Subject: [PATCH 1/2] gh-155389: Return bytes from _pyio.BytesIO.peek() peek() returned a slice of the internal bytearray, where read() converts with take_bytes(). It also did not coerce its size through __index__ and did not hold the lock while slicing, both of which read() and the C implementation do. --- Lib/_pyio.py | 15 ++++++++++++--- Lib/test/test_io/test_memoryio.py | 4 ++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 1118b54633b7cc1..27f5aa2490c8b88 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -1003,9 +1003,18 @@ def tell(self): def peek(self, size=0): if self.closed: raise ValueError("peek on closed file") - if size < 1: - return self._buffer[self._pos:self._pos + io.DEFAULT_BUFFER_SIZE] - return self._buffer[self._pos:self._pos + size] + try: + size_index = size.__index__ + except AttributeError: + raise TypeError(f"{size!r} is not an integer") + else: + size = size_index() + + with self._lock: + if size < 1: + size = io.DEFAULT_BUFFER_SIZE + b = self._buffer[self._pos:self._pos + size] + return b.take_bytes() def truncate(self, pos=None): if self.closed: diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index f53525d77b7dbaf..d1af0bad9ddf7e5 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -570,6 +570,10 @@ def test_peek(self): buf = self.buftype("1234567890") with self.ioclass(buf) as memio: self.assertEqual(memio.tell(), 0) + # bytearray(b'1') == b'1', so the type has to be asserted separately. + self.assertIsInstance(memio.peek(), bytes) + self.assertIsInstance(memio.peek(1), bytes) + self.assertEqual(memio.peek(IntLike(3)), buf[:3]) self.assertEqual(memio.peek(1), buf[:1]) self.assertEqual(memio.peek(1), buf[:1]) self.assertEqual(memio.peek(), buf) From deeeb333dfffab94b86d1a311f1b39b65fb3f283 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Sat, 8 Aug 2026 21:39:54 +0300 Subject: [PATCH 2/2] Keep only the slicing under the lock, and test a float size The size normalisation touches no shared state, so it does not need the lock. --- Lib/_pyio.py | 5 +++-- Lib/test/test_io/test_memoryio.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 27f5aa2490c8b88..9035db0964544fc 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -1010,9 +1010,10 @@ def peek(self, size=0): else: size = size_index() + if size < 1: + size = io.DEFAULT_BUFFER_SIZE + with self._lock: - if size < 1: - size = io.DEFAULT_BUFFER_SIZE b = self._buffer[self._pos:self._pos + size] return b.take_bytes() diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index d1af0bad9ddf7e5..b929b56a7d81213 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -574,6 +574,7 @@ def test_peek(self): self.assertIsInstance(memio.peek(), bytes) self.assertIsInstance(memio.peek(1), bytes) self.assertEqual(memio.peek(IntLike(3)), buf[:3]) + self.assertRaises(TypeError, memio.peek, 1.5) self.assertEqual(memio.peek(1), buf[:1]) self.assertEqual(memio.peek(1), buf[:1]) self.assertEqual(memio.peek(), buf)