Carbon memory safety

chandlerc.blog · 2026-08-20 · by Long Nguyen ·generic
Chandler's blog about programming, languages, compilers, performance, cpus, tech, and everything else.

Carbon memory safety

Carbon memory safety

A first deep dive

<https://chandlerc.blog/slides/2026-memory-safety-deep-3/>

Goal of Carbon’s memory safety design

#### Smooth and incremental transition from C++ to memory safety in Carbon

  • Result needs to be memory safe
  • Expressivity to represent common C++ code patterns
  • * Makes the transition smooth

  • Incremental in two ways
  • * Migrate to Carbon a little at a time * Incrementally add safety once migrated

    Two modes: permissive and strict

  • Permissive mode, along with C++ interop, supports incremental migration
  • * Allows code that doesn’t yet have safety annotations
  • Strict Carbon is fully memory safe
  • * The destination; goal is to migrate all code to strict mode

    Strict Carbon is fully memory safe

  • Temporal: Preventing “use after free” (UAF) at compile time
  • Spatial: Runtime bounds checking as in 🦀 Rust and being added to C++
  • Type, initialization, null pointer, and data race safety
  • Expressivity

  • Non-exclusive mutable pointers
  • * Directly proving existing correct C++ code is memory safe
  • Support for C++ features like inheritance and specialization
  • More expressive than Rust, but with a complexity and verbosity cost
  • Allows smooth migration of C++
  • * Code patterns translate without cliffs, rearchitecting, or lots of unsafe

    Temporal safety

    Preventing use after free at compile time

    Anatomy of a use after free (C++)

    #include <vector>
    #include <cstdio>

    int main() { `<1>std::vector<int> x { 1, 20, 300 }`; `<2>int* p = &x[0]`; `<3>x.push_back(4000)`; printf("%d\n", `<4>*p`); // <- 💣💥 }

    1. Allocation

    1. Capture a pointer into allocation

    1. Free or reallocation

    1. Use of dangling pointer

    Anatomy of a use after free (Carbon)

    import Core library "io";

    fn Run() { // ``buf(T)`` is Carbon's equivalent of C++'s // ``std::vector<T>`` or Rust's ``Vec<T>`` `<1>var x: buf(i32) = (1, 20, 300)`; `<2>var p: i32* = &x[0]`; `<3>x.PushBack(4000)`; // ❌ Compiler error: use of ``p`` after it was // invalidated by ``x.PushBack(4000)``. Core.Print(`<4>*p`); }

    1. Allocation

    1. Capture a pointer into allocation

    1. Free or reallocation

    1. Prevents use of dangling pointer

    No safety annotations in the calling code

    import Core library "io";

    fn Run() { // ``buf(T)`` is Carbon's equivalent of C++'s // ``std::vector<T>`` or Rust's ``Vec<T>`` var x: `<1>buf`(i32) = (1, 20, 300); `<2>var p: i32* = &x[0]`; `<3>x.PushBack(4000)`; // ❌ Compiler error: use of ``p`` after it was // invalidated by ``x.PushBack(4000)``. Core.Print(*p); }

  • The safety annotations are on the `buf` type
  • * Explains how to safely use that type’s API

  • Need something to connect the capture `p` to the invalidation by `PushBack`
  • Place sets written using `^`

  • A variable has storage at some location in memory
  • var x: i32 = 1;

  • A place represent the information we know about that location at compile-time
  • * The place of `x` is written `^x`
  • A place set is a compile-time representation of set of places, also written using `^`
  • * The type for pointers and references include a place set specifying which places are accessible
  • A container type will have a place set describing the places for its elements
  • How Carbon detects the error

    import Core library "io";
     
    fn Run() {
      var `<2>x`: `<1>buf(i32)` = (1, 20, 300);
      var p: `<4>i32*` = &`<3>x[0]`;
      `<5>x.PushBack(4000)`;
      // ❌ Compiler error: use of ``p`` after it was
      //    invalidated by ``x.PushBack(4000)``.
      Core.Print(`<6>*p`);
    }
    class `<1>buf(T: ...)` {
      disjoint `<2>owned ^Elts` of T;

    impl as `<3>IndexRefWith`(i32) fn (ref self, arg: i32) -> `<4>^Elts ref T`;

    fn PushBack(ref self, x: T) `<5>invalidate(^Elts)`; }

    1. `x` owns a heap allocation.

    1. `&x[0]` has type `^x.Elts i32*`. `^x.Elts` tracks where `p` can point (may be omitted for locals).

    1. Call to `PushBack` has an `invalidate(^x.Elts)` safety effect, invalidating `p`. * The compiler checks that functions mark all needed effects.

    1. Use of invalidated pointer `p` is a compile error.

    Ingredients

    import Core library "io";
     
    fn Run() {
      var x: buf(i32) = (1, 20, 300);
      var p: `<2>i32* = &x[0]`;
      x.PushBack(4000);
      // `<4>❌ Compiler error`: use of ``p`` after it was
      //    invalidated by ``x.PushBack(4000)``.
      Core.Print(*p);
    }
    class buf(T: ...) {
      disjoint `<3>owned` `<1>^Elts` of T;

    impl as IndexRefWith(i32) fn (ref self, arg: i32) -> `<1>^Elts` ref T;

    fn PushBack(ref self, x: T) `<4>invalidate`(`<1>^Elts`); }

  • Places and place set expressions: like `^Elts`
  • Alias tracking: which place sets can overlap?
  • Ownership: types say their objects own some places
  • Invalidation: by functions marked with safety effects
  • Places and place sets

    `^`: the “places of” operator

  • Every binding has a place
  • var `<1>x`: i32;
    var p: `<2>^x` i32* = &x;

  • `^x` is the place where the variable `x` is stored
  • Pointer types include the set of possible places they can point to
  • * `p` can point to `^x`

  • When you take the address of a particular variable using `&x`, you get its runtime address
  • * The `^` “places of” operator is parallel: it gets its compile-time place * So the type of `&x` is a pointer to the `^x` place

    Place set expressions

  • Fields
  • class C {
      var x: i32;
      var y: i32;
    }

    var c: C = {.x = 1, .y = 2}; var px: ^c.x i32 = &c.x; var py: `^c.y` i32 = &c.y; var p_union: `^(c.x, c.y)` i32 = if F() then px else py; var p_any: `^c.any` i32 = p_union;

    Place set expressions

  • Places in an owned allocation: `^x.Elts`
  • * Owning types have a named place set member, e.g. `Elts` * Don’t distinguish between `x[0]` and `x[1]`
  • Place set parameters
  • Similarities to Rust’s lifetimes

    In both cases:

  • Additional parameters to functions and types for safety
  • Capturing a compile-time approximation of runtime behavior
  • Used only for safety checking
  • Differences from Rust

    #### Carbon places `^x`

  • Places are about space (memory)
  • We ask if sets of places overlap
  • Grounded in expressions using locals, parameters, fields
  • #### Rust lifetimes `'a`

  • Lifetimes are about time (source regions)
  • We ask if a lifetime outlives another
  • Abstract generic parameters
  • Alias tracking

    What could this pointer reference?

    Alias tracking

    Three kinds:

  • Aliasing between parameters
  • Aliasing of parameters by returns
  • Aliasing in data structures
  • Aliasing between parameters

    By default, parameters may overlap

    fn OverlapError(ref b: buf(i32), ref s: i32) invalidate(^b.Elts) {
      b.PushBack(s);
      // ❌ Error: ``s`` may overlap ``^b.Elts``,
      //    invalidated by ``b.PushBack(s)``.
      s = b.Size();
    }

    `b.PushBack(s)` invalidates anything that could overlap an element of `b`.

    Aliasing between parameters

    Problematic caller

    fn OverlapError(ref b: buf(i32), ref s: i32) invalidate(^b.Elts) {
      b.PushBack(s);
      // ❌ Error: ``s`` may overlap ``^b.Elts``,
      //    invalidated by ``b.PushBack(s)``.
      s = b.Size();
    }

    fn ProblematicCaller() { var b: buf(i32) = (1, 20, 300); OverlapError(`ref b, ref b[0]`); }

    Aliasing between parameters

    Fix: `^` to mark disjoint parameter

    fn Fixed(ref b: buf(i32), `^` ref s: i32) invalidate(^b.Elts) {
      b.PushBack(s);
      // ✅ Okay: caller is required to ensure
      // ``s`` doesn't overlap elements of ``b``.
      s = b.Size();
    }

    fn ErrorNowInCaller() { var b: buf(i32) = (1, 20, 300);

    // ❌ Error: ``b[0]`` may overlap ``^b.any``, // ``Fixed`` requires them to be disjoint. Fixed(`ref b, ref b[0]`);

    var s: i32 = 1; // ✅ Okay: Every ``var`` gets its own // storage, so ``s`` is disjoint. Fixed(`ref b, ref s`); }

    Overlapping / disjoint parameters

  • Have a whole vocabulary for expressing different “may overlap” vs. disjoint relationships between parameters
  • * `^` for a disjoint parameter is the simplest annotation
  • Every binding has its own place
  • Use `^` with a new name to introduce a new place set that can be used in multiple places
  • `^default` contains all places that are not otherwise given a named container
  • `^any` includes everything that can be referenced from any parameter
  • More to this than I’m going to go into

    fn F(ref a1: i32, ref a2: i32,
         ^ ref b1: i32, ^ ref b2: i32,
         ^C ref c1: i32, ^C ref c2: i32,
         ^any ref d: {.x: i32, .y: i32});

    Venn diagram

    Aliasing between parameters

    🦀 Not an issue in Rust

  • “Shared XOR mutable” means reference parameters can’t interact
  • Parameter aliasing is additional information needed for safe non-exclusive mutable references
  • Aliasing of parameters by returns

    By default, returns are allowed to reference `^default.any`:

    fn First(`<0>ref b: buf(i32)`) -> `<0>i32*` {
      return &b[0];
    }

    fn UseAfterFree() { var b: buf(i32) = (1, 20, 300); `<1>var p: i32* = First(ref b)`; // ✅ Okay: prints "1". Core.Print(*p); `<2>b.PushBack(4000)`; // ❌ Error: ``*p`` may overlap ``^b.Elts``, // invalidated by ``b.PushBack(4000)``. Core.Print(*p); }

    `^default` contains only `^b`, so the return type is `^b.any i32`

    `p` gets type `^b.any i32`

  • `^b.Elts` overlaps `^b.any`, so `p` is invalidated as before
  • Use any of the parameter place names in returns

    fn F(ref a1: i32, ref a2: i32,
         ^ ref b1: i32, ^ ref b2: i32,
         ^C ref c1: i32, ^C ref c2: i32,
         ^any ref e: {.x: i32, .y: i32})
      -> `^__` ref i32;

  • default is `^default.any` \= {`^a1`, `^a2`, `^b1`, `^b2`}
  • place of any parameter: `^a1`, `^a2`, `^b1`, `^b2`, `^c1`, `^c2`
  • fields: `^e.x`, `^e.y`
  • named place sets: `^C` \= {`^c1`, `^c2`}
  • `^any` \= {`^a1`, `^a2`, `^b1`, `^b2`, `^c1`, `^c2`, `^e.x`, `^e.y`}
  • any member: `^e.any` \= {`^e.x`, `^e.y`}
  • union: `^(a1, b1, C)` is {`^a1`, `^b1`, `^c1`, `^c2`}
  • More similar to Rust than parameters

    But still different

  • 🦀 Rust says “return borrows from this parameter”
  • * Parameter must outlive the return (preventing use after free) * What you can do with that parameter is limited until the borrow is done * Enforces “shared XOR mutable” * Connected by using the same lifetime parameter
  • Carbon says “return may reference this field of this parameter”
  • * More precise: specific to a field * No restrictions on parameter while being referenced * Return’s reference is invalidated when parameter is * Connected by the return referencing parameters by name

    Aliasing in data structures

    Types with external pointers

  • No default for place parameters in a class definition
  • * Pointer fields must specify explicitly what they can point to
  • To reference something external, the class needs to have a place parameter
  • * No other way to reference something outside the class

    class HasPtr(`^A of i32`) {
      var p: `^A` i32*;
    }

    fn Example() { var i: i32 = 1; var has_ptr: HasPtr(`^i`) = {.p = &i}; }

    🦀 Rust similarly requires a lifetime parameter when fields reference something outside that struct. The relationship is similar as for returns.

    Automatic aliasing for locals

  • Few safety annotations needed for locals
  • * More concise * More like C++
  • Uses flow-sensitive analysis for precision
  • * Reduces invalidations * Analysis comes after overload resolution * Overloads selected is an input into the analysis

    Ownership and invalidation

    Two intertwined concepts

  • Invalidation effect
  • * An example of safety effects, which are used for other parts of the safety story * Propagated up the call stack * Explicitly in function signatures * Increases precision by making invalidation opt-in instead of assumed * Similar to how knowing places are disjoint allows us to reduce invalidations
  • Ownership
  • * Single owner per allocation * Can only invalidate by writing to the owner * Owner is never invalidated

    `buf` again

    class buf(T: ...) {

    // Declare ownership of a set of places. `<4>disjoint` `<1>owned ^Elts` of T;

    // Destroys elements, invalidating pointers // to them and anything they own. fn Clear(ref self) invalidate(`<2>^Elts.any`);

    // May reallocate, causing a relocation of elements // and invalidating pointers to them. fn PushBack(ref self, x: T) invalidate(`<3>^Elts`); }

  • Destruction invalidates more than relocation
  • Don’t have to invalidate a separate allocation when relocating
  • `disjoint owned ^Elts` means `^Elts` refers to a separate allocation
  • Ownership means “independent fate”

  • Fields share fate with their containing object
  • Owned data can be invalidated earlier
  • * Like when the buffer is resized
  • Ownership of data can be transferred
  • * Owned data can outlive its original owner as a result * Can survive the owner being relocated

    Transfer of ownership

  • Two effects that are refinements of `invalidate`: `mix` and `move`
  • Local pointer types with automatic place sets are updated
  • Allows less invalidation when relocating
  • class buf(T: ...) {
      disjoint owned ^Elts of T;

    // Transfers ownership fn Swap(ref self, ref other: Self) move(^self, ^other) move(^other, ^self); }

    fn UsesSwap() { var x: buf(i32) = (1, 2, 3); var y: buf(i32) = (4, 5) var p: i32* = &x[0]; var q: ^(x, y).Elts i32* = &y[0]; x.Swap(ref y); // ``p`` and ``q`` still valid after ``Swap`` Use(p, q); }

    Owners are never invalidated

    fn Run() {
      var x: buf(i32) = (1, 20, 300);
      var p: i32* = &x[0];

    `<1>x.PushBack(4000)`; // ``p`` invalidated by ``x.PushBack``. // ❌ Core.Print(*p);

    // ✅ ``x`` is the owner, so still valid. `<2>p = &x[0]`;

    // ✅ Okay, ``p`` is valid again; may have // a different value if ``x`` reallocated. `<3>Core.Print(*p)`; }

    Allows recovery after invalidation

    Always a single owner

  • Owner enforces invariants
  • * Never invalid * No double free * Automatically avoid leaks * Ownership is transferred, never duplicated
  • Two objects are disjoint if their owners are disjoint
  • * Used to reduce unnecessary invalidations

    Owning enforcement in very few types

  • Few fundamental owning types:
  • * `Box`, `HeapArray`: does heap allocation * `InlineStorage`: used by sum types, and for small-size optimization
  • Okay that they have unsafe code
  • Other owning types like `buf` are built on top
  • What about shared ownership?

    How do we make reference counted types like `std::shared_ptr<T>` safe?

  • Shared ownership modelled as pointers to a single owner
  • That pointer means those types have a place parameter
  • * Unlike 🦀 Rust where `Rc` and `Arc` don’t have lifetime parameters
  • May reference the same owned data if their place arguments overlap
  • Non-owning pointers to that place set are invalidated when any shared owner is freed
  • * But shared owners remain valid (by using `unsafe` internally)

    Type erasure

    Type erasure: generics

    Here we have an interface (like a Rust trait). Different types will implement this interface in different ways. What effects should the method in the interface have?

    interface Notified {
      fn Event(ref self) ???;
    }

  • A generic user of the `Notified` interface will use whatever effect is in the interface
  • To be safe, a type’s implementation of this interface must have a subset of the interface’s safety effects
  • Type erasure: generics

    Here we have an interface (like a Rust trait). Different types will implement this interface in different ways. What effects should the method in the interface have?

    interface Notified {
      fn Event(ref self) unknown;
    }

  • A generic user of the `Notified` interface will use whatever effect is in the interface
  • To be safe, a type’s implementation of this interface must have a subset of the interface’s safety effects
  • * `unknown` is the maximum effect that the type allows, invalidating anything reachable that is owned by something writable

    Type erasure: generics

    interface Notified {
      fn Event(ref self) unknown;
    }

  • Can use a non-owning type to avoid invalidation
  • * Use `std::span` instead of `std::vector` if you aren’t changing the vector’s size * Leverages Carbon’s freedom to have multiple pointers to the same thing
  • Casting to `const` also limits effects
  • Type erasure: inheritance

    base class B(^A) {
      virtual fn F(ref self) unknown;
    }

    class D(^X, ^Y) { extend base: B(^(X, Y)); override fn F(ref self); }

    Virtual methods in base class must have safety effects that encompass derived implementation effects

  • Similar to an interface.
  • Type erasure: inheritance

    base class B(^A) {
      virtual fn F(ref self) unknown;
    }

    class D(^X, ^Y) { extend base: B(^(X, Y)); override fn F(ref self); var p: ^X i32*; var q: ^Y i32*; }

    Additionally need to track aliasing

  • Base class must have place parameters that encompass place parameters of derived classes
  • Maintains invariant that pointers can only reference external objects from type parameters
  • Type erasure: erased place parameters

    `<1>interface I`;
    `<2>fn Generic``<8>T`: I;
    `<3>class C(^A)`;
    `<4>impl C(^B) as I`;

    fn `<5>ConcreteCaller`(`<6>^ ref x`: C(`<7>^D`), `<6>^ ref y`: C(`<7>^D`)) { `<8>Generic`(ref x, ref y); }

  • `^x.any` and `^y.any` in `ConcreteCaller` are disjoint
  • but both `x` and `y` can reference `^D`
  • Call to `Generic` deduces `T` to be `C(^D)`
  • So inside `Generic`, `^z.any` and `^w.any` overlap
  • Invariant: every reachable place must have a place name in the local scope
  • * When the generic call erases `^D`, those places get added to `^z.any` and `^w.any`

    Expressivity

    Support for common C++ patterns

    Expressivity is important

    Benefits:

  • Can show more things memory safe
  • Adds flexibility when structuring code
  • Enables migration of C++ code to Carbon
  • Goal: Represent common C++ coding patterns in strict Carbon with minimal use of `unsafe` and rearchitecting.

    Mutability without exclusivity

    // Parameters can optionally alias
    fn Swap(`<2>ref x: i32`, `<2>ref y: i32`) {
      // Implementation works even if &x == &y.
      let tmp: i32 = x;
      x = y;
      y = tmp;
    }

    fn FisherYatesShuffle(ref vec: buf(i32)) { for (i: i32 in Core.IntRange(vec.Size() - 2)) { let j: i32 = Random(i, vec.Size()); // Two simultaneous mutable references into ``vec`` Swap(`<1>ref vec[i]`, `<1>ref vec[j]`); } }

  • 🦀 Would need changes to work under Rust’s “shared XOR mutable” restriction
  • * The two references may point to the same element

  • Unsafe code making an aliasing pointer won’t introduce UB
  • Self reference

  • An object can have pointers to other fields in the same instance
  • class Form {
      var first: strbuf;
      var last: strbuf;
      var current: ^(first, last) strbuf*;
    }

  • This is used in some small-size optimization implementations
  • * Pointer either points to an inline buffer or heap allocation

    C++ features supported by Carbon but not Rust

    Carbon has a large commitment to supporting C++ features independent of safety

  • Inheritance
  • Specialization
  • Templates
  • Implicit conversions
  • Function overloading
  • …
  • Other kinds of safety

    Same mechanisms

    The same mechanisms used to enforce use-after-free:

  • Safety effects (like invalidation)
  • Places and place sets
  • Function input requirements (like overlap/disjoint)
  • Types
  • are also used to enforce other safety properties.

    Thread safety goals

  • Straight translation of Clang’s \-Wthread-safety thread-safety annotations
  • Prevent data races
  • Not trying to prevent deadlock, no guarantee of forward progress

    Thread safety approach

  • Each mutex has a place set of `guarded` variables
  • Safety effects for important events (similar to invalidation)
  • * Acquiring or releasing locks * Sharing places across threads
  • Status tracked in flow-sensitive state and function signatures
  • * Shared places can only be passed to `shared` parameters * Lock requirements are function constraints
  • Restrict access to `shared` data unless `guarded` by a lock that is held
  • `shared` is an addition beyond the C++ annotations
  • Thread safety example

    class BankAccount {
     private:
      std::mutex mu;
      int balance `<1>GUARDED_BY(mu)`;

    void Withdraw(int amount) `<2>REQUIRES(mu)` { balance -= amount; }

    void Deposit(int amount) { `<7>balance += amount; // ⚠️ Warning` } public: void TransferFrom(BankAccount& b, int amount) { `<5>mu.lock()`; b.Withdraw(amount); // ⚠️ Warning Deposit(amount); mu.unlock(); } };

    class BankAccount {
      private var mu: Core.Mutex;
      private `<1>guarded(mu)` var balance: i32;

    private fn Withdraw( `<4>shared` ref self, amount: i32) `<2>where locked(mu)` { `<3>self.balance -= amount`; }

    private fn Deposit(`<9>ref` self, amount: i32) { `<7>self.balance += amount;` }

    fn TransferFrom(`<9>shared ref` self, shared ref b: Self, amount: i32)`<10> `{ `<5>self.mu.Lock()`; `<6>b.Withdraw(amount); // ❌ Error` `<8>self.Deposit(amount); // ❌ Error` `<10>self.mu.Unlock()`; } }

    `shared` references are similar to Rust’s `&`

  • Deeply immutable until you reach something with interior mutability
  • 🦀 Rust: getting that `&` reference to share across threads uses a shared borrow
  • Carbon: gets a similar result using sharing safety effects
  • Differences from Rust

  • Approach to interior mutability
  • * 🦀 Rust: a mutex contains the guarded data * Carbon: a mutex guards other variables
  • Use of shared references
  • * 🦀 Rust: pervasive * Carbon: only when sharing across threads
  • Carbon doesn’t mark thread safety on types
  • * Contrast with 🦀 Rust’s `Send` and `Sync` traits * Can always make a `shared` reference to an object * Methods opt-in to working on a `shared` reference

    Differences from Rust

  • Approach to interior mutability
  • Use of shared references
  • Carbon doesn’t mark thread safety on types
  • * Contrast with 🦀 Rust’s `Send` and `Sync` traits * Can always make a `shared` reference to an object * Methods opt-in to working on a `shared` reference

    class BankAccount {
      // Can operate on shared references
      private fn Withdraw(`<1>shared ref` self, amount: i32) ...;

    // Can't operate on shared references private fn Deposit(`<2>ref` self, amount: i32); }

    Initialization safety

  • Safety effects mark functions that perform initialization or destructive move
  • Flow-sensitive state tracks initialization status for locals
  • * No full path-sensitivity or correlated conditions * No inlining * Just simple static rules
  • For non-locals, fields and parameters are required to be initialized unless a wrapper type is used
  • * Similar to 🦀 Rust’s `MaybeUninit`

    Migrating C++ → strict Carbon

    Using permissive mode and interop

    Incremental migration from C++ → strict Carbon

    Non-goals

  • Adding safety annotations to C++ code
  • Proving arbitrary C++ code is safe
  • Incremental migration from C++ → strict Carbon

    Instead, our goals are:

  • Mechanical migration of C++ to permissive Carbon
  • Deploy safety annotations and safety checking in Carbon
  • Incremental steps for each these migrations
  • * Fine-grained C++ → Carbon migration * Safety annotations can be introduced gradually * Flexible order and layering

    Incremental migration from C++ → strict Carbon

    How we achieve those goals

    Permissive mode acts as an intermediate step between C++ and strict Carbon

  • Allows a mechanical migration from C++
  • Syntax and semantics of Carbon
  • Safety checks are relaxed, no safety annotations required
  • C++

    ↓

    permissive Carbon

    ↓

    strict Carbon

    Incremental migration from C++ → strict Carbon

    How we achieve those goals

    C++ interop in both directions

  • Migrate in smaller pieces
  • In any order
  • Permissive Carbon can call C++ freely
  • Strict Carbon can call C++, with restrictions
  • Carbon ⇄ C++

    Every step improves safety

  • Permissive Carbon is safer than C++
  • * Less undefined behavior (UB)
  • Strict checking doesn’t introduce UB even when interacting with permissive or unsafe code
  • “unsafe”

    `unsafe`: Escape hatch to perform dangerous unchecked operations

  • No `unsafe` blocks, just `unsafe` operations
  • Operations that would be considered dangerous in C++ are marked `unsafe` even in permissive mode (reinterpret or const cast)
  • Anything that can’t be checked is an `unsafe` operation in strict mode
  • Calls to unsafe functions must be marked `unsafe`
  • Multi-step migration strategy

    1. Migrate C++ → permissive Carbon 2. Define safety contract in permissive Carbon * Uses Carbon-specific safety annotations * Affects strict Carbon callers 3. Switch Carbon from permissive → strict * Requires fixing violations or `unsafe`

    Alternatively:

    1. Define safety contract with a Carbon wrapper * Implemented with C++ interop 2. Migrate C++ to Carbon later

    Example: C++ → permissive Carbon

    C++ code

    class Tournament {
     private:
      std::vector<Location> venues_;
      std::vector<Team> teams_;

    public: auto EliminationRound( const Matches& semis) -> void { // ... teams_.resize(new_size); }

    auto Venue(const Matches& semis) const -> const Location*; // ... };

    Permissive Carbon

    class Tournament {

    private venues_: buf(Location); private teams_: buf(Team);

    fn EliminationRound( ref self, semis: Matches) { // ... self.teams_.Resize(new_size); }

    fn Venue(self, semis: Matches) -> const Location*; // ... }

    Example: C++ → permissive Carbon

    C++ code

    class Tournament {
     public:
      auto EliminationRound(
          const Matches& semis) -> void;
      auto Venue(const Matches& semis) const
          -> const Location*;
    };
    auto Finals(
        Tournament& t,
        const Matches& semis) -> void {
      const Location* l = t.Venue(semis);
      t.EliminationRound(semis);
      ScheduleGame(l, t);
    }

    Permissive Carbon

    class Tournament {

    fn EliminationRound( ref self, semis: Matches); fn Venue(self, semis: Matches) -> const Location*; }

    fn Finals(
        ref t: Tournament,
        semis: Matches) {
      let l: const Location* = t.Venue(semis);
      t.EliminationRound(semis);
      ScheduleGame(l, ref t);
    }

    Example: permissive → strict (step 1)

    Permissive Carbon

    class Tournament {
      private venues_: buf(Location);
      private teams_: buf(Team);

    fn EliminationRound( ref self, semis: Matches) { // ...

    self.teams_.Resize(new_size); }

    fn Venue(self, semis: Matches) -> const Location*; // ... }

    Strict Carbon

    class Tournament {
      private venues_: buf(Location);
      private teams_: buf(Team);

    fn EliminationRound( ref self, semis: Matches) { // ... // ❌ Error: call to ``Resize`` // invalidates ``^teams_.Elts``, // effect not in function signature. `self.teams_.Resize(new_size)`; }

    fn Venue(self, semis: Matches) -> const Location*; // ... }

    Example: permissive → strict (step 1)

    Strict Carbon with error

    class Tournament {
      private venues_: buf(Location);
      private teams_: buf(Team);

    fn EliminationRound( ref self, semis: Matches) { // ... // ❌ Error: call to ``Resize`` // invalidates `<3>^teams_.Elts`, // `<2>effect not in function signature`. self.teams_.Resize(new_size); }

    fn Venue(self, semis: Matches) -> const Location*; // ... }

    Strict Carbon (fixed)

    class Tournament {
      private venues_: buf(Location);
      private teams_: buf(Team);
      alias `<3>^Teams = ^teams_.Elts`;

    fn EliminationRound( ref self, semis: Matches) `<2>invalidate`(`<3>^Teams`) { // ...

    self.teams_.Resize(new_size); }

    fn Venue(self, semis: Matches) -> const Location*; // ... }

    Example: permissive → strict (step 2)

    Permissive Carbon

    fn Finals(ref t: Tournament,
              semis: Matches) {
      let l: const Location* = t.Venue(semis);
      t.EliminationRound(semis);

    ScheduleGame(l, ref t);

    }

    Strict Carbon

    class Tournament {
      fn EliminationRound(
          ref self, semis: Matches)
          `<3>invalidate(^Teams)`;
      fn Venue(self, semis: Matches)
          -> const Location*;
    }
    fn Finals(ref t: Tournament,
              semis: Matches) {
      let l: const Location* = t.Venue(semis);
      `<3>t.EliminationRound(semis)`;
      // ❌ Error: use of ``l`` after invalidation
      // by ``t.EliminationRound(semis)``
      ScheduleGame(`<4>l`, ref t);
      // ❌ Error: call to ``t.EliminationRound``
      // invalidates ``^t.Teams``, effect not in
      // function signature.
    }

    Example: permissive -> strict (step 2)

    Strict Carbon with errors

    class Tournament {

    fn EliminationRound( ref self, semis: Matches) invalidate(^Teams); fn Venue(self, semis: Matches) -> const Location*; }

    fn Finals(ref t: Tournament,
              semis: Matches) {

    let l: const Location* = t.Venue(semis); t.EliminationRound(semis); // ❌ Error: use of

    <2>l``` after invalidation // by ``t.EliminationRound(semis)`` ScheduleGame(l, ref t); // ❌ Error: call to ``t.EliminationRound`` // invalidates ``​`<4>^t.Teams`​``, effect not in // function signature. }

    ### Strict Carbon (fixed)
    class Tournament { alias `<3>^Venues = ^venues_.Elts`; fn EliminationRound( ref self, semis: Matches) invalidate(^Teams); fn Venue(self, semis: Matches) -> const `<3>^Venues` Location*; }

    fn Finals(ref t: Tournament, semis: Matches) `<4>invalidate(^t.Teams)` { let l: const Location* = `<2>t.Venue(semis)`; t.EliminationRound(semis);

    ScheduleGame(l, ref t);

    }

    ## Field granularity

    We could prove this example is safe due to the additional precision of field granularity.

  • This example is a retheming of code from Dawn, a WebGPU implementation
  • 🦀 There is a proposed change to the Rust safety model called view types for providing field granularity

  • A work-in-progress
  • Carbon / C++ interop

    Allows mixing C++ and Carbon during migration

    Interop: Strict Carbon calling C++

    Conservative heuristic C++ contract assumed

    import Cpp library "<vector>";

    fn Run() { // C++ std::vector<T> var x: `<1>Cpp.std.vector(i32)` = (1, 20, 300); // `<2>Pointer into ^x.any` var p: i32* = &x[0]; // `<3>Any non-const method invalidates` // iterators from and pointers into ``x``. x.push_back(4000); // ❌ Compiler error: use of ``p`` after it was // invalidated by ``x.PushBack(4000)``. Core.Print(*p); }

    * Assumes returns can alias anything reachable from parameters, including `*this`

  • `unknown` effect: all non-const `std::vector` methods will invalidate all derived pointers and iterators
  • Interop: Strict Carbon calling C++

    Conservative heuristic will have false positives

    fn Run() { // C++ std::vector<T> var x: Cpp.std.vector(i32) = (1, 20, 300); // Pointer into ``^x.any``. var p: `<1>i32* = &x[0]`; // `<2>x[1] is a non-const method call!` // so it invalidates ``p``! var q: `<2>i32* = &x[1]`; // ❌ Compiler error: use of ``p`` after it was // invalidated by ``x[1]``. Core.Print(*p); }

    * `[]` operator can invalidate in other containers, such as `absl::flat_hash_map`

  • Can’t be more precise without additional information about the C++ API
  • Interop: Carbon calling C++

    Argument aliasing heuristic

    “shared XOR mutable” rule for allowed aliasing of reference parameters

    void F(`<1>const int& a`, `<1>const int& b`, `<2>int& c`, `<2>int& d`);

    * `a` and `b` are `const` and so may overlap

  • `c` and `d` are not `const` and so must be disjoint from each other and from `a` and `b`
  • As if this Carbon function:
  • void F(const ref a: i32, const ref b: i32, ^ ref c: i32, ^ ref d: i32);

    ## Interop: Carbon calling C++

    Data structure aliasing

  • C++ classes that can reference external/unowned memory have an additional place set parameter in Carbon
  • * Includes C++ base classes accessed by a pointer or reference
  • Assumption is returned objects could reference anything passed in
  • Interop: Strict Carbon calling C++

    Heuristic is precise for simple APIs

    Don’t have to write a wrapper to call C++ functions unless there is something interesting to say about the safety contract (some choice not expressed in the C++ signature)

  • Functions that don’t take pointer parameters beyond `this`
  • Functions that don’t return pointers or references
  • * Or the caller shouldn’t hold onto the return, like with `Cpp.std.cout <<`…
  • Or the heuristic is close enough that no wrapper is needed
  • Interop: Strict Carbon calling C++

    Heuristic is sufficient for light usage

  • Fewer wrappers means less boilerplate, toil, and friction
  • Reverse interop: C++ calling Carbon

  • Actual argument values of place parameters are erased, and so are not required to call Carbon
  • * (also needed for separate compilation)
  • Carbon assumes C++ code respects the aliasing constraints specified in Carbon signatures
  • Eliminating undefined behavior

    by migrating to strict Carbon

    Undefined behavior (UB)

  • Fully strict code has no UB
  • Permissive code has a subset of the UB of the corresponding C++ code
  • * Even with no safety annotations * Some UB is diagnosed (e.g. ODR) * Some UB becomes erroneous behavior (signed arithmetic overflow) * Means won’t optimize based on unvalidated assumptions that could introduce UB
  • Carbon code always has at most the UB of the corresponding C++ code
  • Execution within Carbon, with no C++, only has UB in permissive mode or after the execution passes through an operation marked `unsafe`
  • Undefined behavior (UB)

    Mixing modes doesn’t compromise safety

    Adding safety annotations to permissive code or switching to strict mode never introduces UB

  • May optimize based on safety information, but not in ways that introduce UB if unsafe code compromises safety assumptions
  • 🦀 No Rustonomicon saying it is UB to to alias a mutable reference
  • Unsafe Rust can be more unsafe than C++ because it introduces new requirements that are not checked
  • Means giving up some performance optimization opportunities
  • Undefined behavior (UB)

    Reasonable C++ code doesn’t compromise safety

    As long as C++ code follows a set of rules, it won’t compromise Carbon’s safety checks

  • Code violating the rules should be recognized as either buggy or dangerous by C++ developers
  • Should be reasonable to build a sanitizer to detect violations of the rules
  • * Like Clang’s upcoming `-fbounds-safety`
  • Example: `delete this;` is considered dangerous, and Carbon assumes the C++ code won’t do it
  • Undefined behavior (UB)

    Carbon won’t introduce UB based on safety assumptions

  • C++ UB, or unreasonable but well-defined C++, may compromise safety checks
  • Even when safety checks have been compromised, Carbon code will have less UB than the equivalent C++
  • Conclusion

    In summary

    Smooth and incremental transition from C++ to safe Carbon

  • Memory Safe
  • * Compile-time use-after-free checking
  • Expressive
  • * Common C++ patterns made safe without rearchitecting
  • Incremental
  • * Interop and permissive mode

    More resources: Safety units

    Safety units are how we’ve developed the memory safety design

  • Google Drive folder
  • Will be distilled into official proposals that update Carbon’s design
  • Some safety units of interest:

  • Catalog of place set expressions and safety annotations: safety unit 40
  • Initialization safety: safety unit 26
  • Thread safety WIP: examples, safety units 27, 37, 43
  • This presentation: <https://chandlerc.blog/slides/2026-memory-safety-deep-3/>

    Feedback

    Please join us in the #safety channel in Carbon’s Discord

    <https://docs.carbon-lang.dev/#join-us> has instructions for joining the community

  • I am josh11b on Carbon’s Discord and GitHub
  • I am Josh L on rust-lang Zulip
  • Appendix: Flow-sensitivity

    Flow-sensitivity is used in a few places

  • Automatic aliasing for locals
  • Which places are currently shared across threads
  • Which places have been initialized
  • Gives a precise answer without having to be explicit in the source, but flow-sensitive analysis comes after semantics, so can’t influence overload or impl selection. Only affects validity.

    Automatic aliasing for locals

    fn F() { var x: i32 = 1; var y: i32 = 2; var p: `<0>i32*` = `<1>&x`; if (true) { `<8>var z`: i32 = 3; `<2>p = &z`; while (true) `<6>{` if (G(p)) { `<3>p = &x`; } else { `<4>p = &y`; } `<5>if` (H(p)) { `<7>break`; } } `<8>}` `<9>J(p)`; }

    `<0>p has type ^p i32`, `<1>^*p = ^x`

    `<2>^*p = ^z`

    `<6>^*p = ^(x, y, z)` `<3>^*p = ^x`

    `<4>^*p = ^y`

    `<5>^*p = ^(x, y)` `<7>^*p = ^(x, y)`

    `<8>^*p = ^(x, y), ^z invalidated` `<9>^*p = ^(x, y)`, ``p`` is still valid

    ## Properties of places

    Fixed:

  • Single place or multiple?
  • Type?
  • Overlap with other places?
  • Flow-sensitive:

  • Initialized?
  • Can write?
  • Shared across threads?
  • Guarding mutex is currently acquired?
  • Addendum

    How we got here

    This is the third safety model we considered

    First approach: Ante’s Rust-like model

  • Take Rust and add non-exclusive mutable borrows
  • Struggled with transitions between the three modes:
  • * Exclusive (needed for things that e.g. reallocated) * Non-exclusive mutable (more flexible when applicable) * Immutable shared
  • Concerns about having too many pointer types
  • Lacked expressivity and precision
  • * Field granularity requires additional mechanisms (view types) * Doesn’t handle different methods requiring different capabilities from fields
  • Expected to work, and so became our backup plan while we explored other options
  • The Algebra of Loans in Rust: most promising direction
  • This is the third safety model we considered

    2nd: Safety effects model with flow-sensitive checking

  • Inspired by group borrows blog posts (nmsmith, verdagon.dev)
  • Very precise in simple cases
  • Delays commitment to what capabilities are needed by a type until actually calling the method that needs that capabilities
  • Can add more kinds of effects to increase precision
  • Example: writes were an effect rather than `const` restricting what the type could do
  • * Less code duplication to be const correct (nim-lang.org)
  • Struggles with precision when there is type erasure
  • Found potential workarounds, but ends up far down the road of reproducing the body of the function in the signature
  • * `F` calls `G` and so has whatever effects `G` has

    This is the third safety model we considered

    Final model: Goldilocks hybrid

  • Hybrid between the two previous models
  • Uses types, `const`, and ownership to limit possible effects with type erasure
  • Uses safety effects and flow-sensitivity sparingly where they are most helpful
  • * Reference card of effects, not a wall poster
  • Fewer pointer types (since never exclusive) and more compatibility between them than the Ante approach
  • “`const` only on pointers/references not types” and “preserve `const`” reduce the need for code duplication for const correctness
  • Addendum

    `const`

    `const`

  • Means: restricting permission to mutate through this path
  • Const in Carbon is on places (and therefore pointers and references), not types
  • Const ≠ immutability
  • * Arises naturally from non-exclusive pointers: if pointer `p` can point to `x` or `y`, and `x` is immutable, can’t change through `p`, but a change to `y` might be observed through `p`. * Better match for C++
  • Invalidation requires mutation, can use const to limit permissions, reducing the effects of generic or otherwise type erased code
  • Immutable borrows instead of expensive copies

  • If you don’t mutate an object, a `const` reference is equivalent to a copy
  • * Much cheaper for some types
  • Carbon defaults to “value” calling convention
  • * Different behavior for different types * Copies types where that is cheap * Or an immutable borrow + `const` reference * An error if a copy is needed * Means expensive copies will be explicit in source
  • Good for generics where `T` or `const T&` will be more efficient depending on `T`
  • Immutable borrows

  • Rule: something that could overlap an immutable borrow can’t be passed mutably
  • * Validation that signatures of calls agrees with flow-sensitive state * Checked in the second “safety effect checking” step that doesn’t affect semantics
  • But Carbon’s immutable borrows are not first class
  • * Only for parameters and locals, not in data structures
  • Tracking which places are immutable similar to which places have been shared across threads
  • * Both are flow-sensitive tracking of status of places * Difference in triggers (value parameter borrows vs. safety effects) * Difference in capabilities (no writes vs. no reads or writes)

    Preserve `const`

  • “Preserve `const`” means:
  • * Function doesn’t mutate through parameter * Function returns a derived reference * Caller can figure out if derived reference should be `const` based on what was passed in to the parameter
  • Avoids `const` and non-`const` versions of the same function
class C { var field: i32; // ``const ref self`` -> doesn't mutate ``self`` // ``^field i32*`` -> returns pointer to ``self.field`` // ``const`` if ``self`` is ``const`` in caller fn PreservesConst(const ref self) -> ^field i32* { return &self.field; } }

```

chandlerc.blog · 1 images · 39 links

Comments (0)

No comments yet