Trusted-CPP Documentation

Назначение проекта

Проект Trusted-CPP демонстрирует реализацию концепции гарантий безопасной разработки программного обеспечения на C++ на уровне синтаксиса языка с сохранением обратной совместимости с уже существующим исходным кодом.

Это позволяет изменить подход к безопасности программного обеспечения с точечного поиска и исправления отдельных ошибок и уязвимостей на гарантию их отсутствия в исходном коде программы. Другими словами, если код скомпилировался корректно, значит, в нём отсутствуют определённые классы ошибок, а следовательно - и связанные с ними уязвимости.

Проект Trusted-CPP состоит из двух компонентов. Первый компонент - это заголовочный файл «trusted-cpp.h». Он содержит шаблонные программные классы и основные настройки, необходимые для анализатора безопасного кода. Второй компонент - это статический анализатор кода C++ в виде плагина для компилятора clang.

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

Детали реализации Trusted-CPP:


Простой пример для начала работы

Загрузите заголовочный файл, плагин компилятора и вспомогательный скрипт запуска, который упрощает использование плагина.

Вспомогательный скрипт trusted-cpp.sh автоматически добавляет аргументы clang для загрузки плагина и передачи ему аргументов командной строки, чтобы каждый раз не писать что-то вроде этого:

    $ clang++ -Xclang -load -Xclang ./trusted-cpp_clang.so -Xclang -add-plugin -Xclang trust -Xclang -plugin-arg-trust -Xclang verbose example.cpp

заменить на простой вызов trusted-cpp.sh -trust verbose example.cpp

При компиляции файла invalidate.cpp, содержащего следующий код, не выявится никаких ошибок.

    std::vector vect(100000, 0);
    auto x = vect.begin();
    auto &y = vect[0];
    vect = {};
    std::sort(x, vect.end()); // Error
    y += 1; // Error

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

    $ ./trusted-cpp.sh -std=c++20 -fsyntax-only invalidate.cpp

../invalidate.cpp:26:5: warning: using main variable 'vect'
   26 |     vect = {};
      |     ^
../invalidate.cpp:31:15: error: Using the dependent variable 'x' after changing the main variable 'vect'!
   31 |     std::sort(x, vect.end()); // Error
      |               ^
../invalidate.cpp:36:5: error: Using the dependent variable 'y' after changing the main variable 'vect'!
   36 |     y += 1; // Error
      |     ^
3 warnings and 2 errors generated.

Описание концепции безопасной разработки на C++

В основе проекта лежит концепция безопасной работы с памятью из языка NewLang (trust-lang), которая была портирована на C++ в виде отдельной библиотеки memsafe и впоследствии расширена, в том числе за счёт реализации части требований STEELMAN, адаптированных для C++.

Под термином «безопасная разработка на C++» имеется в виду:

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

Другими словами, если исходный код C++ скомпилировался корректно, значит, в нём отсутствуют ошибки, а следовательно - и уязвимости из-за:

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

Непосредственный анализ исходного кода Trusted-CPP выполняется плагином компилятора, но сам исходный код остаётся обычной программой на C++, и его можно компилировать любым компилятором без использования плагина, а также применять линтеры и дополнительные статические анализаторы.

Анализ исходного кода в Trusted-CPP основан на маркировке элементов с помощью пользовательских C++ атрибутов, которые появились в C++11. Это очень похоже на предложения в профилях безопасности p3038 от Bjarne Stroustrup и P3081 от Herb Sutter, но не требует разработки нового стандарта C++.

В настоящий момент проверка синтаксических правил при подключении плагина активируется автоматически во время компиляции за счёт использования встроенной функции __has_attribute (trust). Если плагин во время компиляции отсутствует, то использование пользовательских атрибутов отключается с помощью макросов препроцессора, чтобы подавить предупреждения вида warning: unknown attribute 'trust' ignored [-Wunknown-attributes].

Принципиальной особенностью Trusted-CPP является возможность маркировки различных элементов с помощью пользовательских C++ атрибутов не только во время определения класса или функции, но и в произвольном месте исходного текста программы (или даже во внешнем конфигурационном файле). Эта особенность позволяет маркировать важные классы, функции или их аргументы уже после их определения и без необходимости изменять ранее написанный код.


Номинальная (именная) типизация typedef

В C/C++ объявление typedef создаёт псевдоним для существующего типа, но при приведении типов тип и его псевдоним не различаются. Добавление номинальной типизации для typedef позволяет предотвратить случайную эквивалентность типов по сравнению со структурной типизацией и означает, что две переменные являются типосовместимыми тогда и только тогда, когда их объявления содержат имена одного и того же типа.

