]> granicus.if.org Git - php/commitdiff
Fixed bug #72530
authorNikita Popov <nikita.ppv@gmail.com>
Thu, 1 Aug 2019 10:55:39 +0000 (12:55 +0200)
committerNikita Popov <nikita.ppv@gmail.com>
Tue, 13 Aug 2019 12:53:53 +0000 (14:53 +0200)
For objects with destructors, we will now only call the destructor
in the initial GC run, and remove any nested data. The object is
marked purple so it will be considered a root for the next GC run,
at which point it will be fully destroyed, if possible.

GC counts change on a number of tests, as the objects now get
destroyed later.

NEWS
Zend/tests/bug71818.phpt
Zend/tests/bug72530.phpt [new file with mode: 0644]
Zend/tests/gc_011.phpt
Zend/tests/gc_016.phpt
Zend/tests/gc_017.phpt
Zend/tests/gc_028.phpt
Zend/tests/gc_029.phpt
Zend/tests/gc_035.phpt
Zend/tests/generators/bug76427.phpt
Zend/zend_gc.c

diff --git a/NEWS b/NEWS
index 1254024d36dd8f96b5c0385661b02d1ec0be89e0..aac83845b56a68e1a43063ef9dace0762b8d8d60 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -7,6 +7,7 @@ PHP                                                                        NEWS
     (Nikita)
   . Fixed bug #78406 (Broken file includes with user-defined stream filters).
     (Nikita)
+  . Fixed bug #72530 (Use After Free in GC with Certain Destructors). (Nikita)
 
 - Date:
   . Fixed bug #78383 (Casting a DateTime to array no longer returns its
index e09255ddac2061fb530cd4dcf94f4988163fa463..67094c09f7bcd552c13695e15c70bd3c66bb0555 100644 (file)
@@ -19,7 +19,7 @@ class MemoryLeak
     private $things = [];
 }
 
-ini_set('memory_limit', '10M');
+ini_set('memory_limit', '20M');
 
 for ($i = 0; $i < 100000; ++$i) {
     $obj = new MemoryLeak();
diff --git a/Zend/tests/bug72530.phpt b/Zend/tests/bug72530.phpt
new file mode 100644 (file)
index 0000000..b70be0b
--- /dev/null
@@ -0,0 +1,31 @@
+--TEST--
+Bug #72530: Use After Free in GC with Certain Destructors
+--FILE--
+<?php
+
+class ryat {
+    var $ryat;
+    var $chtg;
+
+    function __destruct() {
+        $this->chtg = $this->ryat;
+        $this->ryat = 1;
+    }
+}
+
+$o = new ryat;
+$o->ryat = $o;
+$x =& $o->chtg;
+
+unset($o);
+gc_collect_cycles();
+var_dump($x);
+
+?>
+--EXPECT--
+object(ryat)#1 (2) {
+  ["ryat"]=>
+  int(1)
+  ["chtg"]=>
+  *RECURSION*
+}
index 9c4cc2cc0e570728da55db6a1a2712399283cc7d..d11d7b6b46e4dd5492c2b963c4c040344652c9aa 100644 (file)
@@ -15,6 +15,7 @@ $a->a = $a;
 var_dump($a);
 unset($a);
 var_dump(gc_collect_cycles());
+var_dump(gc_collect_cycles());
 echo "ok\n"
 ?>
 --EXPECTF--
@@ -23,5 +24,6 @@ object(Foo)#%d (1) {
   *RECURSION*
 }
 __destruct
+int(0)
 int(1)
 ok
index f082d60973e882363c8fe22eef884c6aae9d55c9..211f03a605305581cd017c5b01f73de166512723 100644 (file)
@@ -23,6 +23,6 @@ echo "ok\n"
 ?>
 --EXPECT--
 -> int(0)
-int(1)
-int(1)
+int(0)
+int(2)
 ok
index 102c2b6bcbe0f1548baf9ad957f82b83b2bd312d..55f381992e6984e43a5207287b4a6225243d555e 100644 (file)
@@ -32,11 +32,13 @@ unset($a);
 unset($b);
 unset($c);
 var_dump(gc_collect_cycles());
+var_dump(gc_collect_cycles());
 echo "ok\n"
 ?>
 --EXPECTF--
 string(1) "%s"
 string(1) "%s"
 string(1) "%s"
-int(4)
+int(0)
+int(1)
 ok
