rCanary

What is rCanary?

Rust is an effective system programming language that guarantees memory safety via compile-time verifications. It employs a novel ownership-based resource management model to facilitate automated deallocation. This model is anticipated to eliminate memory leaks.

However, we observed that user intervention drives it into semi-automated memory management and makes it error-prone to cause leaks. In contrast to violating memory-safety guarantees restricted by the unsafe keyword, the boundary of leaking memory is implicit, and the compiler would not emit any warnings for developers.

We present RCANARY, a static, non-intrusive, and fully automated model checker to detect leaks across the semi-automated boundary. We design an encoder to abstract data with heap allocation and formalize a refined leak-free memory model based on boolean satisfiability. It can generate SMT-Lib2 format constraints for Rust MIR and is implemented as a Cargo component. We evaluate R C ANARY by using flawed package benchmarks collected from the pull requests of open-source Rust projects. The results indicate that it is possible to recall all these defects with acceptable false positives.

We apply our tool to more than 1,200 real-world crates from crates.io and GitHub, identifying 19 crates having memory leaks. Our analyzer is also efficient, that costs 8.4 seconds per package.

How rCanary rCanary?

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

POC

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

fn main() {
    let mut buf = Box::new("buffer"); 
    // heap item ’buf’ becomes an orphan object 
    let ptr = &mut * ManuallyDrop::new(buf) as * mut _; 
    // leak by missing free operation on ’ptr’ 
    // unsafe { drop_in_place(ptr); } 
}

Front-end Entry

First, in the RAP front-end, we give an entry in the source file.

#![allow(unused)]
fn main() {
impl Callbacks for RapCompilerCalls {
    fn after_analysis<'tcx>(
        &mut self,
        compiler: &Compiler,
        queries: &'tcx Queries<'tcx>,
    ) -> Compilation {
        compiler.session().abort_if_errors();
        Verbosity::init_rap_log_system_with_verbosity(self.rap_config.verbose()).expect("Failed to set up RAP log system");

        rap_info!("RAP Start");
        queries.global_ctxt().unwrap().enter(
            |tcx| start_analyzer(tcx, self.rap_config)
        );
        rap_info!("RAP Stop");

        compiler.session().abort_if_errors();
        Compilation::Continue
    }
}
}

This function is an implementation of the Callbacks trait in Rustc, where the after_analysis method is called after the compiler completes MIR analysis of the local crate. We use this function to intercept Rustc's metadata.

The start_analyzer function defines all the analysis processes of rCanary, with two important steps being TypeAnalysis and FlowAnalysis, corresponding to the ADT-DEF Analysis and constraint construction, and constraint solving described in the paper.

We will introduce the internal implementation in the following sections.

#![allow(unused)]
fn main() {
pub fn start_analyzer(tcx: TyCtxt, config: RapConfig) {
    let rcx_boxed = Box::new(RapGlobalCtxt::new(tcx, config));
    let rcx = Box::leak(rcx_boxed);

    if config.is_rcanary_enabled() {
        run_analyzer(
            "Type Analysis",
            ||
            TypeAnalysis::new(rcx).start()
        );

        run_analyzer(
            "Flow Analysis",
            ||
            FlowAnalysis::new(rcx).start()
        );
    }
}
}