MISRA C: борьба за качество и безопасность кода

Пару лет назад в статическом анализаторе кода PVS-Studio появился ряд диагностических правил для проверки соответствия текста программ стандарту MISRA C и MISRA C++. Увидев интерес и собрав feedback, команда разработчиков стала дальше развивать анализатор в этом направлении. В статье будет рассказано про стандарт MISRA C/C++, отчёт MISRA Compliance, про то, что мы уже успели сделать и что собираемся достичь до конца года.

0866_MISRA_C_ru/image1.png


С чего все начиналось?

Наша компания начала работу над статическим анализатором кода ещё в 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 году мы начали поддержку встраиваемых систем. В анализаторе были поддержаны:


  • Windows. IAR Embedded Workbench, C/C++ Compiler for ARM C, C++;
  • Windows/Linux. Keil µVision, DS-MDK, ARM Compiler 5/6 C, C++;
  • Windows/Linux. Texas Instruments Code Composer Studio, ARM Code Generation Tools C, C++;
  • Windows/Linux/macOS. GNU Arm Embedded Toolchain, Arm Embedded GCC compiler, C, C++.

На нашем сайте можно найти подробную инструкцию по использованию PVS-Studio для embedded-разработки.

В отличие от десктопных проектов, многие embedded-разработчики уже пишут проекты с учётом MISRA рекомендаций. И мы подумали, что поддержка стандарта в нашем анализаторе будет однозначно полезна. С тех пор мы неспеша реализовывали правила этого стандарта и собирали фидбек.

0866_MISRA_C_ru/image2.png

Мы ждали появления спроса, и он появился. Люди писали нам, интересовались возможностями анализатора, пробовали анализировать свои проекты. Для нас это означало, что пора развивать MISRA-направление дальше. Интерес был больше к стандарту MISRA C, нежели к MISRA C++, поэтому мы решили для начала повысить покрытие MISRA C. Еще пользователей интересовал отчёт MISRA Compliance, который мы тоже недавно поддержали.

А теперь давайте поговорим о самом стандарте MISRA C/C++.


Про стандарт MISRA C/С++

Стандарт MISRA предназначен для встраиваемых критическиx систем, где к программам предъявляются высокие требования по безопасности, надёжности и переносимости. Такие системы используются в автомобильной промышленности, авиастроении, медицине, космонавтике и других сферах. В общем, везде, где цена программной ошибки — жизнь и здоровье людей или очень большие финансовые и/или репутационные потери.

Стандарт MISRA C предназначен для программ на языке C. Стандарт периодически обновляется и на данный момент содержит 143 правила и 16 директив. Правила классифицируют по категориям:


  • Mandatory (10 правил) — самые строгие правила. Их невыполнение почти всегда приводит к ошибке;
  • Required (101 правило) — менее строгие правила. Улучшают читаемость кода, запрещают опасные конструкции языка и использование функций, неправильное использование которых приводит к сбоям, например malloc.
  • Advisory (32 правила) — рекомендации, не обязательные к исполнению.

Так как 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 и разом наложили все правила на ваш код, то он будет выглядеть примерно так:

0866_MISRA_C_ru/image3.png

Почти для каждой конструкции языка есть своё правило, а то и несколько, поэтому писать код в соответствии со стандартом MISRA более затруднительно. Хорошо, что в этом помогают статические анализаторы кода, которые имеют в своём арсенале диагностики нарушений правил стандарта.

Правила MISRA написаны с учётом тонкостей и опасных возможностей языка. Соблюдение дотошных правил позволяет разработчикам легче писать безопасный код. Они легче замечают ошибки. Им не приходится держать все неочевидности языка в голове и продумывать, как поведет себя программа в другой программной среде или на другом железе. Разработчики из сообщества MISRA досконально изучили весь стандарт языка C в поисках способа прострелить себе ногу, и теперь мы смело можем использовать их опыт, а не учить стандарт языка от корки до корки.

Соблюдение правил накладывает много ограничений на код, однако довольно сильно уменьшает вероятность возникновения ошибки во время работы программы, например, в двигателе самолёта. Высокая безопасность приложения окупает время и средства, затраченные на поддержание соответствия кода стандарту. Подробнее о стандарте и об использовании его в ваших проектах можно почитать в статье Что такое MISRA и как её готовить.

А теперь перейдём к развитию нашего статического анализатора в сторону MISRA.


Наши планы и текущий прогресс

Сейчас, когда мы знаем, что есть разработчики, которые хотят использовать наш анализатор для проверки своего кода на предмет соответствия стандарту MISRA, мы вкладываем много сил на его развитие в этом направлении.

