From 95abd482bf66c052f1e08f4d9b30ff887ffb0e55 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Tue, 26 May 2026 11:07:06 +0900 Subject: [PATCH 1/4] sqlite3: fix Blob.__setitem__ value range validation Previously, assigning an out-of-range integer (negative or > 255) or an integer too large for i64 (e.g. 2**65) to a Blob index raised OverflowError instead of ValueError. Mirror CPython's ass_subscript_index logic: - Convert the value via to_i64(), treating any overflow as -1 - Validate the result is in [0, 255], raising ValueError("byte must be in range(0, 256)") otherwise - Separate deletion error messages: "item deletion" for index, "slice deletion" for slice --- Lib/test/test_sqlite3/test_dbapi.py | 1 - crates/stdlib/src/_sqlite3.rs | 22 +++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 8962fe00ed8..2174e14e7cb 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1444,7 +1444,6 @@ def test_blob_get_item_error_bigint(self): with self.assertRaisesRegex(IndexError, "cannot fit 'int'"): self.blob[_testcapi.ULLONG_MAX] - @unittest.expectedFailure # TODO: RUSTPYTHON def test_blob_set_item_error(self): with self.assertRaisesRegex(TypeError, "cannot be interpreted"): self.blob[0] = b"multiple" diff --git a/crates/stdlib/src/_sqlite3.rs b/crates/stdlib/src/_sqlite3.rs index fd7fec4feb3..26df0fdf3b7 100644 --- a/crates/stdlib/src/_sqlite3.rs +++ b/crates/stdlib/src/_sqlite3.rs @@ -44,6 +44,7 @@ mod _sqlite3 { sqlite3_value_text, sqlite3_value_type, }; use malachite_bigint::Sign; + use num_traits::ToPrimitive; use rustpython_common::{ atomic::{Ordering, PyAtomic, Radium}, hash::PyHash, @@ -2551,28 +2552,35 @@ mod _sqlite3 { value: Option, vm: &VirtualMachine, ) -> PyResult<()> { - let Some(value) = value else { - return Err(vm.new_type_error("Blob doesn't support slice deletion")); - }; self.ensure_connection_open(vm)?; let inner = self.inner(vm)?; if let Some(index) = needle.try_index_opt(vm) { // Handle single item assignment: blob[i] = b - let Some(value) = value.downcast_ref::() else { + let Some(value) = value else { + return Err(vm.new_type_error("Blob doesn't support item deletion")); + }; + let Some(int_val) = value.downcast_ref::() else { return Err(vm.new_type_error(format!( "'{}' object cannot be interpreted as an integer", value.class() ))); }; - let value = value.try_to_primitive::(vm)?; let blob_len = inner.blob.bytes(); let index = Self::wrapped_index(index?, blob_len, vm)?; - Self::expect_write(blob_len, 1, index, vm)?; - let ret = inner.blob.write_single(value, index); + // Mirror CPython ass_subscript_index: use PyLong_AsLong, treat any + // overflow (e.g. 2**65) as -1, then validate the [0, 255] range. + let val = int_val.as_bigint().to_i64().unwrap_or(-1); + if val < 0 || val > 255 { + return Err(vm.new_value_error("byte must be in range(0, 256)")); + } + let ret = inner.blob.write_single(val as u8, index); self.check(ret, vm) } else if let Some(slice) = needle.downcast_ref::() { // Handle slice assignment: blob[a:b:c] = b"..." + let Some(value) = value else { + return Err(vm.new_type_error("Blob doesn't support slice deletion")); + }; let value_buf = PyBuffer::try_from_borrowed_object(vm, &value)?; let buf = value_buf From 6f3eae877f2e18ee521e632bdbd7aca309fc4a3b Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Tue, 26 May 2026 11:27:10 +0900 Subject: [PATCH 2/4] sqlite3: fix Blob.__setitem__ negative-step slice write In the step != 1 branch of Blob.ass_subscript, the loop used i_in_temp += step as usize where step is isize. For negative steps (e.g. step = -2), (-2isize) as usize = 18446744073709551614 causing an out-of-bounds panic whenever slice_len >= 2. Fix: use SaturatedSliceIter (already used by the read path) to iterate over the correct absolute blob indices, then map each index back to a temp buffer offset via abs_idx - range_start. Also fix a Clippy lint: replace val < 0 || val > 255 with the idiomatic !(0..=255).contains(&val) Add a regression test in extra_tests/snippets/stdlib_sqlite.py that exercises blob[9:0:-2] (negative step, slice_len=5). --- crates/stdlib/src/_sqlite3.rs | 14 +++++++------- extra_tests/snippets/stdlib_sqlite.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/stdlib/src/_sqlite3.rs b/crates/stdlib/src/_sqlite3.rs index 26df0fdf3b7..5dfdf6f4f1e 100644 --- a/crates/stdlib/src/_sqlite3.rs +++ b/crates/stdlib/src/_sqlite3.rs @@ -2571,7 +2571,7 @@ mod _sqlite3 { // Mirror CPython ass_subscript_index: use PyLong_AsLong, treat any // overflow (e.g. 2**65) as -1, then validate the [0, 255] range. let val = int_val.as_bigint().to_i64().unwrap_or(-1); - if val < 0 || val > 255 { + if !(0..=255).contains(&val) { return Err(vm.new_value_error("byte must be in range(0, 256)")); } let ret = inner.blob.write_single(val as u8, index); @@ -2612,25 +2612,25 @@ mod _sqlite3 { self.check(ret, vm) } else { let span_len = range.end - range.start; + let range_start = range.start; let mut temp_buf = vec![0u8; span_len]; let ret = inner.blob.read( temp_buf.as_mut_ptr().cast(), span_len as c_int, - range.start as c_int, + range_start as c_int, ); self.check(ret, vm)?; - let mut i_in_temp: usize = 0; - for i_in_src in 0..slice_len { - temp_buf[i_in_temp] = buf[i_in_src]; - i_in_temp += step as usize; + let iter = SaturatedSliceIter::from_adjust_indices(range, step, slice_len); + for (i_in_src, abs_idx) in iter.enumerate() { + temp_buf[abs_idx - range_start] = buf[i_in_src]; } let ret = inner.blob.write( temp_buf.as_ptr().cast(), span_len as c_int, - range.start as c_int, + range_start as c_int, ); self.check(ret, vm) } diff --git a/extra_tests/snippets/stdlib_sqlite.py b/extra_tests/snippets/stdlib_sqlite.py index f2e02b48cf1..2d0a9bb7daf 100644 --- a/extra_tests/snippets/stdlib_sqlite.py +++ b/extra_tests/snippets/stdlib_sqlite.py @@ -53,3 +53,14 @@ def finalize(self): cx.create_aggregate("aggtxt", 1, AggrText) cur.execute("select aggtxt(key) from foo") assert cur.fetchone()[0] == "341011" + +# Blob extended-slice assignment with negative step +cx.execute("CREATE TABLE blobtest(b BLOB)") +data = b"this blob data string is exactly fifty bytes long!" +cx.execute("INSERT INTO blobtest(b) VALUES (?)", (data,)) +blob = cx.blobopen("blobtest", "b", 1) +blob[9:0:-2] = b"12345" # writes to indices 9, 7, 5, 3, 1 +actual = cx.execute("select b from blobtest").fetchone()[0] +expected = b"t5i4 3l2b1" + data[10:] +assert actual == expected, f"got {actual!r}, expected {expected!r}" +blob.close() From 1df29c5f065026922ee96a374a6d31d019666cd9 Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Tue, 26 May 2026 12:06:55 +0900 Subject: [PATCH 3/4] fix: guard blob negative-step snippet from CPython 3.11 bug --- extra_tests/snippets/stdlib_sqlite.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/extra_tests/snippets/stdlib_sqlite.py b/extra_tests/snippets/stdlib_sqlite.py index 2d0a9bb7daf..5e5ecad3f54 100644 --- a/extra_tests/snippets/stdlib_sqlite.py +++ b/extra_tests/snippets/stdlib_sqlite.py @@ -55,12 +55,16 @@ def finalize(self): assert cur.fetchone()[0] == "341011" # Blob extended-slice assignment with negative step -cx.execute("CREATE TABLE blobtest(b BLOB)") -data = b"this blob data string is exactly fifty bytes long!" -cx.execute("INSERT INTO blobtest(b) VALUES (?)", (data,)) -blob = cx.blobopen("blobtest", "b", 1) -blob[9:0:-2] = b"12345" # writes to indices 9, 7, 5, 3, 1 -actual = cx.execute("select b from blobtest").fetchone()[0] -expected = b"t5i4 3l2b1" + data[10:] -assert actual == expected, f"got {actual!r}, expected {expected!r}" -blob.close() +# Guard: CPython 3.11 has a SystemError bug with negative-step Blob slicing; +# this test only runs on RustPython where the fix is being validated. +import sys +if sys.implementation.name == "rustpython": + cx.execute("CREATE TABLE blobtest(b BLOB)") + data = b"this blob data string is exactly fifty bytes long!" + cx.execute("INSERT INTO blobtest(b) VALUES (?)", (data,)) + blob = cx.blobopen("blobtest", "b", 1) + blob[9:0:-2] = b"12345" # writes to indices 9, 7, 5, 3, 1 + actual = cx.execute("select b from blobtest").fetchone()[0] + expected = b"t5i4 3l2b1" + data[10:] + assert actual == expected, f"got {actual!r}, expected {expected!r}" + blob.close() From c074556a20a594341e2df7f704e9e92fc6a5fb8d Mon Sep 17 00:00:00 2001 From: Jiseok CHOI Date: Tue, 26 May 2026 12:27:26 +0900 Subject: [PATCH 4/4] style: add blank line after import sys in stdlib_sqlite snippet (ruff) --- extra_tests/snippets/stdlib_sqlite.py | 1 + 1 file changed, 1 insertion(+) diff --git a/extra_tests/snippets/stdlib_sqlite.py b/extra_tests/snippets/stdlib_sqlite.py index 5e5ecad3f54..e23c61146f3 100644 --- a/extra_tests/snippets/stdlib_sqlite.py +++ b/extra_tests/snippets/stdlib_sqlite.py @@ -58,6 +58,7 @@ def finalize(self): # Guard: CPython 3.11 has a SystemError bug with negative-step Blob slicing; # this test only runs on RustPython where the fix is being validated. import sys + if sys.implementation.name == "rustpython": cx.execute("CREATE TABLE blobtest(b BLOB)") data = b"this blob data string is exactly fifty bytes long!"