Buffer overflows находятся в CWE Top 25 уже двадцать лет. У нас есть stack canaries, ASLR, DEP, control-flow integrity и memory-safe языки, но они всё равно продолжают появляться в критическом коде. Причина проста: каждая из этих mitigation выполняется в software, а software можно обойти, неправильно настроить или просто не использовать.
А что, если бы сам hardware отказался позволять вам читать за пределы массива?
Почему software mitigation продолжают проигрывать
Buffer overflow происходит, когда программа записывает за пределы выделенного региона памяти. В C это тривиально, потому что pointer — это просто address. Компилятор и CPU доверяют тому, что программист знает, что делает. Если вы выделите 64 байта и запишете по смещению 80, CPU сделает это. Он понятия не имеет, где заканчивается buffer.
Software mitigation пытаются поймать это постфактум. Stack canaries помещают известное значение перед return address и проверяют его перед возвратом. ASLR рандомизирует layout памяти, чтобы усложнить построение эксплойтов. Control-flow integrity ограничивает, куда могут попадать indirect jumps. Это помогает, но это вероятностно или неполно. Определённый attacker с достаточным временем и infoleak обычно может их обойти.
Memory-safe языки вроде Rust решают проблему на уровне языка, но не помогают миллиардам строк C и C++, уже находящихся в production. Переписывание ядра Linux, OpenSSL или вашего legacy backend на Rust в этом десятилетии не произойдёт. Нам нужна защита, которая охраняет существующий код, не требуя rewrite.
Что CHERI на самом деле делает с pointer
CHERI, что расшифровывается как Capability Hardware Enhanced RISC Instructions, — это расширение ISA, разработанное в University of Cambridge и теперь поддерживаемое Arm в форме Morello. Оно меняет то, что такое pointer.
В обычной 64-битной системе pointer занимает 64 бита: просто address. В системе CHERI pointer становится 128-битной или 256-битной capability. Дополнительные биты хранят метаданные: base address выделения, bounds (насколько далеко он простирается) и permissions (read, write, execute). Capability защищена hardware integrity tag, так что подделка metadata делает её недействительной.
Когда вы компилируете код для CHERI, malloc возвращает не просто address. Он возвращает capability, bounds которой точно соответствуют запрошенному размеру. Когда вы инкрементируете этот pointer, hardware проверяет каждое обращение против этих bounds. Если вы попытаетесь читать или писать за их пределами, CPU генерирует synchronous exception. Нет способа подделать capability с более широкими bounds. Hardware просто не позволит.
Это ключевое отличие. Software bounds checking вставляет проверки вокруг операций с памятью, которые компиляторы могут оптимизировать, программисты забыть, а attackers обойти. Hardware bounds checking происходит при каждом load и store безусловно, без добавления инструкций к вашей программе.
Как enforcement bounds работает на уровне instruction
Вот как выглядит типичный buffer overflow на C:
#include <string.h>
void vulnerable(char *input) {
char buf[64];
strcpy(buf, input); // No bounds check. Classic overflow.
}
На обычной архитектуре это портит stack. На CHERI buf — не raw address. Это capability, bounds которой точно 64 байта. Когда strcpy пытается записать за пределы байта 64, hardware генерирует исключение Capability Bounds Violation. Программа немедленно падает на точной instruction, совершившей ошибку.
То же защита применяется к heap-выделениям:
#include <stdlib.h>
#include <cheri.h> // CHERI intrinsics header
void heap_example(void) {
char *buf = malloc(32);
// buf is a capability with base=buf, length=32
// This works fine
buf[0] = 'A';
buf[31] = 'Z';
// This traps at the hardware level
buf[32] = 'X'; // Capability bounds violation
}
Краш точен. Вы получаете точную instruction, точную capability и точные bounds, которые были нарушены. Отладка этого проще, чем охота за испорченным stack frame на три frame позже.
CHERI также предотвращает pointer forgery. Вы не можете привести integer к pointer и начать разыменовывать произвольную память. Hardware распознаёт только capabilities, созданные легитимными instructions вроде malloc, stack allocation или явного capability derivation. Приведение integer-to-pointer производит untagged значения, которые trap при использовании.
Проблема совместимости реальна
Если CHERI так хорош, почему не каждый сервер работает на нём? Потому что изменение того, что такое pointer, ломает предположения, на которые десятилетиями опирался код на C.
Первая проблема — размер. Capabilities больше, чем raw pointers. На Morello capability составляет 128 бит плюс 1-битный tag, отдельно хранимый hardware. Это увеличивает потребление памяти для структур данных с большим количеством pointers. Linked list или дерево, полное pointers, становятся заметно больше. Для многих приложений overhead составляет однозначный процент, но для pointer-chasing workloads это может быть хуже.
Вторая проблема — casting. Код на C приводит pointers к uintptr_t, выполняет арифметику и приводит обратно. На CHERI uintptr_t на самом деле является типом capability, а не integer. Код, который предполагает возможность произвольной integer-арифметики над значениями pointers, не скомпилируется или будет trap в runtime. Исправление обычно заключается в использовании ptrdiff_t для offsets и применении их через cheri_address_set, но это требует изменений в source.
Третья проблема — экосистема. Hardware CHERI редок. Платы Arm Morello существуют, но это не commodity servers. CheriBSD и CHERI-enabled Linux достаточно зрелы для запуска реального software, но большинство Linux-дистрибутивов не поставляют CHERI-пакеты. Ваш container registry, CI runners и cloud provider ещё не поддерживают это.
Вы можете попробовать это сегодня без custom silicon
Вам не нужна плата Morello для экспериментов с CHERI. Проект CHERI поддерживает QEMU, так что вы можете запустить CHERI userspace на любом Linux- или macOS-хосте.
Вот как получить CHERI toolchain и запустить простой пример в QEMU:
# Clone the CHERI SDK setup
$ git clone https://github.com/CTSRD-CHERI/cheribuild.git
$ cd cheribuild
# Build the CheriBSD disk image and QEMU
$ ./cheribuild.py cheribsd-riscv64-purecap -d
$ ./cheribuild.py qemu -d
# Boot CheriBSD in QEMU
$ ./cheribuild.py run-cheribsd-riscv64
Оказавшись внутри CheriBSD, компилятором является clang с поддержкой CHERI target. Вы компилируете с принудительным bounds checking по умолчанию:
$ clang -o test test.c
$ ./test
# Buffer overflows trap immediately with a clear message
Если вы хотите протестировать существующий software, cheribuild может скомпилировать популярные пакеты из FreeBSD ports с поддержкой CHERI. Команда CHERI портировала OpenSSH, nginx, PostgreSQL и большие части базовой системы FreeBSD. Многие программы работают без изменений. Те, что ломаются, обычно из-за unsafe pointer casts или предположений о размере pointer.
Для более быстрого старта без QEMU University of Cambridge также публикует Docker-образ с предустановленной CHERI LLVM toolchain. Вы можете компилировать CHERI binaries и инспектировать сгенерированный capability-aware assembly без загрузки полноценной ОС.
Что означает hardware memory safety для существующих кодовых баз
CHERI не заменяет написание безопасного кода. Это safety net для кода, который у вас уже есть. CHERI-система, выполняющая legacy C, всё равно будет иметь logic bugs, race conditions и use-after-free ошибки. Но в ней не будет buffer overflows, перезаписывающих return addresses, портящих соседние heap metadata или утечек secrets за границы выделения.
Прогресс уже виден. Arm интегрировал CHERI-derived features в свою Memory Tagging Extension (MTE), которая сегодня поставляется на production Android-устройствах. MTE грубее, чем CHERI (он taggaет 16-байтные гранулы вместо отдельных выделений), но ловит многие из тех же bugs с меньшим overhead. Полноценный CHERI, вероятно, последует по мере созревания экосистемы.
Если вы поддерживаете код на C или C++, обрабатывающий untrusted input, вопрос не в том, придёт ли hardware memory safety. Вопрос в том, будете ли вы готовы, когда это произойдёт. Начните с аудита вашего кода на предмет pointer-to-integer casts, pointer-арифметики на несвязанных объектах и предположений о sizeof(void*). Это patterns, которые ломаются под CHERI, и их исправление делает ваш код чище даже на обычном hardware.
Hardware наконец говорит «нет». Нам следует позволить ему это делать.