То, что данные нужно инициализировать перед использованием, знают все. Но иногда правильная инициализация — хитрая штука. Я с этим столкнулся, когда писал расширение для PHP, работающее с Voxel Hosting API.
Одна из проблем PHP — плохая документация (отсутствие таковой) по внутреннему API. А из кода Zend Engine не всегда всё однозначно ясно, чо временами приводит к очень милым ошибкам вида “фиг ты меня найдешь” (смягчено из соображений цензуры).
Об одной из таких особенностей я хочу рассказать.
Те, кому приходилось работать с внутренностями PHP, знают, что основной тип для представления данных — это zval
(и множественные указатели — zval*
, zval**
, zval***
). В частности, указатели на указатели на (указатели на)
Для повышения производительности кажется логичным использование статически выделенных zval*
'ов вместо выделения памяти через ALLOC_ZVAL
/MAKE_STD_ZVAL
. К моему удивлению, так практически никто не поступает. И даже Sara Golemon в примерах использует исключительно zval*
и MAKE_STD_ZVAL
.
Здесь нужно сделать одно лирическое отступление: "прямая" инициализация у разработчиков Zend Engine не в почёте, поэтому для совместимости с будущими версиями Zend Engine нужно использовать всякие дикие макросы. Посмотреть, во что расширяется макрос, не всегда просто — макросы используют другие макросы (и так на несколько уровней вложенности). Поэтому зачастую значительно проще поискать в исходном коде примеры, нежели пытаться изобрести велосипед.
Возвращаемся к статически распределённым zval
. В ext/standard/array.c
я нашел функцию, которая использует статически распределённые zval
.
{
Bucket *f;
Bucket *s;
zval result;
zval first;
zval second;
f = *((Bucket **) a);
s = *((Bucket **) b);
if (f->nKeyLength == 0) {
Z_TYPE(first) = IS_LONG;
Z_LVAL(first) = f->h;
} else {
Z_TYPE(first) = IS_STRING;
Z_STRVAL(first) = f->arKey;
Z_STRLEN(first) = f->nKeyLength-1;
}
if (s->nKeyLength == 0) {
Z_TYPE(second) = IS_LONG;
Z_LVAL(second) = s->h;
} else {
Z_TYPE(second) = IS_STRING;
Z_STRVAL(second) = s->arKey;
Z_STRLEN(second) = s->nKeyLength-1;
}
if (ARRAYG(compare_func)(&result, &first, &second TSRMLS_CC) == FAILURE) {
return 0;
}
if (Z_TYPE(result) == IS_DOUBLE) {
if (Z_DVAL(result) < 0) {
return -1;
} else if (Z_DVAL(result) > 0) {
return 1;
} else {
return 0;
}
}
convert_to_long(&result);
if (Z_LVAL(result) < 0) {
return -1;
} else if (Z_LVAL(result) > 0) {
return 1;
}
return 0;
}
Теперь обращаем внимание на инициализацию (это очень важно):
Z_TYPE(first) = IS_STRING;
Z_STRVAL(first) = f->arKey;
Z_STRLEN(first) = f->nKeyLength-1;
Кстати, странно, что разработчики не использовали макрос ZVAL_STRINGL
.
А теперь proof of concept, который вываливает PHP по segmentation fault:
PHP_FUNCTION(test_zval_init)
{
smart_str* s = (smart_str*)emalloc(sizeof(smart_str));
s->c = NULL;
smart_str_appends(s, "");
smart_str_0(s);
{
zval json[200];
ZVAL_STRINGL(&json[199], s->c, s->len, 0);
efree(s);
printf("1\n");
json_decode(&json[199], &return_value);
printf("2\n");
zval_dtor(&json[199]);
}
}
static int json_decode(zval* json, zval** result TSRMLS_DC)
{
assert(NULL != json);
assert(NULL != result && NULL != *result);
zval param2;
zval* params[2] = { json, ¶m2 };
int res = 0;
zval func;
INIT_ZVAL(param2);
ZVAL_TRUE(¶m2);
ZVAL_STRING(&func, "json_decode", 0);
if (FAILURE == call_user_function(EG(function_table), NULL, &func, *result, 2, params TSRMLS_CC)) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to call json_decode()");
res = 1;
}
zval_dtor(¶m2);
return res;
}
PHP вылетает по ошибке сегментации в call_user_function()
. Все дело в магических пузырьках инициализации json[199]
(и, возможно, в порядке вызова функций из тестового PHP-файла). Обращаю внимание: инициализация в PoC-коде тождественна инициализации в array_key_compare()
из ядра PHP.
Проблема в том, что при инициализации членов структуры zval
при помощи макросов ZVAL_XXX
мы не инициализируем поля zval.is_ref
и zval.refcount
(неинициализированное значение refcount
и вызывает ошибку сегментации).
Решение проблемы:
INIT_ZVAL(json[199]); /* Sic! */
ZVAL_STRINGL(&json[199], s->c, s->len, 0);
efree(s);
printf("1\n");
json_decode(&json[199], &return_value);
printf("2\n");
zval_dtor(&json[199]);
Так что следование примерам из исходного кода — не всегда удачная идея
PS — а если бы разработчики добавили явный вызов INIT_ZVAL в исходный код array_key_compare()
, такой ошибки бы не было.
PPS — интересно, а можно под это дело эксплойт написать?