Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix: guard __class__ reassignment layout
Co-authored-by: youknowone <69878+youknowone@users.noreply.github.com>
  • Loading branch information
Copilot and youknowone committed Dec 26, 2025
commit a3fb6fed740a862e216e674b52932663f8dde1f6
19 changes: 16 additions & 3 deletions crates/vm/src/builtins/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,16 +497,29 @@ impl PyBaseObject {
) -> PyResult<()> {
match value.downcast::<PyType>() {
Ok(cls) => {
let both_module = instance.class().fast_issubclass(vm.ctx.types.module_type)
let current_cls = instance.class();
let both_module = current_cls.fast_issubclass(vm.ctx.types.module_type)
&& cls.fast_issubclass(vm.ctx.types.module_type);
let both_mutable = !instance
.class()
let both_mutable = !current_cls
.slots
.flags
.has_feature(PyTypeFlags::IMMUTABLETYPE)
&& !cls.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE);
// FIXME(#1979) cls instances might have a payload
if both_mutable || both_module {
let has_dict = |typ: &Py<PyType>| typ.slots.flags.has_feature(PyTypeFlags::HAS_DICT);
if current_cls.slots.basicsize != cls.slots.basicsize
|| current_cls.slots.member_count != cls.slots.member_count
|| has_dict(current_cls) != has_dict(&cls)
|| (cls.slots.flags.has_feature(PyTypeFlags::HEAPTYPE)
&& instance.typeid() != PyBaseObject::payload_type_id())
{
return Err(vm.new_type_error(format!(
"__class__ assignment: '{}' object layout differs from '{}'",
cls.name(),
current_cls.name()
)));
}
instance.set_class(cls, vm);
Ok(())
} else {
Expand Down
27 changes: 27 additions & 0 deletions crates/vm/src/vm/interpreter.rs
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of adding test in rust code, add test test in builtin_type.py

Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,31 @@ mod tests {
assert_eq!(value.as_str(), "Hello Hello Hello Hello ")
})
}

#[test]
fn class_assignment_layout_mismatch_error() {
Interpreter::without_stdlib(Default::default()).enter(|vm| {
let scope = vm.new_scope_with_builtins();
let source = r#"
class TypeA:
def __init__(self):
self.a = 1

class TypeB:
__slots__ = "b"
def __init__(self):
self.b = 2

obj = TypeA()
try:
obj.__class__ = TypeB
except TypeError as e:
assert str(e) == "__class__ assignment: 'TypeB' object layout differs from 'TypeA'"
else:
raise AssertionError("TypeError not raised")
"#;
vm.run_code_string(scope, source, "<test>".to_owned())
.expect("script should complete without uncaught exceptions");
})
}
}