Skip to content

Can CFI be made compatible with type erasure schemes? #128728

Open
@RalfJung

Description

@RalfJung

There is a kind of common type erasure scheme used in Rust that goes something like this:

/// Invariant: there exists some type `T` such that `data` is actually a `&'a T` and `op`
/// is actually a `fn(&T)`.
struct ErasedTypeAndOp<'a> {
  data: *const (),
  op: fn(*const ()),
  _phantom: PhantomData<&'a ()>,
}

impl<'a> ErasedTypeAndOp<'a> {
  pub fn new<T>(data: &'a T, op: fn(&'a T)) {
    Self {
      data: data as *const T,
      op: unsafe { std::mem::transmute(op) },
      _phantom: PhantomData,
    }
  }

  pub fn call_op(&self) {
    (self.op)(self.data)
  }
}

This is used, in particular, in the standard library for type-erased fmt arguments.

Unfortunately, CFI is not happy with this since the function pointer op was transmuted, and is not invoked at its original type. Our documented ABI compatibility rules are fine with this (and Miri also won't complain), but CFI is actually more restrictive than those rules and (at least in some configurations) rejects this call since caller and callee do not agree on the type of the argument.

This is not good: ideally we could just tell people they can turn on CFI in any Rust program and expect it to work. In other words, ideally CFI would only reject programs that we consider buggy (in an official Rust lang/opsem sense), since they have either UB or erroneous behavior.

I am not sure what is the best way to fix this. I also know very little about what CFI can and cannot do (and I understand there's actually a bunch of CFI implementations that differ in their capabilities). My completely naive first idea would be to say that we have a new magic primitive type Erased and then declare ErasedTypeAndOp as follows:

struct ErasedTypeAndOp<'a> {
  data: *const Erased,
  op: fn(*const Erased),
  _phantom: PhantomData<&'a ()>,
}

Then we say that if caller and callee use different pointer types for their arguments, that is erroneous behavior unless one of them uses the special Erased pointee type, in which case the call is permitted. (Or maybe only the call site is allowed to use Erased like that?)

Is that something CFI can do -- basically have pointee type checking "turned off" if the call site uses *const Erased? If yes, would this be an acceptable trade-off between still rejecting accidental type mismatches (and catching as many attacks as possible) while accepting legitimate type erasure patterns?

Cc @rust-lang/opsem @maurer @rcvalle

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-control-flow-integrityArea: Control Flow Integrity (CFI) security mitigationA-sanitizersArea: Sanitizers for correctness and code qualityC-discussionCategory: Discussion or questions that doesn't represent real issues.PG-exploit-mitigationsProject group: Exploit mitigationsT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-opsemRelevant to the opsem team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions