From af0eb656e77f13b542e8956f1dec987857ff082e Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Tue, 26 May 2026 14:17:22 +0900 Subject: [PATCH 01/14] 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 73b40e82a96811f..321d253bf52237e 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 000000000000000..d5f3227844fbbdf --- /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 2cc62751054278f..548b70d744e6de0 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 02/14] 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 548b70d744e6de0..7d906d18cfc1af7 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 03/14] 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 321d253bf52237e..fd996d5824dca9a 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 04/14] 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 7d906d18cfc1af7..3f7ef23a529ce5a 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); From f7c9f53f26e9d96b974c353114696ddfc5a34606 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Sat, 30 May 2026 13:42:58 +0900 Subject: [PATCH 05/14] gh-150449: Address review: docs, tests, and rename write_offset Classify negative-step slice support as a new feature per reviewer feedback (serhiy-storchaka): - Add versionchanged:: 3.16 directive to sqlite3.Blob documentation noting that negative-step slices are now supported. - Add What's New entry under Doc/whatsnew/3.16.rst. - Update NEWS entry wording to describe the change as a feature. - Add tests for empty-slice assignment edge cases (blob[5:5] = None and blob[5:5] = b'123') to verify TypeError and IndexError are raised correctly. - Rename read_offset to write_offset in ass_subscript_slice() since that variable is used as the offset for inner_write(), not a read. --- Doc/library/sqlite3.rst | 4 ++++ Doc/whatsnew/3.16.rst | 8 ++++++++ Lib/test/test_sqlite3/test_dbapi.py | 12 ++++++++++++ .../2026-05-26-15-53-50.gh-issue-150449.GfDWxl.rst | 6 +++--- Modules/_sqlite/blob.c | 8 ++++---- 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 484260e63dd5f2f..5a16eb7b36064d5 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -1751,6 +1751,10 @@ Blob objects .. versionadded:: 3.11 + .. versionchanged:: 3.16 + :class:`Blob` now supports negative-step slices + (e.g. ``blob[9:0:-2]``) for both reading and writing. + A :class:`Blob` instance is a :term:`file-like object` that can read and write data in an SQLite :abbr:`BLOB (Binary Large OBject)`. Call :func:`len(blob) ` to get the size (number of bytes) of the blob. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 8dac804b9519dad..8eda2f28cd333d5 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -101,6 +101,14 @@ os process via a pidfd. Available on Linux 5.6+. (Contributed by Maurycy Pawłowski-Wieroński in :gh:`149464`.) +sqlite3 +------- + +* :class:`sqlite3.Blob` now supports negative-step slices for reading and + writing (e.g. ``blob[9:0:-2]``). Previously, such slices would crash or + raise :exc:`SystemError`. + (Contributed by Jiseok CHOI in :gh:`150449`.) + .. Add improved modules above alphabetically, not here at the end. Optimizations diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index fd996d5824dca9a..826646e3269f30b 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1410,6 +1410,18 @@ def test_blob_set_empty_slice(self): self.blob[0:0] = b"" self.assertEqual(self.blob[:], self.data) + def test_blob_set_empty_slice_wrong_type(self): + # Assigning a non-buffer object to an empty slice must raise TypeError + # even when the slice length is zero (gh-150449). + with self.assertRaises(TypeError): + self.blob[5:5] = None + + def test_blob_set_empty_slice_wrong_size(self): + # Assigning a non-empty bytes object to an empty slice must raise + # IndexError because the sizes do not match (gh-150449). + with self.assertRaisesRegex(IndexError, "wrong size"): + self.blob[5:5] = b"123" + def test_blob_set_slice_with_skip(self): self.blob[0:10:2] = b"12345" actual = self.cx.execute("select b from test").fetchone()[0] 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 index d5f3227844fbbdf..ae60498fb978294 100644 --- 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 @@ -1,3 +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]``. +:class:`sqlite3.Blob` now supports negative-step slices for reading and +writing (e.g. ``blob[9:0:-2]``). Previously, such slices would crash with +:exc:`SystemError` or raise :exc:`ValueError`. diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index 3f7ef23a529ce5a..50c478a18a2c9db 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -569,19 +569,19 @@ ass_subscript_slice(pysqlite_Blob *self, PyObject *item, PyObject *value) // 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 write_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); + PyObject *blob_bytes = read_multiple(self, read_length, write_offset); if (blob_bytes != NULL) { 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] = + blob_buf[(Py_ssize_t)cur - write_offset] = ((char *)vbuf.buf)[i]; } - rc = inner_write(self, blob_buf, read_length, read_offset); + rc = inner_write(self, blob_buf, read_length, write_offset); Py_DECREF(blob_bytes); } } From 5087b422665452ca1c70ae2aaf66766d8d1aaaed Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Sat, 30 May 2026 13:44:35 +0900 Subject: [PATCH 06/14] gh-150449: Fix versionchanged version: use 'next' not '3.16' Per devguide markup guidelines, the version argument for versionchanged should be 'next' during development; the release manager replaces it with the actual version number at release time. --- Doc/library/sqlite3.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 5a16eb7b36064d5..f3c09a193239701 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -1751,7 +1751,7 @@ Blob objects .. versionadded:: 3.11 - .. versionchanged:: 3.16 + .. versionchanged:: next :class:`Blob` now supports negative-step slices (e.g. ``blob[9:0:-2]``) for both reading and writing. From d774ab7d66e6a1ccfb846346c1c3859ca2949301 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Sat, 30 May 2026 14:43:30 +0900 Subject: [PATCH 07/14] =?UTF-8?q?gh-150449:=20Fix=20wording=20in=20NEWS=20?= =?UTF-8?q?and=20whatsnew:=20'crash'=20=E2=86=92=20'raise=20SystemError=20?= =?UTF-8?q?or=20ValueError'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Doc/whatsnew/3.16.rst | 4 ++-- .../Library/2026-05-26-15-53-50.gh-issue-150449.GfDWxl.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 8eda2f28cd333d5..9078b7998de36fc 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -105,8 +105,8 @@ sqlite3 ------- * :class:`sqlite3.Blob` now supports negative-step slices for reading and - writing (e.g. ``blob[9:0:-2]``). Previously, such slices would crash or - raise :exc:`SystemError`. + writing (e.g. ``blob[9:0:-2]``). Previously, such slices would raise + :exc:`SystemError` or :exc:`ValueError`. (Contributed by Jiseok CHOI in :gh:`150449`.) .. Add improved modules above alphabetically, not here at the end. 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 index ae60498fb978294..f849fe791356a1b 100644 --- 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 @@ -1,3 +1,3 @@ :class:`sqlite3.Blob` now supports negative-step slices for reading and -writing (e.g. ``blob[9:0:-2]``). Previously, such slices would crash with -:exc:`SystemError` or raise :exc:`ValueError`. +writing (e.g. ``blob[9:0:-2]``). Previously, such slices would raise +:exc:`SystemError` or :exc:`ValueError`. From 5bf02de6365b15fc222773228b62d63d342029d1 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Sat, 30 May 2026 14:52:51 +0900 Subject: [PATCH 08/14] gh-150449: Remove redundant issue references from test comments --- Lib/test/test_sqlite3/test_dbapi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 826646e3269f30b..966b77046640d09 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1412,13 +1412,13 @@ def test_blob_set_empty_slice(self): def test_blob_set_empty_slice_wrong_type(self): # Assigning a non-buffer object to an empty slice must raise TypeError - # even when the slice length is zero (gh-150449). + # even when the slice length is zero. with self.assertRaises(TypeError): self.blob[5:5] = None def test_blob_set_empty_slice_wrong_size(self): # Assigning a non-empty bytes object to an empty slice must raise - # IndexError because the sizes do not match (gh-150449). + # IndexError because the sizes do not match. with self.assertRaisesRegex(IndexError, "wrong size"): self.blob[5:5] = b"123" From d4b4b43fb625a57437c8315aff0071acac6ab033 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Sat, 30 May 2026 14:58:43 +0900 Subject: [PATCH 09/14] gh-150449: Fix missing blank line after xml section in whatsnew --- Doc/whatsnew/3.16.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 35696eb9ee736f3..14b0f1ae69e445e 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -131,6 +131,7 @@ xml multi-byte encodings such us "ISO-2022-JP" or "raw-unicode-escape" instead of failing later, when encounter non-ASCII data. (Contributed by Serhiy Storchaka in :gh:`62259`.) + .. Add improved modules above alphabetically, not here at the end. Optimizations From 0936d6b02038a0712412d743d7a40fc4c83ecdb4 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Sat, 30 May 2026 15:20:32 +0900 Subject: [PATCH 10/14] gh-150449: Rename read_length to write_length in ass_subscript_slice --- Modules/_sqlite/blob.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index 50c478a18a2c9db..6bc4305fb1005d6 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -570,8 +570,8 @@ ass_subscript_slice(pysqlite_Blob *self, PyObject *item, PyObject *value) // handles both positive and negative steps via unsigned arithmetic. Py_ssize_t last = start + (len - 1) * step; Py_ssize_t write_offset = Py_MIN(start, last); - Py_ssize_t read_length = Py_ABS(start - last) + 1; - PyObject *blob_bytes = read_multiple(self, read_length, write_offset); + Py_ssize_t write_length = Py_ABS(start - last) + 1; + PyObject *blob_bytes = read_multiple(self, write_length, write_offset); if (blob_bytes != NULL) { char *blob_buf = PyBytes_AS_STRING(blob_bytes); size_t cur; @@ -581,7 +581,7 @@ ass_subscript_slice(pysqlite_Blob *self, PyObject *item, PyObject *value) blob_buf[(Py_ssize_t)cur - write_offset] = ((char *)vbuf.buf)[i]; } - rc = inner_write(self, blob_buf, read_length, write_offset); + rc = inner_write(self, blob_buf, write_length, write_offset); Py_DECREF(blob_bytes); } } From 5f2f91625c3da69dbd67bd8733b61f2800daa5e1 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Thu, 4 Jun 2026 21:40:06 +0900 Subject: [PATCH 11/14] gh-150449: Move empty-slice validation fix to dedicated PR for gh-150913 The empty-slice assignment validation change (moving the len==0 early return to after buffer/type/size checks) has been split out to a separate bug-fix PR targeting issue #150913. This branch retains only the negative-step slice feature. --- Lib/test/test_sqlite3/test_dbapi.py | 15 --------------- Modules/_sqlite/blob.c | 19 ++++++------------- 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 966b77046640d09..16d6f910f013cc4 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1410,18 +1410,6 @@ def test_blob_set_empty_slice(self): self.blob[0:0] = b"" self.assertEqual(self.blob[:], self.data) - def test_blob_set_empty_slice_wrong_type(self): - # Assigning a non-buffer object to an empty slice must raise TypeError - # even when the slice length is zero. - with self.assertRaises(TypeError): - self.blob[5:5] = None - - def test_blob_set_empty_slice_wrong_size(self): - # Assigning a non-empty bytes object to an empty slice must raise - # IndexError because the sizes do not match. - with self.assertRaisesRegex(IndexError, "wrong size"): - self.blob[5:5] = b"123" - def test_blob_set_slice_with_skip(self): self.blob[0:10:2] = b"12345" actual = self.cx.execute("select b from test").fetchone()[0] @@ -1448,9 +1436,6 @@ def test_blob_set_slice_with_negative_step(self): 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" diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index 6bc4305fb1005d6..bafb52b8da0259c 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -540,31 +540,24 @@ 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; } - // For extended slices the right-hand side must have the exact same - // element count as the slice, even when that count is zero. + int rc = -1; 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; } - - int rc; - if (step == 1) { + else 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 be1aa18c77e38d820e9d69bea4cb257677e3e976 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Fri, 5 Jun 2026 16:58:00 +0900 Subject: [PATCH 12/14] gh-150449: Add tests for extreme step values in sqlite3.Blob slice operations Add test cases for sys.maxsize and -sys.maxsize-1 step values in both read (test_blob_get_slice_with_negative_step) and write (test_blob_set_slice_with_negative_step) to cover the unsigned wrap-around arithmetic path that avoids signed integer overflow. --- Lib/test/test_sqlite3/test_dbapi.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 19125a27afd4c22..38e77124aa96f2a 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1399,6 +1399,9 @@ def test_blob_get_slice_with_negative_step(self): # 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"" + # Extreme step values: cur += (size_t)step must not overflow. + self.assertEqual(self.blob[5::sys.maxsize], self.data[5::sys.maxsize]) + self.assertEqual(self.blob[::-sys.maxsize - 1], self.data[::-sys.maxsize - 1]) def test_blob_set_slice(self): self.blob[0:5] = b"12345" @@ -1449,6 +1452,10 @@ def test_blob_set_slice_with_negative_step(self): self.blob[3:8:-1] = b"" self.assertEqual(bytes(self.blob[:]), state_before) + # Extreme step values: cur += (size_t)step must not overflow. + self.blob[5::sys.maxsize] = self.data[5::sys.maxsize] + self.blob[::-sys.maxsize - 1] = self.data[::-sys.maxsize - 1] + def test_blob_mapping_invalid_index_type(self): msg = "indices must be integers" with self.assertRaisesRegex(TypeError, msg): From f76ebf77d037d7d906c7161bda05d88f774ae501 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Fri, 5 Jun 2026 17:24:06 +0900 Subject: [PATCH 13/14] gh-150449: Assert correctness of extreme-step slice assignment in sqlite3.Blob Extend the existing extreme-step test cases (sys.maxsize / -sys.maxsize-1) in test_blob_set_slice_with_negative_step to verify the blob contents were actually written correctly, not just that no crash occurred. --- Lib/test/test_sqlite3/test_dbapi.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 38e77124aa96f2a..cdec344cab0704a 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1453,8 +1453,19 @@ def test_blob_set_slice_with_negative_step(self): self.assertEqual(bytes(self.blob[:]), state_before) # Extreme step values: cur += (size_t)step must not overflow. - self.blob[5::sys.maxsize] = self.data[5::sys.maxsize] - self.blob[::-sys.maxsize - 1] = self.data[::-sys.maxsize - 1] + # Use the current blob state (already modified above) as baseline. + current = bytes(self.blob[:]) + expected3 = bytearray(current) + expected3[5::sys.maxsize] = current[5::sys.maxsize] + self.blob[5::sys.maxsize] = current[5::sys.maxsize] + actual3 = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual3, bytes(expected3)) + + expected4 = bytearray(actual3) + expected4[::-sys.maxsize - 1] = bytes(actual3)[::-sys.maxsize - 1] + self.blob[::-sys.maxsize - 1] = bytes(actual3)[::-sys.maxsize - 1] + actual4 = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual4, bytes(expected4)) def test_blob_mapping_invalid_index_type(self): msg = "indices must be integers" From 52084f9e07249bf99e907dc971c9c4503f5b6ef8 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Fri, 5 Jun 2026 17:52:28 +0900 Subject: [PATCH 14/14] gh-150449: Isolate extreme-step Blob assignment tests --- Lib/test/test_sqlite3/test_dbapi.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index cdec344cab0704a..a58ea12a19c7853 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1452,20 +1452,21 @@ def test_blob_set_slice_with_negative_step(self): self.blob[3:8:-1] = b"" self.assertEqual(bytes(self.blob[:]), state_before) - # Extreme step values: cur += (size_t)step must not overflow. - # Use the current blob state (already modified above) as baseline. - current = bytes(self.blob[:]) - expected3 = bytearray(current) - expected3[5::sys.maxsize] = current[5::sys.maxsize] - self.blob[5::sys.maxsize] = current[5::sys.maxsize] - actual3 = self.cx.execute("select b from test").fetchone()[0] - self.assertEqual(actual3, bytes(expected3)) - - expected4 = bytearray(actual3) - expected4[::-sys.maxsize - 1] = bytes(actual3)[::-sys.maxsize - 1] - self.blob[::-sys.maxsize - 1] = bytes(actual3)[::-sys.maxsize - 1] - actual4 = self.cx.execute("select b from test").fetchone()[0] - self.assertEqual(actual4, bytes(expected4)) + def test_blob_set_slice_with_extreme_positive_step(self): + expected = bytearray(self.data) + expected[5::sys.maxsize] = b"\xab" + self.blob[5::sys.maxsize] = b"\xab" + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, bytes(expected)) + self.assertEqual(actual[5], 0xab) + + def test_blob_set_slice_with_extreme_negative_step(self): + expected = bytearray(self.data) + expected[::-sys.maxsize - 1] = b"\xcd" + self.blob[::-sys.maxsize - 1] = b"\xcd" + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, bytes(expected)) + self.assertEqual(actual[-1], 0xcd) def test_blob_mapping_invalid_index_type(self): msg = "indices must be integers"