https://github.com/python/cpython/commit/b758ff486907e398cc90acd0c64933b3fb35597a
commit: b758ff486907e398cc90acd0c64933b3fb35597a
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-14T07:38:45+03:00
summary:

gh-155702: Fix sqlite3.Blob slice assignment with a step (GH-155703)

It patched the bytes object read from the blob, which for a single byte
is an immortal singleton, so that the value of that byte was changed in
the whole process.

files:
A Misc/NEWS.d/next/Library/2026-08-13-16-02-13.gh-issue-155702.Kb7Qm4.rst
M Lib/test/test_sqlite3/test_dbapi.py
M Modules/_sqlite/blob.c

diff --git a/Lib/test/test_sqlite3/test_dbapi.py 
b/Lib/test/test_sqlite3/test_dbapi.py
index 5f6cb527955ca17..2cf3556f66d9639 100644
--- a/Lib/test/test_sqlite3/test_dbapi.py
+++ b/Lib/test/test_sqlite3/test_dbapi.py
@@ -1396,6 +1396,18 @@ def test_blob_set_slice(self):
         actual = self.cx.execute("select b from test").fetchone()[0]
         self.assertEqual(actual, expected)
 
+    def test_blob_set_slice_with_step_keeps_bytes_intact(self):
+        # The buffer used for the read-patch-write cycle must not be the
+        # bytes object read from the blob: for a single byte it is an
+        # immortal singleton.
+        old_byte = self.data[5]
+        self.blob[5:6:2] = b"\xab"
+        self.assertEqual(bytes([old_byte])[0], old_byte)
+        self.assertEqual(self.blob[5:6], b"\xab")
+        expected = self.data[:5] + b"\xab" + self.data[6:]
+        actual = self.cx.execute("select b from test").fetchone()[0]
+        self.assertEqual(actual, expected)
+
     def test_blob_set_empty_slice(self):
         self.blob[0:0] = b""
         self.assertEqual(self.blob[:], self.data)
diff --git 
a/Misc/NEWS.d/next/Library/2026-08-13-16-02-13.gh-issue-155702.Kb7Qm4.rst 
b/Misc/NEWS.d/next/Library/2026-08-13-16-02-13.gh-issue-155702.Kb7Qm4.rst
new file mode 100644
index 000000000000000..7fe505e60393b4c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-13-16-02-13.gh-issue-155702.Kb7Qm4.rst
@@ -0,0 +1,4 @@
+Fix :class:`sqlite3.Blob` slice assignment with a step.
+It patched the bytes object read from the blob,
+which for a single byte is an immortal singleton,
+so that the value of that byte was changed in the whole process.
diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c
index d81784409e5d91a..53d28a06181a9c1 100644
--- a/Modules/_sqlite/blob.c
+++ b/Modules/_sqlite/blob.c
@@ -139,26 +139,35 @@ read_single(pysqlite_Blob *self, Py_ssize_t offset)
     return PyLong_FromUnsignedLong((unsigned long)buf);
 }
 
-static PyObject *
-read_multiple(pysqlite_Blob *self, Py_ssize_t length, Py_ssize_t offset)
+static int
+inner_read(pysqlite_Blob *self, char *buf, Py_ssize_t length,
+           Py_ssize_t offset)
 {
     assert(length <= sqlite3_blob_bytes(self->blob));
     assert(offset < sqlite3_blob_bytes(self->blob));
 
-    PyBytesWriter *writer = PyBytesWriter_Create(length);
-    if (writer == NULL) {
-        return NULL;
-    }
-    char *raw_buffer = PyBytesWriter_GetData(writer);
-
     int rc;
     Py_BEGIN_ALLOW_THREADS
-    rc = sqlite3_blob_read(self->blob, raw_buffer, (int)length, (int)offset);
+    rc = sqlite3_blob_read(self->blob, buf, (int)length, (int)offset);
     Py_END_ALLOW_THREADS
 
     if (rc != SQLITE_OK) {
-        PyBytesWriter_Discard(writer);
         blob_seterror(self, rc);
+        return -1;
+    }
+    return 0;
+}
+
+static PyObject *
+read_multiple(pysqlite_Blob *self, Py_ssize_t length, Py_ssize_t offset)
+{
+    PyBytesWriter *writer = PyBytesWriter_Create(length);
+    if (writer == NULL) {
+        return NULL;
+    }
+
+    if (inner_read(self, PyBytesWriter_GetData(writer), length, offset) < 0) {
+        PyBytesWriter_Discard(writer);
         return NULL;
     }
     return PyBytesWriter_Finish(writer);
@@ -553,14 +562,28 @@ ass_subscript_slice(pysqlite_Blob *self, PyObject *item, 
PyObject *value)
         rc = inner_write(self, vbuf.buf, len, start);
     }
     else {
-        PyObject *blob_bytes = read_multiple(self, stop - start, start);
-        if (blob_bytes != NULL) {
-            char *blob_buf = PyBytes_AS_STRING(blob_bytes);
-            for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) {
-                blob_buf[j] = ((char *)vbuf.buf)[i];
+        /* Read the affected region, patch it and write it back.  The
+           object returned by read_multiple() cannot be used as the buffer,
+           because for a single byte it is an immortal singleton. */
+        Py_ssize_t length = stop - start;
+        if (length <= 0) {
+            /* start > stop for a negative step; see gh-150449. */
+            PyErr_SetString(PyExc_ValueError, "size must be >= 0");
+        }
+        else {
+            char *buf = PyMem_Malloc(length);
+            if (buf == NULL) {
+                PyErr_NoMemory();
+            }
+            else {
+                if (inner_read(self, buf, length, start) == 0) {
+                    for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) {
+                        buf[j] = ((char *)vbuf.buf)[i];
+                    }
+                    rc = inner_write(self, buf, length, start);
+                }
+                PyMem_Free(buf);
             }
-            rc = inner_write(self, blob_buf, stop - start, start);
-            Py_DECREF(blob_bytes);
         }
     }
     PyBuffer_Release(&vbuf);

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to