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

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.