Статья в продолжение темы безопасной разработки на С++ с примером работающего кода. Кратко предыдущие тезисы:


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


А раз текущее состояние С++ не может гарантировать безопасную разработку на уровне стандартов, то выходит, что:


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

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


Proof of Concept


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


В качестве основы был использован прототип концепции безопасной работы с памятью на С++.


Способ маркировки объектов в программном коде С++ с помощью атрибутов очень похож на предложенный в профилях безопасности p3038 от Bjarne Stroustrup и P3081 от Herb Sutter, но не требует принятия нового стандарта С++ (достаточно использовать уже существующий С++20).


Проект для безопасной работы с памятью на С++ сейчас реализован частично и в качестве основы для разработки плагина компилятора была использована данная концепция безопасной работы с памятью С++.


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


Это немного похоже на владение и заимствование из языка Rust, но реализованное на базе сильных и слабых ссылок (стандартных механизмов С++ shared_ptr и weak_ptr), и поэтому полностью совместима с последним на низком уровне.


Отказ от ответственности


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

Детали реализации:


  • Реализация сделана в виде одного заголовочного файла memsafe.h + плагин для компилятора.
  • Синтаксическая проверка корректности использования классов происходит в плагине, который подключается динамически при компиляции.
  • Каждый используемый шаблонный класс помечен атрибутом [[memsafe(...)]] в соответствии со своим назначением и при проверке лексики поиск имен шаблонных классов выполняется по наличию указанных атрибутов.
  • Плагин разработан для Clang (использована актуальная версия clang-19)
  • Проверка синтаксических правил активируется автоматически во время при подключении плагина за счет использования встроенной функции __has_attribute (memsafe). Если плагин во время компиляции отсутствует, то использование атрибутов отключается с помощью макросов препроцессора для подавления предупреждений вида warning: unknown attribute 'memsafe' ignored [-Wunknown-attributes].
  • Для целей тестирования и отладки плагин может создавать копию анализируемого файла, с добавлением в него меток в виде обычных комментариев о применении синтаксических правил или найденных ошибках для их последующего анализа.
  • Для создания копии файла использует стандартный способ clang::FixItRewriter, вывод которого можно использовать в том числе и в любой среде разработки, которая его поддерживает.

Быстрые примеры:


Фрагмент корректного С++ кода:
    #include "memsafe.h"

    using namespace memsafe;

    namespace MEMSAFE_ATTR("unsafe") {
        // Проверки плагина игнорируются
        VarShared<int> var_unsafe1(1);
        memsafe::VarShared<int> var_unsafe2(2);
        memsafe::VarShared<int> var_unsafe3(3);
    }

    memsafe::VarValue<int> var_value(1);
    memsafe::VarShared<int> var_share(1);
    memsafe::VarGuard<int, memsafe::VarSyncMutex> var_guard(1);

    static memsafe::VarValue<int> var_static(1);
    static auto static_fail1(var_static.take()); // Корректный С++ код, но неправлиный с точки зрения memsafe

    memsafe::VarShared<int> stub_function(memsafe::VarShared<int> arg, memsafe::VarValue<int> arg_val) {

        memsafe::VarShared<int> var_shared1(1);
        memsafe::VarShared<int> var_shared2(1);

        var_shared1 = var_shared2; // Корректный С++ код, но неправлиный с точки зрения memsafe
        {
            memsafe::VarShared<int> var_shared3(3);
            var_shared3 = var_shared1; // OK

            std::swap(var_shared1, var_shared3); // Корректный С++ код, но неправлиный с точки зрения memsafe
        }

        return 777; // OK
    }

    memsafe::VarShared<int> stub_function8(memsafe::VarShared<int> arg) {
        return arg; // Корректный С++ код, но неправлиный с точки зрения memsafe
    }

Командная строка для подключения плагина clang++ -std=c++20 -Xclang -load -Xclang ./memsafe_clang.so -Xclang -add-plugin -Xclang memsafe _example.cpp


Вывод плагина компилятора с сообщениями об ошибках использования классов библиотеки MemSafe:
clang++-19 -std=c++20   -c -g -DBUILD_UNITTEST -I. -std=c++20 -ferror-limit=500 -Xclang -load -Xclang ./memsafe_clang.so -Xclang -add-plugin -Xclang memsafe -MMD -MP -MF "build/UNITTEST/CLang-19-Linux/_example.o.d" -o build/UNITTEST/CLang-19-Linux/_example.o _example.cpp

