Skip to main content

Module ferrocene

Module ferrocene 

Source
Expand description

This module contains the ferrocene::unvalidated lint pass.

§Architecture

There are two main passes: the THIR pass and the post-monomorphization MIR pass. THIR runs on both cargo check and cargo build. post-mono only runs on cargo build.

The THIR pass is exclusively for diagnostics; our soundness argument does not rely on it at all. It only exists because it sucks to only see errors multiple crates later than they happened, especially for highly generic crates like core.

The post-mono pass only runs on code that has been monomorphized for codegen. In particular, it only runs on reachable code; it’s very possible to have dead code that uses an unvalidated item, which is fine as long as it’s never actually sent to LLVM. In most cases, but not all, this will be caught by the THIR pass.

§instantiations

We need a post-mono pass because we may not be able to resolve all function calls immediately. Consider this program:

fn uninstantiated<T: Clone>(x: T) { x.clone(); }

At the time we first see it, we have no idea what the type of T is, so we cannot resolve <T as Clone>::clone. We have to wait until we see a caller that monomorphizes it as (e.g.) uninstantiated::<i32>(0). Only then do we know whether the implementation is validated.

§macros

Because we depend on this lint for our validity argument, we report the lint even through external macros; just because a macro was defined in core does not mean the functions it calls are validated.

§function pointers

Normally we only lint at call sites. However, once a function is cast to a function pointer, we no longer have a way to retrieve its #[ferrocene::prevalidated] attribute. We want to avoid having to ban function pointers altogether, so instead we force a decision of whether to lint at the time of the cast. Consider this program:

fn unvalidated() {}
#[ferrocene::prevalidated]
fn returns_ptr() -> fn() { unvalidated } // not ok

We have no idea whether some validated code is going to call option.map(returns_ptr()). So we need to lint at the cast site instead.

It might be possible to do fancy dataflow analysis to only disallow this if the pointer “escapes” the current function, but that’s complicated, and always checking at the cast site is simple.

§const blocks

Some function calls occur in the initializer of a const or static, not in a function body. Usually this is totally fine: we argue to the assessor that compile-time code doesn’t need to (and can’t) have line-coverage.

const PATH_MAX: usize = 2048;
let buffer = [0; PATH_MAX]; // totally fine

However, if there’s a function pointer anywhere in the constant, we need to make sure that function can’t be called at runtime. In that case, we require the const or static to be marked with ferrocene::prevalidated at each use site:

fn unvalidated_panic(_: &PanicHookInfo) {}
const PANIC_HOOK: fn(&PanicHookInfo) = unvalidated_panic;
set_hook(Box::new(PANIC_HOOK)); //~ ERRROR PANIC_HOOK is unvalidated

Then, once the user adds the annotation, we walk the const body at the definition site.

fn unvalidated_panic(_: &PanicHookInfo) {}
#[ferrocene::prevalidated]
const PANIC_HOOK: fn(&PanicHookInfo) = unvalidated_panic;
//~^ ERROR unvalidated_panic is unvalidated

§trait object coercions

These are similar to function pointers, except trait objects bundle many function pointers together, and determining which functions those actually are is non-trivial. See LintState::check_dyn_trait_coercion for examples of how this works.

struct Unvalidated;
impl PartialEq<()> for Unvalidated {
    fn eq(&self, _: &()) -> bool { false }
}
// not ok: might call x.eq() later.
let x: &dyn PartialEq<()> = &Unvalidated;

§THIR

The THIR pass runs as a (mostly) standard LateLintPass. Unfortunately, LateLintPasses normally work on HIR and run near the end of compilation, which means that THIR would normally not be available. We preserve THIR all the way through the end of compilation, which causes Ferrocene to use slightly more memory in exchange for getting better diagnostics.

If the THIR pass cannot resolve an uninstantiated call (see “instantiations” above), it simply silences the warning, assuming the post-mono pass will catch it.

§post-mono

This pass is hacked into the collect_and_partition_mono_items query, which runs on MIR just before the time we actually generate LLVM IR for a given function. That allows us to assume that all function calls can be resolved to an Instance (and error out otherwise). It also runs after “elaborate drops” expands each drop to an explicit TerminatorKind::Drop.

However, it means we cannot depend on the function to be local to the current crate, or that we have a lint node for the failing call, or that we have source spans or HIR available for the failing call.

This sucks a lot! What we do instead is look at the caller of the unvalidated function. For example, in our example above, our lint is on the uninstantiated(0_i32) call, not the x.clone call. We show x.clone as the primary span, but our decision of whether or not to emit the lint comes from the uninstantiated() call.

§Implementation

First, some background on Rust’s type system and compilation model. Each function in a Rust program is only defined in one place, but it may be instantiated many times with different generic arguments. Our definition above was fn uninstantiated, and our generic arguments were [i32], which means our Instance was uninstantiated::<i32>.

For our purposes, we care only about the instantiations of a function, not about any declarations in a trait. In order to instantiate a function, we need to know both its definition (DefId) and generic arguments (GenericArgsRef). We may also need to resolve type variables in scope. For example, in this program below, we cannot instantiate inherent unless we know the type of T from the impl:

struct S<T>(T);
impl<T: Default> S<T> { fn inherent() -> T { T::default() } }

We get these type variables from a ParamEnv.

Modules§

diagnostics 🔒
Recommended reading
dynamic_casts 🔒
post_mono 🔒
Run a post-mono pass on MIR, possibly from other crates. In post-mono MIR, all functions are possible to resolve to an Instance.
thir 🔒
Run a pre-mono THIR pass on the current crate. In THIR, all operator overloads have been resolved to a function call, but we still may have uninstantiated generic functions.

Structs§

LintState 🔒
LintUnvalidated
Use 🔒
A use of an unvalidated item.

Enums§

InstantiateResult 🔒
UnvalidatedImplCause 🔒
UseKind 🔒

Statics§

UNVALIDATED
The ferrocene::unvalidated lint detects verified code that calls unverified functions. This may result in unverified code running in a safety critical context.

Functions§

lint_validated_roots
Lint all used items recursively, starting from validated roots. Validated roots are calculated in rustc_monomorphize::collector::ferrocene, see there for details.