То, что данные нужно инициализировать перед использованием, знают все. Но иногда правильная инициализация — хитрая штука. Я с этим столкнулся, когда писал расширение для PHP, работающее с Voxel Hosting API.

Одна из проблем PHP — плохая документация (отсутствие таковой) по внутреннему API. А из кода Zend Engine не всегда всё однозначно ясно, чо временами приводит к очень милым ошибкам вида “фиг ты меня найдешь” (смягчено из соображений цензуры).

Об одной из таких особенностей я хочу рассказать.

Те, кому приходилось работать с внутренностями PHP, знают, что основной тип для представления данных — это zval (и множественные указатели — zval*, zval**, zval***). В частности, указатели на указатели на (указатели на) zval приходится использовать при вызове userspace- и zif-функций.

Для повышения производительности кажется логичным использование статически выделенных zval*'ов вместо выделения памяти через ALLOC_ZVAL/MAKE_STD_ZVAL. К моему удивлению, так практически никто не поступает. И даже Sara Golemon в примерах использует исключительно zval* и MAKE_STD_ZVAL.

Здесь нужно сделать одно лирическое отступление: "прямая" инициализация у разработчиков Zend Engine не в почёте, поэтому для совместимости с будущими версиями Zend Engine нужно использовать всякие дикие макросы. Посмотреть, во что расширяется макрос, не всегда просто — макросы используют другие макросы (и так на несколько уровней вложенности). Поэтому зачастую значительно проще поискать в исходном коде примеры, нежели пытаться изобрести велосипед.

Возвращаемся к статически распределённым zval. В ext/standard/array.c я нашел функцию, которая использует статически распределённые zval.

[-]
View Code C
static int array_key_compare(const void *a, const void *b TSRMLS_DC)
{
    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;
}

Теперь обращаем внимание на инициализацию (это очень важно):

[-]
View Code C
    zval first;

    Z_TYPE(first) = IS_STRING;
    Z_STRVAL(first) = f->arKey;
    Z_STRLEN(first) = f->nKeyLength-1;

Кстати, странно, что разработчики не использовали макрос ZVAL_STRINGL.

А теперь proof of concept, который вываливает PHP по segmentation fault:

[-]
View Code C
static int json_decode(zval* json, zval** result TSRMLS_DC);

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, &param2 };

    int res = 0;
    zval func;

    INIT_ZVAL(param2);
    ZVAL_TRUE(&param2);
    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(&param2);
    return res;
}

PHP вылетает по ошибке сегментации в call_user_function(). Все дело в магических пузырьках инициализации json[199] (и, возможно, в порядке вызова функций из тестового PHP-файла). Обращаю внимание: инициализация в PoC-коде тождественна инициализации в array_key_compare() из ядра PHP.

Проблема в том, что при инициализации членов структуры zval при помощи макросов ZVAL_XXX мы не инициализируем поля zval.is_ref и zval.refcount (неинициализированное значение refcount и вызывает ошибку сегментации).

Решение проблемы:

[-]
View Code C
    zval json[200];
    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 — интересно, а можно под это дело эксплойт написать?

Добавить в закладки

Связанные записи

7
Май
2009

Комментарии к статье «О пользе избыточной инициализации, или, В исходный код смотреть вредно»  »

К статье «О пользе избыточной инициализации, или, В исходный код смотреть вредно» комментариев пока нет. Не хотите ли стать первым?

Подписаться на RSS-ленту комментариев к статье «О пользе избыточной инициализации, или, В исходный код смотреть вредно» Trackback URL: http://blog.sjinks.org.ua/c-cpp/553-the-use-of-superfluous-initialization/trackback/

Оставить комментарий к записи «О пользе избыточной инициализации, или, В исходный код смотреть вредно»

Вы можете использовать данные тэги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Оставляя комментарий, Вы выражаете своё согласие с Правилами комментирования.

Подписаться, не комментируя