index 8dc70fc39706af6861f9be8c516e3d82b5618ea3..fb2ea92c91330d37a7f2720eff1a1178c6c1c30b 100644 (file)
@@ -28,6 +28,8 @@ $bar->foo = $foo;
 unset($foo);
 unset($bar);
 var_dump(gc_collect_cycles());
+var_dump(gc_collect_cycles());
 ?>
 --EXPECT--
-int(2)
+int(0)
+int(1)
index 215d0e0e3bc6fe0d215dd7af7920dd90863a3214..89c55e5ba76d334197b8831c01558f36f20f682c 100644 (file)
@@ -30,6 +30,8 @@ $bar->foo = $foo;
 unset($foo);
 unset($bar);
 var_dump(gc_collect_cycles());
+var_dump(gc_collect_cycles());
 ?>
---EXPECTREGEX--
-int\([23]\)
+--EXPECT--
+int(0)
+int(1)
index 177c3101f9f111104e16cc587a809c46125779cf..187af9108bd7808a1a6b5eee6b3e4ea9f8a3a4aa 100644 (file)
@@ -22,5 +22,5 @@ var_dump(gc_collect_cycles());
 var_dump(gc_collect_cycles());
 --EXPECT--
 int(0)
-int(2)
 int(0)
+int(2)
index 09ec61a340a6f9e75af50b07075deddc1d95dfb7..53851b0f35fe0257bfb17be01c7d48494dfc82cd 100644 (file)
@@ -21,4 +21,4 @@ var_dump(gc_collect_cycles());
 
 ?>
 --EXPECT--
-int(4)
+int(2)
index e1c2295d740b61dc7a95d8847a184506ec9049ad..12922a26c85f98d1d612bd6acace69cda4d417b6 100644 (file)
 #define GC_ROOT    0x0 /* possible root of circular garbage     */
 #define GC_UNUSED  0x1 /* part of linked list of unused buffers */
 #define GC_GARBAGE 0x2 /* garbage to delete                     */
+#define GC_DTOR_GARBAGE 0x3 /* garbage on which only the dtor should be invoked */
 
 #define GC_GET_PTR(ptr) \
        ((void*)(((uintptr_t)(ptr)) & ~GC_BITS))
        ((((uintptr_t)(ptr)) & GC_BITS) == GC_UNUSED)
 #define GC_IS_GARBAGE(ptr) \
        ((((uintptr_t)(ptr)) & GC_BITS) == GC_GARBAGE)
+#define GC_IS_DTOR_GARBAGE(ptr) \
+       ((((uintptr_t)(ptr)) & GC_BITS) == GC_DTOR_GARBAGE)
 
 #define GC_MAKE_GARBAGE(ptr) \
        ((void*)(((uintptr_t)(ptr)) | GC_GARBAGE))
+#define GC_MAKE_DTOR_GARBAGE(ptr) \
+       ((void*)(((uintptr_t)(ptr)) | GC_DTOR_GARBAGE))
 
 /* GC address conversion */
 #define GC_IDX2PTR(idx)      (GC_G(buf) + (idx))
