From eedc052d0d48f777d8ee748c67efb8f854ebb277 Mon Sep 17 00:00:00 2001 From: emerard <113128214+emerardd@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:27:13 +0800 Subject: [PATCH 1/6] fix(io): reject readonly buffers before reading in _pyio --- Lib/_pyio.py | 2 ++ Lib/test/test_io/test_memoryio.py | 15 +++++++++++++++ ...2026-10-07-06-28-00.gh-issue-158952.tR7kLp.rst | 3 +++ 3 files changed, 20 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-07-06-28-00.gh-issue-158952.tR7kLp.rst diff --git a/Lib/_pyio.py b/Lib/_pyio.py index cf4ef04f37d26cc..95646e47c79cf7a 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -728,6 +728,8 @@ def _readinto(self, b, read1): if not isinstance(b, memoryview): b = memoryview(b) b = b.cast('B') + if b.readonly: + raise TypeError("readinto() argument must be read-write bytes-like object") if read1: data = self.read1(len(b)) diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index b6f3aa93e7aa43f..91f182a8d185422 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -568,6 +568,21 @@ def test_readinto(self): memio.readinto(b) self.assertEqual(b, b"") + def test_readinto_readonly_buffer(self): + data = b"abcdef" + for method in ("readinto", "readinto1"): + for buffer in (b"xxx", memoryview(b"xxx"), + b"", memoryview(b"")): + for position in (0, 4, 10): + with self.subTest(method=method, buffer=buffer, + position=position): + with self.ioclass(data) as memio: + memio.seek(position) + with self.assertRaises(TypeError): + getattr(memio, method)(buffer) + self.assertEqual(memio.tell(), position) + self.assertEqual(memio.read(), data[position:]) + def test_relative_seek(self): buf = self.buftype("1234567890") memio = self.ioclass(buf) diff --git a/Misc/NEWS.d/next/Library/2026-10-07-06-28-00.gh-issue-158952.tR7kLp.rst b/Misc/NEWS.d/next/Library/2026-10-07-06-28-00.gh-issue-158952.tR7kLp.rst new file mode 100644 index 000000000000000..06383eab7766ff9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-07-06-28-00.gh-issue-158952.tR7kLp.rst @@ -0,0 +1,3 @@ +The pure Python implementation of :meth:`io.BufferedIOBase.readinto` and +:meth:`~io.BufferedIOBase.readinto1` now rejects read-only buffers before +reading input, without changing the stream position. From 3831e0c0e274ed241e13307dd4ec2989dabd7b73 Mon Sep 17 00:00:00 2001 From: emerard <113128214+emerardd@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:58:43 +0800 Subject: [PATCH 2/6] refactor(io): address readinto review feedback --- Lib/_pyio.py | 2 +- Lib/test/test_io/test_memoryio.py | 25 ++++++++++++------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 95646e47c79cf7a..9cbc75a4cf93535 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -727,9 +727,9 @@ def readinto1(self, b): def _readinto(self, b, read1): if not isinstance(b, memoryview): b = memoryview(b) - b = b.cast('B') if b.readonly: raise TypeError("readinto() argument must be read-write bytes-like object") + b = b.cast('B') if read1: data = self.read1(len(b)) diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index 91f182a8d185422..9cb718c9481d2fc 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -568,20 +568,19 @@ def test_readinto(self): memio.readinto(b) self.assertEqual(b, b"") - def test_readinto_readonly_buffer(self): + @support.subTests("method", ("readinto", "readinto1")) + @support.subTests("buffer", ( + b"xxx", memoryview(b"xxx"), b"", memoryview(b""), + )) + @support.subTests("position", (0, 4, 10)) + def test_readinto_readonly_buffer(self, method, buffer, position): data = b"abcdef" - for method in ("readinto", "readinto1"): - for buffer in (b"xxx", memoryview(b"xxx"), - b"", memoryview(b"")): - for position in (0, 4, 10): - with self.subTest(method=method, buffer=buffer, - position=position): - with self.ioclass(data) as memio: - memio.seek(position) - with self.assertRaises(TypeError): - getattr(memio, method)(buffer) - self.assertEqual(memio.tell(), position) - self.assertEqual(memio.read(), data[position:]) + with self.ioclass(data) as memio: + memio.seek(position) + with self.assertRaises(TypeError): + getattr(memio, method)(buffer) + self.assertEqual(memio.tell(), position) + self.assertEqual(memio.read(), data[position:]) def test_relative_seek(self): buf = self.buftype("1234567890") From a7d0e02bb0760b75aa1a1e4fb4ec2f39b68ba168 Mon Sep 17 00:00:00 2001 From: emerard <113128214+emerardd@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:16:35 +0800 Subject: [PATCH 3/6] test(io): isolate readinto exception assertion --- Lib/test/test_io/test_memoryio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index 9cb718c9481d2fc..36f205e935772aa 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -577,8 +577,8 @@ def test_readinto_readonly_buffer(self, method, buffer, position): data = b"abcdef" with self.ioclass(data) as memio: memio.seek(position) - with self.assertRaises(TypeError): - getattr(memio, method)(buffer) + op = getattr(memio, method) + self.assertRaises(TypeError, op, buffer) self.assertEqual(memio.tell(), position) self.assertEqual(memio.read(), data[position:]) From f46989fdba5f4fae6e6995060038403e6117126c Mon Sep 17 00:00:00 2001 From: emerard <113128214+emerardd@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:26:24 +0800 Subject: [PATCH 4/6] test(io): cover readonly buffers in buffered readers --- Lib/test/test_io/test_bufferedio.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Lib/test/test_io/test_bufferedio.py b/Lib/test/test_io/test_bufferedio.py index e83dd0d4e28d006..107bdf2c2dc7b02 100644 --- a/Lib/test/test_io/test_bufferedio.py +++ b/Lib/test/test_io/test_bufferedio.py @@ -313,6 +313,23 @@ def test_readinto(self): self.assertEqual(bufio.readinto(b), 1) self.assertEqual(b, b"cb") + @support.subTests("method", ("readinto", "readinto1")) + @support.subTests("buffer", ( + b"xxx", memoryview(b"xxx"), + )) + @support.subTests("position", (0, 4)) + @support.subTests("buffered", (False, True)) + def test_readinto_readonly_buffer(self, method, buffer, position, buffered): + data = b"abcdef" + with self.tp(self.BytesIO(data)) as bufio: + bufio.seek(position) + if buffered: + bufio.peek(1) + op = getattr(bufio, method) + self.assertRaises(TypeError, op, buffer) + self.assertEqual(bufio.tell(), position) + self.assertEqual(bufio.read(), data[position:]) + def test_readinto1(self): buffer_size = 10 rawio = self.MockRawIO((b"abc", b"de", b"fgh", b"jkl")) From 5c5f966a308b43b86bf6a1de266068a55fbe45d6 Mon Sep 17 00:00:00 2001 From: emerard <113128214+emerardd@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:54:17 +0800 Subject: [PATCH 5/6] test(io): share readonly readinto assertions --- Lib/test/test_io/test_bufferedio.py | 7 +++---- Lib/test/test_io/test_memoryio.py | 7 +++---- Lib/test/test_io/utils.py | 8 ++++++++ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_io/test_bufferedio.py b/Lib/test/test_io/test_bufferedio.py index 107bdf2c2dc7b02..e2257549e05b201 100644 --- a/Lib/test/test_io/test_bufferedio.py +++ b/Lib/test/test_io/test_bufferedio.py @@ -12,6 +12,7 @@ from test import support from test.support import check_sanitizer, os_helper, threading_helper from .utils import byteslike, CTestCase, PyTestCase +from .utils import check_readinto_readonly_buffer import io # C implementation. @@ -325,10 +326,8 @@ def test_readinto_readonly_buffer(self, method, buffer, position, buffered): bufio.seek(position) if buffered: bufio.peek(1) - op = getattr(bufio, method) - self.assertRaises(TypeError, op, buffer) - self.assertEqual(bufio.tell(), position) - self.assertEqual(bufio.read(), data[position:]) + check_readinto_readonly_buffer( + self, bufio, method, buffer, position, data[position:]) def test_readinto1(self): buffer_size = 10 diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index 36f205e935772aa..b2e554e7f2f9bd1 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -5,6 +5,7 @@ import unittest from test import support +from .utils import check_readinto_readonly_buffer import gc import io @@ -577,10 +578,8 @@ def test_readinto_readonly_buffer(self, method, buffer, position): data = b"abcdef" with self.ioclass(data) as memio: memio.seek(position) - op = getattr(memio, method) - self.assertRaises(TypeError, op, buffer) - self.assertEqual(memio.tell(), position) - self.assertEqual(memio.read(), data[position:]) + check_readinto_readonly_buffer( + self, memio, method, buffer, position, data[position:]) def test_relative_seek(self): buf = self.buftype("1234567890") diff --git a/Lib/test/test_io/utils.py b/Lib/test/test_io/utils.py index dde49337a24f0b9..42cb53241da9775 100644 --- a/Lib/test/test_io/utils.py +++ b/Lib/test/test_io/utils.py @@ -26,6 +26,14 @@ def byteslike(*pos, **kw): return obj +def check_readinto_readonly_buffer(testcase, stream, method, buffer, + position, remaining): + op = getattr(stream, method) + testcase.assertRaises(TypeError, op, buffer) + testcase.assertEqual(stream.tell(), position) + testcase.assertEqual(stream.read(), remaining) + + class MockRawIOWithoutRead: """A RawIO implementation without read(), so as to exercise the default RawIO.read() which calls readinto().""" From e1f76eb3dbe2b452528868772be379e8eafb9df0 Mon Sep 17 00:00:00 2001 From: emerard <113128214+emerardd@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:26:07 +0800 Subject: [PATCH 6/6] test(io): share readonly readinto test setup and cases --- Lib/test/test_io/test_bufferedio.py | 17 +++-------------- Lib/test/test_io/test_memoryio.py | 13 ++----------- Lib/test/test_io/utils.py | 29 +++++++++++++++++++++++------ 3 files changed, 28 insertions(+), 31 deletions(-) diff --git a/Lib/test/test_io/test_bufferedio.py b/Lib/test/test_io/test_bufferedio.py index e2257549e05b201..da7917d3a72ee6c 100644 --- a/Lib/test/test_io/test_bufferedio.py +++ b/Lib/test/test_io/test_bufferedio.py @@ -314,20 +314,9 @@ def test_readinto(self): self.assertEqual(bufio.readinto(b), 1) self.assertEqual(b, b"cb") - @support.subTests("method", ("readinto", "readinto1")) - @support.subTests("buffer", ( - b"xxx", memoryview(b"xxx"), - )) - @support.subTests("position", (0, 4)) - @support.subTests("buffered", (False, True)) - def test_readinto_readonly_buffer(self, method, buffer, position, buffered): - data = b"abcdef" - with self.tp(self.BytesIO(data)) as bufio: - bufio.seek(position) - if buffered: - bufio.peek(1) - check_readinto_readonly_buffer( - self, bufio, method, buffer, position, data[position:]) + def test_readinto_readonly_buffer(self): + check_readinto_readonly_buffer( + self, lambda data: self.tp(self.BytesIO(data)), prefill=True) def test_readinto1(self): buffer_size = 10 diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index b2e554e7f2f9bd1..8bb5ad33bf2a763 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -569,17 +569,8 @@ def test_readinto(self): memio.readinto(b) self.assertEqual(b, b"") - @support.subTests("method", ("readinto", "readinto1")) - @support.subTests("buffer", ( - b"xxx", memoryview(b"xxx"), b"", memoryview(b""), - )) - @support.subTests("position", (0, 4, 10)) - def test_readinto_readonly_buffer(self, method, buffer, position): - data = b"abcdef" - with self.ioclass(data) as memio: - memio.seek(position) - check_readinto_readonly_buffer( - self, memio, method, buffer, position, data[position:]) + def test_readinto_readonly_buffer(self): + check_readinto_readonly_buffer(self, self.ioclass, empty_and_eof=True) def test_relative_seek(self): buf = self.buftype("1234567890") diff --git a/Lib/test/test_io/utils.py b/Lib/test/test_io/utils.py index 42cb53241da9775..f0414a44a7ce9fe 100644 --- a/Lib/test/test_io/utils.py +++ b/Lib/test/test_io/utils.py @@ -2,6 +2,7 @@ import threading import time import unittest +from itertools import product import io # C implementation of io import _pyio as pyio # Python implementation of io @@ -26,12 +27,28 @@ def byteslike(*pos, **kw): return obj -def check_readinto_readonly_buffer(testcase, stream, method, buffer, - position, remaining): - op = getattr(stream, method) - testcase.assertRaises(TypeError, op, buffer) - testcase.assertEqual(stream.tell(), position) - testcase.assertEqual(stream.read(), remaining) +def check_readinto_readonly_buffer(testcase, stream_factory, *, + empty_and_eof=False, prefill=False): + data = b"abcdef" + buffers = [b"xxx", memoryview(b"xxx")] + positions = [0, 4] + if empty_and_eof: + buffers.extend([b"", memoryview(b"")]) + positions.append(10) + methods = ("readinto", "readinto1") + prefilled = (False, True) if prefill else (False,) + for method, buffer, position, buffered in product( + methods, buffers, positions, prefilled): + with testcase.subTest(method=method, buffer=buffer, position=position, + buffered=buffered): + with stream_factory(data) as stream: + stream.seek(position) + if buffered: + stream.peek(1) + op = getattr(stream, method) + testcase.assertRaises(TypeError, op, buffer) + testcase.assertEqual(stream.tell(), position) + testcase.assertEqual(stream.read(), data[position:]) class MockRawIOWithoutRead: