Skip to content

Re-evaluate ABI compatibility rules in light of CFI #489

Open
@RalfJung

Description

@RalfJung

The goal of CFI (control-flow integrity) is to make it harder to exploit bugs. It does that, in my rough understanding, by adding extra checks to each indirect jump checking that the destination we are jumping to can plausibly have happened in a "proper" execution of the program. I am focusing on calls of function pointers here, though CFI also needs to consider other indirect jumps.

In C, basically all mismatches between caller and callee signatures on such function calls are UB. Therefore, CFI can do whatever it wants with such calls without causing any issues for correct C programs.

In Rust, we have recently defined the behavior of some cases where caller and callee have a different type. This means if CFI rejects such calls that we consider ABI-compatible, then entirely well-defined Rust programs might be affected. That seems undesirable.

I had a long chat with @rcvalle about this earlier this week, and from my understanding, the main ABI compatibility rule that CFI people really dislike is us declaring all pointers mutually compatible (if their metadata matches), and similar for declaring all function pointers mutually compatible.

(There was also some discussion about the case of different ways of writing "the same" integer type, such as u64 vs usize on a 64-bit-system, or char vs u32. But allowing such mismatches seems to have negligible impact on CFI's ability to detect exploits, so this is much less relevant than the pointers. This becomes more relevant when considering C bindings, where "fully integer-type aware CFI" requires distinguishing the plethora of integer types that C has, even though Rust makes no different between them. Currently I think there is no plan to try to expect the wider ecosystem to follow such a discipline, but this may come up in the future.)

So... should we restrict what we consider ABI-compatible? Of course it would be ridiculous to introduce extra UB for this purpose, but we could declare certain forms of ABI mismatches as "erroneous behavior" (term stolen from recent C++ discussions): these mismatches are unambiguously errors, but the error may or may not be detected. If they do not get detected, the program is still well-behaved. If they get detected, that might be a panic or abort (details TBD). This is basically how Rust treats integer overflow.

I don't know what such restricted ABI compatibility rules should look like. Having ABI mismatches to begin with should already be extremely rare, so it seems hard to evaluate what a good trade-off might be here. @rcvalle, if you have examples of such violations you found that the current ABI rules would allow, that would be quite useful. :) This issue is tracking CFI violations in the standard library, but two of them seem to be violations due to the compiler somehow not tracking the required metadata (i.e., unrelated to what we do or do not consider ABI compatible), and the other two I was not able to find in the current standard library.

The only other idea I have for exploring this is to implement some form of restricted check in Miri, and see if anyone complains. I am not sure what good candidate rules for such a check would be though.

Some concrete questions that are part of this:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions