microsoft / microsoft/injectorppforrust

Unsoundness via breaking internal invariants in other crates

Open
#41 3 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

discussion
Dominant language
Rust
Stars
404
Forks
19
Avg merge
23h 57m
Merged PRs (30d)
1

Description

InjectorPP can replace functions in a different crate in a way that breaks invariants of that crate. This can lead to UB as the second crate might use unsafe that relies on those invariants. A real example would be something like replacing get_capacity() on some vector type. But one could also imagine other examples where cross-crate mocking could more subtly break invariants that unsafe code relies on. I'll provide a more minimal example instead:

Let us have a crate always_zero, which provides us with a structure that always holds the number zero. Due to this invariant we can for example put unreachable_unchecked into if branches where the value would be non-zero and it's perfectly sound code:

use std::hint::unreachable_unchecked;

pub struct AlwaysZero {
    value: i32,
}

impl AlwaysZero {
    pub fn new() -> Self {
        AlwaysZero { value: 0 }
    }

    #[inline(never)]
    pub fn get_value(&self) -> i32 {
        self.value
    }

    #[inline(always)]
    pub fn frobnicate(&self) {
        if self.get_value() == 0 {
            println!(":)");
        } else {
            // SAFETY: AlwaysZero's invariant is that value is always 0, this can never happen.
            unsafe {
                unreachable_unchecked();
            }
        }
    }
}

Now let us consider a second crate that decides to use InjectorPP to replace get_value:

use injectorpp::interface::injector::InjectorPP;
use always_zero::AlwaysZero;

fn main() {
    let foo = AlwaysZero::new();
    let mut injector = InjectorPP::new();
    injector
        .when_called(injectorpp::func!(AlwaysZero::get_value))
        .will_execute_raw(injectorpp::closure!(|| {
            println!("get_value bypassed");
            42
        }, fn() -> i32));

    foo.frobnicate();
}

Executing this in dev profile leads to unsafe precondition(s) violated: hint::unreachable_unchecked must never be reached.

If InjectorPP could only override functions from the crate it is invoked from you could argue that the unsoundness is caused by the piece of unsafe that relied on the invariant. The one writing that unsafe code should have checked that no InjectorPP invocations can mess with it. However, this sort of argument no longer works when the effect can apply across crates.

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 reproducing the minimal two-crate example with always_zero, AlwaysZero::get_value, frobnicate, and the InjectorPP caller. Trace how the cross-crate replacement is applied; done means the behavior no longer permits an override that causes the invariant-dependent unsafe path to be reached.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
security, testing-qa
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.