Пример кода с использованием номинальной (именной) типизации typedef

Безопасная работа с памятью

Безопасная работа с памятью полностью совместима с C++ кодом и STL шаблонами и реализована за счёт использования следующих видов адресации (адресных переменных):

Основное отличие сильных и слабых ссылок от соответствующих стандартных шаблонов заключается в способе обращения к объекту при разыменовании ссылки: это выполняется с помощью создания временной переменной, и уже через неё получается непосредственный доступ к самим данным (объекту). Пример кода

Все остальные переменные по значению (variable by value) хранят данные непосредственно в самой переменной. Сздать ссылку на переменную по значению нельзя, и для этих целей нужно использовать умный указатель (ссылочную переменную).

Контроль многопоточного доступа к данным

Безопасное многопоточное программирование - это автоматическое устранение проблем, приводящих к «состоянию гонки данных».

А чтобы минимизировать логические ошибки при захвате объекта синхронизации (если это требуется у переменных с контролем многопоточного доступа), попытка захвата доступа и разыменования ссылки выполняются как одна операция.

Атоматическая переменная доступа к данным является не только временным владельцем сильной ссылки, но и выполняет функции владения объектом межпотоковой синхронизации в стиле std::lock_guard, время жизни которого ограничено текущей областью видимости и управляется компилятором автоматически.

Открытые и закрытые области видимости переменных

Реализация безопасного многопоточного программирования основана на требованиях STEELMAN на основе открытых и закрытых областей видимости для внешних переменных. Фактически это реализация пункта 5G из требований STEELMAN, только доработанная под C++ и ООП.

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

Закрытые области видимости - это по своей сути инверсия ООП (ООП наоборот), когда скрываются не внутренние данные объекта от внешнего окружения, а наоборот: из текущей области видимости ограничивается доступ к переменным из внешнего окружения, доступ к которым возможен только после их явного перечисления в исходном коде программы. (На этой же основе реализованы и чистые функции, когда внешнее окружение становится недоступно из тела функции).

Создание закрытой области видимости или её переопределение происходит с помощью макроса TRUST_USING_EXTERNAL(""), который применяется к определению функции, класса, метода класса или отдельного выражения. Пример использования закрытых областей видимости можно посмотреть в примерах ниже.

Для целей безопасного многопоточного доступа в C++ реализуется следующая схема:

Маркировка атрибутами происходит однократно и наследуется для производных классов, а дальше компилятор сам автоматически следит за их корректным использованием, т.е. чтобы при создании потока выполнения его тело было отмечено атрибутом THREAD, а передаваемые внутрь потока аргументы были THREADSAFE.

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

Пример контроля многопоточного доступа к данным приведён ниже

Дополнительные возможности

Ручной и автоматический контроль стека от переполнения

Ручной и автоматический контроль стека от переполнения - это единственная функциональность, которая вносит изменения в генерируемый код при компиляции программы, поэтому данная часть была выделена в отдельный проект stack-check, который можно использовать как совместно с Trusted-CPP, так и без него.

Формальный анализ доказательства корректности программы *

Фактически, это реализация статической проверки динамических выражений AoRTE (“Absence of Run-Time Errors”), которая не даёт ложных срабатываний, хотя возможны ложные отрицательные результаты. То есть, если ошибки при компиляции отсутствуют, то можно быть уверенными, что проблем в коде нет, тогда как указание на возможную ошибку не всегда соответствует действительности и инструмент может ошибаться.

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

Формальный анализ доказательства корректности программы реализуется по принципу gnatprove для языка Ada и использует три макроса для определения предусловий, постусловий и утверждений: TRUST_ASSERT_PRED(), TRUST_ASSERT_POST() и TRUST_ASSERT() соответственно. Это среднее между assert и static_assert, которое выполняется во время компиляции программы, но в выражении могут использоваться неконстантные значения (неконстантные выражения должны быть вычислимы на уровне типов данных или описаны в пред- и постусловиях).


*) - Данная функциональность запланирована для реализации, но пока находится на паузе до завершения основной части проекта

Примеры кода

Пример использования номинальной (именной) типизации typedef

Создание типа данных с номинальной типизацией выполняется с помощью атрибута, который раскрывается при использовании макроса TRUST_NOMINAL или перечисляется в макросе TRUST_NOMINAL_TYPES(…).

    typedef int IntType;

    int int_value = 0;
    IntType IntType_value = 0;
    int int_value_cast = IntType_value;  // OK
    IntType IntType_cast = int_value; // OK

    TRUST_NOMINAL typedef int IntSubType; // Номинальная типизация во время определения типа

    IntSubType IntSubType_value = 0; // OK
    int int_value_cast2 = IntSubType_value; // OK
    IntSubType IntSubType_cast = int_value; // ERROR

    IntType IntType_cast2 = IntSubType_value; // ERROR

    TRUST_NOMINAL_TYPES("IntType"); // Номинальная типизация для существующего типа

    IntType IntType_value2 = 0;
    IntType IntType_cast3 = int_value; // ERROR
    IntType IntType_cast4 = IntSubType_value; // ERROR

