Memory safety everywhere with both Carbon and Rust
Chandler Carruth @chandlerc1024 chandlerc@{google,gmail}.com
RustConf 2025
What does “memory safety everywhere ” mean?
Reminder of why memory safety is important
Memory unsafety remains the dominant cause of security vulnerabilities
Over 65% of high / critical vulnerabilities (sources 1 , 2 , 3 , 4 ,
5 )
Only durable solution found: memory safe programming languages
Evidence clearly shows this works
You probably know how to write memory safe software!
Java / Kotlin
JavaScript / TypeScript
Python
... and yes, Rust! 🦀
The “everywhere ” is the tricky part…
A lot of software in the world
All part of an ecosystem
Each piece you move has to integrate with the rest
Interop spectrum
extern C
(c)bindgen
cxx crate
zngur
…
Greenfield
Brownfield
Less tightly coupled to an existing C++ ecosystem:
Strong abstraction boundaries
Modular architecture
More tightly coupled to an existing C++ ecosystem:
Large API surface
API uses large language surface area
Greenfield
Brownfield
The spectrum here is really how tightly coupled code is to existing C++ software
and infrastructure. The more this is the case, the more difficult it is to start
cutting new code over to any other language, including Rust.
TODO: figure out how to sequence display here
This is really what we’re talking about when we use terms like “greenfield” and
“brownfield”. We’re discussing how tightly coupled some new code will be to the
existing C++ code and infrastructure.
Some interesting realizations – brownfield only applies to new code really.
Because if it isn’t new code, its not even brownfield, its not development
activity at all. It’s just extant, legacy code.
Also means that there isn’t a really binary split between green- and brownfield.
It’s a spectrum, and in my experience a very large and continuous spectrum.
Rust
🦀
🦀
🦀
But will it get here?
Greenfield
Brownfield
Rust is starting from the greenfield side of this spectrum. Not 100%, there is
no 100%, but towards that side. It is a mature, production quality language that
exists.
And the interop efforts are shifting its applicability right so that we can use
it in ever less greenfield and more brownfield situations.
Already, Android and other OS and low-level systems code are essentially
completely covered and work great. Now we’re pushing into larger and larger, and
over more interdependent and tightly coupled systems.
But will it ever reach the most extreme brownfield end of this spectrum?
Everywhere requires a maximalist approach
That would require covering a really dauntingly large design space. We could try
to it with a single language, but IMO, stretching a single language across such
a large space has a serious risk of it ending up poorly addressing many parts of
the space and being burdened with especially high complexity.
Think you can see this with C++ and C before we started really pushing towards
memory safety in low-level languages. C++ tried to cover everything in the
low-level domain, but struggled in a number of places where its complexity
wasn’t the right tradeoff. And C remains used and even preferred in a number of
contexts as a consequence.
Ideally we’d add memory safety directly to C++ … 😢
Unfortunately, the committee is not moving in this direction.
Need an alternative that is almost as brownfield optimized as adding memory
safety directly to C++ would be… This sounds familiar…
C
→
C++
JavaScript
→
TypeScript
C
→
C++
JavaScript
→
TypeScript
Objective-C
→
Swift
C
→
C++
JavaScript
→
TypeScript
Objective-C
→
Swift
Java
→
Kotlin
C
→
C++
JavaScript
→
TypeScript
Objective-C
→
Swift
Java
→
Kotlin
C++
→
Carbon
Carbon
An incremental path to evolve and migrate off C++
And to add memory safety to existing software
Prioritizing the most brownfield codebases
Rust 🦀 ⇐
⇐ Carbon
Greenfield
Brownfield
Essentially, Carbon approaches the entire problem from the opposite end of the
spectrum.
And a key thesis is that this spectrum is really too large for some users to
cover with a single language. By bringing two languages that have these very
different priorities and designs, we can better cover the entire space.
And to repeat, my goal is to get more new code to be memory safe. Happy to pay
the cost of multiple languages if it actually allows us to achieve that outcome.
Realistically, we already have a bunch of languages in the world, and I think
by-and-large they serve their different purposes.
Now, given these different goals and the totally different direction from how
Rust and Carbon are evolving, it’s not too surprising that they end up
diverging. That’s what I want to spend the rest of today looking at…
Let’s look at how they are diverging
Syntax is similar
fn `main`() {
let mut `s` = String::from("Hello");
s.`push_str`(" world!");
println!(`s`);
}
fn `Run`() {
var `s`: strbuf = "Hello";
s.`Append`(" world!");
Core.Print(`s`);
}
// tables.rs
`struct Table` { ... }
// `hosting`.rs
pub use crate::tables::Table;
pub fn add_to_waitlist() { ... }
pub fn seat_at_table() {
...
`crate::serving::add_table(t)`;
}
`pub fn clear_table`(t: Table) { ... }
// `serving`.rs
pub use crate::tables::Table;
`pub fn add_table`(t: Table) { ... }
pub fn take_order() {}
pub fn take_payment() {
...
`crate::hosting::clear_table(t)`;
}
// tables.carbon
class `Table` { ... }
fn `AddSeatedTable`(t: Table) { ... }
fn `ClearTable`(t: Table) { ... }
// hosting.carbon
import library "`tables.carbon`"
fn AddToWaitlist() { ... }
fn SeatAtTable() {
...
`AddSeatedTable`(t);
}
// serving.carbon
import library "`tables.carbon`"
fn TakeOrder() {}
fn TakePayment() {
...
`ClearTable`(t);
}
Rust uses a very simple code organization system of crates that form packages.
All the code for a crate is compiled at once. No forward declarations or
anything fancy.
Carbon’s organization is more complex in order to support organizing code in the
same ways that fairly advanced C++ libraries do:
Forward declarations
Separate compilation of an API file and an implementation file
Potentially multiple sharded implementation files
Ability to decompose a package into multiple separate API files that are
individually importable
// tables.rs
struct Table { ... }
// hosting.rs
pub use crate::tables::Table;
pub fn add_to_waitlist() { ... }
pub fn seat_at_table() {
...
crate::serving::add_table(t);
}
pub fn clear_table(t: Table) { ... }
// serving.rs
pub use crate::tables::Table;
pub fn add_table(t: Table) { ... }
pub fn take_order() {}
pub fn take_payment() {
...
crate::hosting::clear_table(t);
}
// tables.carbon
class Table { ... }
fn AddSeatedTable(t: Table);
fn `ClearTable`(t: Table);
// tables.`impl`.carbon
fn AddSeatedTable(t: Table) { ... }
fn `ClearTable`(t: Table) { `...` }
// hosting.carbon
` `
fn AddToWaitlist();
fn `SeatAtTable`();
// hosting.`impl`.carbon
import library "`tables`.carbon"
fn AddToWaitlist() { ... }
fn SeatAtTable() {
...
`AddSeatedTable`(t);
}
Carbon code organization follows fom C++
C++ “library” (Abseil, SQLite, …) -> Carbon package
Typical unit of versioning, repository, distribution
C++ header file -> Carbon API file
Unit of API that can be imported
C++ source file -> Carbon impl file
Enabling physical separation of implementation, including dependencies
C++ *_fwd.h headers like ios_fwd.h -> Carbon extern declarations
Allows wide APIs with sparse client usage to have narrow client dependencies
The big thing here is that this is a lot of complexity. But it allows us to
support the existing organizational schemes of large-scale C++ code bases.
struct `Up` {
`count`: i32,
}
impl `Up` {
fn `inc`(&mut self) { self.count += 1; }
}
class `Up` {
fn `Inc`[ref self: Self]() {
self.count += 1;
}
private var `count`: i32;
}
Rust types have an API separate from their storage, while Carbon types look much
more like C++ – a closed set of members, both methods and fields.
Not sure either of these is more or less complex, or better or worse, but Carbon
is specifically choosing to follow more closely to how C++ builds types here.
And it goes further than just this, in Carbon we also support inheritance …
struct Up {
count: i32,
}
impl Up {
fn inc(&mut self) { self.count += 1; }
}
`base` class Up {
`virtual` fn Inc[ref self: Self]() {
self.count += 1;
}
`protected` var `count`: i32;
}
base class UpOrDown {
`extend base: Up`;
`override` fn Inc[ref self: Self]() {
self.Add(1);
}
`virtual` fn Dec[ref self: Self]() {
self.Add(-1);
}
private fn Add[ref self: Self](
delta: i32) {
self.count += delta;
}
}
We also directly support inheritance in Carbon, just like C++. We don’t expect
to support multiple inheritance in the same way as C++, but we do expect to
support the use cases for it through a mixins system.
We can debate which way of defining a type and its API is “better”, but its not
a big difference. The real difference here is anchoring on something much more
familiar coming from C++.
But inheritance and especially mixins add a ton of complexity to the language.
In Rust, you can still achieve similar results, but rather than the complexity
of that being in the language, it is kept separate with tools like #[Derive]
built on proc macros.
Functions
fn `append_i32`(s: &mut String, i: i32) { ... }
fn `append_f32`(s: &mut String, f: f32) { ... }
`overload` Append {
`fn` (ref s: strbuf, i: i32) { ... }
`fn` (ref s: strbuf, f: i32) { ... }
}
Functions in Rust are super straightforward.
In Carbon, they sometimes are as well…
But other times, they’re complex as they also support overloading.
This doesn’t support all overloading patterns of C++, some of those are mapped
instead into the generics mechanism. But it supports a large body of the
patterns directly which makes it able to very ergonomically match C++ API
designs. The down side is this comes with a ton of language complexity.
And here, again, Rust can model something like overloading, and in fact it
uses a similar pattern to the fallback in Carbon with its generics system. But
similar to with inheritance, this is pushing the complexity out of the language,
and into the library code that wants to have this style of API design.
And again, I’m not saying that’s a bad choice! It’s a huge simplification to
the language. But it is a tradeoff that makes Rust APIs feel less like C++ APIs
and vice versa.
Generics
// TODO: Rust generics example
// TODO: Carbon generics example, and then add specialization, and then a template
Both Rust and Carbon have fairly similar generics systems. There are some minor
differences, for example how name lookup works.
But C++ generic programming relies heavily on specialization for performance
optimizations. So we ended up needing to bake specialization directly into the
generics system from day one, which adds a bunch of complexity that I know Rust
folks have been grappling with.
And it gets even worse for Carbon, because Carbon doesn’t just have these
definition checked generics like Rust, it also has template generics that
model how C++ templates work. These are instantiated and late-type-checked. And
they can even do the really difficult parts of specialization like C++ does
where different specializations have different API s, not just different
implementations.
Conversions
fn f(i: i64) {
let truncated = `<1>i as i32`;
let fallible =
`<2>i32::try_from(i).unwrap_or(-1)`;
}
fn make_i64<I: `<6>Into<i64>`>(i: I) -> i64 {
return `<7>i.into()`;
}
fn g(i: i32) {
let larger: i64 = `<3>i.into()`;
let larger2 = `<4>i64::from(i)`;
let larger3 = `<5>make_i64(i)`;
}
fn F(i: i64) {
let truncated: auto = `<9>i as i32`;
// Unwrapping syntax not yet decided...
}
fn Make64[I:! `<12>ImplicitAs(i64)`](i: I) -> i64 {
return `<13>i`;
}
fn G(i: i32) {
let larger: i64 = `<10>i`;
let larger3: auto = `<11>Make64(i)`;
}
Conversions seem like a small thing, but its another place where we actually
have surprising complexity hiding. Rust only supports explicit conversions. But
Carbon needs to be able to support C++ API designs that rely on implicit
conversions. Adding implicit conversions can seem simple, but is actually a huge
deal – you need to make them consistent, and reason about where all they
happen.
And it adds even more complexity when combined with overloading.
This pattern ends up pervasive
I’ve only skimmed a few of the differences at a high level, but hopefully its
clear how broad this ends up being.
Also, I’ve sneakily introduced you to a bunch of Carbon code. ;]
Now let’s get into the really interesting part…
What about safety?
🚧 🚧 Warning: very early, work-in-progress! 🚧 🚧
Unsafety scope
use std::mem::MaybeUninit;
fn f<'a>(x: &mut `<1>MaybeUninit<&'a i32>`)
-> &'a i32 {
`<2>unsafe` { (`<4>*`x).`<3>assume_init()` }
}
fn g<'a>(x: `*`mut MaybeUninit<&'a i32>)
-> &'a i32 {
`unsafe` { (`*`x).assume_init() }
}
fn F[^a](x: `Core.MaybeUninit`(`i32*` ^a)`*`)
-> i32* ^a {
return (*x).`unsafe` `assume_init`();
}
fn G[^a](x: Core.MaybeUninit(i32* ^a)`* unsafe`)
-> i32* ^a {
return (`unsafe` `*`x).`unsafe` assume_init();
}
A simple difference in safety looks a lot like the other parts of the language.
Rust uses a really nice and simple model where you have an unsafe region that
allows code to do things that aren’t provably correct in the type system, as
long as they uphold the underlying model requirements.
With Carbon, we want to have even more incrementality coming from C++, and so
expect to have two big differences:
First, we want to narrowly classify each operation as safe or unsafe rather
than using an entire region.
And second, we want to combine that with regions that select different modes for
which unsafe operations have to be marked explicitly.
This is a much more complex way to model unsafety in some respects – we have to
come up with distinct syntax for each construct, but also manage for that syntax
to be consistently auditable and recognizable.
But it lets us be much more incremental coming from C++. The initial migration
of largely unsafe C++ code into Carbon can use a permissive mode that only
requires explicit unsafe markings on things that were already similarly
annotated in C++ like reinterpret_cast. The key is that we don’t regress the
visibility of unsafe constructs coming from C++. And then we can incrementally
add more markers and ratchet up the region-based marking requirements.
Mutability and exclusivity
fn g(_: &i32, _: &i32) {}
fn g_mutate(_: &mut i32, _: &mut i32) {}
fn f(`x: &i32`, `y: &mut i32`) {
// OK: Can have many shared borrows.
`g(x, x)`;
// ERROR: Can't mutate.
`*x = 42`;
// OK: Can mutate.
`*y = 42`;
// ERROR: Can't have more than one
// mutable borrow.
`g_mutate`(`y`, `y`);
}
fn G(_: i32, _: i32) {}
fn GMutate(ref _: i32, ref _: i32) {}
fn f(`x: i32`, `ref` y: i32) {
// OK: Can have many immutable values.
`G(x, x)`;
// ERROR: Can't mutate.
`x = 42`;
// OK: Can mutate.
`y = 42`;
// OK: Can mutate multiple objects.
`GMutate`(`ref y`, `ref y`);
}
Let’s talk about the main show of safety though, pointer-like constructs.
Borrows in Rust, and pointers in Carbon.
Rust has a very simple and powerful model: mutable borrows must be exclusive,
while shared borrows must be immutable.
We expect to have a more complex safety model in Carbon specifically design to
let us express more code patterns and prove their memory safety: exclusive
pointers, immutable pointers, and mutable non-exclusive pointers. This last
category is a big new set of complexity that Carbon is taking on.
Deep dive into the safety model differences
Let’s take a deeper dive into this specific area as the safety differences are
in some ways some of the most interesting to look at. And here, we’re going to
switch up and start looking at very concrete and real world examples to
understand how they fit into these different models in different ways.
OPENSSL_EXPORT int EVP_AEAD_CTX_seal_scatter(
const EVP_AEAD_CTX *ctx,
`uint8_t *out`,
`uint8_t *out_tag`, size_t *out_tag_len, `size_t max_out_tag_len`,
`const uint8_t *nonce`, `size_t nonce_len`,
const uint8_t *in, size_t in_len,
const uint8_t *extra_in, size_t extra_in_len,
const uint8_t *ad, size_t ad_len);
int EVP_AEAD_CTX_seal_scatter(
const EVP_AEAD_CTX *ctx,
`<2>std::span<uint8_t> out`,
`<3>std::span<uint8_t> out_tag`,
size_t *out_tag_len,
std::span<const uint8_t> nonce,
`<1>std::span<const uint8_t> in`,
std::span<const uint8_t> extra_in,
std::span<const uint8_t> ad);
fn EVP_AEAD_CTX_seal_scatter[`<5>^a`](
ctx: const EVP_AEAD_CTX `<6>^a` *,
out: slice(u8 `<7>^a`),
out_tag: slice(u8 `<8>^a`),
out_tag_len: u64 ^a *,
nonce: slice(const u8 ^a),
input: slice(const u8 `<9>^a`),
extra_input: slice(const u8 ^a),
ad: slice(const u8 ^a) ad) -> i32;
EVP_AEAD_CTX_seal_scatter encrypts and authenticates bytes from in and from
ad. It writes in.size() bytes of ciphertext to out and the authentication
tag to out_tag. It returns one on success and zero otherwise.
If in and out alias then out == in. out_tag may not alias any other
argument.
There are two ways this API can appear in a migration to memory safety in
Carbon:
The API and its implementation migrated to Carbon, along with callers
Just callers to the API migrated to Carbon, with a binding generated for the
function.
Rust migration
In both cases, this same migration to Rut can not be done mechanically, as the
API can not be represented directly in Rust:
The in and out can alias, but the in is const so that if they don’t
alias the function will not modify in. This is not expressible with
immutable refs.
The in and out can alias while writing to out. This is not expressible
with mutable refs.
So in Rust, you’d need to split the API into two versions: an in-place one and a
non-in-place one. And then rewrite all callers to choose between the two based
on whether they have aliasing pointers or not, which may require non-local
knowledge to determine, pushing the same issue down the call stack. Once a
choice is made the callers have to be rewritten to work with the new API. This
introduces multiple non-trivial steps to a migration of the callers from C++ to
Rust. This may also require larger changes in callers in order to ensure
exclusive access for any mutable pointers given.
Carbon migration
Carbon can directly represent this API in both cases. A naive conversion to
Carbon can use the same loan parameter for all pointer arguments, allowing them
all to alias (aka to point to overlapping places).
This can then later be incrementally improved by splitting apart the loan
parameters, giving a unique one to each group of pointer parameters that do not
alias other groups. Note that none of the pointers impose exclusive access on
callers.
But this has more complexity, as it does require writing a lot of loan
parameters which you can omit in the migrated Rust APIs.
int EVP_AEAD_CTX_seal_scatter(
const EVP_AEAD_CTX *ctx,
`<2>std::span<uint8_t> out`,
std::span<uint8_t> out_tag,
size_t *out_tag_len,
std::span<const uint8_t> nonce,
`<1>std::span<const uint8_t> in`,
std::span<const uint8_t> extra_in,
std::span<const uint8_t> ad);
fn EVP_AEAD_CTX_seal_scatter[^a](
ctx: const EVP_AEAD_CTX ^a *,
`<2>out: slice(u8 ^a)`,
out_tag: slice(u8 ^a),
out_tag_len: u64 ^a *,
nonce: slice(const u8 ^a),
`<1>input: slice(const u8 ^a)`,
extra_input: slice(const u8 ^a),
ad: slice(const u8 ^a) ad) -> i32;
This uses a single loan parameter throughout, so anything can alias. This is not
what the docs say, but it is what the code says in C++. Now that we’re in
Carbon, we can improve on this.
int EVP_AEAD_CTX_seal_scatter(
const EVP_AEAD_CTX *ctx,
`<1>std::span<uint8_t> out`,
`<4>std::span<uint8_t> out_tag`,
size_t *out_tag_len,
std::span<const uint8_t> nonce,
`<1>std::span<const uint8_t> in`,
std::span<const uint8_t> extra_in,
std::span<const uint8_t> ad);
fn EVP_AEAD_CTX_seal_scatter[`<2>^inout`](
ctx: const EVP_AEAD_CTX ^*,
`<3>out: slice(u8 ^inout)`,
`<5>out_tag`: slice(u8 `<6>^`),
out_tag_len: u64 ^*,
nonce: slice(const u8 ^),
`<3>input: slice(const u8 ^inout)`,
extra_input: slice(const u8 ^),
ad: slice(const u8 ^)) -> i32;
Now the aliasing can be specified without requiring splitting into two functions
with different APIs. Nothing can alias except in and out. This can be done
incrementally as a post-migration step.
Note that in can not be an immutable reference still, however. Since it can
alias with out we can only say that the view of the data through in will not be
used to mutate, which is what the pointer-to-const says.
Equivalent APIs for this do exist in Rust, and we can look at how they are
structured…
pub trait Aead {
fn encrypt<'msg, 'aad>(
&self,
nonce: &Nonce<Self>,
`plaintext`: impl Into<Payload<'msg, 'aad>>,
) -> Result<`Vec<u8>`>;
}
pub trait AeadInPlace {
fn encrypt_in_place_detached(
&self,
nonce: &Nonce<Self>,
associated_data: &[u8],
`buffer`: &mut [u8],
) -> Result<Tag<Self>>;
}
https://docs.rs/aead/latest/aead/trait.Aead.html
https://docs.rs/aead/latest/aead/trait.AeadInPlace.html
These are actual Rust APIs for these operations. And maybe they’re even better
APIs, that’s a separate debate. But the real point here is that they have had to
use a very different shape, which makes migrating from the original API to these
a much larger change.
Carbon has to have a lot of extra complexity in order to model the original
shape of API, but a consequence is that the migration becomes much easier.
Not all mutations are sound with aliasing…
We do still need to model exclusivity, and when supporting both we’ll need to
handle the complexity of modeling the relationship between them.
Let’s look at an even more complex real-world example that will let us examine
this in detail. This comes from the Dawn project.
class DeviceBase {
public:
// Returns a pointer into an internal buffer.
auto `GetInternalFormat`(wgpu::TextureFormat format) const
-> `const Format*` {
return &mFormatTable[ComputeFormatIndex(format)];
}
// Adds a value to an internal set.
auto `EmitWarningOnce`(std::string_view message) -> void {
`mWarnings.insert`(std::string{message});
}
private:
std::array<Format, 109> `mFormatTable`;
std::set<std::string> `mWarnings`;
};
auto ValidateStorageTextureFormat(
DeviceBase* device,
wgpu::TextureFormat storageTextureFormat,
wgpu::StorageTextureAccess access) -> MaybeError {
// Holding a pointer into ``device``
const Format* `format` =
device->GetInternalFormat(storageTextureFormat);
if (storageTextureFormat == wgpu::TextureFormat::BGRA8Unorm &&
access == wgpu::StorageTextureAccess::ReadOnly) {
// A shape change inside ``device``.
`device->EmitWarningOnce`(
"bgra8unorm with read-only access is deprecated.");
}
// Dereferencing the pointer after a shape change of ``device``.
if (!TextureFormatSupportStorageAccess(`*format`, access)) {
return ErrorData("Format does not support storage texture access.");
}
return {};
}
Here we know the “shape change” in device is orthogonal to the format
pointer. In fact, since the pointer comes from an array, it can’t be
invalidated by a shape change.
Nonetheless we’d expect this code to be rejected unless the two methods
GetInternalFormat and EmitWarningOnce can express that they do not touch
aliasing parts of DeviceBase.
class DeviceBase {
// Returns a pointer into an internal buffer.
fn GetInternalFormat[`ref` self: `const Self`](
format: Wgpu.TextureFormat) -> const Format `^self` `*` {
return &self.mFormatTable[ComputeFormatIndex(format)];
}
// Adds a value to an internal set.
fn EmitWarningOnce[`ref` `exclusive` self: Self](message: Core.Str) {
// insert() requires exclusive access to ``self.mWarnings`` which
// requires exclusive access to ``self``.
self.mWarnings.insert(Core.StrBuf.Make(message));
}
private var mFormatTable: array(Format, 109);
private var mWarnings: Cpp.std.set(Core.StrBuf);
}
fn ValidateStorageTextureFormat(
device: DeviceBase `exclusive` *,
storageTextureFormat: Wgpu.TextureFormat,
access: Wgpu.StorageTextureAccess) -> MaybeError {
// Holding a pointer into ``device``, holds a non-exclusive borrow on ``device``.
let `format`: const Format`*` =
`device`->GetInternalFormat(storageTextureFormat);
if (storageTextureFormat == Wgpu.TextureFormat.BGRA8Unorm &&
access == Wgpu.StorageTextureAccess.ReadOnly) {
// ERROR under strict compilation, but allowed in permissive mode.
// A shape change inside ``device``. Needs an exclusive borrow on ``device``
`device->EmitWarningOnce`(
"bgra8unorm with read-only access is deprecated.");
}
// Dereferencing the pointer after a shape change of ``device``.
if (!TextureFormatSupportStorageAccess(*format, access)) {
return ErrorData("Format does not support storage texture access.");
}
return {};
}
This example requires holding a reference across a shape change of a set. It’s
not immediately a security bug because the reference is to a different part of
the larger class, not into the set that may be reallocated.
It will build in permission Carbon but be rejected in strict Carbon, unless we
extend the expressivity of the language enough to determine it is sound.
In Rust this code would always be rejected with safe constructs, and would be UB
with unsafe pointers.
In simpler cases, where the reference is held into the container being mutated
with a shape-changing allocation, we want to reject a migration into strict
Carbon as doing so is already a latent security bug.
fn ValidateStorageTextureFormat(
device: &mut DeviceBase,
storageTextureFormat: Wgpu::TextureFormat,
access: Wgpu::StorageTextureAccess) -> Result<(), String> {
// Holding a pointer into ``device``, holds a non-exclusive borrow on ``device``.
let `format`: `&`Format =
`device`.GetInternalFormat(storageTextureFormat);
if storageTextureFormat == Wgpu::TextureFormat::BGRA8Unorm &&
access == Wgpu::StorageTextureAccess::ReadOnly {
// ERROR: A shape change inside ``device``.
// Needs an exclusive borrow on ``device``.
`device.EmitWarningOnce`(
"bgra8unorm with read-only access is deprecated.");
}
// Dereferencing the pointer after a shape change of ``device``.
if !TextureFormatSupportStorageAccess(*format, access) {
return Err("Format does not support storage texture access.".to_string());
}
return Ok(());
}
fn ValidateStorageTextureFormat(
device: &mut DeviceBase,
storageTextureFormat: Wgpu::TextureFormat,
access: Wgpu::StorageTextureAccess) -> Result<(), String> {
// Holding an unsafe pointer into ``device``.
let `format`: `*`const Format =
device.GetInternalFormat(storageTextureFormat) as `*const _`;
if storageTextureFormat == Wgpu::TextureFormat::BGRA8Unorm &&
access == Wgpu::StorageTextureAccess::ReadOnly {
// OK because we used a raw pointer instead of a non-exclusive borrow.
// But still does a shape change.
`device`.EmitWarningOnce(
"bgra8unorm with read-only access is deprecated.");
}
// Dereferencing the pointer after a shape change of ``device``.
// **UB** in Rust with unsafe pointers: The format pointer was invalidated
// by the exclusive borrow of ``device``.
if !TextureFormatSupportStorageAccess(unsafe { `*format` }, access) {
return Err("Format does not support storage texture access.".to_string());
}
return Ok(());
}
error: Undefined Behavior: attempting a read access using <370> at alloc308[0x18],
but that tag does not exist in the borrow stack for this location
--> src/main.rs:60:50
|
60 | if !TextureFormatSupportStorageAccess(unsafe { *format }, access) {
| ^^^^^^^ this error occurs as part of an
| access at alloc308[0x18..0x1c]
|
help: <370> was created by a SharedReadOnly retag at offsets [0x18..0x1c]
--> src/main.rs:46:31
|
47 | device.GetInternalFormat(storageTextureFormat) as *const _;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: <370> was later invalidated at offsets [0x0..0x1d0] by a Unique function-entry retag
inside this call
--> src/main.rs:52:5
|
53 | / device.EmitWarningOnce(
54 | | "bgra8unorm with read-only access is deprecated.");
| |__________________________________________________________^
fn ValidateStorageTextureFormat(
device: DeviceBase exclusive *,
storageTextureFormat: Wgpu.TextureFormat,
access: Wgpu.StorageTextureAccess) -> MaybeError {
// Holding a pointer into ``device``, holds a non-exclusive borrow on ``device``.
let format: const Format* = device->GetInternalFormat(storageTextureFormat);
if (storageTextureFormat == Wgpu.TextureFormat.BGRA8Unorm &&
access == Wgpu.StorageTextureAccess.ReadOnly) {
// ERROR under strict compilation, but allowed in permissive mode.
// A shape change inside ``device``. Needs an exclusive borrow on ``device``
device->EmitWarningOnce(
"bgra8unorm with read-only access is deprecated.");
}
// Dereferencing the pointer after a shape change of ``device``.
// **Not UB** in Carbon.
if (!TextureFormatSupportStorageAccess(`*format`, access)) {
return ErrorData("Format does not support storage texture access.");
}
return {};
}
Carbon ends up very different from Rust
Designed around needs of the most brownfield C++ code
Ends up with a better fitting model, but only …
… for interop!
… or for migration!
Comes at a high cost: complexity
Simplicity vs. complexity
Hard migration vs. easy migration
Rust 🦀 ⇐
⇐ Carbon
Greenfield
Brownfield
Together, we can get closer to:
Memory safety everywhere
Memory safety everywhere with both Carbon and Rust
Chandler Carruth @chandlerc1024 chandlerc@{google,gmail}.com
RustConf 2025