You have a C codebase that is too large to rewrite in Rust and too critical to leave exposed to buffer overflows. CHERI capability hardware promises to catch memory safety violations at the CPU level, but the internet keeps telling you that CHERI pointers are 128 bits and your code assumes 64.
The good news is that CHERI does not force an all-or-nothing migration. The hybrid ABI lets you compile existing C code with minimal changes, run it on real CHERI hardware, and incrementally tighten security where it matters most. You do not need to port a million lines of code before you get your first protected pointer.
Why incremental porting is possible now
Early capability architectures were uncompromising. The iAPX 432 required every pointer to be a capability, full stop. That broke every existing compiler and operating system, and the project died.
CHERI learned from that failure. It supports three compilation modes: baseline (legacy integer pointers only), hybrid (integer and capability pointers coexist), and purecap (every pointer is a capability). The hybrid ABI is the bridge. It lets you keep your existing code mostly untouched while selectively introducing capabilities where they add the most value.
In hybrid mode, void* is still 64 bits. A capability pointer is a distinct type, void* __capability, that you opt into explicitly. Your structs, your linked lists, and your hash tables do not change size unless you ask them to. This is not a compatibility layer. It is a first-class ABI supported by the hardware and the compiler.
What actually breaks when you compile for CHERI hybrid
The first step is to try compiling your code with a CHERI toolchain and see what explodes. Most well-behaved C code compiles without changes. The problems cluster around a handful of patterns that CHERI makes illegal, which is exactly the point.
Pointer-to-integer casts. Code that casts a pointer to uintptr_t, masks some bits, and casts back will fail. On CHERI, uintptr_t is a capability type, not a plain integer. Bitwise operations on capabilities strip the tag bit, and the resulting value traps on dereference.
Assumptions about pointer size. Code that hardcodes sizeof(void*) == 8 or serializes pointers as 8-byte values will break in purecap mode. In hybrid mode this mostly works, but it becomes a problem the moment you mix capability and integer pointers in the same struct.
Pointer arithmetic across unrelated objects. C programmers sometimes compute the distance between two arbitrary pointers or compare pointers from different allocations. CHERI capabilities carry bounds metadata, and subtracting capabilities from different allocations is undefined. The compiler will reject it or the hardware will trap.
Inline assembly. Any hand-written assembly that moves pointers in integer registers will need updating. CHERI has dedicated capability registers, and the compiler needs to know which ones you are touching.
The CHERI LLVM toolchain gives you excellent diagnostics. You do not get cryptic linker errors. You get clear messages like “cast from capability to integer is not allowed” or “arithmetic on capabilities from different allocations.” Fix the first error, recompile, and chase the next one.
A concrete example: wrapping a parser with capabilities
Imagine you have a network packet parser that reads untrusted input into a buffer and then parses headers. This is exactly the kind of code that benefits from hardware bounds checking. Here is how to wrap it without rewriting the parser itself.
First, compile the parser in hybrid mode. The CHERI toolchain is standard LLVM with a CHERI target:
# Compile a single file with the hybrid ABI
$ clang --target=riscv64-unknown-freebsd \
-march=rv64imafdcxcheri \
-mabi=lp64d \
-mno-relax \
-c parser.c -o parser.o
The -mabi=lp64d flag keeps integer pointers 64-bit. Your existing void* and char* types stay unchanged. The parser compiles as-is.
Now add a capability-aware wrapper in a separate file:
#include <cheriintrin.h>
#include <stddef.h>
#include <stdint.h>
// Existing parser from parser.c
extern int parse_packet(const char *data, size_t len);
// Capability-aware entry point
int parse_packet_safe(const char * __capability data, size_t len) {
// Narrow the capability to exactly the buffer length.
// Even if the caller passed a larger allocation,
// the parser cannot read past 'len' bytes.
const char * __capability narrowed =
cheri_bounds_set(data, len);
// In hybrid mode, we pass a plain pointer to the legacy parser.
// The compiler inserts a capability-to-integer conversion here.
// If the capability does not fit in 64 bits (it won't for
// large-capability addresses), this will warn or error.
//
// For purecap migration, you would instead modify parse_packet
// to accept a capability pointer.
return parse_packet((const char *)narrowed, len);
}
In this example, parse_packet still uses legacy integer pointers. The wrapper narrows the caller’s capability to the exact length of the input, then converts it back. The narrowing step means that even if the caller accidentally passes a 4KB buffer when they meant to pass 64 bytes, the hardware will enforce the 64-byte bound on the narrowed capability.
This is not the final state. It is a stepping stone. You get bounds enforcement at the API boundary today, and you can migrate parse_packet to purecap in a later refactor.
The migration path from hybrid to purecap
Hybrid mode is a starting point, not a destination. It gives you compatibility but not the full security benefit of capabilities. The long-term goal is purecap, where every pointer carries bounds.
The practical migration looks like this:
-
Compile in hybrid mode and fix build errors. This usually means fixing pointer-to-integer casts and
sizeof(void*)assumptions. Do not change your data structures yet. Just get a clean build. -
Identify high-value targets. Network parsers, file format decoders, and deserialization routines are the best candidates for capability enforcement. They process untrusted input and they are where most memory safety bugs live.
-
Wrap those targets with narrowed capabilities. Use
cheri_bounds_setto create restricted capabilities at trust boundaries. Pass them into your existing code. -
Migrate leaf functions to purecap. Start with utility functions that allocate and return pointers. Change their signatures to use
__capabilityand compile them with-mabi=purecap. Work your way up the call stack. -
Eventually flip the whole module to purecap. When a compilation unit has no integer pointers left, compile it with
-mabi=purecapand link it with the rest of your hybrid code. The CHERI toolchain supports mixed-ABI linking.
This is not a weekend project for a large codebase. But it is also not a rewrite. You can protect your most vulnerable code paths in days, and migrate the rest incrementally as you touch it.
What the trade-offs actually look like
The hybrid ABI has real costs, and you should know them before you commit.
First, mixed ABIs complicate your build. You now have object files compiled with different pointer sizes in the same binary. The linker must handle capability and integer relocations. The CHERI toolchain supports this, but your build system probably does not know about it yet. You will need to teach it.
Second, the capability-to-integer conversion at hybrid boundaries is lossy. If you narrow a capability and then cast it to a plain pointer, you lose the bounds. The underlying memory is still protected by the capability you started with, but the legacy function receives no hardware enforcement. This is why purecap is the goal: every pointer in the call chain carries its own bounds.
Third, debugging changes. GDB on CHERI understands capability registers and can print bounds metadata. LLDB support is improving. If your current debugging workflow relies on inspecting raw pointer values, you will need to learn the CHERI register names.
The performance overhead of hybrid mode is usually negligible for code that mostly uses integer pointers. Purecap overhead is typically single-digit percentages for most workloads, rising to 10-15% for pointer-heavy data structures. That is competitive with software mitigations like ASAN, and unlike ASAN, CHERI runs at full speed in production.
You can start today with QEMU
You do not need a Morello board to experiment. The CHERI project maintains QEMU support and Docker images with the LLVM toolchain preinstalled.
# Pull the CHERI toolchain Docker image
$ docker run --rm -it ctsrd/cheri-sdk:latest
# Inside the container, compile your code for RISC-V CHERI
$ clang --target=riscv64-unknown-freebsd \
-march=rv64imafdcxcheri \
-mabi=lp64d \
-o myapp myapp.c
Start by compiling your project in hybrid mode and counting the errors. The number will tell you how much work is ahead. A clean build on the first try is rare but not impossible for modern, standards-compliant C. A few hundred errors is typical for older codebases with lots of pointer casts.
Fix the errors in this order: integer-to-pointer casts first, then pointer arithmetic across allocations, then inline assembly. Each category has a mechanical fix. The CHERI project publishes a porting guide with before-and-after examples for the most common patterns.
Capability hardware is not a theoretical future. It is a working toolchain, a supported ABI, and a migration path that does not require burning your codebase to the ground. Start with one file, one function, one narrowed capability. The hardware will do the rest.