Пример использования закрытых областей видимости

Для указания импортируемых переменных можно использовать маску для имени переменной или области имён

int global = 0;

int func_default() {
    // Открытые области видимости по умолчанию
    return global;
}

TRUST_USING_EXTERNAL("") // Запретить доступ ко всем внешним переменным
int func_closed() {
    // Фактически, это чистая функция без побочных эффектов
    return global; // ERROR
}

TRUST_USING_EXTERNAL("global") // Разрешён доступ только к переменной global
int func_using_external() {
    return global; // OK
}

Применение безопасного многопоточного программирования

Однократная маркировка функций и классов, создающих отдельные потоки выполнения.

// Установить атрибут 'thread' для первых аргументов конструктора для классов std::thread и std::jthread
TRUST_SET_ATTR_ARGS(thread, std::thread, 1);
TRUST_SET_ATTR_ARGS(thread, std::jthread, 1);

// Установить атрибут 'threadsafe' для всех аргументов конструкторов классов std::thread и std::jthread
TRUST_SET_ATTR_ARGS(threadsafe, std::thread::thread, 0);
TRUST_SET_ATTR_ARGS(threadsafe, std::jthread::jthread, 0);

// Отметить у функции pthread_create атрибутами
// 'thread' третий аргумент и 'threadsafe' четвёртый
TRUST_SET_ATTR_ARGS(thread, pthread_create, 3);
TRUST_SET_ATTR_ARGS(threadsafe, pthread_create, 4);

// Установить атрибут 'threadsafe' для потокобезопасных шаблонов
TRUST_SET_ATTR(threadsafe, std::atomic);
TRUST_SET_ATTR(threadsafe, trust::SyncTimedShared);

Пример кода с контролем функции потока от «состояния гонки»

uint64_t notrust_count = 0; // Без установки атрибута THREADSAFE
void *thread_notrust(void *arg) { // Поток без атрибута THREAD
    ++notrust_count;              // Гонка
    return nullptr;
}

std::atomic<uint64_t> trust_count = 0; // Автоматическая маркировка THREADSAFE для std::atomic
TRUST_THREAD void *thread_trust(void *arg) { // Функция потока (маркировка атрибутом THREAD)
    trust_count++;
    notrust_count++; // ERROR: Expected attribute 'threadsafe' for 'notrust_count'
    return nullptr;
}

Пример кода с созданием потоков с контролем потенциальных ошибок «состояния гонки данных»


    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_t tid;

    pthread_create(&tid, &attr, thread_notrust, nullptr); // ERROR
    // error: Expected attribute: 'thread' for 3 argument
    //     pthread_create(&tid, &attr, thread_notrust, nullptr);
    //                                 ^

    pthread_create(&tid, &attr, thread_trust, nullptr); // OK

    {
        std::thread t_lambda([&]() {
            for (auto i = 0; i < 1'000'0000; ++i)
                ++notrust_count; // ERROR
            // error: Expected attribute 'threadsafe' for 'notrust_count'
            //              ++notrust_count;
            //                ^
        });

        std::thread t_notrust(thread_notrust, nullptr); // ERROR
        // error: Expected attribute: 'thread' for 1 argument
        //        std::thread t_notrust(thread_notrust, nullptr);
        //                              ^

        std::thread t_trust(thread_trust, nullptr);

        t_lambda.join();
        t_notrust.join();
        t_trust.join();
    }

Пример разыменования ссылки и захвата блокировки доступа для умных указателей


trust::Shared<int, trust::SyncTimedMutex> var_sync(1); // наследник от std::shared_ptr
trust::Weak< trust::Shared<int, trust::SyncTimedMutex> > var_weak = var_sync.weak(); // наследник от std::weak_ptr


TRUST_THREAD void func_thread(){
    try{
        // Нельзя захватить в статическую переменную
        // static auto static_fail1(var_sync.lock());

        auto sync = var_sync.lock();  // или *var_sync
        auto weak = var_weak.lock();  // или *var_weak

        *sync += *weak;

    } catch(...){
        // Обработка ошибки блокировки доступа или разыменования ссылки
    }
}

Выводы и итоги

Этот документ представляет собой простое описание проекта с примерами решения очевидных и понятных задач безопасного программирования на C++.

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