# V576. Incorrect format. Consider checking the Nth actual argument of the 'Foo' function.

Анализатор обнаружил потенциальную ошибку при использовании функций форматного вывода ('printf', 'sprintf', 'wprintf' и так далее). Строка форматирования не соответствует передаваемым в функцию фактическим аргументам.

Рассмотрим простой пример:

    int A = 10;
    double B = 20.0;
    printf("%i %i\n", A, B);

Согласно строке форматирования, функция 'printf' ожидает два фактических аргумента типа 'int'. Однако второй аргумент имеет значение типа 'double'. Подобное несоответствие приводит к неопределённому поведению программы. Например, к распечатке бессмысленных значений.

Корректный вариант:

    int A = 10;
    double B = 20.0;
    printf("%i %f\n", A, B);

Ошибочных вариантов использования функции 'printf' можно привести огромное количество. Рассмотрим только несколько типовых примеров, которые чаще всего можно встретить в программах.

Распечатка адреса

Очень часто значение указателя пытаются распечатать, используя следующий код:

    int *ptr = new int[100];
    printf("0x%0.8X\n", ptr);

Этот код ошибочен, поскольку будет работать только в тех системах, где размер указателя совпадает с размером типа 'int'. А, например, в Win64 этот код уже распечатает только младшую часть указателя 'ptr'. Корректный вариант кода:

    int *ptr = new int[100];
    printf("0x%p\n", ptr);

Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в качестве фактического аргумента в функцию передаётся очень странное значение.

Неиспользуемые аргументы

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

    int nDOW;
    #define KEY_ENABLED "Enabled"
    ...
    wsprintf(cDowKey, L"EnableDOW%d", nDOW, KEY_ENABLED);

Очевидно, что параметр 'KEY_ENABLED' здесь лишний, или код должен был выглядеть следующим образом:

    wsprintf(cDowKey, L"EnableDOW%d%s", nDOW, KEY_ENABLED);

Недостаточное количество аргументов

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

    char* salloc(register int nbytes)
    {
        register char* p;
        p = (char*) malloc((unsigned)nbytes);
        if (p == (char *)NULL)
        {
            fprintf(stderr, "%s: out of memory\n");
            exit(1);
        }
        return (p);
    }

Если функция 'malloc' вернёт значение 'NULL', то программа не сможет корректно сообщить о нехватке памяти и завершить свою работу. Она аварийно завершится или распечатает непонятный текст. В любом случае, подобное поведение усложнит анализ причины неработоспособности программы.

Путаница с signed/unsigned

Очень часто программисты используют спецификатор печати знаковых значений (например '%i') для печати переменных типа 'unsigned'. И наоборот. Эта ошибка, как правило, не критична и так сильно распространена, что в анализаторе она имеет низкий приоритет. Во многих случаях подобный код успешно работает и даёт сбой только при больших или отрицательных значениях. Рассмотрим код, который хотя не корректен, но успешно работает:

    int A = 10;
    printf("A = %u\n", A);
    for (unsigned i = 0; i != 5; ++i)
      printf("i = %d\n", i);

Хотя здесь имеется несоответствие, это код на практике печатает корректные значения. Конечно, всё равно так лучше не делать и написать корректно:

    int A = 10;
    printf("A = %d\n", A);
    for (unsigned i = 0; i != 5; ++i)
      printf("i = %u\n", i);

Ошибка проявит себя в том случае, если в программе имеются большие или отрицательные значения. Пример:

    int A = -1;
    printf("A = %u", A);

Вместо строки "A = -1" программа распечатает "A = 4294967295". Корректный вариант:

    printf("A = %i", A);

Широкие строки (Wide character string)

У Visual Studio есть неприятная особенность, что он нестандартно интерпретирует формат строки для печати широких символов. В результате анализатор помогает диагностировать ошибку, например, в таком коде:

    const wchar_t *p = L"abcdef";
    wprintf(L"%S", p);

В Visual C++ считается, что '%S' предназначен для печати строки типа 'const char *'. Поэтому с точки зрения Visual C++ правильным является код:

    wprintf(L"%s", p);

Начиная с Visual Studio 2015 предлагается решение этой проблемы, чтобы писать переносимый код. Для совместимости с ISO C (C99) следует указать препроцессору макрос _CRT_STDIO_ISO_WIDE_SPECIFIERS.

В этом случае, код:

    const wchar_t *p = L"abcdef";
    wprintf(L"%S", p);

является правильным.

Анализатор знает про '_CRT_STDIO_ISO_WIDE_SPECIFIERS' и учитывает его при анализе.

Кстати, если вы включили режим совместимости с ISO C (объявлен макрос '_CRT_STDIO_ISO_WIDE_SPECIFIERS'), вы можете в отдельных местах вернуть старое приведение, используя спецификатор формата '%Ts'.

Вся эта история с широкими символами достаточно запутанная и выходит за пределы документации. Чтобы лучше разобраться в вопросе, предлагаем ознакомиться со следующими ссылками:

- Bug 1121290 - distinguish specifier s and ls in the printf family of functions
- Visual Studio swprintf is making all my %s formatters want wchar_t * instead of char *
- Update. В 2019 году появилась статья, которая рассказывает, почему получилась путаница: The sad history of Unicode printf-style format specifiers in Visual C++.

Дополнительная настройка диагностики

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

Дополнительные ресурсы:

- Wikipedia. Printf.
- MSDN. Format Specification Fields: printf and wprintf Functions.

Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования форматной строки, Ошибки разыменования нулевого указателя/нулевой ссылки [* см. примечание касательно языков C#, Java].

Данная диагностика классифицируется как:

CWE-628 CWE-685 CWE-686 CWE-687 CERT-DCL11-C CERT-FIO47-C

Взгляните на примеры ошибок, обнаруженных с помощью диагностики V576.