From af0eb656e77f13b542e8956f1dec987857ff082e Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Tue, 26 May 2026 14:17:22 +0900 Subject: [PATCH 1/4] gh-150449: Fix sqlite3.Blob crash with negative-step slices Reading or writing a sqlite3.Blob with a negative-step slice such as blob[9:0:-2] caused a crash (SystemError on older versions, ValueError on current main) because the C code computed stop - start as the read length, which is negative for negative steps. Fix subscript_slice() and ass_subscript_slice() in Modules/_sqlite/blob.c to handle both positive and negative steps correctly: - For step > 1: keep reading [start, stop) and indexing forward as before. - For step < -1: read the contiguous range [start+(len-1)*step, start] and index backward with stride -step. --- Lib/test/test_sqlite3/test_dbapi.py | 21 ++++++++++++ ...-05-26-15-53-50.gh-issue-150449.GfDWxl.rst | 3 ++ Modules/_sqlite/blob.c | 33 ++++++++++++++----- 3 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-26-15-53-50.gh-issue-150449.GfDWxl.rst diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 73b40e82a96811..321d253bf52237 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1390,6 +1390,12 @@ def test_blob_get_slice_negative_index(self): def test_blob_get_slice_with_skip(self): self.assertEqual(self.blob[0:10:2], b"ti lb") + def test_blob_get_slice_with_negative_step(self): + # gh-150449: negative-step slices must not crash + self.assertEqual(self.blob[9:0:-2], self.data[9:0:-2]) + self.assertEqual(self.blob[9::-2], self.data[9::-2]) + self.assertEqual(self.blob[::-1], self.data[::-1]) + def test_blob_set_slice(self): self.blob[0:5] = b"12345" expected = b"12345" + self.data[5:] @@ -1406,6 +1412,21 @@ def test_blob_set_slice_with_skip(self): expected = b"1h2s3b4o5 " + self.data[10:] self.assertEqual(actual, expected) + def test_blob_set_slice_with_negative_step(self): + # gh-150449: negative-step slice assignment must not crash + expected = bytearray(self.data) + expected[9:0:-2] = b"12345" + self.blob[9:0:-2] = b"12345" + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, bytes(expected)) + + # Also verify a slice that includes index 0 + expected2 = bytearray(self.data) + expected2[9::-2] = b"12345" + self.blob[9::-2] = b"12345" + actual2 = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual2, bytes(expected2)) + def test_blob_mapping_invalid_index_type(self): msg = "indices must be integers" with self.assertRaisesRegex(TypeError, msg): diff --git a/Misc/NEWS.d/next/Library/2026-05-26-15-53-50.gh-issue-150449.GfDWxl.rst b/Misc/NEWS.d/next/Library/2026-05-26-15-53-50.gh-issue-150449.GfDWxl.rst new file mode 100644 index 00000000000000..d5f3227844fbbd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-26-15-53-50.gh-issue-150449.GfDWxl.rst @@ -0,0 +1,3 @@ +Fix :class:`sqlite3.Blob` raising :exc:`SystemError` (or :exc:`ValueError` +on recent versions) when reading or writing with a negative-step slice such +as ``blob[9:0:-2]``. diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index 2cc62751054278..548b70d744e6de 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -445,7 +445,14 @@ subscript_slice(pysqlite_Blob *self, PyObject *item) return read_multiple(self, len, start); } - PyObject *blob = read_multiple(self, stop - start, start); + // Compute the contiguous blob region covering all slice elements, then + // copy each element using the standard size_t-cursor pattern that handles + // both positive and negative steps via unsigned arithmetic. + Py_ssize_t last = start + (len - 1) * step; + Py_ssize_t read_offset = Py_MIN(start, last); + Py_ssize_t read_length = Py_ABS(start - last) + 1; + + PyObject *blob = read_multiple(self, read_length, read_offset); if (blob == NULL) { return NULL; } @@ -456,10 +463,12 @@ subscript_slice(pysqlite_Blob *self, PyObject *item) return NULL; } char *res_buf = PyBytesWriter_GetData(writer); - char *blob_buf = PyBytes_AS_STRING(blob); - for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) { - res_buf[i] = blob_buf[j]; + + size_t cur; + Py_ssize_t i; + for (cur = (size_t)start, i = 0; i < len; cur += (size_t)step, i++) { + res_buf[i] = blob_buf[(Py_ssize_t)cur - read_offset]; } Py_DECREF(blob); return PyBytesWriter_Finish(writer); @@ -549,13 +558,21 @@ 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); + // Compute the contiguous blob region covering all slice elements, then + // update each element using the standard size_t-cursor pattern that + // handles both positive and negative steps via unsigned arithmetic. + Py_ssize_t last = start + (len - 1) * step; + Py_ssize_t read_offset = Py_MIN(start, last); + Py_ssize_t read_length = Py_ABS(start - last) + 1; + PyObject *blob_bytes = read_multiple(self, read_length, read_offset); 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]; + size_t cur; + Py_ssize_t i; + for (cur = (size_t)start, i = 0; i < len; cur += (size_t)step, i++) { + blob_buf[(Py_ssize_t)cur - read_offset] = ((char *)vbuf.buf)[i]; } - rc = inner_write(self, blob_buf, stop - start, start); + rc = inner_write(self, blob_buf, read_length, read_offset); Py_DECREF(blob_bytes); } } From 85312b4ed5ece748468f79875bd1dc926f6c62e1 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Tue, 26 May 2026 17:42:43 +0900 Subject: [PATCH 2/4] gh-150449: validate slice assignment size even for empty slices When a negative-step slice has start <= stop (e.g. blob[3:8:-1]), the slice is empty (slicelength == 0). Previously ass_subscript_slice() returned 0 immediately without acquiring the value buffer, so assigning a non-empty bytes object to such a slice silently succeeded instead of raising IndexError. Move PyObject_GetBuffer() and the size check before the len == 0 early return so that the element-count mismatch is always detected. --- Modules/_sqlite/blob.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index 548b70d744e6de..7d906d18cfc1af 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -540,24 +540,31 @@ ass_subscript_slice(pysqlite_Blob *self, PyObject *item, PyObject *value) return -1; } - if (len == 0) { - return 0; - } - Py_buffer vbuf; if (PyObject_GetBuffer(value, &vbuf, PyBUF_SIMPLE) < 0) { return -1; } - int rc = -1; + // For extended slices the right-hand side must have the exact same + // element count as the slice, even when that count is zero. if (vbuf.len != len) { PyErr_SetString(PyExc_IndexError, "Blob slice assignment is wrong size"); + PyBuffer_Release(&vbuf); + return -1; + } + + if (len == 0) { + PyBuffer_Release(&vbuf); + return 0; } - else if (step == 1) { + + int rc; + if (step == 1) { rc = inner_write(self, vbuf.buf, len, start); } else { + rc = -1; // Compute the contiguous blob region covering all slice elements, then // update each element using the standard size_t-cursor pattern that // handles both positive and negative steps via unsigned arithmetic. From c0c20185e8cff47684ce18597e50b9c759896a28 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Tue, 26 May 2026 17:42:53 +0900 Subject: [PATCH 3/4] gh-150449: add tests for negative-step slices with start <= stop Exercise the case where step is negative but start <= stop, which produces an empty slice. Verify that: - blob[3:8:-1] returns b"" (no crash) - blob[3:8:-1] = b"" is a no-op - blob[3:8:-1] = b"abc" raises IndexError (wrong size) Also add blob[5:5:-1] == b"" to cover the start == stop edge case. --- Lib/test/test_sqlite3/test_dbapi.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 321d253bf52237..fd996d5824dca9 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1395,6 +1395,10 @@ def test_blob_get_slice_with_negative_step(self): self.assertEqual(self.blob[9:0:-2], self.data[9:0:-2]) self.assertEqual(self.blob[9::-2], self.data[9::-2]) self.assertEqual(self.blob[::-1], self.data[::-1]) + # When start <= stop with a negative step the slice is empty; this + # must return b"" rather than crashing or raising an exception. + self.assertEqual(self.blob[3:8:-1], self.data[3:8:-1]) # b"" + self.assertEqual(self.blob[5:5:-1], self.data[5:5:-1]) # b"" def test_blob_set_slice(self): self.blob[0:5] = b"12345" @@ -1427,6 +1431,15 @@ def test_blob_set_slice_with_negative_step(self): actual2 = self.cx.execute("select b from test").fetchone()[0] self.assertEqual(actual2, bytes(expected2)) + # When start <= stop with a negative step the slice is empty; + # assigning b"" to it must be a no-op (blob contents unchanged). + state_before = bytes(self.blob[:]) + self.blob[3:8:-1] = b"" + self.assertEqual(bytes(self.blob[:]), state_before) + # Assigning a non-empty sequence to an empty slice must raise. + with self.assertRaisesRegex(IndexError, "wrong size"): + self.blob[3:8:-1] = b"abc" + def test_blob_mapping_invalid_index_type(self): msg = "indices must be integers" with self.assertRaisesRegex(TypeError, msg): From 83d3a3c295cd60a8847c45687d1bd2612ca12eed Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Tue, 26 May 2026 17:56:31 +0900 Subject: [PATCH 4/4] Fix PEP 7 line-length violations in blob.c (ass_subscript_slice) --- Modules/_sqlite/blob.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index 7d906d18cfc1af..3f7ef23a529ce5 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -576,8 +576,10 @@ ass_subscript_slice(pysqlite_Blob *self, PyObject *item, PyObject *value) char *blob_buf = PyBytes_AS_STRING(blob_bytes); size_t cur; Py_ssize_t i; - for (cur = (size_t)start, i = 0; i < len; cur += (size_t)step, i++) { - blob_buf[(Py_ssize_t)cur - read_offset] = ((char *)vbuf.buf)[i]; + for (cur = (size_t)start, i = 0; i < len; + cur += (size_t)step, i++) { + blob_buf[(Py_ssize_t)cur - read_offset] = + ((char *)vbuf.buf)[i]; } rc = inner_write(self, blob_buf, read_length, read_offset); Py_DECREF(blob_bytes);