php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #78032 Circular references from unset objects keep references to array elements
Submitted: 2019-05-18 20:25 UTC Modified: 2019-05-19 12:06 UTC
From: tutano2004 at gmail dot com Assigned:
Status: Not a bug Package: Scripting Engine problem
PHP Version: Irrelevant OS:
Private report: No CVE-ID: None
View Add Comment Developer Edit
Welcome! If you don't have a Git account, you can't do anything here.
You can add a comment by following this link or if you reported this bug, you can edit this bug over here.
(description)
Block user comment
Status: Assign to:
Package:
Bug Type:
Summary:
From: tutano2004 at gmail dot com
New email:
PHP Version: OS:

 

 [2019-05-18 20:25 UTC] tutano2004 at gmail dot com
Description:
------------
When an array is transversed with the value passed by reference, in a "foreach" loop, with the value then being passed by reference to another function (in this case "doReferenceStuff"), if the function internally instantiates 2 objects, each one with a reference to the other (circular reference), and with at least 1 of them with a reference to the value passed by reference, despite the fact that these objects are implicitly unset at the end of function (and thus "seize to exist"), the array still becomes an array of references in the end (by the time "var_dump" is called) rather than an array of values.

As noted in the test script, calling gc_collect_cycles prevents this bug from happening, meaning that something is not cleaned up well in these cases before the garbage collection kicks in.

If the circular reference is not made between both objects, this doesn't happen at all either.

The consequence of this later on is, in cases like this:

$array2 = $array;
$array2[1] = 'bar';

both $array and $array2 would have the element with the index 1 set as 'bar', rather than only the element in $array2 being set as such, given that this element would be a reference rather than a value, incorrectly so.

From my testing, this happens in every PHP version all the way back to PHP 5.0 at the very least, and all the way forward up to the latest stable version PHP 7.3.5.

I spent about 30min checking if there were any other bug reports of this kind given the number of PHP versions affected, and I found none thus far, hence creating this report, but finding the right words to summarize this problem is hard, so there's a good chance this was already reported but described differently.

None of the suggestions upon the submission of this report seem to point out to this problem having been reported either.


On another note, what led me to find this bug is a use case that I have in my own code, namely my own custom Exception class in a small library/framework I have been developing over the years, extending from the PHP Exception class, in which I instantiate with properties rather than a message, and internally I have a properties manager class/object that has a back reference to the Exception itself, so when an Exception is thrown, it also has a reference to this manager, and the manager has a reference to the Exception (creating a circular reference), and whenever an Exception is thrown, the PHP engine itself stores a stack trace within the Exception, and it's this stack trace which ends up holding the reference (&$v), since in the stack one of the functions receives the value by reference.

Test script:
---------------
function doReferenceStuff(&$v)
{
	$c1 = new stdClass;
	$c2 = new stdClass;
	$c1->c2 = $c2;
	$c2->c1 = $c1;
	$c1->v = &$v;
}

$array = [1, 'foo', new stdClass];
foreach ($array as &$v) {
	doReferenceStuff($v);
}
unset($v);

//gc_collect_cycles(); --> adding this here prevents the bug

var_dump($array);

Expected result:
----------------
array(3) {
  [0]=>
  int(1)
  [1]=>
  string(3) "foo"
  [2]=>
  object(stdClass)#7 (0) {
  }
}

Actual result:
--------------
array(3) {
  [0]=>
  &int(1)
  [1]=>
  &string(3) "foo"
  [2]=>
  &object(stdClass)#7 (0) {
  }
}

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2019-05-18 23:43 UTC] requinix@php.net
-Status: Open +Status: Not a bug
 [2019-05-19 10:48 UTC] tutano2004 at gmail dot com
I am sorry, but I am having a bit of a hard time wrapping my head around the way this is explained in that particular page (the diagram used is very confusing, at least for me), although the paper it's based off (linked in that page) seems to explain it a bit more clearly, so if I understood the paper correctly (although I didn't read it entirely), do you mean this has to do with the root buffer?

As in, if there are no circular references ever, the refcount is decreased and always hits zero, so the zval is immediately released, however if the reference is circular, then the only thing which can free it is the actual GC cycle, by checking these zvals starting from the root buffer itself, since the refcount will never be zero for the zvals with circular references, is that it?

If that's so, then it makes sense.
Sorry for the bug report, and thank you for the link. :)
 [2019-05-19 11:23 UTC] requinix@php.net
Internals aside, the main point from that page is that PHP doesn't clean up circular references as immediately as it does normal references. It keeps them until a certain threshold is reached, at which point it will figure what's unused and deal with them. gc_collect_cycles() is a userland way to trigger that collection without having to wait for the threshold.

Try modifying your script to have an array of at least 5000 elements, then check what gc_status() is returning each time: you'll see the cleaning kick in after 10k (by default) references, which will happen on the 5000th array entry.
 [2019-05-19 12:06 UTC] tutano2004 at gmail dot com
Indeed, even something simple like this:

$a = [];
for ($i = 0; $i < 10e3; $i++) {
	$c = new stdClass;
	$a[] = $c;
	unset($c);
}

before the var_dump was enough for the cycle to be triggered and for references to be cleaned up.


Thank you for your insights. :)
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Fri Apr 19 18:01:28 2024 UTC