Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 57 additions & 13 deletions crates/capi/src/tupleobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use crate::object::define_py_check;
use crate::pystate::with_vm;
use crate::util::FfiPtrExt;
use core::ffi::c_int;
use core::ptr;
use core::slice;
use rustpython_vm::PyResult;
use rustpython_vm::AsObject;
use rustpython_vm::builtins::PyTuple;
use rustpython_vm::sliceable::SliceableSequenceOp;

Expand All @@ -15,12 +16,13 @@ define_py_check!(exact fn PyTuple_CheckExact, types.tuple_type);
pub extern "C" fn PyTuple_New(len: isize) -> *mut PyObject {
with_vm(|vm| {
if len == 0 {
return Ok(vm.ctx.empty_tuple.to_owned());
Ok(vm.ctx.empty_tuple.to_owned())
} else {
let len: usize = len
.try_into()
.map_err(|_| vm.new_system_error("Negative size passed to PyTuple_New"))?;
Ok(vm.new_tuple(vec![vm.ctx.none(); len]))
Comment thread
bschoenmaeckers marked this conversation as resolved.
Comment thread
bschoenmaeckers marked this conversation as resolved.
Comment thread
bschoenmaeckers marked this conversation as resolved.
}

Err(vm.new_not_implemented_error(
"PyTuple_New for non zero sized tuples is not supported, use PyTuple_FromArray instead",
))
})
}

Expand Down Expand Up @@ -61,14 +63,30 @@ pub unsafe extern "C" fn PyTuple_Pack(len: isize, mut args: ...) -> *mut PyObjec
}

#[unsafe(no_mangle)]
pub extern "C" fn PyTuple_SetItem(
_tuple: *mut PyObject,
_pos: isize,
_value: *mut PyObject,
pub unsafe extern "C" fn PyTuple_SetItem(
tuple: *mut PyObject,
pos: isize,
value: *mut PyObject,
) -> c_int {
with_vm::<PyResult<()>, _>(
|vm| Err(vm.new_not_implemented_error("Tuple objects are immutable")),
)
with_vm(|vm| {
let tuple = unsafe { tuple.assume_borrowed_and_cast::<PyTuple>(vm)? };
let value = unsafe { value.assume_owned() };
let index: usize = pos
.try_into()
.map_err(|_| vm.new_index_error("negative position"))?;

if tuple.len() <= index {
Err(vm.new_index_error("tuple index out of range"))
} else if tuple.as_object().strong_count() > 1 {
Err(vm.new_system_error("Tuple objects are immutable when the reference count > 1"))
Comment thread
bschoenmaeckers marked this conversation as resolved.
} else {
// SAFETY: We have exclusive access to the tuple payload (strong_count == 1),
// so mutating the selected element in place is valid.
let data_ptr = unsafe { tuple.as_ptr().add(index).cast_mut() };
unsafe { ptr::replace(data_ptr, value) };
Comment on lines +85 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route tuple writes through the UnsafeCell

On every successful insertion, tuple.as_ptr() is the slice as_ptr reached through PyTuple's Deref, so data_ptr is derived from an immutable &[PyObjectRef]; converting it with cast_mut() does not authorize the write in ptr::replace. TupleElements::set_initializing in crates/vm/src/builtins/tuple.rs deliberately writes via UnsafeCell::get, while as_slice assumes published elements are never replaced, so the intended PyTuple_New/PyTuple_SetItem path now has Rust UB and can be misoptimized. Expose a C-API initialization method that writes through the UnsafeCell rather than through the shared slice.

Useful? React with 👍 / 👎.

Ok(())
}
})
}

#[unsafe(no_mangle)]
Expand Down Expand Up @@ -111,6 +129,7 @@ pub unsafe extern "C" fn PyTuple_GetSlice(

#[cfg(test)]
mod tests {
use pyo3::ffi;
use pyo3::prelude::*;
use pyo3::types::PyTuple;

Expand Down Expand Up @@ -138,4 +157,29 @@ mod tests {
assert_eq!(slice.extract::<(u32,)>().unwrap(), (2,));
})
}

#[test]
fn tuple_is_mutable_with_one_ref() {
Python::attach(|py| {
let value: Bound<'_, _> = 1.into_pyobject(py).unwrap();
let tuple: Bound<'_, PyTuple> =
unsafe { Bound::from_owned_ptr(py, ffi::PyTuple_New(1)).cast_into_unchecked() };
assert!(tuple.get_item(0).unwrap().is_none());
unsafe {
assert_eq!(
ffi::PyTuple_SetItem(tuple.as_ptr(), 0, value.clone().into_ptr()),
0
);
}
assert_eq!(tuple.get_item(0).unwrap().extract::<u32>().unwrap(), 1);
let _new_ref = tuple.clone();
unsafe {
assert_eq!(
ffi::PyTuple_SetItem(tuple.as_ptr(), 0, value.clone().into_ptr()),
-1
);
}
assert!(PyErr::take(py).is_some())
})
}
}
Loading