Безопасное форматирование строк, когда vsnprintf недоступен - PullRequest
0 голосов
/ 27 сентября 2018

Я пишу код, который должен отформатировать строку, и я хочу избежать переполнения буфера.

Я знаю, что если доступно vsnprintf (C99 и далее), мы можем сделать:

char* formatString(const char *format, ...)
{
    char* result = NULL;
    va_list ap;
    va_start(ap, format);

    /* Get the size of the formatted string by getting vsnprintf return the
     * number of remaining characters if we ask it to write 0 characters */
    int size = vsnprintf(NULL, 0, format, ap);

    if (size > 0)
    {
        /* String formatted just fine */
        result = (char *) calloc(size + 1, sizeof(char));
        vsnprintf(result, size + 1, format, ap);
    }

    va_end(ap);
    return result;
}

Я не могу найти способ сделать что-то подобное в C90 (без vsnprintf).Если это окажется невозможным без написания чрезвычайно сложной логики, я был бы рад установить максимальную длину для результата, но я не уверен, как этого можно достичь, не рискуя переполнением буфера.

Ответы [ 2 ]

0 голосов
/ 28 сентября 2018

Передача комментариев для ответа.

Основная причина vsnprintf() была добавлена ​​в C99 в том, что это труднодля защиты vsprintf() или аналогичный.Один из обходных путей - открыть /dev/null, использовать vfprintf(), чтобы отформатировать данные, отметить, насколько велик был результат, а затем решить, безопасно ли продолжать.Ики, особенно если вы открываете устройство при каждом вызове.

Это означает, что ваш код может стать:

#include <assert.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>

extern char *formatString(const char *format, ...);

char *formatString(const char *format, ...)
{
    static FILE *fp_null = NULL;

    if (fp_null == NULL)
    {
        fp_null = fopen("/dev/null", "w");
        if (fp_null == NULL)
            return NULL;
    }

    va_list ap;

    va_start(ap, format);
    int size = vfprintf(fp_null, format, ap);
    va_end(ap);

    if (size < 0)
        return NULL;

    char *result = (char *) malloc(size + 1);
    if (result == NULL)
        return NULL;

    va_start(ap, format);
    int check = vsprintf(result, format, ap);
    va_end(ap);

    assert(check == size);

    return result;
}

int main(void)
{
    char *r1 = formatString("%d Dancing Pigs = %4.2f%% of annual GDP (grandiose dancing pigs!)\n",
                            34241562, 21.2963);
    char *r2 = formatString("%s [%-13.10s] %s is %d%% %s\n", "Peripheral",
                            "sub-atomic hyperdrive", "status", 99, "of normality");

    if (r1 != NULL)
        printf("r1 = %s", r1);

    if (r2 != NULL)
        printf("r2 = %s", r2);

    free(r1);
    free(r2);
    return 0;
}

Как написано с fp_null статической переменной внутри функции,Поток файла не может быть закрыт.Если это беспокоит, сделайте его переменной внутри файла и предоставьте функцию для if (fp_null != NULL) { fclose(fp_null); fp_null = NULL; }.

Я не подозреваю, что Unix-подобная среда с /dev/null;вы можете перевести это на NUL:, если вы работаете в Windows.

Обратите внимание, что исходный код в вопросе не использовал va_start() и va_end() дважды (в отличие от этого кода);это приведет к катастрофе.На мой взгляд, неплохо ставить va_end() как можно скорее после va_start(), как показано в этом коде.Ясно, что если ваша функция сама проходит через va_list, то здесь будет больший разрыв, чем показано здесь, но когда вы просто передаете аргументы переменной другой функции, как здесь, должна быть только одна строка между.

Код корректно компилируется на Mac под управлением macOS 10.14 Mojave с использованием GCC 8.2.0 (скомпилировано в macOS 10.13 High Sierra) с командной строкой:

$ gcc -O3 -g -std=c90 -Wall -Wextra -Werror -Wmissing-prototypes \
>     -Wstrict-prototypes vsnp37.c -o vsnp37
$

При запуске он выдает:

r1 = 34241562 Dancing Pigs = 21.30% of annual GDP (grandiose dancing pigs!)
r2 = Peripheral [sub-atomic   ] status is 99% of normality
0 голосов
/ 27 сентября 2018

Pre-C99 не предоставляет простого решения для форматирования строк с высокой степенью безопасности, предотвращая переполнение буфера.

Именно такие надоедливые спецификаторы формата "%s", "%[]", "%f" требуюточень тщательное рассмотрение с их потенциальной длинной продукцией.Таким образом, потребность в такой функции. @ Jonathan Leffler

Для этого с ранними компиляторами код обязывает анализировать format и аргументы, чтобы найти требуемый размер.На данный момент, код почти готов сделать вашу собственную полную my_vsnprintf().Я бы искал существующие решения для этого.@user694733.


Даже с C99 существуют ограничения среды для *printf().

Количество символов, которое может быть создано любымединичное преобразование должно быть не менее 4095. C11dr §7.21.6.1 15

Так что любой код, который пытается char buf[10000]; snprintf(buf, sizeof buf, "%s", long_string);, рискует столкнуться даже с достаточным buf[], но с strlen(long_string) > 4095.

Это означает, что быстрый и грязный код может подсчитать % и длину формата и сделать разумное предположение, что необходимый размер не превышает:

size_t sz = 4095*percent_count + strlen(format) + 1;

Конечно, дальнейший анализ спецификаторовможет привести к более консервативному sz.Продолжая этот путь , мы заканчиваем , когда пишем наши собственные my_vsnprintf().


Даже с вашей собственной my_vsnprintf() безопасностью только так хорошо.Во время выполнения нет проверки, что format (который может быть динамическим) соответствует следующим аргументам.Для этого требуется новый подход.

Дерзкая самореклама для решения C99 для обеспечения совпадения спецификаторов и аргументов: Отформатированная печать без необходимости указывать спецификаторы сопоставления типов с помощью _Generic .

...