Sie haben eine C-codebase, die zu groß ist, um sie in Rust zu rewrite, und zu kritisch, um sie Buffer-Overflows auszusetzen. CHERI-Capability-Hardware verspricht, Verstöße gegen die Speichersicherheit auf CPU-Ebene aufzufangen, aber das Internet erzählt Ihnen ständig, dass CHERI-Zeiger 128 Bit groß sind und Ihr Code von 64 ausgeht.
Die gute Nachricht ist, dass CHERI keine Alles-oder-Nichts-Migration erzwingt. Die hybride ABI ermöglicht es Ihnen, bestehenden C-Code mit minimalen Änderungen zu kompilieren, auf echter CHERI-Hardware auszuführen und die Sicherheit schrittlich dort zu erhöhen, wo sie am meisten zählt. Sie müssen keine Million Zeilen Code portieren, bevor Sie Ihren ersten geschützten Zeiger erhalten.
Warum schrittweise Portierung jetzt möglich ist
Frühe Capability-Architekturen waren kompromisslos. Der iAPX 432 verlangte, dass jeder Zeiger eine Capability war – Punkt. Das brach jeden bestehenden Compiler und jedes bestehende Betriebssystem, und das Projekt scheiterte.
CHERI hat aus diesem Scheitern gelernt. Es unterstützt drei Kompilierungsmodi: Baseline (nur legacy Integer-Zeiger), Hybrid (Integer- und Capability-Zeiger koexistieren) und Purecap (jeder Zeiger ist eine Capability). Die hybride ABI ist die Brücke. Sie ermöglicht es Ihnen, Ihren bestehenden Code weitgehend unverändert zu lassen, während Sie selektiv dort Capabilities einführen, wo sie den größten Nutzen bringen.
Im Hybrid-Modus ist void* weiterhin 64 Bit groß. Ein Capability-Zeiger ist ein eigener Typ, void* __capability, den Sie explizit wählen. Ihre Structs, Ihre verketteten Listen und Ihre Hash-Tabellen ändern ihre Größe nicht, wenn Sie es nicht wollen. Das ist keine Kompatibilitätsschicht. Es ist eine erstklassige ABI, die von der Hardware und dem Compiler unterstützt wird.
Was tatsächlich kaputt geht, wenn Sie für CHERI Hybrid kompilieren
Der erste Schritt besteht darin, Ihren Code mit einer CHERI-Toolchain zu kompilieren und zu sehen, was explodiert. Der meiste wohlgeformte C-Code kompiliert ohne Änderungen. Die Probleme ballen sich um eine Handvoll Muster, die CHERI illegal macht – und das ist genau der Punkt.
Zeiger-zu-Integer-Casts. Code, der einen Zeiger zu uintptr_t castet, einige Bits maskiert und zurückcastet, wird fehlschlagen. Auf CHERI ist uintptr_t ein Capability-Typ, keine einfache Integer. Bitweise Operationen auf Capabilities entfernen das Tag-Bit, und der resultierende Wert löst bei der Dereferenzierung eine Trap aus.
Annahmen über die Zeigergröße. Code, der sizeof(void*) == 8 fest kodiert oder Zeiger als 8-Byte-Werte serialisiert, wird im Purecap-Modus brechen. Im Hybrid-Modus funktioniert das meist, aber es wird zum Problem, sobald Sie Capability- und Integer-Zeiger in demselben Struct mischen.
Zeigerarithmetik über nicht zusammenhängende Objekte hinweg. C-Programmierer berechnen manchmal die Distanz zwischen zwei beliebigen Zeigern oder vergleichen Zeiger aus verschiedenen Allokationen. CHERI-Capabilities tragen Bounds-Metadaten, und die Subtraktion von Capabilities aus verschiedenen Allokationen ist undefiniert. Der Compiler wird sie ablehnen oder die Hardware löst eine Trap aus.
Inline-Assembly. Jedes handgeschriebene Assembly, das Zeiger in Integer-Registern verschiebt, muss aktualisiert werden. CHERI hat dedizierte Capability-Register, und der Compiler muss wissen, welche Sie berühren.
Die CHERI-LLVM-Toolchain liefert Ihnen hervorragende Diagnosen. Sie bekommen keine kryptischen Linker-Fehler. Sie bekommen klare Meldungen wie “cast from capability to integer is not allowed” oder “arithmetic on capabilities from different allocations”. Beheben Sie den ersten Fehler, kompilieren Sie neu und jagen Sie den nächsten.
Ein konkretes Beispiel: Einen Parser mit Capabilities ummanteln
Stellen Sie sich vor, Sie haben einen Netzwerk-Paket-Parser, der nicht vertrauenswürdige Eingaben in einen Puffer liest und dann Header parst. Das ist genau die Art von Code, die von hardwareseitiger Bounds-Checking profitiert. So ummanteln Sie ihn, ohne den Parser selbst zu rewrite.
Kompilieren Sie zuerst den Parser im Hybrid-Modus. Die CHERI-Toolchain ist Standard-LLVM mit einem CHERI-Ziel:
# Eine einzelne Datei mit der hybriden ABI kompilieren
$ clang --target=riscv64-unknown-freebsd \
-march=rv64imafdcxcheri \
-mabi=lp64d \
-mno-relax \
-c parser.c -o parser.o
Das Flag -mabi=lp64d hält Integer-Zeiger auf 64 Bit. Ihre bestehenden void*- und char*-Typen bleiben unverändert. Der Parser kompiliert wie bisher.
Fügen Sie nun in einer separaten Datei einen Capability-bewussten Wrapper hinzu:
#include <cheriintrin.h>
#include <stddef.h>
#include <stdint.h>
// Bestehender Parser aus parser.c
extern int parse_packet(const char *data, size_t len);
// Capability-bewusster Einstiegspunkt
int parse_packet_safe(const char * __capability data, size_t len) {
// Die Capability auf exakt die Pufferlänge einschränken.
// Selbst wenn der Aufrufer eine größere Allokation übergeben hat,
// kann der Parser nicht über 'len' Bytes hinauslesen.
const char * __capability narrowed =
cheri_bounds_set(data, len);
// Im Hybrid-Modus übergeben wir einen einfachen Zeiger an den Legacy-Parser.
// Der Compiler fügt hier eine Capability-zu-Integer-Konvertierung ein.
// Wenn die Capability nicht in 64 Bit passt (was sie bei
// Large-Capability-Adressen nicht tut), wird dies warnen oder fehlschlagen.
//
// Für die Purecap-Migration würden Sie stattdessen parse_packet
// so ändern, dass er einen Capability-Zeiger akzeptiert.
return parse_packet((const char *)narrowed, len);
}
In diesem Beispiel verwendet parse_packet weiterhin legacy Integer-Zeiger. Der Wrapper schränkt die Capability des Aufrufers auf die exakte Länge der Eingabe ein und konvertiert sie dann zurück. Der Einschränkungsschritt bedeutet, dass selbst wenn der Aufrufer versehentlich einen 4-KB-Puffer übergibt, obwohl er 64 Bytes meinte, die Hardware die 64-Byte-Grenze auf der eingeschränkten Capability durchsetzt.
Das ist nicht der Endzustand. Es ist ein Zwischenschritt. Sie bekommen heute schon Bounds-Enforcement an der API-Grenze, und Sie können parse_packet bei einem späteren refactor auf Purecap migrieren.
Der Migrationspfad von Hybrid zu Purecap
Der Hybrid-Modus ist ein Ausgangspunkt, kein Ziel. Er bietet Kompatibilität, aber nicht den vollen Sicherheitsnutzen von Capabilities. Das langfristige Ziel ist Purecap, wo jeder Zeiger Bounds trägt.
Die praktische Migration sieht so aus:
-
Im Hybrid-Modus kompilieren und Build-Fehler beheben. Das bedeutet in der Regel, Zeiger-zu-Integer-Casts und Annahmen über
sizeof(void*)zu korrigieren. Ändern Sie Ihre Datenstrukturen noch nicht. Bringen Sie einfach einen sauberen Build zustande. -
Hochwertige Ziele identifizieren. Netzwerk-Parser, Dateiformat-Dekodierer und Deserialisierungsroutinen sind die besten Kandidaten für Capability-Enforcement. Sie verarbeiten nicht vertrauenswürdige Eingaben und dort leben die meisten Bugs der Speichersicherheit.
-
Ummanteln Sie diese Ziele mit eingeschränkten Capabilities. Verwenden Sie
cheri_bounds_set, um eingeschränkte Capabilities an Vertrauensgrenzen zu erstellen. Übergeben Sie sie an Ihren bestehenden Code. -
Migrieren Sie Leaf-Funktionen zu Purecap. Beginnen Sie mit Hilfsfunktionen, die Zeiger allokieren und zurückgeben. Ändern Sie ihre Signaturen, um
__capabilityzu verwenden, und kompilieren Sie sie mit-mabi=purecap. Arbeiten Sie sich den Call-Stack hinauf. -
Schalten Sie schließlich das gesamte module auf Purecap um. Wenn eine Kompilierungseinheit keine Integer-Zeiger mehr enthält, kompilieren Sie sie mit
-mabi=purecapund linken Sie sie mit dem Rest Ihres Hybrid-Codes. Die CHERI-Toolchain unterstützt Mixed-ABI-Linking.
Das ist für eine große codebase kein Wochenendprojekt. Aber es ist auch kein rewrite. Sie können Ihre verwundbarsten Code-Pfade innerhalb von Tagen schützen und den Rest schrittweise migrieren, wenn Sie ihn anfassen.
Wie die Trade-offs tatsächlich aussehen
Die hybride ABI hat echte Kosten, und Sie sollten sie kennen, bevor Sie sich festlegen.
Zuerst: Gemischte ABIs verkomplizieren Ihren Build. Sie haben jetzt Objektdateien mit unterschiedlichen Zeigergrößen in derselben Binärdatei kompiliert. Der Linker muss Capability- und Integer-Relokationen verarbeiten. Die CHERI-Toolchain unterstützt das, aber Ihr Build-System weiß wahrscheinlich noch nichts davon. Sie müssen es ihm beibringen.
Zweitens: Die Capability-zu-Integer-Konvertierung an Hybrid-Grenzen ist verlustbehaftet. Wenn Sie eine Capability einschränken und sie dann in einen einfachen Zeiger casten, verlieren Sie die Bounds. Der zugrunde liegende Speicher ist weiterhin durch die Capability geschützt, mit der Sie begonnen haben, aber die Legacy-Funktion erhält kein hardware enforcement. Deshalb ist Purecap das Ziel: Jeder Zeiger in der Aufrufkette trägt seine eigenen Bounds.
Drittens: Das Debugging ändert sich. GDB auf CHERI versteht Capability-Register und kann Bounds-Metadaten ausgeben. Die LLDB-Unterstützung verbessert sich. Wenn Ihr aktueller Debugging-Workflow darauf beruht, rohe Zeigerwerte zu inspizieren, müssen Sie die CHERI-Registernamen lernen.
Der Performance-Overhead des Hybrid-Modus ist für Code, der hauptsächlich Integer-Zeiger verwendet, normalerweise vernachlässigbar. Der Purecap-Overhead liegt typischerweise bei einstelligen Prozentzahlen für die meisten Workloads und steigt auf 10–15 % für zeigerintensive Datenstrukturen. Das ist konkurrenzfähig mit Software-Mitigations wie ASAN, und im Gegensatz zu ASAN läuft CHERI in der Produktion mit voller Geschwindigkeit.
Sie können heute schon mit QEMU loslegen
Sie brauchen kein Morello-Board, um zu experimentieren. Das CHERI-Projekt pflegt QEMU-Unterstützung und Docker-Images mit vorinstallierter LLVM-Toolchain.
# Das CHERI-Toolchain-Docker-Image herunterladen
$ docker run --rm -it ctsrd/cheri-sdk:latest
# Innerhalb des Containers Ihren Code für RISC-V CHERI kompilieren
$ clang --target=riscv64-unknown-freebsd \
-march=rv64imafdcxcheri \
-mabi=lp64d \
-o myapp myapp.c
Beginnen Sie damit, Ihr Projekt im Hybrid-Modus zu kompilieren und die Fehler zu zählen. Die Zahl wird Ihnen sagen, wie viel Arbeit bevorsteht. Ein sauberer Build beim ersten Versuch ist selten, aber für modernen, standardkonformen C nicht unmöglich. Einige hundert Fehler sind typisch für ältere Codebasen mit vielen Zeiger-Casts.
Beheben Sie die Fehler in dieser Reihenfolge: Zuerst Integer-zu-Zeiger-Casts, dann Zeigerarithmetik über Allokationen hinweg, dann Inline-Assembly. Jede Kategorie hat eine mechanische Lösung. Das CHERI-Projekt veröffentlicht einen Portierungsleitfaden mit Vorher-Nachher-Beispielen für die häufigsten Muster.
Capability-Hardware ist keine theoretische Zukunft. Sie ist eine funktionierende Toolchain, eine unterstützte ABI und ein Migrationspfad, der es nicht erfordert, Ihre codebase in Schutt und Asche zu legen. Beginnen Sie mit einer Datei, einer Funktion, einer eingeschränkten Capability. Die Hardware erledigt den Rest.