]> granicus.if.org Git - python/commitdiff
Issue #24912: Prevent __class__ assignment to immutable built-in objects.
authorGuido van Rossum <guido@dropbox.com>
Sat, 5 Sep 2015 03:54:07 +0000 (20:54 -0700)
committerGuido van Rossum <guido@dropbox.com>
Sat, 5 Sep 2015 03:54:07 +0000 (20:54 -0700)
Lib/test/test_descr.py
Misc/NEWS
Objects/typeobject.c

index 0ef1a31e0f449742651ce3db3c98110c2502f3a7..c74ebae4bf6e7dc8e095d71addc42db9e3a566ee 100644 (file)
@@ -1036,6 +1036,51 @@ order (MRO) for bases """
         self.assertTrue(m.__class__ is types.ModuleType)
         self.assertFalse(hasattr(m, "a"))
 
+        # Make sure that builtin immutable objects don't support __class__
+        # assignment, because the object instances may be interned.
+        # We set __slots__ = () to ensure that the subclasses are
+        # memory-layout compatible, and thus otherwise reasonable candidates
+        # for __class__ assignment.
+
+        # The following types have immutable instances, but are not
+        # subclassable and thus don't need to be checked:
+        #   NoneType, bool
+
+        class MyInt(int):
+            __slots__ = ()
+        with self.assertRaises(TypeError):
+            (1).__class__ = MyInt
+
+        class MyFloat(float):
+            __slots__ = ()
+        with self.assertRaises(TypeError):
+            (1.0).__class__ = MyFloat
+
+        class MyComplex(complex):
+            __slots__ = ()
+        with self.assertRaises(TypeError):
+            (1 + 2j).__class__ = MyComplex
+
+        class MyStr(str):
+            __slots__ = ()
+        with self.assertRaises(TypeError):
+            "a".__class__ = MyStr
+
+        class MyBytes(bytes):
+            __slots__ = ()
+        with self.assertRaises(TypeError):
+            b"a".__class__ = MyBytes
+
+        class MyTuple(tuple):
+            __slots__ = ()
+        with self.assertRaises(TypeError):
+            ().__class__ = MyTuple
+
+        class MyFrozenSet(frozenset):
+            __slots__ = ()
+        with self.assertRaises(TypeError):
+            frozenset().__class__ = MyFrozenSet
+
     def test_slots(self):
         # Testing __slots__...
         class C0(object):
index 288fa62cfa3f24b121122ec048c36af45da7682c..2b1f2785833dbee825128f566b31e106c5132477 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -10,6 +10,8 @@ Release date: 2015-09-06
 Core and Builtins
 -----------------
 
+- Issue #24912: Prevent __class__ assignment to immutable built-in objects.
+
 - Issue #24975: Fix AST compilation for PEP 448 syntax.
 
 Library
index 1beed724587e0c9af75cab10d4bfcd10a493f0f8..bb83530d588780979a24b8de0afe5558a5d6ac22 100644 (file)
@@ -3666,6 +3666,65 @@ object_set_class(PyObject *self, PyObject *value, void *closure)
         return -1;
     }
     newto = (PyTypeObject *)value;
+    /* In versions of CPython prior to 3.5, the code in
+       compatible_for_assignment was not set up to correctly check for memory
+       layout / slot / etc. compatibility for non-HEAPTYPE classes, so we just
+       disallowed __class__ assignment in any case that wasn't HEAPTYPE ->
+       HEAPTYPE.
+
+       During the 3.5 development cycle, we fixed the code in
+       compatible_for_assignment to correctly check compatibility between
+       arbitrary types, and started allowing __class__ assignment in all cases
+       where the old and new types did in fact have compatible slots and
+       memory layout (regardless of whether they were implemented as HEAPTYPEs
+       or not).
+
+       Just before 3.5 was released, though, we discovered that this led to
+       problems with immutable types like int, where the interpreter assumes
+       they are immutable and interns some values. Formerly this wasn't a
+       problem, because they really were immutable -- in particular, all the
+       types where the interpreter applied this interning trick happened to
+       also be statically allocated, so the old HEAPTYPE rules were
+       "accidentally" stopping them from allowing __class__ assignment. But
+       with the changes to __class__ assignment, we started allowing code like
+
+         class MyInt(int):
+             ...
+         # Modifies the type of *all* instances of 1 in the whole program,
+         # including future instances (!), because the 1 object is interned.
+         (1).__class__ = MyInt
+
+       (see https://bugs.python.org/issue24912).
+
+       In theory the proper fix would be to identify which classes rely on
+       this invariant and somehow disallow __class__ assignment only for them,
+       perhaps via some mechanism like a new Py_TPFLAGS_IMMUTABLE flag (a
+       "blacklisting" approach). But in practice, since this problem wasn't
+       noticed late in the 3.5 RC cycle, we're taking the conservative
+       approach and reinstating the same HEAPTYPE->HEAPTYPE check that we used
+       to have, plus a "whitelist". For now, the whitelist consists only of
+       ModuleType subtypes, since those are the cases that motivated the patch
+       in the first place -- see https://bugs.python.org/issue22986 -- and
+       since module objects are mutable we can be sure that they are
+       definitely not being interned. So now we allow HEAPTYPE->HEAPTYPE *or*
+       ModuleType subtype -> ModuleType subtype.
+
+       So far as we know, all the code beyond the following 'if' statement
+       will correctly handle non-HEAPTYPE classes, and the HEAPTYPE check is
+       needed only to protect that subset of non-HEAPTYPE classes for which
+       the interpreter has baked in the assumption that all instances are
+       truly immutable.
+    */
+    if (!(PyType_IsSubtype(newto, &PyModule_Type) &&
+          PyType_IsSubtype(oldto, &PyModule_Type)) &&
+        (!(newto->tp_flags & Py_TPFLAGS_HEAPTYPE) ||
+         !(oldto->tp_flags & Py_TPFLAGS_HEAPTYPE))) {
+        PyErr_Format(PyExc_TypeError,
+                     "__class__ assignment only supported for heap types "
+                     "or ModuleType subclasses");
+        return -1;
+    }
+
     if (compatible_for_assignment(oldto, newto, "__class__")) {
         if (newto->tp_flags & Py_TPFLAGS_HEAPTYPE)
             Py_INCREF(newto);