Пару лет назад в статическом анализаторе кода PVS-Studio появился ряд диагностических правил для проверки соответствия текста программ стандарту MISRA C и MISRA C++. Увидев интерес и собрав feedback, команда разработчиков стала дальше развивать анализатор в этом направлении. В статье будет рассказано про стандарт MISRA C/C++, отчёт MISRA Compliance, про то, что мы уже успели сделать и что собираемся достичь до конца года.
Наша компания начала работу над статическим анализатором кода ещё в 2006 году. В то время в цифровом мире начался плавный процесс миграции приложений с 32-битных систем на 64-битные. И многие разработчики стали сталкиваться с неожиданными проблемами. Продукт, который тогда ещё назывался Viva64, помогал искать программные ошибки, возникавшие после переноса приложения на 64-битные системы. Далее анализатор учился находить в проектах паттерны, связанные с опечатками, неинициализированными переменными, недостижимым кодом, неопределённым поведением и пр. На данный момент в арсенале анализатора уже свыше 1000 диагностик.
До 2018 мы позиционировали PVS-Studio как инструмент для выявления ошибок в коде. В 2018 мы поняли, что существенная часть ошибок, которые мы научились находить, одновременно является потенциальными уязвимостями. Начиная с 2018 года, PVS-Studio является средством статического тестирования защищённости приложений (Static Application Security Testing, SAST). Тогда же мы начали классифицировать уже написанные и новые диагностики в соответствии с Common Weakness (CWE), SEI CERT Coding (CERT), MISRA C/C++. В 2021 году этот список пополнил AUTOSAR.
На появление поддержки стандарта MISRA и AUTOSAR повлияло и то, что в 2018 году мы начали поддержку встраиваемых систем. В анализаторе были поддержаны:
На нашем сайте можно найти подробную инструкцию по использованию PVS-Studio для embedded-разработки.
В отличие от десктопных проектов, многие embedded-разработчики уже пишут проекты с учётом MISRA рекомендаций. И мы подумали, что поддержка стандарта в нашем анализаторе будет однозначно полезна. С тех пор мы неспеша реализовывали правила этого стандарта и собирали фидбек.
Мы ждали появления спроса, и он появился. Люди писали нам, интересовались возможностями анализатора, пробовали анализировать свои проекты. Для нас это означало, что пора развивать MISRA-направление дальше. Интерес был больше к стандарту MISRA C, нежели к MISRA C++, поэтому мы решили для начала повысить покрытие MISRA C. Еще пользователей интересовал отчёт MISRA Compliance, который мы тоже недавно поддержали.
А теперь давайте поговорим о самом стандарте MISRA C/C++.
Стандарт MISRA предназначен для встраиваемых критическиx систем, где к программам предъявляются высокие требования по безопасности, надёжности и переносимости. Такие системы используются в автомобильной промышленности, авиастроении, медицине, космонавтике и других сферах. В общем, везде, где цена программной ошибки – жизнь и здоровье людей или очень большие финансовые и/или репутационные потери.
Стандарт MISRA C предназначен для программ на языке C. Стандарт периодически обновляется и на данный момент содержит 143 правила и 16 директив. Правила классифицируют по категориям:
Так как Required правил намного больше остальных, давайте рассмотрим несколько примеров.
Rule MISRA-C-11.8. Преобразование типов не должно удалять квалификатор const/volatile из типа, на который указывает указатель. Отклонение от правила ищет диагностика V2567. Пример отклонения, найденный в проекте reliance-edge, использующем стандарт MISRA C:
V2567 [MISRA-C-11.8] The cast should not remove 'const' qualification from the type that is pointed to by a pointer. toolcmn.c 66
uint8_t RedFindVolumeNumber(const char *pszVolume)
{
const char *pszEndPtr;
....
ulNumber = strtoul(pszVolume, (char **)&pszEndPtr, 10);
....
}
Правило предупреждает, что данный паттерн ведет к неопределённому поведению.
Rule MISRA-C-7.1. Восьмеричные числовые литералы не должны использоваться. Отклонение от правила ищет диагностика V2501. В том же проекте reliance-edge были найдены такие числовые литералы:
V2501 [MISRA-C-7.1] Octal constant '0666' should not be used. fsstress.c 1376
static void creat_f(int opno, long r)
{
int e;
pathname_t f;
int fd;
....
fd = creat_path(&f, 0666); // <=
e = fd < 0 ? errno : 0;
....
}
Правило предупреждает, что использование восьмеричных литералов может затруднить восприятие кода, особенно при быстром просмотре. Неправильная интерпретация фактического числового значения может приводить к разнообразным ошибкам.
Rule MISRA-C-11.1. Преобразование между указателем на функцию и любым другим типом не должно происходить. Отклонение от правила ищет диагностика V2590.
V2590 Conversions should not be performed between pointer to function and any other type. Consider inspecting the '(fp) & foo' expression.
void foo(int32_t x);
typedef void (*fp)(int16_t x);
void bar(void)
{
fp fp1 = (fp)&foo;
}
Указатель на функцию fp1 принимает значение указателя на функцию foo, которая не соответствует по аргументам и возвращаемому значению. Стандарт языка позволяет такие преобразования, но правило MISRA С предупреждает, что они приводят к неопределенному поведению.
Если вы только начинаете использовать стандарт MISRA и разом наложили все правила на ваш код, то он будет выглядеть примерно так:
Почти для каждой конструкции языка есть своё правило, а то и несколько, поэтому писать код в соответствии со стандартом MISRA более затруднительно. Хорошо, что в этом помогают статические анализаторы кода, которые имеют в своём арсенале диагностики нарушений правил стандарта.
Правила MISRA написаны с учётом тонкостей и опасных возможностей языка. Соблюдение дотошных правил позволяет разработчикам легче писать безопасный код. Они легче замечают ошибки. Им не приходится держать все неочевидности языка в голове и продумывать, как поведет себя программа в другой программной среде или на другом железе. Разработчики из сообщества MISRA досконально изучили весь стандарт языка C в поисках способа прострелить себе ногу, и теперь мы смело можем использовать их опыт, а не учить стандарт языка от корки до корки.
Соблюдение правил накладывает много ограничений на код, однако довольно сильно уменьшает вероятность возникновения ошибки во время работы программы, например, в двигателе самолёта. Высокая безопасность приложения окупает время и средства, затраченные на поддержание соответствия кода стандарту. Подробнее о стандарте и об использовании его в ваших проектах можно почитать в статье Что такое MISRA и как её готовить.
А теперь перейдём к развитию нашего статического анализатора в сторону MISRA.
Сейчас, когда мы знаем, что есть разработчики, которые хотят использовать наш анализатор для проверки своего кода на предмет соответствия стандарту MISRA, мы вкладываем много сил на его развитие в этом направлении.
Увидев спрос на анализ кода в соответствии с MISRA C, мы стали развивать это направление до конкурентоспособного уровня, выставив себе цели:
Начиная с апреля, мы повысили приоритет для задач по написанию MISRA С диагностик. Также мы увеличили команду, что ещё больше ускорило процесс. На данный момент покрытие MISRA C составляет уже 65%, к ноябрю планируется достичь покрытия в 75%, а к январю 2022 года – 80% или более.
Пока писалась эта статья генерация отчёта MISRA Compliance уже была добавлена в бета-версию анализатора. Утилита PlogConverter.exe для Windows и plog-converter для Linux теперь могут преобразовывать сырой отчёт анализатора в отчёт формата MISRA Compliance. Подробнее об этом отчёте будет написано ниже.
Приведу пару примеров из недавно написанных диагностик MISRA C.
V2594. MISRA. Controlling expressions should not be invariant.
Контролирующее выражение в управляющих конструкциях if, ?:, while, for, do, switch не должно быть инвариантно, то есть не должно всегда приводить к выполнению одной и той же ветки кода. Если контролирующее выражение содержит инвариантное значение, то это может свидетельствовать о программной ошибке.
void adjust(unsigned error)
{
if (error < 0)
{
increase_value(-error);
}
else
{
decrease_value(error);
}
}
В данном примере допущена ошибка: из-за того, что функция принимает беззнаковое число, результат проверки условия всегда будет ложным. В итоге всегда будет вызываться только функция decrease_value, а ветка кода с вызовом функции increase_value может быть удалена компилятором.
V2598. MISRA. Variable length array types are not allowed.
Объявление массивов, имеющих неконстантный размер (variable-length array, VLA), может привести к переполнению стека и потенциальным уязвимостям в программе.
void foo(size_t n)
{
int arr[n];
// ....
}
Передача большого числа n может привести к переполнению стека, так как массив станет слишком большим и займет больше памяти, чем есть на самом деле.
Стандарт MISRA C насчитывает 143 правила и 16 директив. Хотелось бы иметь какой-то общий отчёт, который в удобной форме показывал бы соответствие кода стандарту, а также содержал информацию об отклонениях от каждого из правил. Такой отчёт существует, и называется он MISRA Compliance.
Стандарт MISRA C признаёт, что придерживаться всех MISRA правил может быть нецелесообразно. Поэтому он требует выдавать отчёт MISRA Compliance о соответствии кода всем Mandatory правилам и допускает отклонения (deviations) от Required правил. При этом все такие отклонения должны быть подтверждены разработчиком и задокументированы.
Как было сказано ранее, бета с возможностью генерации такого отчёта уже вышла. Сейчас он представляет собой HTML-страницу, которая генерируется утилитой PlogConverter.exe и plog-converter на Windows и Linux соответственно.
Отчёт содержит таблицу соответствия кода каждому из правил MISRA C и общее заключение.
В столбце Guideline содержатся номера правил и директив стандарта MISRA C.
В столбце Category – категория правила или директивы, указанная в стандарте.
Стандарт MISRA C разрешает пользователям поднимать уровень значимости правила. Поэтому в столбце Recategorication будет отражена новая категория правила или директивы, установленная пользователем в соответствии с GRP (Guideline Re-categorization Plan). Возможны только три перехода:
В нашем случае GRP представляет собой txt-файл. Пример содержания файла с допустимыми переходами:
Rule 15.3 = Mandatory
Rule 16.4 = Mandatory
Rule 17.5 = Required
Если этот файл содержит понижение категории, то plog-converter выдаст сообщение об ошибке и не сгенерирует отчёт.
Столбец Compliance содержит статус соответствия проверяемого кода правилу:
Под таблицей в отчёте будет выведено заключение о том, соответствует ли проверенный код стандарту MISRA C или нет. Код соответствует стандарту, если:
Если проверяемый код не соответствует стандарту, то в таблице будут выделены красным статусы правил, которые были нарушены.
До начала октября 2021 года отчёт MISRA Compliance будет доступен в бете, которую можно запросить через форму обратной связи. После планируется выпустить релиз. PVS-Studio версии 7.15 уже будет иметь возможность генерации такого отчёта.
Для генерации отчёта MISRA Compliance под Windows необходимо сначала выполнить анализ проекта. Затем запустить утилиту Plog-converter.exe со следующими аргументами:
"C:\Program Files (x86)\PVS-Studio\PlogConverter.exe" "path_to_report_file" \
-t misra -o "path_to_MISRA_report" --grp "path_to_grp.txt"
Для генерации под Linux также нужно выполнить анализ, а затем позвать plog-converter.
plog-converter "path_to_report_file" -t misra -o "path_to_MISRA_report" \
--grp "path_to_grp.txt"
Отчет MISRA Compliance будет свидетельствовать о соответствии кода вашего проекта стандарту MISRA. Мы работаем над тем, чтобы статусов Not supported было как можно меньше в вашем отчёте. Результатом процесса создания новой MISRA диагностики является не только код диагностики и текст документации, а ещё много чего полезного. Именно об этом пойдет речь дальше.
Что же ещё даёт процесс создания MISRA-диагностик?
Во-первых, это осознание принципов написания безопасного кода. Если при разработке диагностик общего назначения, мы стараемся снизить число срабатываний до минимума, то очередная MISRA диагностика может выдать тысячи срабатываний на среднем проекте. После прогона новой диагностики на нашей тестовой базе проектов отчёт может выглядеть вот так:
Во-вторых, это изучение неожиданных возможностей и специфики языка. Например, все ли помнят о назначенной инициализации и знают, как правильно использовать ключевое слово static в деклараторе формального параметра в виде массива?
int array[] = { 1, 2, 4, [8]={256} };
void foo(int [static 20]);
В-третьих, знакомство с миллионом способов получить неуточненное, неопределенное или зависящее от реализации поведение. Понимание того, какой код является потенциально опасным.
А еще работа с новой MISRA-диагностикой может послужить возникновению идей для диагностик общего назначения.
Про последний пункт расскажу поподробней. Идеи для новых диагностик общего назначения обычно возникают:
Так вот, совсем недавно появление новой диагностики общего назначения было обязано реализации одного из MISRA C правил. Правило гласит: 'Octal and hexadecimal escape sequences should be terminated'. Зачем же так делать? Приведу пример:
const char *str = "\x0exit";
Строковый литерал в данном примере имеет длину в 4 символа, а не 5, как может показаться на первый взгляд. Последовательность \x0e считается за один символ с кодом 0xE, а не за символ с нулевым кодом и букву e.
Поэтому стандарт настаивает на завершении escape-последовательности одним из двух способов:
Например, так:
const char *str1 = "\x0" "exit";
const char *str2 = "\x1f\x2f";
Это правило мы посчитали полезным и для проектов, которые не пишутся в соответствии со стандартом MISRA C. Таким образом у нас появилось сразу две диагностики: V1074 и V2602. Под коробкой это, конечно, один и тот же код.
Другой случай, когда работа с MISRA послужила причиной создания новой диагностики, был во время добавления проекта covid-sim в нашу базу проектов для тестирования анализатора. Проект небольшой и кроссплатформенный, поэтому он также подошел и для тестирования MISRA диагностик. Перед добавлением нового проекта в тестовую базу его следует проанализировать на предмет ложных срабатываний. Этот процесс был ничем не примечателен, если бы не одно, казалось, ложное срабатывание V2507:
if (radiusSquared > StateT[tn].maxRad2) StateT[tn].maxRad2 = radiusSquared;
{
SusceptibleToLatent(a->pcell);
if (a->listpos < Cells[a->pcell].S)
{
UpdateCell(Cells[a->pcell].susceptible, a->listpos, Cells[a->pcell].S);
a->listpos = Cells[a->pcell].S;
Cells[a->pcell].latent[0] = ai;
}
}
StateT[tn].cumI_keyworker[a->keyworker]++;
Диагностика V2507 находит тела условных операторов, не обернутые в фигурные скобки.
Как видно, фигурные скобки есть, значит мы зря наговариваем на код? Если присмотреться внимательнее, становится понятно, что тело оператора if находится на одной строчке с выражением условия. А вот фигурные скобки уже никак не относятся к оператору if.
Во-первых, данный пример доказывает, что стандарт MISRA работает и действительно уменьшает число ошибок в критических встраиваемых системах. Ведь если бы тело оператора if было заключено в фигурные скобки, то логическую ошибку было бы легко заметить.
Во-вторых, у нас родилась идея для новой диагностики общего назначения. Она должна выдавать предупреждение, если для оператора if выполняются следующие условия:
Более подробно прочитать про появление диагностики V1073 можно в этой заметке.
Надежность и безопасность кода требует ограничений в виде соблюдения строгих и дотошных правил, касающихся определенного стиля написания кода, отказа от опасных конструкций языка и функций, неправильное использование которых приводит к сбоям. Соблюдение правил может проверить статический анализатор, например PVS-Studio. Результатом проверки на соответствие будет являться отчёт MISRA Compliance.
Еще больше про повышение безопасности кода с помощью статического анализа можно узнать в следующих статьях: