Skip to content

Commit 27ab62d

Browse files
Copilotyouknowonegithub-actions[bot]
authored
Prevent __class__ reassignment across incompatible layouts (#6521)
--------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: youknowone <69878+youknowone@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Jeong YunWon <jeong@youknowone.org>
1 parent a7d7f81 commit 27ab62d

File tree

2 files changed

+82
-3
lines changed

2 files changed

+82
-3
lines changed

crates/vm/src/builtins/object.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -497,16 +497,45 @@ impl PyBaseObject {
497497
) -> PyResult<()> {
498498
match value.downcast::<PyType>() {
499499
Ok(cls) => {
500-
let both_module = instance.class().fast_issubclass(vm.ctx.types.module_type)
500+
let current_cls = instance.class();
501+
let both_module = current_cls.fast_issubclass(vm.ctx.types.module_type)
501502
&& cls.fast_issubclass(vm.ctx.types.module_type);
502-
let both_mutable = !instance
503-
.class()
503+
let both_mutable = !current_cls
504504
.slots
505505
.flags
506506
.has_feature(PyTypeFlags::IMMUTABLETYPE)
507507
&& !cls.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE);
508508
// FIXME(#1979) cls instances might have a payload
509509
if both_mutable || both_module {
510+
let has_dict =
511+
|typ: &Py<PyType>| typ.slots.flags.has_feature(PyTypeFlags::HAS_DICT);
512+
// Compare slots tuples
513+
let slots_equal = match (
514+
current_cls
515+
.heaptype_ext
516+
.as_ref()
517+
.and_then(|e| e.slots.as_ref()),
518+
cls.heaptype_ext.as_ref().and_then(|e| e.slots.as_ref()),
519+
) {
520+
(Some(a), Some(b)) => {
521+
a.len() == b.len()
522+
&& a.iter()
523+
.zip(b.iter())
524+
.all(|(x, y)| x.as_str() == y.as_str())
525+
}
526+
(None, None) => true,
527+
_ => false,
528+
};
529+
if current_cls.slots.basicsize != cls.slots.basicsize
530+
|| !slots_equal
531+
|| has_dict(current_cls) != has_dict(&cls)
532+
{
533+
return Err(vm.new_type_error(format!(
534+
"__class__ assignment: '{}' object layout differs from '{}'",
535+
cls.name(),
536+
current_cls.name()
537+
)));
538+
}
510539
instance.set_class(cls, vm);
511540
Ok(())
512541
} else {

extra_tests/snippets/builtin_type.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,56 @@ class C(B, BB):
240240
assert C.mro() == [C, B, A, BB, AA, object]
241241

242242

243+
class TypeA:
244+
def __init__(self):
245+
self.a = 1
246+
247+
248+
class TypeB:
249+
__slots__ = "b"
250+
251+
def __init__(self):
252+
self.b = 2
253+
254+
255+
obj = TypeA()
256+
with assert_raises(TypeError) as cm:
257+
obj.__class__ = TypeB
258+
assert "__class__ assignment: 'TypeB' object layout differs from 'TypeA'" in str(
259+
cm.exception
260+
)
261+
262+
263+
# Test: same slot count but different slot names should fail
264+
class SlotX:
265+
__slots__ = ("x",)
266+
267+
268+
class SlotY:
269+
__slots__ = ("y",)
270+
271+
272+
slot_obj = SlotX()
273+
with assert_raises(TypeError) as cm:
274+
slot_obj.__class__ = SlotY
275+
assert "__class__ assignment: 'SlotY' object layout differs from 'SlotX'" in str(
276+
cm.exception
277+
)
278+
279+
280+
# Test: same slots should succeed
281+
class SlotA:
282+
__slots__ = ("a",)
283+
284+
285+
class SlotA2:
286+
__slots__ = ("a",)
287+
288+
289+
slot_a = SlotA()
290+
slot_a.__class__ = SlotA2 # Should work
291+
292+
243293
assert type(Exception.args).__name__ == "getset_descriptor"
244294
assert type(None).__bool__(None) is False
245295

0 commit comments

Comments
 (0)