Register template class 'memsafe::VarAuto' and 'VarAuto' with 'auto' attribute!
Register template class 'memsafe::VarValue' and 'VarValue' with 'value' attribute!
Register template class 'memsafe::VarShared' and 'VarShared' with 'shared' attribute!
Register template class 'memsafe::VarGuard' and 'VarGuard' with 'shared' attribute!
Register template class 'memsafe::VarWeak' and 'VarWeak' with 'weak' attribute!
In file included from _example.cpp:1:
./memsafe.h:645:38: remark: Memory safety plugin is enabled!
  645 |     namespace MEMSAFE_ATTR("enable") {
      |                                      ^
_example.cpp:25:17: error: Create auto variabe as static
   25 |     static auto static_fail1(var_static.take());
      |                 ^
      |                 /* [[memsafe(error, 3002)]] */ 
_example.cpp:26:17: error: Create auto variabe as static
   26 |     static auto static_fail2 = var_static.take();
      |                 ^
      |                 /* [[memsafe(error, 3003)]] */ 
_example.cpp:52:21: error: Cannot copy a shared variable to an equal or higher lexical level
   52 |         var_shared1 = var_shared1;
      |                     ^
      |                     /* [[memsafe(error, 4024)]] */ 
_example.cpp:53:21: error: Cannot copy a shared variable to an equal or higher lexical level
   53 |         var_shared1 = var_shared2;
      |                     ^
      |                     /* [[memsafe(error, 4025)]] */ 
_example.cpp:57:25: error: Cannot copy a shared variable to an equal or higher lexical level
   57 |             var_shared1 = var_shared1;
      |                         ^
      |                         /* [[memsafe(error, 4029)]] */ 
_example.cpp:58:25: error: Cannot copy a shared variable to an equal or higher lexical level
   58 |             var_shared2 = var_shared1;
      |                         ^
      |                         /* [[memsafe(error, 4030)]] */ 
_example.cpp:59:25: error: Cannot copy a shared variable to an equal or higher lexical level
   59 |             var_shared3 = var_shared1;
      |                         ^
      |                         /* [[memsafe(error, 4031)]] */ 
_example.cpp:65:29: error: Cannot copy a shared variable to an equal or higher lexical level
   65 |                 var_shared1 = var_shared1;
      |                             ^
      |                             /* [[memsafe(error, 4037)]] */ 
_example.cpp:66:29: error: Cannot copy a shared variable to an equal or higher lexical level
   66 |                 var_shared2 = var_shared1;
      |                             ^
      |                             /* [[memsafe(error, 4038)]] */ 
_example.cpp:67:29: error: Cannot copy a shared variable to an equal or higher lexical level
   67 |                 var_shared3 = var_shared1;
      |                             ^
      |                             /* [[memsafe(error, 4039)]] */ 
_example.cpp:69:29: error: Cannot copy a shared variable to an equal or higher lexical level
   69 |                 var_shared4 = var_shared1;
      |                             ^
      |                             /* [[memsafe(error, 4041)]] */ 
_example.cpp:70:29: error: Cannot copy a shared variable to an equal or higher lexical level
   70 |                 var_shared4 = var_shared3;
      |                             ^
      |                             /* [[memsafe(error, 4042)]] */ 
_example.cpp:72:29: error: Cannot copy a shared variable to an equal or higher lexical level
   72 |                 var_shared4 = var_shared4;
      |                             ^
      |                             /* [[memsafe(error, 4044)]] */ 
_example.cpp:77:13: error: Cannot swap a shared variables of different lexical levels
   77 |             std::swap(var_shared1, var_shared3);
      |             ^
      |             /* [[memsafe(error, 4049)]] */ 
_example.cpp:95:16: error: Return share type
   95 |         return arg;
      |                ^
      |                /* [[memsafe(error, 5004)]] */ 
15 errors generated.

Описание и основные команды (атрибуты в С++ коде)


По умолчанию проверка дополнительных лексическая правил отключена. Включение или отключение лекстической проверки правил для безопасной работы с памятью производится следующими командами:


  • namespace [[memsafe("define")]] NAME { ... } — определение области имен NAME в которой будут присутствовать шаблонные классы для работы модуля.
  • [[memsafe("value")]], [[memsafe("shared")]], [[memsafe("guard")]], [[memsafe("auto")]] и [[memsafe("weak")]] — атрибуты для маркировки шаблонных классов в заголовочном файле проекта.
  • namespace [[memsafe("enable")]] { } — команда включения синтаксического анализатора плагина компилятора для шаблонных классов.
  • namespace [[memsafe("disable")]] { } — команда выключает синтаксический анализатор плагина компилятора до конца файла или до команды включения.
  • namespace [[memsafe("unsafe")]] { ... } — определение пространства имен в котором синтаксический анализатор игнорирует ошибки безопасного использования memsafe классов. Возможно использование атрибута [[memsafe("unsafe")]] и для отдельных операторов, но в настоящий момент это не реализовано. (Например, сейчас нельзя сделать вот так [[memsafe("unsafe")]] return nullptr; чтобы подавить проверку одного конкретного оператора. Для этого нужна более новая версия clang с реализацией Pull requests #110334)

В настоящий момент (для целей демонстрации работоспособности концепции) в плагине сделаны следующие проверки лексики:


  • Запрет копирования ссылочных и защищенных переменных в рамках одного уровня (отмеченных [[memsafe("shared")]])
  • Запрет обмена значениями между двумя ссылочными переменными разных уровней (отмеченных [[memsafe("shared")]])
  • Запрет создания статических захваченных переменных отмеченных [[memsafe("auto")]]
  • Запрет возврата из функции захваченных (автоматических) переменных (отмеченных [[memsafe("auto")]])
  • Запрет возврата из функции ссылочных и защищенных переменных, кроме созданных непостредственно в операторе возврата

Комментарии (9)


  1. Nansch
    19.01.2025 05:24

    Тупо прибить ограничения к такому языку как С++ ради безопасности - это деградация и тупик развития. Для этого есть другие языки, которые этим козыряют. Для Си нужны профили, типа профиль лютой безопасности, где вообще ничего нельзя опасного написать. Потому что в других модных языках как только нужно сделать опасно, начинается удаление гланд через задний проход. Зачем эта потеря времени для плюсовиков?!

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


    1. rsashka Автор
      19.01.2025 05:24

      Для этого есть другие языки, которые этим козыряют.

      Ну и пусть козыряют.

      Для Си нужны профили, типа профиль лютой безопасности ...

      Профили, как в p3038 требуют разработки и принятия нового стандарта С++, тогда как аналогичное по функциональности решение можно сделать уже на C++20.


  1. Goron_Dekar
    19.01.2025 05:24

    Вот согласен с прошлым коментарием.

    дело в том, что часто НАДО писать код, который "небезопасен". Это и работа с железом, и низкоуровневые оптимизации, и работа с другими компонентами системы, которые не предоставляют "безопасного" интерфейса.

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


    1. rsashka Автор
      19.01.2025 05:24

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

      И главный плюс подобного похода, полная обратная совместимость с существующим кодом и не требуется принимать новых стандартов языка (или переписывать всё на новом козырном языке). Но об этом я уже писал в самой статье.


      1. Jijiki
        19.01.2025 05:24

        у вас предпологается еще включение [] в код и на большом проекте где очень много файлов и строк будет проблематично включать в компиляцию ваш плагин, тоесть это не построение лексического анализа и проверки виртуальных адресов(может и есть построение лексического дерева и проверка каких-то адресов, просто у вас написано не для промышленных масштабов), о чем и говорит ваше предупреждение, плюс ограничение версия clang с каким-то пулом, учитваются ли особенности кланга, или просто наивно происходит проверка как-то?

        например std::vector<float>(5,0); clang++(17)/g++(13) по разному чтото делают


        1. rsashka Автор
          19.01.2025 05:24

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

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

          например std::vector(5,0); clang++(17)/g++(13) по разному чтото делают

          Особенности версии STL вообще не учитываются при анализе синтаксиса


  1. Jijiki
    19.01.2025 05:24

    я не совсем про уровня, но на С++ есть же возможность если надо про уровню написать счетчик байт на вектор допустим, пользоваться unique_ptr, и использовать итераторы, поэтому владения и заимствования интересно конечно, но ведь есть вектор и итератор, есть move и есть memcpy, где скрыта опасность не пойму. а адрес в функцию с перезаписью в адрес это ошибка? допустим void test(int &a){ a=1+2;}, одно из последних вот тоже интересно - есть менеджер ресурсор в нем указатели я сделал создание указателей в менеджере а в игре копирую указатели, это опасно? другой вопрос тоже не менее интересен, не будет ли подход безопасности пересекаться с возможно какими-то доступами потоковыми и прочее?

    будет ли код плагина подключенного в бинарнике? просто чтоб такой плагин использовать это же надо добавить этот код в код как я понял


    1. rsashka Автор
      19.01.2025 05:24

      Вообще не понял комментария. Набор знакомых слов, но общий смысл не понял. Можете сформулировать вопросы как-то по другому?

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


      1. Jijiki
        19.01.2025 05:24

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

        я себе представляю владение - unique_ptr заимствование iterator, lifetime обусловлен инстансом, получается всё сводится к счету байт если это необходимо например для операций с вектором, отсюда много вопросов, другой вопрос что если в самой конструкции языка есть механизм какойнибудь его будет уже же ведь так не проверить вроде