Цифровой дайджест #1

Цифровой дайджест #1

Каждое воскресенье я готовлю три выпуска дайджестов для компании Start X:

  • для сотрудников ИТ и ИБ;
  • для разработчиков;
  • для обычных пользователей.

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

Герой сегодняшнего выпуска — типобезопасный компилятор для языка C под названием Fil-C.

Fil-C: безопасность памяти для C без переписывания кода

Пока индустрия спорит о переходе на Rust и Zig, появился неожиданный игрок — Fil-C. Это не новый язык, а радикально переосмысленный компилятор для обычного C/C++, который ловит все опасные операции с памятью прямо во время выполнения программы.

Что это и зачем

Fil-C — это форк современного Clang (версия 20.1.8), который добавляет полную динамическую проверку памяти ко всем небезопасным операциям. Главная фишка: вам не нужно переписывать код. Берёте существующий проект на C — CPython, OpenSSH, GNU Emacs, Wayland — собираете с помощью Fil-C, и получаете защиту от переполнений буфера, use-after-free и компании.

Звучит как магия? Почти так и есть, но с интересными инженерными решениями под капотом.

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

Секретный соус называется InvisiCaps (Invisible Capabilities). Каждый указатель в программе незаметно для неё самой несёт метаданные: границы выделенной памяти, права доступа, привязку к конкретному объекту.

Попытались прочитать за пределами массива? Паника. Обратились к освобождённой памяти? Паника. Записали в read-only область? Паника.

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

Дополняет картину FUGC — параллельный сборщик мусора, который работает без остановки всех потоков и даёт детерминированный отлов двойного освобождения памяти. Объекты не перемещаются, накладные расходы — простой store-барьер.

Что говорит легенда

Дэниел Дж. Бернстайн (D. J. Bernstein, он же djb) — одна из знаковых фигур в мире системного программирования и криптографии. Автор qmail (почтовый сервер, который годами работал без единой уязвимости), djbdns, curve25519 и crypto-библиотеки NaCl. Известен фанатичным вниманием к безопасности и корректности кода. Когда такой человек хвалит инструмент — это о чём-то говорит.

Бернстайн провёл масштабное тестирование Fil-C:

По совместимости: «Многие библиотеки и приложения, которые я пробовал, работают без изменений». Он успешно собрал bash, coreutils, CPython, OpenSSH, OpenSSL, vim, zsh, zstd и десятки других пакетов. Его цель — перевести на Fil-C все машины, которыми он управляет.

По производительности: Бернстайн прогнал около 9000 микробенчмарков на своём криптографическом коде (самая требовательная нагрузка). Результат: замедление от 1× до 4× по сравнению с обычным Clang на AMD Zen 4. Для IO-bound приложений overhead часто вообще незаметен.

Практические детали: Есть нюансы — нет vfork (пришлось патчить Boost build), не работает с Valgrind, некоторые системные вызовы требуют внимания. Но это решаемые проблемы, не архитектурные ограничения.

Для энтузиастов

Бернстайн выпустил Filian — набор инструментов для Debian 13, который автоматически пересобирает выбранные пакеты под Fil-C (в отдельную архитектуру amd64fil0). Есть и Filnix от Mikael Brockman — пакет для Nix. Можно быстро пощупать на практике.

Код Fil-C под Apache 2.0, runtime под BSD-2-Clause. Поддерживаются musl и glibc. Доступны дистрибутивы для сборки полностью безопасного Linux-userland.

Почему это важно

Fil-C — это не компромисс «или совместимость, или безопасность». Это попытка получить обе вещи сразу:

  • не нужно переписывать миллионы строк существующего кода на Rust/Zig;
  • защита от целых классов уязвимостей становится автоматической;
  • привычная экосистема — make, CMake, Meson, обычный workflow;
  • предсказуемая цена — замедление в 1-4 раза для кода с «тяжёлыми» вычислениями, для обычных приложений, как правило, меньше.

Конечно, 4× замедление — это не бесплатно. Но для критичного к безопасности кода (а сегодня это почти всё, что видит сеть) это может быть разумный трейд-офф. Особенно если альтернатива — полное переписывание на новый язык.

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

Чтобы два раза не вставать, выскажусь и по поводу модных «безопасных» языков

Rust и Zig: что теряем при переходе с C

Rust и Zig позиционируются как современная замена C, но у медали есть обратная сторона. Вот о чём редко говорят евангелисты этих языков.

Проблема №1: Экосистема — это не только язык

50+ лет наследия C — это не просто синтаксис. Это:

  • миллиарды строк рабочего кода в production;
  • тысячи библиотек с устоявшимся API;
  • инструменты отладки, профилирования, анализа, отточенные десятилетиями;
  • знания в головах разработчиков;
  • интеграция с операционными системами на уровне «родного языка».

Rust требует переписать всё с нуля. FFI (Foreign Function Interface) для вызова C-кода работает, но это костыль: вы теряете все гарантии безопасности Rust на границе, плюс получаете overhead на маршалинг данных. Хотите использовать старую надёжную библиотеку? Либо пишите unsafe-обёртку, либо ищите переписанную на Rust (часто недозрелую) версию.

Zig лучше в плане совместимости — он может напрямую импортировать C-заголовки и компилировать C-код. Но это односторонняя интеграция: C-проект не может просто так взять кусок Zig-кода. Вы всё равно стоите перед выбором: постепенная миграция с гибридной кодовой базой или большой переписывание.

Проблема №2: Сложность vs простота

C прост до неприличия. Вся спецификация языка умещается в голове одного разработчика. Указатель — это адрес, структура — это байты в памяти, функция — это точка входа. Что видишь, то и получаешь.

Rust — это когнитивная перегрузка:

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

