SafeDrop

What is SafeDrop?

Rust is an emerging programming language that aims to prevent memory-safety bugs. However, the current design of Rust also brings side effects, which may increase the risk of memory-safety issues. In particular, it employs OBRM (ownership-based resource management) and enforces automatic deallocation of unused resources without the garbage collector. It may therefore falsely deallocate reclaimed memory and lead to use-after-free or double-free issues.

We study the problem of invalid memory deallocation and propose SafeDrop, a static path-sensitive data-flow analysis approach to detect such bugs. Our approach analyzes each API of a Rust crate iteratively by traversing the control-flow graph and extracting all aliases of each data-flow. To guarantee precision and scalability, we leverage a modified Tarjan algorithm to achieve scalable path-sensitive analysis, and a cache-based strategy to achieve efficient inter-procedural analysis.

Our experiment results show that our approach can successfully detect all existing CVEs of such issues with a limited number of false positives. The analysis overhead ranges from 1.0% to 110.7% in comparison with the original compilation time. We further apply our tool to several real-world Rust crates and find 8 Rust crates involved with invalid memory deallocation issues.

How SafeDrop works?

This section will demonstrate the execution process of SafeDrop using a test case, linking the detection process to the main code of SafeDrop.

POC

For the test case, we chose the same PoC as in the paper to facilitate user understanding and testing:

fn genvec() -> Vec<u8> {
    let mut s = String::from("a_tmp_string");
    let ptr = s.as_mut_ptr();
    let v;
    unsafe {
        v = Vec::from_raw_parts(ptr, s.len(), s.len());
    }
        // mem:: forgets); // do not drop s
        // otherwise, s is dropped before return
        return v;
}
fn main() {
    let v = genvec();
    // use v -> use after free
    // drop v before return →> double free
}

SafeDrop Query

First, in the RAP backend, we embedded a Query in the compiler called SafeDrop.

This Query can be enabled through a command line parameter in the frontend, located in the file compiler/rustc_interface/src/passes.rs:

#![allow(unused)]
fn main() {
if env::var_os("RUSTC_BOOTSTRAP").is_none() && env::var_os("SAFEDROP").is_some() {
        sess.time("safedrop_check", || { tcx.hir().par_body_owners(|def_id| tcx.ensure().safedrop_check(def_id));});
}
}

The environment variable "SAFEDROP" is set by the front end, while "RUSTC_BOOTSTRAP" is used to prevent calling this query during Rustc's bootstrap process. It should only be used when analyzing third-party programs.

We have inserted the query code into compiler/rustc_mir_transform/src/lib.rs. This query's function body is called whenever Rust analyzes each function.

#![allow(unused)]
fn main() {
fn safedrop_check<'tcx>(tcx: TyCtxt<'tcx>, def_id: DefId) -> () {
    if let Some(_other) = tcx.hir().body_const_context(def_id.expect_local()){
        return;
    }
    if tcx.is_mir_available(def_id) {
        rap_info!("{:?}", def_id);
        let body = tcx.optimized_mir(def_id);
        let mut func_map = FuncMap::new();
        let mut safedrop_graph = SafeDropGraph::new(&body, tcx, def_id);
        safedrop_graph.solve_scc();
        safedrop_graph.safedrop_check(0, tcx, &mut func_map);
        if safedrop_graph.visit_times <= 10000{ safedrop_graph.output_warning(); }
        else{ rap_error!("SafeDrop: Over_visited: {:?}", def_id); }
    }
}
}

Data Structure

For SafeDrop, the data structure SafeDropGraph<'tcx> maintains the context of the analysis results.

Our analyzer constructs an instance for each analyzed function, the definition is provided as follows:

#![allow(unused)]
fn main() {
pub struct SafeDropGraph<'tcx>{
    pub def_id: DefId,
    pub span: Span,
    // contains all varibles (including fields) as nodes.
    pub nodes: Vec<Node>,
    // contains all blocks in the CFG
    pub blocks: Vec<BlockNode<'tcx>>,
    pub arg_size: usize, 
    // we shrink a SCC into a node and use a father node to represent the SCC.
    pub father_block: Vec<usize>,
    // record the constant value during safedrop checking.
    pub constant_bool: FxHashMap<usize, usize>,
    // used for tarjan algorithmn.
    pub count: usize,
    // contains the return results for inter-procedure analysis.
    pub return_results: ReturnResults,
    // used for filtering duplicate alias assignments in return results.
    pub return_set: FxHashSet<(usize, usize)>,
    // record the information of bugs for the function.
    pub bug_records: BugRecords,
    // a threhold to avoid path explosion.
    pub visit_times: usize
}
}

Next, we will mainly discuss several important functions of SafeDropGraph<'tcx>. It is evident that the processing logic in the query is quite simple, with two key methods: solve_scc and safedrop_check.

Graph Traversal

#![allow(unused)]
fn main() {
    safedrop_graph.solve_scc();
    safedrop_graph.safedrop_check(0, tcx, &mut func_map);
}

Bug Reporter

Type Filter