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 okWe 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 fineHowever, 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 unvalidatedThen, 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.
§Recommended reading
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§
- Lint
State 🔒 - Lint
Unvalidated - Use 🔒
- A use of an unvalidated item.
Enums§
Statics§
- UNVALIDATED
- The
ferrocene::unvalidatedlint 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.