@@ -1328,9 +1333,6 @@ static int gc_remove_nested_data_from_buffer(zend_refcounted *ref, gc_root_buffe
 tail_call:
        do {
                if (root) {
-                       GC_TRACE_REF(ref, "removing from buffer");
-                       gc_remove_from_roots(root);
-                       GC_REF_SET_INFO(ref, 0);
                        root = NULL;
                        count++;
                } else if (GC_REF_ADDRESS(ref) != 0
@@ -1461,67 +1463,79 @@ ZEND_API int zend_gc_collect_cycles(void)
                end = GC_G(first_unused);
 
                if (gc_flags & GC_HAS_DESTRUCTORS) {
-                       uint32_t *refcounts;
-
                        GC_TRACE("Calling destructors");
 
-                       // TODO: may be use emalloc() ???
-                       refcounts = pemalloc(sizeof(uint32_t) * end, 1);
-
-                       /* Remember reference counters before calling destructors */
+                       /* During a destructor call, new externally visible references to nested data may
+                        * be introduced. These references can be introduced in a way that does not
+                        * modify any refcounts, so we have no real way to detect this situation
+                        * short of rerunning full GC tracing. What we do instead is to only run
+                        * destructors at this point, and leave the actual freeing of the objects
+                        * until the next GC run. */
+
+                       /* Mark all roots for which a dtor will be invoked as DTOR_GARBAGE. Additionally
+                        * color them purple. This serves a double purpose: First, they should be
+                        * considered new potential roots for the next GC run. Second, it will prevent
+                        * their removal from the root buffer by nested data removal. */
                        idx = GC_FIRST_ROOT;
                        current = GC_IDX2PTR(GC_FIRST_ROOT);
                        while (idx != end) {
                                if (GC_IS_GARBAGE(current->ref)) {
                                        p = GC_GET_PTR(current->ref);
-                                       refcounts[idx] = GC_REFCOUNT(p);
+                                       if (GC_TYPE(p) == IS_OBJECT && !(OBJ_FLAGS(p) & IS_OBJ_DESTRUCTOR_CALLED)) {
+                                               zend_object *obj = (zend_object *) p;
+                                               if (obj->handlers->dtor_obj != zend_objects_destroy_object
+                                                       || obj->ce->destructor) {
+                                                       current->ref = GC_MAKE_DTOR_GARBAGE(obj);
+                                                       GC_REF_SET_COLOR(obj, GC_PURPLE);
+                                               } else {
+                                                       GC_ADD_FLAGS(obj, IS_OBJ_DESTRUCTOR_CALLED);
+                                               }
+                                       }
                                }
                                current++;
                                idx++;
                        }
 
-                       /* Call destructors
-                        *
-                        * The root buffer might be reallocated during destructors calls,
-                        * make sure to reload pointers as necessary. */
+                       /* Remove nested data for objects on which a destructor will be called.
+                        * This will not remove the objects themselves, as they have been colored
+                        * purple. */
                        idx = GC_FIRST_ROOT;
+                       current = GC_IDX2PTR(GC_FIRST_ROOT);
                        while (idx != end) {
-                               current = GC_IDX2PTR(idx);
-                               if (GC_IS_GARBAGE(current->ref)) {
+                               if (GC_IS_DTOR_GARBAGE(current->ref)) {
                                        p = GC_GET_PTR(current->ref);
-                                       if (GC_TYPE(p) == IS_OBJECT
-                                        && !(OBJ_FLAGS(p) & IS_OBJ_DESTRUCTOR_CALLED)) {
-                                               zend_object *obj = (zend_object*)p;
-
-                                               GC_TRACE_REF(obj, "calling destructor");
-                                               GC_ADD_FLAGS(obj, IS_OBJ_DESTRUCTOR_CALLED);
-                                               if (obj->handlers->dtor_obj != zend_objects_destroy_object
-                                                               || obj->ce->destructor) {
-                                                       GC_ADDREF(obj);
-                                                       obj->handlers->dtor_obj(obj);
-                                                       GC_DELREF(obj);
-                                               }
-                                       }
+                                       count -= gc_remove_nested_data_from_buffer(p, current);
                                }
+                               current++;
                                idx++;
                        }
 
-                       /* Remove values captured in destructors */
+                       /* Actually call destructors.
+                        *
+                        * The root buffer might be reallocated during destructors calls,
+                        * make sure to reload pointers as necessary. */
                        idx = GC_FIRST_ROOT;
-                       current = GC_IDX2PTR(GC_FIRST_ROOT);
                        while (idx != end) {
-                               if (GC_IS_GARBAGE(current->ref)) {
+                               current = GC_IDX2PTR(idx);
+                               if (GC_IS_DTOR_GARBAGE(current->ref)) {
                                        p = GC_GET_PTR(current->ref);
-                                       if (GC_REFCOUNT(p) > refcounts[idx]) {
-                                               count -= gc_remove_nested_data_from_buffer(p, current);
+                                       /* Mark this is as a normal root for the next GC run,
+                                        * it's no longer garbage for this run. */
+                                       current->ref = p;
+                                       /* Double check that the destructor hasn't been called yet. It could have
+                                        * already been invoked indirectly by some other destructor. */
+                                       if (!(OBJ_FLAGS(p) & IS_OBJ_DESTRUCTOR_CALLED)) {
+                                               zend_object *obj = (zend_object*)p;
+                                               GC_TRACE_REF(obj, "calling destructor");
+                                               GC_ADD_FLAGS(obj, IS_OBJ_DESTRUCTOR_CALLED);
+                                               GC_ADDREF(obj);
+                                               obj->handlers->dtor_obj(obj);
+                                               GC_DELREF(obj);
                                        }
                                }
-                               current++;
                                idx++;
                        }
 
-                       pefree(refcounts, 1);
-
                        if (GC_G(gc_protected)) {
                                /* something went wrong */
                                return 0;