Skip to content

Commit cfa51db

Browse files
bors[bot]CAD97
andauthored
Merge #34
34: BREAKING: Soundness fix for Erasable edge case r=CAD97 a=CAD97 Co-authored-by: CAD97 <[email protected]>
2 parents 2eecddb + d6241c6 commit cfa51db

File tree

6 files changed

+123
-7
lines changed

6 files changed

+123
-7
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/erasable/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "erasable"
3-
version = "1.0.0"
3+
version = "1.1.0"
44
edition = "2018"
55

66
authors = ["Christopher Durham (cad97) <[email protected]>"]

crates/erasable/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,58 @@ There are two main useful reasons to type erase pointers in Rust:
1616
We provide the [`Thin`](https://cad97.github.io/pointer-utils/erasable/struct.Thin.html)
1717
wrapper type to provide thin pointer types.
1818

19+
## Changelist
20+
21+
### 1.1.0
22+
#### Breaking changes
23+
24+
Unfortunately, a subtle problem with the requirements of `Erasable::unerase`
25+
on implementors was found, and needed to be corrected in this version.
26+
27+
It has always been the intent that a pointer of any provenance should roundtrip
28+
through `Erasable::erase`/`unerase` without any impact on provenance validity
29+
of any pointer. Unfortunately, in 1.0.0, `Eraseable::unerase` explicitly suggested
30+
the use of temporary shared references (`&_`) in its implementation, which do not
31+
hold up this desired semantics. To quote `Erasable::unerase`'s new documentation:
32+
33+
> No references to the pointee _at all_ should be created,
34+
> as their mere temporary existence may impact the validity and
35+
> usable provenance of other pointers to the same location.
36+
>
37+
> Creating a shared reference sounds on the surface like it should be ok.
38+
> After all, you have a known-valid pointer to your type, and you can
39+
> borrow from whatever pointer was erased. However, in the face of raw
40+
> pointers with a shared mutable provenance, this is problematic.
41+
> If a write to the pointee location even potentially races with any
42+
> invocation of `unerase`, and it creates a reference to the location,
43+
> we have immediate undefined behavior for writing behind a shared ref.
44+
>
45+
> The root issue is that there may be external synchronization that this
46+
> implementation has no way of knowing about. An implementation of this
47+
> trait must only read the mimimum amount of data required to re-type the
48+
> pointer, and must do so with a raw pointer read, or, if and only if
49+
> there is a known `UnsafeCell` point (such as an atomic), a reference to
50+
> that `UnsafeCell` point and the safe API of that `UnsafeCell` point.
51+
52+
For more information around the discovery of this issue see
53+
[this Twitter thread](https://twitter.com/CAD97_/status/1231057021623054336).
54+
55+
To make this update easier, we additionally added `const Erasable::ACK_1_1_0: bool`.
56+
This value defaults to `false`, but _must_ be overriden to `true` in new
57+
implementations of `Erasable` as an explicit acknowledgement of the new requirement.
58+
To make finding impls that do not do so, the `ERASABLE_ENFORCE_1_1_0_SEMANTICS`
59+
environment variable can be set to remove the default implementation,
60+
explicitly breaking any implementor that did not override its value.
61+
62+
If you have a use of `unerase` that would be made unsound by `unerase` creating
63+
a shared reference, then it is recommended to assert the value of `ACK_1_1_0`
64+
to guard against old impls that may create a shared reference. Also, please tell
65+
me what it is, because I have not been able to construct a reasonable example
66+
that would be made unsound by this hole, just a theoretical attack vector.
67+
Similarly, if you can show that this isn't required, get in touch.
68+
I'd love to remove this hack if it turns out unnecessary after all
69+
(but I think I'm confident ruling out that possibility).
70+
1971
## Related Crates
2072

2173
- [`ptr-union`](https://lib.rs/crates/ptr-union): Pointer unions the size of a pointer.

crates/erasable/build.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
use std::env;
2+
13
fn main() {
24
let cfg = autocfg::new();
5+
36
cfg.emit_expression_cfg("{ extern { type T; } () }", "has_extern_type");
47
// NB: Requires this impl to cover `T: ?Sized`, which is not the case as of 2020-09-01.
58
// cfg.emit_type_cfg("std::sync::Weak::into_raw", "has_Weak__into_raw");
69
cfg.emit_type_cfg("!", "has_never");
10+
11+
if let Ok(var) = env::var("ERASABLE_ENFORCE_1_1_0_SEMANTICS") {
12+
if !var.is_empty() && var != "0" {
13+
autocfg::emit("enforce_1_1_0_semantics");
14+
}
15+
}
16+
17+
autocfg::rerun_env("ERASABLE_ENFORCE_1_1_0_SEMANTICS");
718
autocfg::rerun_path("build.rs");
819
}

crates/erasable/src/lib.rs

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,19 +201,68 @@ pub unsafe trait Erasable {
201201

202202
/// Unerase this erased pointer.
203203
///
204-
/// Note that this _must_ be sound for erasable pointer types
205-
/// with both unique/write provenance and shared/read provenance.
206-
/// (In other words, both `&mut _` and `&_` can be unerased with this.)
204+
/// Note that this _must_ be sound to roundtrip pointers with any provenance,
205+
/// shared, unique, raw, frozen, mutable, and any valid combination thereof.
206+
/// (In other words, `&mut _` and `&_` can be safely erased and unerased, and
207+
/// any raw pointer should roundtrip without losing the provenance it had.)
207208
///
208209
/// Concretely, this means that the resulting pointer _must_ be derived
209210
/// from the input pointer without any intervening references manifested.
210-
/// Temporary shared references may be used in the implementation,
211-
/// so long as the returned pointer is not derived from them.
211+
/// Additionally, no references to the pointee _at all_ should be created,
212+
/// as their mere temporary existence may impact the validity and
213+
/// usable provenance of other pointers to the same location.
214+
///
215+
/// Creating a shared reference sounds on the surface like it should be ok.
216+
/// After all, you have a known-valid pointer to your type, and you can
217+
/// borrow from whatever pointer was erased. However, in the face of raw
218+
/// pointers with a shared mutable provenance, this is problematic.
219+
/// If a write to the pointee location even potentially races with any
220+
/// invocation of `unerase`, and it creates a reference to the location,
221+
/// we have immediate undefined behavior for writing behind a shared ref.
222+
///
223+
/// The root issue is that there may be external synchronization that this
224+
/// implementation has no way of knowing about. An implementation of this
225+
/// trait must only read the mimimum amount of data required to re-type the
226+
/// pointer, and must do so with a raw pointer read, or, if and only if
227+
/// there is a known `UnsafeCell` point (such as an atomic), a reference to
228+
/// that `UnsafeCell` point and the safe API of that `UnsafeCell` point.
212229
///
213230
/// # Safety
214231
///
215232
/// The erased pointer must have been created by `erase`ing a valid pointer.
216233
unsafe fn unerase(this: ErasedPtr) -> ptr::NonNull<Self>;
234+
235+
/// Whether this implementor has acknowledged the 1.1.0 update to
236+
/// `unerase`'s documented implementation requirements.
237+
///
238+
/// Prior to 1.0.0, creating a temporary shared reference (`&_`) in
239+
/// `unerase` was explicitly listed as allowed, but for the 1.1.0 release
240+
/// it was determined that this in fact can cause problems for some use
241+
/// cases that `Erasable` is designed to support.
242+
///
243+
/// Implementing this as `false` is _not allowed_ and is _not_ permission
244+
/// to create references within `unerase`. It only exists as a way to
245+
/// make the soundness fix in 1.1.0 disallowing references not breaking.
246+
/// You _must_ override this with a value of `true` and follow the current
247+
/// documented requirements for `unerase`, and not create references.
248+
///
249+
/// If your use of `unerase` would be problematic if it creates a temporary
250+
/// shared reference, you should assert that this value is `true`.
251+
/// Not doing so will expose you to potentially unsound implementations
252+
/// written against 1.0.0 before the reference clarification was made.
253+
///
254+
/// The environment variable `ACK_1_1_0` can be set to enforce that all
255+
/// implementors have provided an override for this acknowledgement.
256+
#[cfg(not(enforce_1_1_0_semantics))]
257+
const ACK_1_1_0: bool = false;
258+
259+
/// Whether this implementor has acknowledged the 1.1.0 update to
260+
/// `unerase`'s documented implementation requirements.
261+
///
262+
/// Implementing this as `false` is _not allowed_.
263+
/// You _must_ override this with a value of `true`.
264+
#[cfg(enforce_1_1_0_semantics)]
265+
const ACK_1_1_0: bool;
217266
}
218267

219268
/// Erase a pointer.
@@ -557,6 +606,8 @@ unsafe impl<T: Sized> Erasable for T {
557606
// SAFETY: must not read the pointer for the safety of the impl directly below.
558607
this.cast()
559608
}
609+
610+
const ACK_1_1_0: bool = true;
560611
}
561612

562613
// ~~~ impl ErasablePtr ~~~ //

crates/slice-dst/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ unsafe impl<Header, Item> Erasable for SliceWithHeader<Header, Item> {
295295
let raw = ptr::NonNull::new_unchecked(slice_from_raw_parts(this.as_ptr().cast(), len));
296296
Self::retype(raw)
297297
}
298+
299+
const ACK_1_1_0: bool = true;
298300
}
299301

300302
pub(crate) mod layout_polyfill;

0 commit comments

Comments
 (0)