-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Modifying a copied by-ref iterated array resets the array position #11244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Minimal reproducible example: https://3v4l.org/oU2Wj $data = range(0,3);
function foo($data): void
{
foreach ($data as $key => &$value) {
echo "{$value}\n";
try {
if($value === 2) {
throw new \Exception;
}
} catch (\Exception $e) {
echo "remove {$value}\n";
unset($data[$key]);
}
}
}
foo($data); Output 8.0+
Output before 8.0
@survik1 It's best to avoid using references whenever possible, they were always big source of problems. Just removing reference from the |
A simpler example:
giving
The problem is modifying a by-ref iterated array whose RC > 1. This causes duplication of the array, assigning a new array to $data. The foreach iterator then thinks it's a new array and starts over... This is not a new issue, but an issue existing since PHP 7: https://3v4l.org/VoceP. It just happens that since PHP 8 exceptions do a CoW copy (reference counter increase) of the underlying value instead of the reference, exhibiting the side effect of the referenced array needing to be duplicated upon modification, causing this issue. Which should be an implementation detail without visible side effects, except in this case where it apparently surfaces this issue. |
When executing a foreach ($ht as &$ref), foreach calls zend_hash_iterator_pos_ex() on every iteration. If the HashTable contained in the $ht variable is not the tracked HashTable, it will reset the position to the internal array pointer of the array currently in $ht. This behaviour is generally fine, but undesirable for copy-on-write copies of the iterated HashTable. This may trivially occur when the iterated over HashTable is assigned to some variable, then the iterated over variable modified, leading to array separation, changing the HashTable pointer in the variable. Thus foreach happily restarting iteration. This behaviour (despite existing since PHP 7.0) is considered a bug, if not only for the behaviour being unexpected to the user, also copy-on-write should not have trivially observable side-effects by mere assignment. The bugfix consists of duplicating HashTableIterators whenever zend_array_dup() is called (the primitive used on array separation). When a further access to the HashPosition through the HashTableIterators API happens and the HashTable does not match the tracked one, all the duplicates (which are tracked by single linked list) are searched for the wanted HashTable. If found, the HashTableIterator is replaced by the found copy and all other copies are removed. This ensures that we always end up tracking the correct HashTable. Fixes phpGH-11244. Signed-off-by: Bob Weinand <bobwei9@hotmail.com>
When executing a foreach ($ht as &$ref), foreach calls zend_hash_iterator_pos_ex() on every iteration. If the HashTable contained in the $ht variable is not the tracked HashTable, it will reset the position to the internal array pointer of the array currently in $ht. This behaviour is generally fine, but undesirable for copy-on-write copies of the iterated HashTable. This may trivially occur when the iterated over HashTable is assigned to some variable, then the iterated over variable modified, leading to array separation, changing the HashTable pointer in the variable. Thus foreach happily restarting iteration. This behaviour (despite existing since PHP 7.0) is considered a bug, if not only for the behaviour being unexpected to the user, also copy-on-write should not have trivially observable side-effects by mere assignment. The bugfix consists of duplicating HashTableIterators whenever zend_array_dup() is called (the primitive used on array separation). When a further access to the HashPosition through the HashTableIterators API happens and the HashTable does not match the tracked one, all the duplicates (which are tracked by single linked list) are searched for the wanted HashTable. If found, the HashTableIterator is replaced by the found copy and all other copies are removed. This ensures that we always end up tracking the correct HashTable. Fixes phpGH-11244. Signed-off-by: Bob Weinand <bobwei9@hotmail.com>
When executing a foreach ($ht as &$ref), foreach calls zend_hash_iterator_pos_ex() on every iteration. If the HashTable contained in the $ht variable is not the tracked HashTable, it will reset the position to the internal array pointer of the array currently in $ht. This behaviour is generally fine, but undesirable for copy-on-write copies of the iterated HashTable. This may trivially occur when the iterated over HashTable is assigned to some variable, then the iterated over variable modified, leading to array separation, changing the HashTable pointer in the variable. Thus foreach happily restarting iteration. This behaviour (despite existing since PHP 7.0) is considered a bug, if not only for the behaviour being unexpected to the user, also copy-on-write should not have trivially observable side-effects by mere assignment. The bugfix consists of duplicating HashTableIterators whenever zend_array_dup() is called (the primitive used on array separation). When a further access to the HashPosition through the HashTableIterators API happens and the HashTable does not match the tracked one, all the duplicates (which are tracked by single linked list) are searched for the wanted HashTable. If found, the HashTableIterator is replaced by the found copy and all other copies are removed. This ensures that we always end up tracking the correct HashTable. Fixes GH-11244. Signed-off-by: Bob Weinand <bobwei9@hotmail.com>
Description
Hello, we are finally pushing our code to PHP 8 and I have encountered this BC break which seems more like bug than feature to me. This behavior is still present in 8.2 and the change was not documented (unless i missed it ofc).
https://3v4l.org/aNejW
Output in PHP >=8.0:
Output in PHP >=7.1 <8.0:
PHP Version
PHP 8.2.6
Operating System
No response
The text was updated successfully, but these errors were encountered: