Approaches to Memory Safety
Memory safety is not one product or technique, but a layered engineering strategy spanning languages, tools, hardware, and isolation.
There is no single migration that fits every software system. A new network service might be practical to write in Rust. A decades-old operating-system component may need to remain in C. A tiny microcontroller may not support the same runtime checks as a server. The useful question is therefore not “Which memory-safety approach wins?” but “Which combination removes the most risk within this system’s constraints?”
Memory-safe programming languages
Languages such as Rust, Java, C#, Go, and Swift prevent or control many unsafe memory operations through their type systems, runtimes, or both. They can eliminate broad classes of defects in code written within their safe language rules.
This is often the strongest choice for new software. The practical constraints are migration cost, library availability, hardware support, real-time requirements, and interfaces with existing C or C++ code. Rust also has an unsafe mode for operations that the compiler cannot prove safe. That code is necessary for some low-level tasks and must be reviewed carefully.
Safer C and C++ practices
Existing C and C++ code can be improved with restricted language subsets, safer libraries, stronger types, ownership conventions, bounds-aware interfaces, and modern compiler warnings. These measures can remove risk without a complete rewrite.
Their effectiveness depends on consistent adoption. A convention is only protective when every relevant caller follows it, and an old interface that accepts a pointer without a length may continue to spread ambiguity through the codebase.
Testing and analysis
Static analysers inspect source or compiled code without running it. Dynamic tools, such as sanitizers, observe a running program and detect invalid behaviour. Fuzzers repeatedly provide unusual inputs to find paths that ordinary tests miss.
These tools are essential for finding bugs and improving code quality. Their boundary is coverage: they reason about selected rules or executions rather than enforcing the intended authority of every pointer in every production run.
Runtime mitigation
Address-space layout randomisation, non-executable memory, stack protection, control-flow integrity, and hardened allocators make exploitation more difficult. They are valuable layers because no prevention approach is perfect.
Most mitigations act after a bug has created an unsafe state. Some are probabilistic, while others protect only particular data or attack techniques. A determined attacker may combine an information leak, data corruption, and reuse of existing code to work around them.
Isolation and compartmentalisation
Processes, virtual machines, language sandboxes, and hardware protection domains separate components. If one component is compromised, isolation can stop it reaching the rest of the system.
The main design challenge is granularity. A whole browser tab or service process may still contain large amounts of code and sensitive data. Smaller compartments can reduce that exposure, but conventional context switching and communication mechanisms can make them costly or complex.
CHERI hardware enforcement
CHERI extends an instruction-set architecture so that software can use hardware-protected capabilities. A capability can identify a memory range, carry permissions, and act as an unforgeable token of authority. The processor checks capabilities when code accesses memory.
This supports two related goals:
- Fine-grained memory protection. Pointers can be bounded to objects and restricted to permitted operations.
- Scalable compartmentalisation. Components can receive only the capabilities needed for their work.
CHERI can protect adapted C and C++ code without requiring every component to be rewritten in a new language. It can also strengthen the platform beneath safe languages and their runtimes.
Choosing a layered strategy
- New application code: Prefer a memory-safe language and minimise unsafe interfaces.
- Existing C or C++ service: Use analysis, sanitizers, and safer interfaces, then evaluate a CHERI pure-capability port.
- Mixed-language system: Define narrow interfaces and protect the boundary with native code.
- Embedded firmware: Compare CHERIoT, safe-language support, resource limits, and real-time needs.
- Large trusted component: Divide authority between compartments and test each boundary.
- Long-lived product: Plan for tools, updates, supplier evidence, and hardware refresh cycles.
The approaches reinforce one another. Safe languages reduce the number of bugs introduced. CHERI constrains low-level accesses and unsafe code. Compartments contain faults. Testing finds the remaining defects. Operational controls handle vulnerabilities that still reach production.
Key takeaways
- Treat memory safety as a system property, not a language label.
- Prefer prevention over relying only on exploit mitigation.
- Keep unsafe code and high authority small, visible, and testable.
- Measure the actual protection of the chosen architecture, software mode, and workload.
See CHERI and Rust for a closer look at how language and hardware safety can work together.
