Ошибки при составлении программ для ЭВМ появились даже раньше, чем были придуманы самые первые языки программирования. Собственно, языки программирования и были придуманы как раз для того, чтобы программы писались проще, а количество ошибок в них было как можно меньше.
Для уменьшения количества ошибок было разработано множество методов, включая создание специализированных инструментов анализа исходного кода и даже целых языков программирования.
Но по прошествии многих десятилетий проблема чисто технических ошибок в программном обеспечении так и остаётся нерешённой по сей день, однако подход, предложенный в языке Rust, меняет всё.
Парадоксы безопасного управления памятью
Последнее время только ленивый не упоминает язык программирования Rust, который за счёт инновационной модели "владения и заимствования" гарантирует безопасность памяти и потоков, позволяя исключить множество видов ошибок во время компиляции. Однако почему-то всегда забывают рассказать, что сама концепция "владения и заимствования" имеет и несколько фундаментальных ограничений.
Например, реализация любых алгоритмов с множественным владением требует обязательного использования unsafe-блоков или перепроектирования архитектуры приложения, что ограничивает применение Rust в legacy-системах, где полный рефакторинг кода экономически нецелесообразен.
Кроме этого, анализ циклических графов (перекрёстных ссылок) в классическом виде не имеет решения во время компиляции в принципе, поэтому всегда требует ручного использования умных счётчиков ссылок (Rc, Arc), что тоже повышает риск утечек памяти из-за ошибок реализации.
Но и продолжать использовать С++ тоже не вариант! Само название языка С++ стало фактически синонимом, когда речь заходит о различных ошибках в ПО. И это несмотря на то, что в нём уже давным-давно имеется полный набор инструментов для безопасной работы с памятью: умные указатели (unique_ptr, shared_ptr, weak_ptr), RAII и move-семантика.
Но отсутствие строгих правил их применения на уровне синтаксиса языка превращает подобные механизмы безопасности в «опциональные». Разработчики могут сознательно или случайно обходить защитные механизмы, используя сырые указатели или неконтролируемое выделение памяти.
Однако любые попытки внедрения в С++ жёстких правил (например, через внедрение синтаксиса, подобного Rust, как Safe C++) сталкиваются с ожидаемым сопротивлением, так как подобные изменения нарушают обратную совместимость и отвергаются как комитетом по стандартизации, так и самими разработчиками, особенно теми, кто работает с унаследованным (легаси) кодом.
Эти проблемы и создают парадокс: разработчики вынуждены тратить время и ресурсы на переписывание существующего кода на Rust и одновременно жертвовать безопасностью ради функциональности, так как из-за своих архитектурных ограничений Rust не может гарантировать отсутствие ошибок для некоторых сценариев использования, чем фактически нивелирует все свои преимущества.
Текущая ситуация в области безопасной разработки
И это только самые явные проблемы, которые касаются только безопасного управления памятью. Тогда как к техническим ошибкам в ПО можно отнести и различные варианты переполнений: разрядности чисел, нехватка оперативной памяти, переполнение стека, который всегда выделяется заранее и имеет фиксированный размер и т. д.
Конечно, и в С++ ситуация постепенно меняется в лучшую сторону, в том числе за счёт внедрения различных механизмов безопасности приложений на уровне генерации кода компилятором ("харденинг"), но основная проблема заключается в том, что для языков программирования в принципе отсутствует единая теория (или подход) к оценке безопасной разработки. Общая теория, которая позволяла бы оценивать возможность реализации типовых алгоритмов и сравнивать языки программирования между собой в части безопасности программного кода.
Сейчас для проверок на наличие ошибок в исходном коде используются множество различных инструментов, начиная от статических анализаторов и заканчивая различными вариантами тестирования. Но ещё несколько десятилетий назад Эдсгер Дейкстра сказал в Dahl, O.-J., Dijkstra, E.W., Hoare, C.A.R. Structured Programming: «Тестирование выявляет только наличие, но никак не отсутствие ошибок».
К тому же каждый инструмент или подход проверят только свою область, но часто непонятно, что осталось "за скобками". Подобная ситуация похожа на разноцветное лоскутное одеяло, где каждый из его кусочков отвечает за свою конкретную часть безопасности, но нет понимания его общего размера.
И если раньше подобная ситуация была нормой, так как при создании языков программирования их авторы старались реализовать как можно больше возможностей, чтобы упростить и ускорить написание программ, то сейчас тенденции значительно изменились. Серебряная пуля, которую безуспешно искал Брукс (о десятикратном снижении стоимости разработки), уже давным-давно найдена, используется, и имя ей - Free Software и Open Source. А с появлением LLM стоимость разработки типовых решений снизилась ещё сильнее.
Поэтому в настоящий момент более актуальной становится не скорость создания ПО, а качество получаемого результата. Но чтобы управлять качеством создаваемого ПО, нужно не только иметь возможность его измерять и сравнивать, но и понимать возможности и ограничения используемых инструментов (применяемых языков программирования) в области безопасной разработки. И с этой точки зрения использование Rust, несмотря на его ограничения, всё равно будет более предпочтительным за счёт хоть каких-то гарантий, чем продолжение использования С++ с его вседозволенностью и возможностью совершать даже самые глупые ошибки на ровном месте.
Безопасность разработки через гарантии языка программирования
Современный подход к обеспечению безопасности программного обеспечения в значительной степени является фрагментарным и основные усилия сосредоточены на обнаружении и исправлении различных классов уязвимостей уже после их появления.
Отчасти это может быть оправданно в случаях, когда уязвимости или векторы воздействия не связаны напрямую с исходным кодом ПО. Но если уязвимости возникают из-за чисто технических ошибо�� и особенностей языка программирования, то их исправление становится очень дорогостоящим при выявлении на поздних стадиях жизненного цикла разработки.
Ответственность за тестирование, поиск и реализацию мероприятий по минимизации ошибок и уязвимостей всегда лежит на плечах разработчиков, от которых требуется быть экспертами не только в своей предметной области, но и в вопросах кибербезопасности.
А ведь Rust показал замечательный подход к обеспечению безопасной разработки программного обеспечения! Но не в части управления памятью, а в смене самой парадигмы обеспечения безопасности на уровне исходного кода, когда безопасность обеспечивается на основе гарантий языка программирования.
Подобный подход - использование языка и его компилятора как основного инструмента предотвращения целых классов уязвимостей - меняет фокус с обнаружения уязвимостей на их предотвращение на самом низком уровне - на уровне написания программного кода.
Безопасная разработка на основе гарантий языка программирования обеспечивает ряд стратегических преимуществ:
Автоматическое устранение уязвимостей: Вместо поиска отдельных ошибок устраняются целые классы уязвимостей на системном уровне из-за самого факта использования подобного подхода.
Снижение когнитивной нагрузки на разработчиков: Разработчики могут сосредоточиться на бизнес-логике, полностью доверяя компилятору и системе типов в вопросах базовой безопасности.
Повышение предсказуемости и надёжности: Безопасность становится измеримым и доказуемым свойством системы, а не результатом стечения обстоятельств и независимо от применения внешних инструментов.
Экономическая эффективность: Предотвращение уязвимостей на этапе написания кода на порядки дешевле, чем их обнаружение и исправление в продуктивной среде.
Заключение
Существующая модель «латания дыр у лоскутного одеяла» в безопасности ПО исчерпала себя. Для создания действительно надёжных и защищённых систем необходим переход к встроенной безопасности, то есть к созданию теории безопасной разработки на основе гарантий языка программирования, что является не просто академическими изысканиями, а насущной необходимостью.
Это позволит заложить безопасность в сам фундамент программного обеспечения, сделав её неотъемлемым свойством исходного кода программы, а реализацию гарантий безопасной разработки в любом языке программирования - делом техники.
Пример реализации гарантий безопасности при работе с памятью для С++ можно посмотреть в этом проекте