На базе Clang для языка Си реализован режим проверки границ буферов

Инженеры из компании Apple объявили о готовности для тестирования режима “-fbounds-safety” для компилятора Clang, предоставляющего гарантии безопасной работы с буферами в коде на языке Си. Режим включён в состав форка LLVM, поддерживаемого компанией Apple для проекта Swift. В дальнейшем запланирована постепенная передача функциональности “-fbounds-safety” в основную кодовую базу LLVM/Clang.

Отмечается, что предложенный механизм защиты уже активно применяется в продуктах Apple, таких как ядро XNU, прошивки, библиотеки для работы со звуком и декодировщики изображений. Включение режима “-fbounds-safety” снижает производительность приложений в среднем на 5% (разброс от -1% до 29%), увеличивает размер кода на 9.1% (разброс от -1.4% до 38%) и замедляет компиляцию на 11%.

Использование режима “-fbounds-safety” для автоматического выявления выхода за границы области памяти, связанной с указателем, требует добавления в код специальных аннотаций и включения заголовочного файла “ptrcheck.h”. Суть предложенного метода защиты в автоматическом прикреплении проверок соблюдения допустимых границ, добавляемых на основе выставленных вручную аннотаций или известных компилятору размеров.

В отличие от использования в коде расширенных указателей (wide pointer), в которых кроме адреса имеются сведения о верхней и нижней границе буфера, использование режима “-fbounds-safety” не нарушает ABI (Application Binary Interface), не меняет формат экспортируемых указателей и не требует переработки сразу для всего проекта. В режиме “-fbounds-safety” расширенные указатели применяются только в областях, не пересекающихся с ABI, а для указателей, влияющих на ABI, применяются обычные указатели с подстановкой проверок, формируемых на основе аннотаций с информацией о границах.

Аннотации необходимо прикреплять к указателям в полях структур и параметрах функций, указывающих на массив объектов, а также к глобальным переменным с указателями. Для указателей в локальных переменных аннотации добавлять не нужно, так как они автоматически обрабатываются как расширенные указатели, уже включающие информацию о допустимых границах. Подсказки о конструкциях в коде, для которых требуется добавление аннотаций, выводятся компилятором при запуске с флагом “-fbounds-safety”.

Модель защиты на основе “-fbounds-safety” можно внедрять постепенно, файл за файлом, не прерывая разработку всего проекта. Добавление защиты в проект сводится к указанию аннотаций в определённом файле с кодом, устранению предупреждений компилятора и проведению тестирования работы программы, после чего данные этапы повторяются для следующего файла. Код с добавленными аннотациями остаётся совместим с обычным Си-кодом и компиляторами, не поддерживающими “-fbounds-safety” (при сборке другими компиляторами или при сборке без флага “-fbounds-safety” просто не будут добавлены дополнительные проверки границ).

Во время работы программы, в случае выявления обращения за пределы допустимых границ, генерируется исключение и программа завершает своё выполнение. Аварийное завершение также может произойти при указании некорректных аннотаций, поэтому при использовании “-fbounds-safety” необходимо проведение дополнительного тестирования работы программы.

В примере ниже в параметр “int *p” вставлена аннотация “__counted_by(n)”, добавляющая дополнительную проверку на допустимые границы, действующую во время выполнения. Если попытаться скомпилировать код в режиме “-fbounds-safety” без указания данной аннотации, компилятор выведет предупреждение об отсутствии информации о границах массива при обработке выражения “p[i] = 0”.

#include void init_buf(int *__counted_by(n) p, int n) { for (int i = 0; i < n; ++i) p[i] = 0; // в режиме “-fbounds-safety” компилятор сам подставит проверку, аналогичную коду “if (i < 0 || i >= n) trap();” }

Для локальных переменных с указателями проверки прикрепляются автоматически, например:

void foo(int i){ char *buf = (char *)malloc(10); // для указателя buf будут сохранены сведения о границах buf[i] = 0xff; // будет автоматически подставлена проверка “if (buf + i < buf || buf + i >= buf + 10) trap();” }

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

for (size_t i = 0; i < count; ++i) { buf[i] = i; // проверка “if (i < 0 || i >= count) trap()” добавлена не будет, так как выше уже имеется условие “i < count” и i не может быть меньше 0. }

Основные аннотации:

  • “__counted_by(N)” – определяет размер буфера в элементах целевого типа.
  • “__sized_by(N)” – определяет размер буфера в байтах.
  • ” __ended_by(P)” – задаёт верхнюю границу буфера.
  • “__null_terminated” – учитывает нулевой символ в качестве конца буфера.
  • “__single” – привязывает указатель к одному объекту. Применяется по умолчанию для указателей, влияющих на ABI, если явно не выставлена аннотация.
  • “__bidi_indexable” – расширенный указатель с информацией о верхней и нижней границах. Применяется по умолчанию для указателей, не влияющих на ABI.
  • “__indexable” – расширенный указатель с информацией о верхней границе.
  • “__unsafe_indexable” – указатель без проверки границ (для переносимости с незащищённым кодом, например, для получения указателей из внешнего кода).

Release. Ссылка here.