]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-143602: Fix duplicate buffer exports in io.BytesIO.write (#143629)
authorzhong <60600792+superboy-zjc@users.noreply.github.com>
Thu, 15 Jan 2026 15:08:55 +0000 (07:08 -0800)
committerGitHub <noreply@github.com>
Thu, 15 Jan 2026 15:08:55 +0000 (16:08 +0100)
Fix an inconsistency issue in io.BytesIO.write() where the buffer was exported
twice, which could lead to unexpected data overwrites and position drift when
the buffer changes between exports.

Lib/_pyio.py
Lib/test/test_io/test_memoryio.py
Misc/NEWS.d/next/Library/2026-01-09-12-37-19.gh-issue-143602.V8vQpj.rst [new file with mode: 0644]

index 77c44addabf2257cdfb58b81e5f06ca55a894f17..3306c8a274760be525c89d7ae9fd29a4b4216e36 100644 (file)
@@ -952,20 +952,21 @@ class BytesIO(BufferedIOBase):
         if isinstance(b, str):
             raise TypeError("can't write str to binary stream")
         with memoryview(b) as view:
-            n = view.nbytes  # Size of any bytes-like object
             if self.closed:
                 raise ValueError("write to closed file")
-        if n == 0:
-            return 0
 
-        with self._lock:
-            pos = self._pos
-            if pos > len(self._buffer):
-                # Pad buffer to pos with null bytes.
-                self._buffer.resize(pos)
-            self._buffer[pos:pos + n] = b
-            self._pos += n
-        return n
+            n = view.nbytes  # Size of any bytes-like object
+            if n == 0:
+                return 0
+
+            with self._lock:
+                pos = self._pos
+                if pos > len(self._buffer):
+                    # Pad buffer to pos with null bytes.
+                    self._buffer.resize(pos)
+                self._buffer[pos:pos + n] = view
+                self._pos += n
+            return n
 
     def seek(self, pos, whence=0):
         if self.closed:
index f730e38a5d6485c99cd37682a01f8828bc5cca8c..482b183da23ffa259664e52592021d5c3c12d34d 100644 (file)
@@ -629,6 +629,28 @@ class PyBytesIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase):
         memio = self.ioclass()
         self.assertRaises(BufferError, memio.writelines, [B()])
 
+    def test_write_mutating_buffer(self):
+        # Test that buffer is exported only once during write().
+        # See: https://github.com/python/cpython/issues/143602.
+        class B:
+            count = 0
+            def __buffer__(self, flags):
+                self.count += 1
+                if self.count == 1:
+                    return memoryview(b"AAA")
+                else:
+                    return memoryview(b"BBBBBBBBB")
+
+        memio = self.ioclass(b'0123456789')
+        memio.seek(2)
+        b = B()
+        n = memio.write(b)
+
+        self.assertEqual(b.count, 1)
+        self.assertEqual(n, 3)
+        self.assertEqual(memio.getvalue(), b"01AAA56789")
+        self.assertEqual(memio.tell(), 5)
+
 
 class TextIOTestMixin:
 
diff --git a/Misc/NEWS.d/next/Library/2026-01-09-12-37-19.gh-issue-143602.V8vQpj.rst b/Misc/NEWS.d/next/Library/2026-01-09-12-37-19.gh-issue-143602.V8vQpj.rst
new file mode 100644 (file)
index 0000000..0eaec90
--- /dev/null
@@ -0,0 +1,2 @@
+Fix a inconsistency issue in :meth:`~io.RawIOBase.write` that leads to
+unexpected buffer overwrite by deduplicating the buffer exports.