Skip to content

Commit

Permalink
Allow types containing references to have finalizers
Browse files Browse the repository at this point in the history
Previously FSA prevented `Gc<T>`s from having a finalizer if `T`
(directly or indirectly) contained a reference. This made retro-fitting
GC to large libraries difficult. It also, somewhat surprisingly,
completely prevents finalizable nested `Gc<dyn Trait>`s (due to the
underlying vptr being a `&'static` reference).

This commit loosens this restriction, allowing FSA to interrogate drop
method bodies (much the same as for `Send + Sync + FinalizerSafe`)
looking for unsound uses of references.

This change is based on the following observation: a reference inside of
`T`'s drop method is safe to dereference provided it never originates
from `T` or one of `T`'s fields. For example:

```rust
fn drop(&mut self) {
    let a = 123;

    let r1 = &1; // const with 'static lifetime
    let r2 = &a; // stack-local
    let r3 = &*(unsafe { ... }); // unsafe code, all bets are off.

    let r4: &u64 = self.a;
    let r5: &u64 = self.b.a;
    let r6: &u64 = self.c[0]; // c: [&u64; 2]
}
```

In this example, `r1`, `r2`, and `r3` are allowed by FSA: `r1` and `r2`
are sound because their referent will always be valid when the finalizer
is called. `r3` uses unsafe code, so FSA gets out of the way here, as it
is up to the user to ensure that this is sound themselves.

`r4`, `r5`, and `r6` on the other hand are not allowed by FSA. This is
because they are references which are obtained through field or array
index projections into `T`. Such projections are nearly always unsound,
so we always prevent these.

This change means that we can remove the `ReferenceFree` trait, since we
now always check for references inside a drop method. As with our
relaxed FSA rules for `FinalizerSafe` and friends -- if for any reason
we can't see into a type, we err on the side of caution and emit an
error.
  • Loading branch information
jacob-hughes committed Nov 8, 2024
1 parent e292b7e commit be985b8
Show file tree
Hide file tree
Showing 14 changed files with 380 additions and 71 deletions.
125 changes: 88 additions & 37 deletions compiler/rustc_mir_transform/src/check_finalizers.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![allow(rustc::untranslatable_diagnostic)]
#![allow(rustc::diagnostic_outside_of_impl)]
use rustc_data_structures::fx::FxHashSet;
use rustc_hir::def_id::DefId;
use rustc_hir::lang_items::LangItem;
use rustc_middle::mir::visit::PlaceContext;
Expand All @@ -20,8 +21,8 @@ enum FinalizerErrorKind<'tcx> {
NotSendAndSync(Span),
/// Does not implement `FinalizerSafe`
NotFinalizerSafe(Ty<'tcx>, Span),
/// Does not implement `ReferenceFree`
NotReferenceFree,
/// Contains a field projection where one of the projection elements is a reference.
UnsoundReference(Ty<'tcx>, Span),
/// Uses a trait object whose concrete type is unknown
UnknownTraitObject,
/// Calls a function whose definition is unavailable, so we can't be certain it's safe.
Expand Down Expand Up @@ -70,19 +71,19 @@ impl<'tcx> FinalizerErrorKind<'tcx> {
err.help("`Gc` runs finalizers on a separate thread, so drop methods\nmust only use values whose types implement `FinalizerSafe`.");
err.span_label(
cx.ctor,
format!("`Gc::new` requires that it implements the `FinalizeSafe` trait.",),
format!(
"`Gc::new` requires that {ty} implements the `FinalizeSafe` trait.",
),
);
}
}
Self::NotReferenceFree => {
Self::UnsoundReference(ty, span) => {
err.span_label(*span, "caused by the expression here in `fn drop(&mut)` because");
err.span_label(
cx.arg,
"contains a reference (&) which is not safe to be used in a finalizer.",
);
err.span_label(
cx.ctor,
format!("`Gc::new` requires finalizable types to be reference free.",),
*span,
format!("it is a reference ({ty}) which is not safe to use in a finalizer."),
);
err.help("`Gc` may run finalizers after the valid lifetime of this reference.");
}
Self::MissingFnDef => {
err.span_label(cx.arg, "contains a function call which may be unsafe.");
Expand Down Expand Up @@ -152,10 +153,6 @@ impl<'tcx> FinalizationCtxt<'tcx> {
return Ok(());
}

if !self.is_reference_free(ty) {
return Err(vec![FinalizerErrorKind::NotReferenceFree]);
}

if self.is_send(ty) && self.is_sync(ty) && self.is_finalizer_safe(ty) {
return Ok(());
}
Expand Down Expand Up @@ -265,16 +262,6 @@ impl<'tcx> FinalizationCtxt<'tcx> {
.must_apply_modulo_regions();
}

fn is_reference_free(&self, ty: Ty<'tcx>) -> bool {
let t = self.tcx.get_diagnostic_item(sym::ReferenceFree).unwrap();
return self
.tcx
.infer_ctxt()
.build()
.type_implements_trait(t, [ty], self.param_env)
.must_apply_modulo_regions();
}

fn is_copy(&self, ty: Ty<'tcx>) -> bool {
ty.is_copy_modulo_regions(self.tcx, self.param_env)
}
Expand Down Expand Up @@ -316,23 +303,73 @@ impl<'tcx> FinalizationCtxt<'tcx> {
}
return false;
}

/// For a given projection, extract the 'useful' type which needs checking for finalizer safety.
///
/// Simplifying somewhat, a projection is a way of peeking into a place. For FSA, the
/// projections that are interesting to us are struct/enum fields, and slice/array indices. When
/// we find these, we want to extract the type of the field or slice/array element for further
/// analysis. This is best explained with an example, the following shows the projection, and
/// what type would be returned:
///
/// a[i] -> typeof(a[i])
/// a.b[i] -> typeof(a.b[i])
/// a.b -> typeof(b)
/// a.b.c -> typeof(c)
///
/// In practice, this means that the type of the last projection is extracted and returned.
fn extract_projection_ty(
&self,
body: &Body<'tcx>,
base: PlaceRef<'tcx>,
elem: ProjectionElem<Local, Ty<'tcx>>,
) -> Option<Ty<'tcx>> {
match elem {
ProjectionElem::Field(_, ty) => Some(ty),
ProjectionElem::Index(_)
| ProjectionElem::ConstantIndex { .. }
| ProjectionElem::Subslice { .. } => {
let array_ty = match base.last_projection() {
Some((last_base, last_elem)) => {
last_base.ty(body, self.tcx).projection_ty(self.tcx, last_elem).ty
}
None => base.ty(body, self.tcx).ty,
};
match array_ty.kind() {
ty::Array(ty, ..) | ty::Slice(ty) => Some(*ty),
_ => None,
}
}
_ => None,
}
}
}

