О пользе избыточной инициализации, или, В исходный код смотреть вредно
Следование примерам из исходного кода — не всегда удачная идея
То, что данные нужно инициализировать перед использованием, знают все. Но иногда правильная инициализация — хитрая штука. Я с этим столкнулся, когда писал расширение для 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 — интересно, а можно под это дело эксплойт написать?
Автор: Vladimir; опубликовано в: C/C++; метки: C/C++, PHP, Zend Engine, ошибкаМай
2009
Комментарии к статье «О пользе избыточной инициализации, или, В исходный код смотреть вредно» »
Пожалуйста, не используйте эту форму для комментирования! Данная форма предназначена исключительно для ботов.
Оставить комментарий к записи «О пользе избыточной инициализации, или, В исходный код смотреть вредно»
गते गते पारगते पारसंगते बोधि स्वाहा
Меня зовут Владимир, я программист-фрилансер, специализирующийся на Web-программировании и програмировании под Linux.
По совместительству занимаюсь администрированием LAMP/LNMP-серверов и техническим переводом.

