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>
This commit is contained in:
youknowdotandJeong, YunWon authored and GitHub committed 2026-10-11 12:35:15 +09:00
1 parent 8d27253fa2
commit 4dc50cc472
8 files changed
+112 -62

No files matched your search

+1 -1
View File
@@ -1 +1 @@
3.14.7
3.14.8
+1 -1
View File
@@ -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]
+25 -33
View File
@@ -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(())
}
})?
}
}
+23 -19
View File
@@ -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::<isize>(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::<isize>(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"));
+1 -4
View File
@@ -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))
+18 -4
View File
@@ -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
+38
View File
@@ -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()
+5
View File
@@ -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 = []