From 71c2d0710c1821e08228ca85b364fbcbda056cae Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 13 Aug 2026 16:02:46 +0300 Subject: [PATCH] gh-155702: Fix 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. Read into a plain buffer instead. The raw read is factored out of read_multiple() into inner_read(). --- Lib/test/test_sqlite3/test_dbapi.py | 12 ++++ ...-08-13-16-02-13.gh-issue-155702.Kb7Qm4.rst | 4 ++ Modules/_sqlite/blob.c | 57 +++++++++++++------ 3 files changed, 56 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-13-16-02-13.gh-issue-155702.Kb7Qm4.rst diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 5f6cb527955ca1..2cf3556f66d963 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 00000000000000..7fe505e60393b4 --- /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 d81784409e5d91..53d28a06181a9c 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);