From 4dc50cc4729e029ce374a5375dde20adb369ff69 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Sat, 10 Oct 2026 20:35:15 -0700 Subject: [PATCH] Validate mmap assignments against current storage (#9071) * Preserve ParamSpec bounds as in Python 3.14.8 Extract the backported bound-validation removal and align the CPython reference pin to 3.14.8 so both snippet interpreters exercise the same contract. Assisted-by: OpenAI Codex:model identifier unavailable * Align TextIOWrapper chunk-size deletion with Python 3.14.8 The 3.14.8 reference pin exposes the Argument Clinic deletion guard moved before initialization and attachment checks by CPython 90a1f02b7da0c356d10a23f1faee7dc063f887ed. Preserve assignment checks and align the exact deletion error, including subclass type names. Assisted-by: OpenAI Codex:model identifier unavailable * Validate mmap assignments against current storage Use the existing mapping guard for bounds and writes after argument conversion, including empty assignments. Match the Python 3.14.8 ordering while preserving export guards and ownership APIs. Assisted-by: OpenAI Codex:model identifier unavailable * Align mmap boundary regression with Python 3.14.8 Include the matching upstream GH-158027 test-data correction: use an integer RHS so the unchanged IndexError assertion checks index bounds after conversion. Preserve the runtime assignment and storage guards. Assisted-by: OpenAI Codex:model identifier unavailable --------- Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com> --- .python-version | 2 +- Lib/test/test_mmap.py | 2 +- crates/stdlib/src/mmap.rs | 58 ++++++++++++--------------- crates/vm/src/stdlib/_io.rs | 42 ++++++++++--------- crates/vm/src/stdlib/typevar.rs | 5 +-- extra_tests/snippets/stdlib_io.py | 22 ++++++++-- extra_tests/snippets/stdlib_mmap.py | 38 ++++++++++++++++++ extra_tests/snippets/stdlib_typing.py | 5 +++ 8 files changed, 112 insertions(+), 62 deletions(-) diff --git a/.python-version b/.python-version index a128d5c0d..9b8fe2fbb 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.14.7 +3.14.8 diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py index bd8744a0c..309c0d8c9 100644 --- a/Lib/test/test_mmap.py +++ b/Lib/test/test_mmap.py @@ -73,7 +73,7 @@ class MmapTests(unittest.TestCase): # Shouldn't crash on boundary (Issue #5292) self.assertRaises(IndexError, m.__getitem__, len(m)) - self.assertRaises(IndexError, m.__setitem__, len(m), b'\0') + self.assertRaises(IndexError, m.__setitem__, len(m), 0) # Modify the file's content m[0] = b'3'[0] diff --git a/crates/stdlib/src/mmap.rs b/crates/stdlib/src/mmap.rs index a242f8f1d..371a800c4 100644 --- a/crates/stdlib/src/mmap.rs +++ b/crates/stdlib/src/mmap.rs @@ -1455,17 +1455,15 @@ mod mmap { value: &PyObject, vm: &VirtualMachine, ) -> PyResult<()> { - let i: usize = i - .wrapped_at(self.__len__()) - .ok_or_else(|| vm.new_index_error("mmap index out of range"))?; - let b = value_from_object(vm, value)?; self.try_writable(vm, |mmap| { + let i = i + .wrapped_at(mmap.len()) + .ok_or_else(|| vm.new_index_error("mmap index out of range"))?; mmap[i] = b; - })?; - - Ok(()) + Ok(()) + })? } fn setitem_by_slice( @@ -1474,43 +1472,37 @@ mod mmap { value: &PyObject, vm: &VirtualMachine, ) -> PyResult<()> { - let (range, step, slice_len) = slice.adjust_indices(self.__len__()); - let bytes = bytes_from_object(vm, value)?; - if bytes.len() != slice_len { - return Err(vm.new_index_error("mmap slice assignment is wrong size")); - } + self.try_writable(vm, |mmap| { + let (range, step, slice_len) = slice.adjust_indices(mmap.len()); - if slice_len == 0 { - // do nothing - Ok(()) - } else if step == 1 { - self.try_writable(vm, |mmap| { + if bytes.len() != slice_len { + return Err(vm.new_index_error("mmap slice assignment is wrong size")); + } + + if slice_len == 0 { + // do nothing + } else if step == 1 { (&mut mmap[range]) .write(&bytes) .map_err(|err| err.to_pyexception(vm))?; - Ok(()) - })? - } else { - let mut bi = 0; // bytes index - if step.is_negative() { - for i in range.rev().step_by(step.unsigned_abs()) { - self.try_writable(vm, |mmap| { - mmap[i] = bytes[bi]; - })?; - bi += 1; - } } else { - for i in range.step_by(step.unsigned_abs()) { - self.try_writable(vm, |mmap| { + let mut bi = 0; // bytes index + if step.is_negative() { + for i in range.rev().step_by(step.unsigned_abs()) { mmap[i] = bytes[bi]; - })?; - bi += 1; + bi += 1; + } + } else { + for i in range.step_by(step.unsigned_abs()) { + mmap[i] = bytes[bi]; + bi += 1; + } } } Ok(()) - } + })? } } diff --git a/crates/vm/src/stdlib/_io.rs b/crates/vm/src/stdlib/_io.rs index d4cd66e14..14ca013a8 100644 --- a/crates/vm/src/stdlib/_io.rs +++ b/crates/vm/src/stdlib/_io.rs @@ -3568,6 +3568,17 @@ mod _io { #[pygetset(setter, name = "_CHUNK_SIZE")] fn set_chunksize(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> { + let PySetterValue::Assign(object_value) = value else { + let type_name = self.class().slot_name(); + let mut end = type_name.len().min(100); + while !type_name.is_char_boundary(end) { + end -= 1; + } + return Err(vm.new_attribute_error(format!( + "attribute '_CHUNK_SIZE' of '{}' objects cannot be deleted", + &type_name[..end] + ))); + }; { let textio = self.lock(vm)?; if self.buffer.deref().is_none() { @@ -3576,27 +3587,20 @@ mod _io { drop(textio); } - let chunk_size: isize = match value { - PySetterValue::Assign(object_value) => { - let integer = object_value.try_index(vm)?; + let integer = object_value.try_index(vm)?; - integer.try_to_primitive::(vm).map_err(|_| { - let class = object_value.class(); - let type_name = class.name(); - let mut end = type_name.len().min(200); - while !type_name.is_char_boundary(end) { - end -= 1; - } - vm.new_value_error(format!( - "cannot fit '{}' into an index-sized integer", - &type_name[..end] - )) - })? + let chunk_size = integer.try_to_primitive::(vm).map_err(|_| { + let class = object_value.class(); + let type_name = class.name(); + let mut end = type_name.len().min(200); + while !type_name.is_char_boundary(end) { + end -= 1; } - PySetterValue::Delete => { - return Err(vm.new_attribute_error("cannot delete attribute")); - } - }; + vm.new_value_error(format!( + "cannot fit '{}' into an index-sized integer", + &type_name[..end] + )) + })?; if chunk_size <= 0 { return Err(vm.new_value_error("a strictly positive integer is required")); diff --git a/crates/vm/src/stdlib/typevar.rs b/crates/vm/src/stdlib/typevar.rs index 19ad35625..3cc4b9b4b 100644 --- a/crates/vm/src/stdlib/typevar.rs +++ b/crates/vm/src/stdlib/typevar.rs @@ -583,10 +583,7 @@ pub(crate) mod typevar { return Err(vm.new_type_error("ParamSpec() takes at most 1 positional argument")); }; - let bound = kwargs - .swap_remove("bound") - .map(|b| type_check(b, "Bound must be a type.", vm)) - .transpose()?; + let bound = kwargs.swap_remove("bound"); let covariant = kwargs .swap_remove("covariant") .map(|v| v.try_to_bool(vm)) diff --git a/extra_tests/snippets/stdlib_io.py b/extra_tests/snippets/stdlib_io.py index 8346ddbb6..07b127999 100644 --- a/extra_tests/snippets/stdlib_io.py +++ b/extra_tests/snippets/stdlib_io.py @@ -166,10 +166,24 @@ expect_value_error( "underlying buffer has been detached", lambda: setattr(detached_textio, "_CHUNK_SIZE", 16), ) -expect_value_error( - "underlying buffer has been detached", - lambda: delattr(detached_textio, "_CHUNK_SIZE"), -) +for wrapper in (textio, uninitialized_textio, detached_textio): + with assert_raises(AttributeError) as error: + del wrapper._CHUNK_SIZE + assert str(error.exception) == ( + "attribute '_CHUNK_SIZE' of '_io.TextIOWrapper' objects cannot be deleted" + ) + + +for wrapper_name in ("ChunkWrapper", "€" * 34): + wrapper_type = type(wrapper_name, (TextIOWrapper,), {}) + wrapper = wrapper_type(BytesIO()) + with assert_raises(AttributeError) as error: + del wrapper._CHUNK_SIZE + displayed_name = wrapper_name.encode("utf-8")[:100].decode("utf-8", "ignore") + assert str(error.exception) == ( + f"attribute '_CHUNK_SIZE' of '{displayed_name}' objects cannot be deleted" + ) + wrapper.close() long_type_name = "X" * 250 diff --git a/extra_tests/snippets/stdlib_mmap.py b/extra_tests/snippets/stdlib_mmap.py index 2c4efdf58..1dc89e29d 100644 --- a/extra_tests/snippets/stdlib_mmap.py +++ b/extra_tests/snippets/stdlib_mmap.py @@ -77,3 +77,41 @@ if sys.platform == "win32": with assert_raises(ValueError): mapped.resize(4) assert mapped.closed + + +# Assignment validates current storage after argument conversion, even if empty. +with mmap.mmap(-1, 4) as mapped: + with assert_raises(ValueError): + mapped[4] = 256 + mapped[0:0] = b"" +with assert_raises(ValueError): + mapped[0:0] = b"" + +with mmap.mmap(-1, 4, access=mmap.ACCESS_READ) as mapped: + with assert_raises(TypeError): + mapped[0:0] = b"" + + +class CloseOnIndex: + def __init__(self, mapped): + self.mapped = mapped + + def __index__(self): + self.mapped.close() + return 0 + + +for close_from in ("key", "value", "slice"): + mapped = mmap.mmap(-1, 4) + closer = CloseOnIndex(mapped) + try: + with assert_raises(ValueError): + if close_from == "key": + mapped[closer] = 0 + elif close_from == "value": + mapped[0] = closer + else: + mapped[closer:0] = b"" + assert mapped.closed + finally: + mapped.close() diff --git a/extra_tests/snippets/stdlib_typing.py b/extra_tests/snippets/stdlib_typing.py index 8ba64844c..4bebfae45 100644 --- a/extra_tests/snippets/stdlib_typing.py +++ b/extra_tests/snippets/stdlib_typing.py @@ -65,6 +65,11 @@ except RecursionError: pass +# Python 3.14.8 retains ParamSpec bounds without converting or validating them. +assert ParamSpec("Unbounded").__bound__ is None +for bound in (None, "int", "not valid Python!", (int, str), object()): + assert ParamSpec("BoundSpec", bound=bound).__bound__ is bound + # Eager aliases without a caller module never inspect their aliased value. alias_module_lookups = []