struct DropMethodChecker<'a, 'tcx> {
body: &'a Body<'tcx>,
cx: &'a FinalizationCtxt<'tcx>,
errors: Vec<FinalizerErrorKind<'tcx>>,
error_locs: FxHashSet<Location>,
}

impl<'a, 'tcx> DropMethodChecker<'a, 'tcx> {
fn new(body: &'a Body<'tcx>, fctxt: &'a FinalizationCtxt<'tcx>) -> Self {
Self { body, cx: fctxt, errors: Vec::new() }
Self { body, cx: fctxt, errors: Vec::new(), error_locs: FxHashSet::default() }
}

fn check(mut self) -> Result<(), Vec<FinalizerErrorKind<'tcx>>> {
self.visit_body(self.body);
if self.errors.is_empty() { Ok(()) } else { Err(self.errors) }
}

fn push_error(&mut self, location: Location, error: FinalizerErrorKind<'tcx>) {
if self.error_locs.contains(&location) {
return;
}

self.errors.push(error);
self.error_locs.insert(location);
}
}

impl<'a, 'tcx> Visitor<'tcx> for DropMethodChecker<'a, 'tcx> {
Expand All @@ -342,18 +379,32 @@ impl<'a, 'tcx> Visitor<'tcx> for DropMethodChecker<'a, 'tcx> {
context: PlaceContext,
location: Location,
) {
for (_, proj) in place_ref.iter_projections() {
match proj {
ProjectionElem::Field(_, ty) => {
let span = self.body.source_info(location).span;
if !self.cx.is_send(ty) || !self.cx.is_sync(ty) {
self.errors.push(FinalizerErrorKind::NotSendAndSync(span));
}
if !self.cx.is_finalizer_safe(ty) {
self.errors.push(FinalizerErrorKind::NotFinalizerSafe(ty, span));
}
}
_ => (),
// A single projection can be comprised of other 'inner' projections (e.g. self.a.b.c), so
// this loop ensures that the types of each intermediate projection is extracted and then
// checked.
for ty in place_ref
.iter_projections()
.filter_map(|(base, elem)| self.cx.extract_projection_ty(self.body, base, elem))
{
let span = self.body.source_info(location).span;
if !self.cx.is_send(ty) || !self.cx.is_sync(ty) {
self.push_error(location, FinalizerErrorKind::NotSendAndSync(span));
break;
}
if ty.is_ref() {
// Unfortunately, we can't relax this constraint to allow static refs for two
// reasons:
// 1. When this MIR transformation is called, all lifetimes have already
// been erased by borrow-checker.
// 2. Unsafe code can and does transmute lifetimes up to 'static then use
// runtime properties to ensure that the reference is valid. FSA would
// not catch this and could allow unsound programs.
self.push_error(location, FinalizerErrorKind::UnsoundReference(ty, span));
break;
}
if !self.cx.is_finalizer_safe(ty) {
self.push_error(location, FinalizerErrorKind::NotFinalizerSafe(ty, span));
break;
}
}
self.super_projection(place_ref, context, location);
Expand Down
1 change: 0 additions & 1 deletion compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,6 @@ symbols! {
RefCell,
RefCellRef,
RefCellRefMut,
ReferenceFree,
Relaxed,
Release,
Result,
Expand Down
10 changes: 0 additions & 10 deletions library/core/src/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,6 @@ impl<T: ?Sized> DerefMut for NonFinalizable<T> {
}
}

#[unstable(feature = "gc", issue = "none")]
#[cfg_attr(not(test), rustc_diagnostic_item = "ReferenceFree")]
pub unsafe auto trait ReferenceFree {}

impl<T> !ReferenceFree for &T {}
impl<T> !ReferenceFree for &mut T {}

/// A wrapper to prevent alloy from performing Finaliser Safety Analysis (FSA)
/// on `T`.
///
Expand Down Expand Up @@ -96,6 +89,3 @@ impl<T: ?Sized> DerefMut for FinalizeUnchecked<T> {

#[cfg(not(bootstrap))]
unsafe impl<T> FinalizerSafe for FinalizeUnchecked<T> {}

#[cfg(not(bootstrap))]
unsafe impl<T> ReferenceFree for FinalizeUnchecked<T> {}
5 changes: 5 additions & 0 deletions library/core/src/marker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,11 @@ impl<T: ?Sized> !FinalizerSafe for *const T {}
#[unstable(feature = "gc", issue = "none")]
impl<T: ?Sized> !FinalizerSafe for *mut T {}

#[unstable(feature = "gc", issue = "none")]
impl<T: ?Sized> !FinalizerSafe for &T {}
#[unstable(feature = "gc", issue = "none")]
impl<T: ?Sized> !FinalizerSafe for &mut T {}

/// Zero-sized type used to mark things that "act like" they own a `T`.
///
/// Adding a `PhantomData<T>` field to your type tells the compiler that your
Expand Down
2 changes: 1 addition & 1 deletion library/std/src/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ impl<T> GcBox<MaybeUninit<T>> {

#[cfg(not(no_global_oom_handling))]
#[unstable(feature = "gc", issue = "none")]
impl<T: Default + Send + Sync + ReferenceFree> Default for Gc<T> {
impl<T: Default + Send + Sync> Default for Gc<T> {
/// Creates a new `Gc<T>`, with the `Default` value for `T`.
///
/// # Examples
Expand Down
1 change: 0 additions & 1 deletion tests/rustdoc/empty-section.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,3 @@ impl !FinalizerSafe for Foo {}
impl !std::marker::Unpin for Foo {}
impl !std::panic::RefUnwindSafe for Foo {}
impl !std::panic::UnwindSafe for Foo {}
unsafe impl std::gc::ReferenceFree for Foo {}
1 change: 0 additions & 1 deletion tests/ui/runtime/gc/check_finalizers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ fn main() {

Gc::new(ShouldFail(Cell::new(123)));
//~^ ERROR: `ShouldFail(Cell::new(123))` has a drop method which cannot be safely finalized.
//~^^ ERROR: `ShouldFail(Cell::new(123))` has a drop method which cannot be safely finalized.

let gcfields = HasGcFields(Gc::new(123));
Gc::new(gcfields);
Expand Down
23 changes: 4 additions & 19 deletions tests/ui/runtime/gc/check_finalizers.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,8 @@ LL | Gc::new(ShouldFail(Cell::new(123)));
= help: `Gc` runs finalizers on a separate thread, so drop methods
must only use values whose types implement `Send` + `Sync`.

error: `ShouldFail(Cell::new(123))` has a drop method which cannot be safely finalized.
--> $DIR/check_finalizers.rs:71:13
|
LL | self.0.replace(456);
| ------
| |
| caused by the expression in `fn drop(&mut)` here because
| it uses a type which is not safe to use in a finalizer.
...
LL | Gc::new(ShouldFail(Cell::new(123)));
| --------^^^^^^^^^^^^^^^^^^^^^^^^^^- `Gc::new` requires that it implements the `FinalizeSafe` trait.
|
= help: `Gc` runs finalizers on a separate thread, so drop methods
must only use values whose types implement `FinalizerSafe`.

error: `gcfields` has a drop method which cannot be safely finalized.
--> $DIR/check_finalizers.rs:76:13
--> $DIR/check_finalizers.rs:75:13
|
LL | println!("Boom {}", self.0);
| ------
Expand All @@ -41,13 +26,13 @@ LL | Gc::new(gcfields);
| --------^^^^^^^^- Finalizers cannot safely dereference other `Gc`s, because they might have already been finalised.

error: `self_call` has a drop method which cannot be safely finalized.
--> $DIR/check_finalizers.rs:80:13
--> $DIR/check_finalizers.rs:79:13
|
LL | Gc::new(self_call);
| ^^^^^^^^^ contains a function call which may be unsafe.

error: `not_threadsafe` has a drop method which cannot be safely finalized.
--> $DIR/check_finalizers.rs:84:13
--> $DIR/check_finalizers.rs:83:13
|
LL | println!("Boom {}", self.0.0);
| --------
Expand All @@ -61,5 +46,5 @@ LL | Gc::new(not_threadsafe);
= help: `Gc` runs finalizers on a separate thread, so drop methods
must only use values whose types implement `Send` + `Sync`.

error: aborting due to 5 previous errors
error: aborting due to 4 previous errors

44 changes: 44 additions & 0 deletions tests/ui/static/gc/fsa/auxiliary/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#[inline(never)]
fn use_val<T: std::fmt::Debug>(x: T) {
dbg!("{:?}", x);
}

#[derive(Debug)]
struct HasNestedRef<'a> {
a: &'a u64,
b: u64,
c: HasRef<'a>,
}

#[derive(Debug)]
struct HasRef<'a> {
a: &'a u64,
b: u64,
c: [&'a u64; 2]
}

impl<'a> HasRef<'a> {
#[inline(never)]
fn new(a: &'a u64, b: u64, c: [&'a u64; 2]) -> Self {
Self { a, b, c }
}

#[inline(always)]
fn new_inlined(a: &'a u64, b: u64, c: [&'a u64; 2]) -> Self {
Self { a, b, c }
}
}

impl<'a> std::default::Default for HasRef<'a> {
#[inline(never)]
fn default() -> Self {
Self { a: &1, b: 1, c: [&1, &2] }
}
}

impl<'a> std::default::Default for HasNestedRef<'a> {
#[inline(never)]
fn default() -> Self {
Self { a: &1, b: 1, c: HasRef::default() }
}
}
31 changes: 31 additions & 0 deletions tests/ui/static/gc/fsa/references.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#![feature(gc)]
#![feature(negative_impls)]
#![allow(dead_code)]
#![allow(unused_variables)]
include!{"./auxiliary/types.rs"}

impl<'a> Drop for HasRef<'a> {
fn drop(&mut self) {
use_val(self.a); // should fail
use_val(self.b); // should pass
use_val(self.c[0]); // should fail

let a = self.a; // should fail
let b = self.b; // should pass
let c = self.c;
use_val(c[1]); // should fail

// should pass, as not a field projection
let c = &1;
use_val(c);
}
}

fn main() {
std::gc::Gc::new(HasRef::default());
//~^ ERROR: `HasRef::default()` has a drop method which cannot be safely finalized.
//~^^ ERROR: `HasRef::default()` has a drop method which cannot be safely finalized.
//~^^^ ERROR: `HasRef::default()` has a drop method which cannot be safely finalized.
//~^^^^ ERROR: `HasRef::default()` has a drop method which cannot be safely finalized.
//~^^^^^ ERROR: `HasRef::default()` has a drop method which cannot be safely finalized.
}
Loading

0 comments on commit be985b8

Please sign in to comment.