rust-lang / rust-lang/rust

Can CFI be made compatible with type erasure schemes?

Open
#128,728 28 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-control-flow-integrity A-sanitizers C-discussion PG-exploit-mitigations T-compiler T-opsem
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing Rust's documented ABI compatibility rules and the CFI behavior described in the issue, then compare the standard library's type-erased fmt arguments with the failing call pattern. Investigate which CFI implementations and configurations are relevant, and define whether an erased pointee type or another mechanism can permit valid type erasure without weakening accidental mismatch checks.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.