google / google/zerocopy

Design for unsafe fields polyfill

Open
#1,931 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
2.6k
Forks
179
Avg merge
1d 19h
Merged PRs (30d)
29

Description

*This design is prototyped in #1929.*

Sometimes, struct (or enum or union) fields have safety invariants:

```rust
struct EvenUsize {
// INVARIANT: `n` is even.
n: usize,
}
```

However, Rust has no mechanism to ensure that reads from or writes to fields with invariants must happen inside an `unsafe` block as is required for `unsafe` functions or to implement `unsafe` traits. I propose that we can support this behavior outside the language, allowing a user to write:

```rust
struct EvenUsize {
// INVARIANT: `n` is even.
#[unsafe]
n: usize,
}
```

The `#[unsafe]` attribute (implemented as a proc macro attribute) modifies the type of `n` to *something like* `Unsafe`, although as we'll see in a moment, it needs to be a tad more complex than that.

## Design take 1: `Unsafe`

Let's start with the basic design, though:

```rust
#[repr(transparent)]
pub struct Unsafe(T);
```

An `Unsafe` is a type whose constructors and accessors are all `unsafe` to call. We can't prevent code from moving an `Unsafe` around, but we can prevent code from *doing* anything with it. Thus, for example, we might imagine the following constructor for `EvenUsize`, calling the unsafe `Unsafe::new` constructor:

```rust
impl EvenUsize {
/// Constructs a new `EvenUsize`.
///
/// Returns `None` if `n` is odd.
pub fn new(n: usize) -> Option {
if n % 2 != 0 {
return None;
}
// SAFETY: We just confirmed that `n` is even.
let n = unsafe { Unsafe::new(n) };
Some(EvenUsize { n })
}
}
```

While this gets us a step in the right direction, it has a soundness hole: There is nothing to stop `Unsafe`s from two different types being swapped:

```rust
struct OddUsize {
// INVARIANT: `n` is odd.
#[unsafe]
n: usize,
}
```

Code operating on an `EvenUsize` and an `OddUsize` could swap the inner `Unsafe`s safely, which is obviously bad!

## Design take 2: `Unsafe`

To prevent this from happening, we can instead design `Unsafe` to also take a parameter which is the type of the field's outer type:

```rust
#[repr(transparent)]
pub struct Unsafe(PhantomData, F);
```

The `#[unsafe]` attribute would then modify the code to something like:

```rust
struct OddUsize {
n: Unsafe,
}
```

This prevents swapping fields between types, but it doesn't prevent swapping between *fields*:

```rust
struct Pair {
// INVARIANT: `m` is even.
#[unsafe]
m: usize,

// INVARIANT: `n` is odd.
#[unsafe]
n: usize,
}
```

`m` and `n` have the same type, and so can be swapped without issue.

## Design take 3: `Unsafe`

To prevent this from happening, we can instead design `Unsafe` to also include the *name* of the field in its type:

```rust
#[repr(transparent)]
pub struct Unsafe(PhantomData, F);
```

Unfortunately, `&str` is not supported in const parameters right now, so we'll instead have to use a `u64` which is the *hash* of the name:

```rust
#[repr(transparent)]
pub struct Unsafe(PhantomData, F);
```

This is ugly, but the user will never see this code, as it will be generated by the proc macro attribute. The example above would expand to:

```rust
struct Pair {
m: Unsafe,
n: Unsafe,
}
```

This gets us *most* of the way there. It's still possible to swap between instances of the *same* type (e.g., given `p: Pair` and `q: Pair`, to swap `p.m` and `q.m`). I'm not yet sure how to prevent this from happening, but it's a pretty small hole.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.