Время обучения junior-разработчика C до продуктивности — недели. Rust — многие месяцы. Спросите любого, кто учил Rust: первые пару месяцев — это борьба с borrow checker, а не решение задач.

Zig проще Rust, но добавляет свои сложности:

  • Comptime — код, выполняемый во время компиляции, с неочевидными ограничениями
  • Явное управление аллокаторами везде (принципиально, но многословно)
  • Ошибки как значения через union types — каждый вызов функции обвешан обработкой
  • Язык ещё не стабилизирован — breaking changes между версиями

Проблема №3: Время компиляции

C компилируется быстро. Инкрементальная сборка большого проекта — секунды. Полная пересборка ядра Linux — минуты.

Rust компилируется мучительно долго. Сотни зависимостей через Cargo, каждая со своими generic-монстрами. Первая сборка проекта средней сложности — десятки минут. Изменили одну строку — минута на пересборку. Это убивает продуктивность: вы не можете быстро итерироваться, не можете держать контекст в голове между сборками.

Типичная история: переписали C-проект на Rust, тесты стали прогоняться не за 30 секунд, а за 10 минут. CI превратилась в узкое место.

Zig лучше, но всё равно медленнее C из-за comptime-вычислений.

Проблема №4: Размер бинарников и потребление ресурсов

C даёт компактные бинарники. Hello World — несколько килобайт. Даже сложная программа — мегабайты.

Rust тащит за собой:

  • массивную стандартную библиотеку;
  • среду выполнения для асинхронного кода (если используется);
  • механизм безопасной обработки паник с автоматической очисткой ресурсов — это требует дополнительных таблиц и кода в программе;
  • монорфизацию обобщённых типов — каждый вариант использования создаёт отдельную копию кода.

Hello World на Rust — сотни килобайт. Реальная программа — десятки мегабайт. Для встраиваемых систем или минималистичных окружений это проблема.

Zig контролируемее, но всё равно крупнее компактного C-кода.

Проблема №5: Стабильность и зрелость

C — это стандарт. C89, C99, C11, C17 — эволюция плавная, обратная совместимость железобетонная. Код, написанный 30 лет назад, компилируется сегодня.

Rust до версии 1.0 дошёл только в 2015. Каждые 6 недель новая версия. Да, обещают совместимость, но:

  • экосистема библиотек постоянно ломается — сторонние библиотеки (в Rust они называются “крейты”) обновляются несинхронно, зависимости конфликтуют между собой, что работало вместе полгода назад, сегодня может не собраться;
  • идиомы меняются — “правильные” способы писать код эволюционируют, старый код выглядит устаревшим и его приходится переписывать (например, асинхронное программирование до 2019 года работало совсем иначе);
  • инструментарий эволюционирует — система сборки, форматировщики, линтеры меняют поведение, то что работало год назад, сегодня помечено как устаревшее или требует других флагов компиляции.

Zig вообще пре-1.0. Язык официально нестабилен. Код, написанный на Zig 0.11, не компилируется на 0.12. Для production это рулетка.

Проблема №6: Предсказуемость vs магия

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

Rust полон неявного поведения:

  • автоматический Deref может вызывать цепочки методов, которые вы не видите;
  • Drop (деструкторы) выполняются автоматически — иногда неожиданно рано или слишком поздно;
  • макросы генерируют код, который вы не контролируете;
  • move semantics — копирование или перемещение? Зависит от Copy trait, который может быть неочевиден.

Отладка Rust-кода иногда превращается в настоящее расследование: что же там на самом деле происходит?

Zig лучше с предсказуемостью, но comptime может выстрелить в ногу неожиданными способами.

Проблема №7: Привязка к единственному компилятору

C — открытый стандарт. Есть GCC, Clang, MSVC, ICC, десятки других компиляторов. Не нравится один — переключайтесь на другой.

Rust — это фактически монокультура rustc. Альтернативные компиляторы (mrustc, gcc-rs) существуют на бумаге, но далеки от feature parity. Вы привязаны к LLVM и решениям команды Rust.

Zig — вообще один компилятор от одного человека (Andrew Kelley и небольшой команды). Bus factor пугающий.

Когда Rust/Zig имеют смысл

Справедливости ради. Преодоление описанных сложностей имеет смысл, если:

  • вы делаете новый проект с нуля, где безопасность критична — Rust даёт гарантии на уровне компилятора;
  • команда готова вкладываться в обучение и принимает компромиссы по времени разработки и сложности;
  • нет привязки к существующему C-коду и можно выбирать экосистему библиотек с чистого листа;
  • вы работаете с WebAssembly, криптографией, парсерами — в области, где Rust показывает себя особенно хорошо.

Но если у вас:

  • большая существующая кодовая база на C/C++;
  • требования к быстрой разработке с частыми изменениями;
  • ограниченный бюджет на поиск и найм разработчиков;
  • встраиваемые системы с жёсткими ограничениями по памяти и размеру программ;
  • нужна простота поддержки кода на годы вперёд,

…то переход на Rust/Zig может оказаться дороже, чем вы думаете.

Почему Fil-C — это третий путь

Fil-C предлагает радикально иной подход: не меняйте язык, измените компилятор. Вы получаете безопасность памяти, не жертвуя:

  • существующим кодом;
  • скоростью компиляции;
  • простотой языка;
  • доступностью разработчиков;
  • зрелостью экосистемы.

Да, есть замедление программ во время работы из-за проверок безопасности. Но оно предсказуемо и измеримо (1-4× по тестам Бернстайна), в отличие от рисков полного переписывания проекта на новый язык.

Rust и Zig — отличные языки для своих ниш. Но они не серебряная пуля, и переход на них — не бесплатный. Иногда эволюция лучше революции.

Предыдущий пост Следующий пост
Наверх