GHSA-5W5R-MF82-595P

When 'Safe' Rust Walks the Plank: Cap'n Proto Undefined Behavior Deep Dive

Alon Barad
Alon Barad
Software Engineer

Jan 29, 2026·6 min read·2 visits

Executive Summary (TL;DR)

The Cap'n Proto Rust crate exposed public fields in `constant::Reader` and `StructSchema` that allowed safe code to inject arbitrary data. This data was subsequently processed by internal `unchecked` functions, leading to Undefined Behavior (UB).

A critical undefined behavior vulnerability in the Rust implementation of Cap'n Proto allows safe code to trigger memory corruption. By exposing internal raw pointers and unchecked offsets through public struct fields, the library violated Rust's safety guarantees, turning malformed schema constants into potential remote code execution or denial of service vectors.

The Hook: Infinity Times Faster, Zero Times Safer?

Cap'n Proto markets itself as "infinity times faster" than Protocol Buffers because it doesn't just serialize data; it lays it out in memory exactly as it will be sent over the wire. Zero encoding, zero decoding. It's a raw memory mapping dream. In C++, this is business as usual—you manage the pointers, you take the risk. But this is the Rust implementation we're talking about.

The promise of Rust is simple: if you don't write the word unsafe, you shouldn't be able to segfault. The compiler is the bouncer, and it checks everyone's ID. But in version 0.23 and below, the capnproto crate left the back door unlocked. It allowed "Safe" Rust code to construct objects that violated internal invariants, handing them off to performance-critical code that assumed everything was kosher. The result? A perfectly valid-looking Rust program that can corrupt memory, read out of bounds, or crash spectacularly.

The Flaw: Trusting the Public Interface

The vulnerability boils down to a classic API design blunder: exposing raw internals that the library logic implicitly trusts. In Cap'n Proto, performance is king, so internal functions often use unchecked operations to skip bounds checks. For example, PointerReader::get_root_unchecked assumes that the pointer logic it's about to perform is valid because it relies on the data having been validated during creation.

The problem? The structs holding that data—specifically constant::Reader and RawStructSchema—had their fields marked as pub. This meant any downstream developer (or attacker writing a plugin) could manually instantiate these structs. You didn't need a valid Cap'n Proto schema; you just needed to fill a struct with garbage data.

Because the fields were public, the library had no way to enforce invariants at construction time. When the user later called a safe method like .get(), the library took that user-supplied garbage, assumed it was a valid memory map, and passed it to get_root_unchecked. It’s like handing a loaded gun to a toddler because you assumed only a trained soldier would ever walk into the armory.

The Code: The Smoking Gun

Let's look at the code that allowed this mutiny. In src/constant.rs, the Reader struct was defined like this:

// The Vulnerable Definition
pub struct Reader<T> {
    #[doc(hidden)]
    pub phantom: PhantomData<T>,
 
    #[doc(hidden)]
    pub words: &'static [crate::Word],
}

See that pub words? That is the root of all evil here. Even though it is hidden from documentation, it is accessible to code. A user can create a Reader with a slice of words pointing to 0xDEADBEEF or random offsets.

The fix in version 0.24.0 is a lesson in Rust encapsulation. The developers locked down the visibility and forced the use of an unsafe constructor, shifting the burden of safety back to the developer:

// The Fixed Definition
pub struct Reader<T> {
    pub(crate) phantom: PhantomData<T>,
    pub(crate) words: &'static [crate::Word], // No longer public!
}
 
impl<T> Reader<T> {
    // You want to build this manually? You sign the waiver.
    pub const unsafe fn new(words: &'static [crate::Word]) -> Self {
        Self {
            phantom: PhantomData,
            words,
        }
    }
}

By marking the constructor unsafe, the library now complies with Rust's safety contract. If you pass bad data and it segfaults, it's your fault, not the library's.

The Exploit: Manufacturing Madness

Exploiting this doesn't require complex heap feng shui or ROP chains; it just requires writing "Safe" Rust that lies. An attacker works within the bounds of the language syntax but violates the semantic expectations of the library.

Here is a conceptual Proof of Concept showing how to trigger Undefined Behavior without using a single unsafe block in the attacking code (prior to the patch):

use capnp::constant;
use core::marker::PhantomData;
 
fn main() {
    // 1. Create a slice of 'words' that represent a garbage pointer.
    // In Cap'n Proto, specific bit patterns represent offsets.
    // We fake a pointer that points way out of bounds.
    let malicious_words = &[capnp::word(0xFFFFFFFF, 0xFFFFFFFF)];
 
    // 2. Manually construct the Reader using the public fields.
    // The compiler allows this because 'words' was public.
    let reader: constant::Reader<some_type::Owned> = constant::Reader {
        phantom: PhantomData,
        words: malicious_words,
    };
 
    // 3. Trigger the UB.
    // .get() calls internal unchecked functions.
    // The library tries to follow the 0xFFFFFFFF offset and reads invalid memory.
    let _ = reader.get(); 
}

When reader.get() executes, the application will likely crash (SIGSEGV) or, worse, read sensitive memory adjacent to the buffer if the offset is carefully calculated. In a scenario where schemas are dynamically loaded or defined by untrusted plugins, this allows a complete bypass of memory safety.

The Impact: Why Panic?

In the Rust ecosystem, a vulnerability labeled "Undefined Behavior in Safe Code" is the highest tier of defect. It erodes the trust model of the entire dependency tree. If capnproto is used in a network service (its primary use case), and that service processes user-supplied schema definitions or constants, an attacker can crash the node trivially.

While this specific vector requires the ability to construct the Reader or Schema object (which is often done at compile time via capnpc), systems that use dynamic schema loading or plugin architectures are vulnerable. The impact ranges from Denial of Service (DoS) via panic/segfault to potential information disclosure if the out-of-bounds read returns valid memory regions to the attacker.

The Fix: Safe Harbors

Remediation is straightforward but requires code changes. Upgrade to capnproto version 0.24.0 immediately. If you were manually constructing these structs (which you shouldn't have been doing, you naughty developer), your build will break.

You will need to switch to using the generated code from capnpc or, if you absolutely must create these manually, wrap your creation logic in an unsafe block and use the new Reader::new() constructor. This explicitly acknowledges that you have verified the input data.

For security teams auditing Rust codebases: Grep for direct struct initialization of capnp::constant::Reader or capnp::introspect::RawStructSchema. If you see them being built with curly braces { ... } rather than a constructor method, you are looking at vulnerable code.

Fix Analysis (2)

Technical Appendix

CVSS Score
Critical/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Probability
0.04%
Top 100% most exploited

Affected Systems

Rust applications using capnproto < 0.24.0Systems using dynamic Cap'n Proto schema loading

Affected Versions Detail

Product
Affected Versions
Fixed Version
capnproto (Rust)
Cap'n Proto
< 0.24.00.24.0
AttributeDetail
Attack VectorLocal / Context-dependent
ImpactMemory Corruption / Undefined Behavior
CWEsCWE-242, CWE-822, CWE-119
StatusPatched in 0.24.0
LanguageRust
Bug ClassUnsound API / Safe Wrapper around Unsafe
CWE-242
Use of Inherently Dangerous Function

The application allows the use of inherently dangerous functions by exposing internal memory structures to safe code, leading to untrusted pointer dereferences.

Vulnerability Timeline

Fix commits pushed by David Renshaw
2025-12-24
GHSA Advisory Published
2026-01-28

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.