Увидев спрос на анализ кода в соответствии с MISRA C, мы стали развивать это направление до конкурентоспособного уровня, выставив себе цели:


  • повысить покрытие стандарта нашими диагностиками до 80% к концу года;
  • сделать возможность генерировать отчёт MISRA Compliance.

Начиная с апреля, мы повысили приоритет для задач по написанию 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 Compliance

Стандарт MISRA C признаёт, что придерживаться всех MISRA правил может быть нецелесообразно. Поэтому он требует выдавать отчёт MISRA Compliance о соответствии кода всем Mandatory правилам и допускает отклонения (deviations) от Required правил. При этом все такие отклонения должны быть подтверждены разработчиком и задокументированы.

Как было сказано ранее, бета с возможностью генерации такого отчёта уже вышла. Сейчас он представляет собой HTML-страницу, которая генерируется утилитой PlogConverter.exe и plog-converter на Windows и Linux соответственно.

Отчёт содержит таблицу соответствия кода каждому из правил MISRA C и общее заключение.

0866_MISRA_C_ru/image4.png

В столбце Guideline содержатся номера правил и директив стандарта MISRA C.

В столбце Category — категория правила или директивы, указанная в стандарте.

Стандарт MISRA C разрешает пользователям поднимать уровень значимости правила. Поэтому в столбце Recategorication будет отражена новая категория правила или директивы, установленная пользователем в соответствии с GRP (Guideline Re-categorization Plan). Возможны только три перехода:


  • Required → Mandatory;
  • Advisory → Required;
  • Advisory → Mandatory.

В нашем случае GRP представляет собой txt-файл. Пример содержания файла с допустимыми переходами:

Rule 15.3 = Mandatory
Rule 16.4 = Mandatory
Rule 17.5 = Required

Если этот файл содержит понижение категории, то plog-converter выдаст сообщение об ошибке и не сгенерирует отчёт.

Столбец Compliance содержит статус соответствия проверяемого кода правилу:


  • Compliant — отклонений от правила не обнаружено;
  • Deviations — отклонения от правила обнаружены, но разработчик обосновал причину, по которой умышлено нарушает это правило. Чтобы анализатор понял об игнорировании конкретного предупреждения, его следует разметить как ложное (Mark as False Alarm). Рядом со статусом Deviations в скобочках указывается число подтвержденных отклонений;
  • Violations — существует хотя бы одно отклонение от правила, которое не задокументировано (не обосновано и не помечено как FA). В скобочках указывается число таких отклонений;
  • Not Supported — данное правило ещё не поддерживается.

Под таблицей в отчёте будет выведено заключение о том, соответствует ли проверенный код стандарту MISRA C или нет. Код соответствует стандарту, если:


  • все Mandatory правила имеют статус Compliant или Not Supported;
  • все Required правила имеют статус Compliant, или Deviations, или Not Supported;
  • правила Advisory имеют любой статус.

Если проверяемый код не соответствует стандарту, то в таблице будут выделены красным статусы правил, которые были нарушены.

До начала октября 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-диагностик?

Во-первых, это осознание принципов написания безопасного кода. Если при разработке диагностик общего назначения, мы стараемся снизить число срабатываний до минимума, то очередная MISRA диагностика может выдать тысячи срабатываний на среднем проекте. После прогона новой диагностики на нашей тестовой базе проектов отчёт может выглядеть вот так:

0866_MISRA_C_ru/image5.png

Во-вторых, это изучение неожиданных возможностей и специфики языка. Например, все ли помнят о назначенной инициализации и знают, как правильно использовать ключевое слово 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-последовательности одним из двух способов:


  • завершением строкового литерала;
  • началом новой 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 выполняются следующие условия:


  • весь условный оператор if записан в одну строчку и имеет только then-ветку;
  • следующий statement после if — это compound statement, и он находится не на той же строке, что и if.

Более подробно прочитать про появление диагностики V1073 можно в этой заметке.


Заключение

Надежность и безопасность кода требует ограничений в виде соблюдения строгих и дотошных правил, касающихся определенного стиля написания кода, отказа от опасных конструкций языка и функций, неправильное использование которых приводит к сбоям. Соблюдение правил может проверить статический анализатор, например PVS-Studio. Результатом проверки на соответствие будет являться отчёт MISRA Compliance.

Еще больше про повышение безопасности кода с помощью статического анализа можно узнать в следующих статьях:


  1. PVS-Studio для поиска дефектов безопасности и защищённости приложений. Отчёт Forrester Research о SAST, Q3 2020.
  2. OWASP, уязвимости и taint анализ в PVS-Studio C#. Смешать, но не взбалтывать.
  3. Технологии, используемые в анализаторе кода PVS-Studio для поиска ошибок и потенциальных уязвимостей.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Konstantin Kochkin. MISRA C: struggle for code quality and security.

© Habrahabr.ru