Buffer overflows stehen seit zwanzig Jahren auf der CWE Top 25. Wir haben Stack Canaries, ASLR, DEP, Control-Flow Integrity und memory-safe Languages, und trotzdem tauchen sie weiterhin in kritischem Code auf. Der Grund ist simpel: Jede dieser Mitigationen läuft in Software, und Software kann umgangen, falsch konfiguriert oder schlicht nicht genutzt werden.
Was, wenn die Hardware selbst sich weigern würde, über das Ende eines Arrays hinauszulesen?
Warum Software-Mitigationen weiterhin versagen
Ein Buffer Overflow passiert, wenn ein Programm über die Grenzen einer allozierten Memory-Region hinausschreibt. In C ist das trivial, weil ein Pointer nur eine Adresse ist. Compiler und CPU vertrauen darauf, dass der Programmer weiß, was er tut. Wenn du 64 Bytes allozierst und an Offset 80 schreibst, macht die CPU das. Sie hat keine Ahnung, wo der Buffer endet.
Software-Mitigationen versuchen, das nachträglich zu erkennen. Stack Canaries platzieren einen bekannten Wert vor die Return Address und prüfen ihn vor dem Rücksprung. ASLR randomisiert das Memory-Layout, um Exploits schwerer zu konstruieren. Control-Flow Integrity schränkt ein, wo indirekte Sprünge landen dürfen. Das hilft, aber es ist probabilistisch oder unvollständig. Ein entschlossener Angreifer mit genug Zeit und einem Infoleak kann sie in der Regel umgehen.
Memory-safe Languages wie Rust lösen das Problem auf Language-Ebene, helfen aber nicht den Milliarden Zeilen C und C++, die bereits produktiv laufen. Den Linux-Kernel, OpenSSL oder dein Legacy-Backend in Rust neu zu schreiben, wird dieses Jahrzehnt nicht passieren. Wir brauchen eine Defense, die existierenden Code schützt, ohne ein Rewrite zu erfordern.
Was CHERI tatsächlich mit einem Pointer macht
CHERI, kurz für Capability Hardware Enhanced RISC Instructions, ist eine ISA-Extension, die an der University of Cambridge entwickelt wurde und jetzt von Arm in Form von Morello unterstützt wird. Sie verändert, was ein Pointer ist.
In einem konventionellen 64-Bit-System ist ein Pointer 64 Bit: nur eine Adresse. In einem CHERI-System wird ein Pointer zu einer 128-Bit- oder 256-Bit-Capability. Die extra Bits speichern Metadaten: die Base Address der Allocation, die Bounds (wie weit sie reicht) und Permissions (read, write, execute). Die Capability ist durch ein hardware integrity tag geschützt, sodass Manipulation der metadata sie invalidiert.
Wenn du Code für CHERI kompilierst, gibt malloc nicht nur eine Adresse zurück. Es gibt eine Capability zurück, deren Bounds exakt die angefragte Größe sind. Wenn du diesen Pointer inkrementierst, prüft die Hardware jeden Zugriff gegen diese Bounds. Wenn du versuchst, außerhalb zu lesen oder zu schreiben, löst die CPU eine synchrone Exception aus. Es gibt keinen Weg, eine Capability mit weiteren Bounds zu fälschen. Die Hardware lässt dich einfach nicht.
Das ist der entscheidende Unterschied. Software Bounds Checking fügt Checks um Memory-Operationen herum ein, die Compiler wegoptimieren, Programmer vergessen können und Angreifer umgehen können. Hardware Bounds Checking passiert bei jedem Load und Store, bedingungslos, ohne dass deinem Programm Instructions hinzugefügt werden.
Wie Bounds Enforcement auf Instruction-Ebene funktioniert
So sieht ein typischer Buffer Overflow in C aus:
#include <string.h>
void vulnerable(char *input) {
char buf[64];
strcpy(buf, input); // No bounds check. Classic overflow.
}
Auf einer normalen Architektur korumpiert das den Stack. Auf CHERI ist buf keine raw address. Es ist eine Capability, deren Bounds exakt 64 Bytes sind. Wenn strcpy versucht, über Byte 64 hinauszuschreiben, löst die Hardware eine Capability Bounds Violation-Exception aus. Das Programm crashed sofort an der exakt fehlerhaften Instruction.
Derselbe Schutz gilt für Heap-Allocationen:
#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
}
Der Crash ist präzise. Du bekommst die exakte Instruction, die exakte Capability und die exakten Bounds, die verletzt wurden. Das Debuggen ist einfacher als die Jagd nach einem korumpierten Stack Frame drei Frames später.
CHERI verhindert auch Pointer Forgery. Du kannst keinen Integer zu einem Pointer casten und beliebiges Memory dereferenzieren. Die Hardware erkennt nur Capabilities, die von legitimen Instructions wie malloc, stack allocation oder expliziter Capability-Derivation erzeugt wurden. Integer-to-Pointer Casts produzieren untagged Values, die bei Verwendung trappen.
Das Kompatibilitätsproblem ist real
Wenn CHERI so gut ist, warum läuft nicht jeder Server darauf? Weil die Veränderung dessen, was ein Pointer ist, Annahmen bricht, auf die Jahrzehnte von C-Code aufbauen.
Das erste Problem ist Size. Capabilities sind größer als raw pointers. Auf Morello ist eine Capability 128 Bit plus ein separat von der Hardware gespeichertes 1-Bit-Tag. Das erhöht den Memory-Verbrauch für pointer-lastige Datenstrukturen. Eine Linked List oder ein Baum voller Pointer wird merklich größer. Für viele Applikationen ist der Overhead einstellige Prozente, aber für pointer-chasing Workloads kann es schlimmer sein.
Das zweite Problem ist Casting. C-Code castet Pointer zu uintptr_t, macht Arithmetik und castet zurück. Auf CHERI ist uintptr_t tatsächlich ein Capability-Type, kein Integer. Code, der annimmt, er könne beliebige Integer-Mathematik auf Pointer-Werten machen, wird nicht kompilieren oder zur Laufzeit trappen. Der Fix ist meist, ptrdiff_t für Offsets zu nutzen und sie mit cheri_address_set anzuwenden, aber das erfordert Source-Änderungen.
Das dritte Problem ist das Ökosystem. CHERI-Hardware ist selten. Arm-Morello-Boards existieren, sind aber keine Commodity-Server. CheriBSD und CHERI-enabled Linux sind reif genug, um echte Software zu betreiben, aber die meisten Linux-Distributionen liefern keine CHERI-Packages. Deine Container-Registry, deine CI-Runner und dein Cloud-Provider unterstützen es noch nicht.
Du kannst es heute ohne Custom Silicon ausprobieren
Du brauchst kein Morello-Board, um mit CHERI zu experimentieren. Das CHERI-Project pflegt QEMU-Support, sodass du einen CHERI-Userspace auf jedem Linux- oder macOS-Host laufen lassen kannst.
So bekommst du eine CHERI-Toolchain und führst ein einfaches Beispiel in QEMU aus:
# 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
Sobald du in CheriBSD drin bist, ist der Compiler clang mit CHERI-Target-Support. Du kompilierst mit enforced Bounds Checking by default:
$ clang -o test test.c
$ ./test
# Buffer overflows trap immediately with a clear message
Wenn du existierende Software testen willst, kann cheribuild populäre Packages aus FreeBSD Ports mit CHERI-Support kompilieren. Das CHERI-Team hat OpenSSH, nginx, PostgreSQL und große Teile des FreeBSD-Base-Systems portiert. Viele Programme laufen ohne Änderungen. Diejenigen, die brechen, tun das meist wegen unsicherer Pointer-Casts oder Annahmen über Pointer-Size.
Für einen schnelleren Start ohne QEMU veröffentlicht die University of Cambridge auch ein Docker-Image mit vorinstallierter CHERI-LLVM-Toolchain. Du kannst CHERI-Binaries kompilieren und den generierten capability-aware Assembly inspizieren, ohne ein ganzes OS zu booten.
Was Hardware-Memory-Safety für existierende Codebases bedeutet
CHERI ist kein Ersatz dafür, sicheren Code zu schreiben. Es ist ein Safety Net für den Code, den du bereits hast. Ein CHERI-System, das legacy C ausführt, wird weiterhin Logic Bugs, Race Conditions und Use-After-Free-Fehler haben. Aber es wird keine Buffer Overflows haben, die Return Addresses überschreiben, adjacent Heap-Metadata korumpieren oder Secrets über Allocations-Grenzen hinweg leaken.
Die Entwicklung ist bereits sichtbar. Arm hat CHERI-derived Features in seine Memory Tagging Extension (MTE) integriert, die heute in produktiven Android-Geräten ausgeliefert wird. MTE ist coarser als CHERI (es taggt 16-Byte-Granulen statt individueller Allocations), fängt aber viele der gleichen Bugs mit geringerem Overhead. Full CHERI wird wahrscheinlich folgen, wenn das Ökosystem reift.
Wenn du C- oder C++-Code maintainst, der untrusted Input verarbeitet, ist die Frage nicht, ob Hardware-Memory-Safety ankommen wird. Es ist, ob du bereit sein wirst, wenn es soweit ist. Beginne damit, deinen Code auf Pointer-to-Integer-Casts, Pointer-Arithmetik auf unverbundenen Objekten und Annahmen über sizeof(void*) zu auditen. Das sind die Patterns, die unter CHERI brechen, und ihre Behebung macht deinen Code sauberer, selbst auf konventioneller Hardware.
Die Hardware ist endlich bereit, Nein zu sagen. Wir sollten sie lassen.