Pull to refresh

Ловля бага #52001 в PHP 5.3: указатели и неициализированные переменные

Reading time2 min
Views1.1K
По следам недавно найденного tvv'ом бага.

При выполнении следующего кода в PHP версий 5.3.0-5.3.2 результат превосходил все ожидания.

<?php
f(0, $$var);
$x = 1;
$y = 2;
echo $x;
function f($a, $b) {};


В результате выводилось '2'. Мне удалось найти багу и поправить её: #52001. Коротко: затирался указатель на специльную переменную-затычку для неинициализированных переменных, через которую и создаются все CV переменные в PHP.



Видя исходники PHP впервые, поиск я начал с проверки на вшивость сканера и парсера PHP. Оказалось, что компиляция проходит правильно: для этого потребовалось включить режим отладки парсера. Это помогло поименовать переменные и разобраться какой структуре что принадлежит. В частности, понять принадлежность всевозможных zend_do_* функций компилятора.

Тогда же стало понятно, что существует два различных режима вызова функций: по имени и по адресу. Первый используется если имя не известно компилятору в режиме компиляции. В этом режиме аргументы передаются несколько иначе, поскольку прототип не известен компилятору.

Псевдослучайно тыкая распечатки адресов переменных я обнаружил, что действительно две переменные (x и y) имеют одинаковые адреса во внутренностях PHP, что явно было багом. Сначала было сомнение, что переменные правильно ищутся в пространстве имён, которое было развеяно включением отладки: печати всего хэша пространства имён при поиске в нём переменных.

Оказалось следующее: вызов по имени приводит к специальной маркировке передаваемых переменных, поскольку они могут быть ссылками (ведь прототип неизвестен).

Переменная $$var, и все переменные на чтение, создаются как специальная неинициализированная переменная. Код-обработчик извлечения переменной для вызова функции следил за тем, что бы переданное значение могло быть использовано как ref, для чего требовалось скопировать эту переменную. При этом с помощью указателя на указатель переписывается значение указателя на ту самую специальную неинициализированную переменную. Она становится равной только что выделенной памяти и имеет reference count = 1.

После этого любая новая инициализируемая переменная получает эту память. «Неправильный» reference count приводит к тому, что при записи в эти переменные они не копируются (copy-on-write) и один и тот же участок памяти используется как общее для всех новых переменных. Это и приводит к тому, что все данные одинаковые, схоже со старым багом в Fortran'е, когда можно было сделать присвоение 1=2.

На этом всё. Это было интересным опытом, очень люблю подобные баги.

Более подробно техническую часть можно прочитать в этом комментарии.
Tags:
Hubs:
Total votes 168: ↑157 and ↓11+146
Comments52

Articles