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]