rapx/check/safedrop/
bug_records.rs

1use crate::compat::FxHashMap;
2use annotate_snippets::{Level, Renderer, Snippet};
3use rustc_span::{Span, symbol::Symbol};
4
5use crate::utils::log::{
6    are_spans_in_same_file, get_basic_block_span, get_variable_name, relative_pos_range,
7    span_to_filename, span_to_line_number, span_to_source_code,
8};
9use rustc_middle::mir::{Body, HasLocalDecls, Local};
10
11use super::drop::LocalSpot;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum BugType {
15    UseAfterFree,
16    DoubleFree,
17    DanglingPointer,
18    UseAfterFreeAndDoubleFree,
19}
20
21#[derive(Debug)]
22pub struct TyBug {
23    pub drop_spot: LocalSpot,
24    pub trigger_info: LocalSpot,
25    pub span: Span,
26    pub confidence: usize,
27    pub bug_type: BugType,
28}
29
30/*
31 * For each bug in the HashMap, the key is local of the value.
32 */
33#[derive(Debug)]
34pub struct BugRecords {
35    pub df_bugs: FxHashMap<usize, TyBug>,
36    pub df_bugs_unwind: FxHashMap<usize, TyBug>,
37    pub uaf_bugs: FxHashMap<usize, TyBug>,
38    pub dp_bugs: FxHashMap<usize, TyBug>,
39    pub dp_bugs_unwind: FxHashMap<usize, TyBug>,
40}
41
42impl BugRecords {
43    pub fn new() -> BugRecords {
44        BugRecords {
45            df_bugs: FxHashMap::default(),
46            df_bugs_unwind: FxHashMap::default(),
47            uaf_bugs: FxHashMap::default(),
48            dp_bugs: FxHashMap::default(),
49            dp_bugs_unwind: FxHashMap::default(),
50        }
51    }
52
53    /// Returns `Some(bug_type)` if the caller should insert the bug with the returned type.
54    /// Returns `None` if the bug should be skipped (already covered by an existing entry).
55    /// When both UAF and DF exist for the same pair, DF wins as the survivor.
56    pub fn try_merge_pair(
57        &mut self,
58        drop_spot: LocalSpot,
59        trigger_bb: usize,
60        in_type: BugType,
61    ) -> Option<BugType> {
62        let pair_match =
63            |bug: &TyBug| bug.drop_spot == drop_spot && bug.trigger_info.bb == Some(trigger_bb);
64        if in_type == BugType::UseAfterFree {
65            for bug in self.df_bugs.values_mut() {
66                if pair_match(bug) {
67                    bug.bug_type = BugType::UseAfterFreeAndDoubleFree;
68                    return None;
69                }
70            }
71            for bug in self.df_bugs_unwind.values_mut() {
72                if pair_match(bug) {
73                    bug.bug_type = BugType::UseAfterFreeAndDoubleFree;
74                    return None;
75                }
76            }
77        } else {
78            let uaf_keys: Vec<usize> = self
79                .uaf_bugs
80                .iter()
81                .filter(|(_, bug)| pair_match(bug))
82                .map(|(k, _)| *k)
83                .collect();
84            if !uaf_keys.is_empty() {
85                for k in &uaf_keys {
86                    self.uaf_bugs.remove(k);
87                }
88                return Some(BugType::UseAfterFreeAndDoubleFree);
89            }
90        }
91        let check = |bugs: &FxHashMap<usize, TyBug>| -> bool { bugs.values().any(pair_match) };
92        if check(&self.uaf_bugs) || check(&self.df_bugs) || check(&self.df_bugs_unwind) {
93            return None;
94        }
95        Some(in_type)
96    }
97
98    pub fn is_bug_free(&self) -> bool {
99        self.df_bugs.is_empty()
100            && self.df_bugs_unwind.is_empty()
101            && self.uaf_bugs.is_empty()
102            && self.dp_bugs.is_empty()
103            && self.dp_bugs_unwind.is_empty()
104    }
105
106    pub fn df_bugs_output<'tcx>(&self, body: &Body<'tcx>, fn_name: Symbol, span: Span) {
107        self.emit_bug_reports(
108            body,
109            &self.df_bugs,
110            fn_name,
111            span,
112            "Double free detected",
113            "Double free detected.",
114            df_uaf_detail,
115        );
116
117        self.emit_bug_reports(
118            body,
119            &self.df_bugs_unwind,
120            fn_name,
121            span,
122            "Double free detected",
123            "Double free detected during unwinding.",
124            df_uaf_detail,
125        );
126    }
127
128    pub fn uaf_bugs_output<'tcx>(&self, body: &Body<'tcx>, fn_name: Symbol, span: Span) {
129        self.emit_bug_reports(
130            body,
131            &self.uaf_bugs,
132            fn_name,
133            span,
134            "Use-after-free detected",
135            "Use-after-free detected.",
136            df_uaf_detail,
137        );
138    }
139
140    pub fn dp_bug_output<'tcx>(&self, body: &Body<'tcx>, fn_name: Symbol, span: Span) {
141        self.emit_bug_reports(
142            body,
143            &self.dp_bugs,
144            fn_name,
145            span,
146            "Dangling pointer detected",
147            "Dangling pointer detected.",
148            dp_detail,
149        );
150
151        self.emit_bug_reports(
152            body,
153            &self.dp_bugs_unwind,
154            fn_name,
155            span,
156            "Dangling pointer detected during unwinding",
157            "Dangling pointer detected during unwinding.",
158            dp_detail,
159        );
160    }
161
162    fn emit_bug_reports<'tcx, F>(
163        &self,
164        body: &Body<'tcx>,
165        bugs: &FxHashMap<usize, TyBug>,
166        fn_name: Symbol,
167        span: Span,
168        log_msg: &str,
169        title: &str,
170        detail_formatter: F,
171    ) where
172        F: Fn(&TyBug, &str, &str, &str, &str) -> String,
173    {
174        if bugs.is_empty() {
175            return;
176        }
177
178        rap_warn!("{} in function {:?}", log_msg, fn_name);
179
180        let code_source = span_to_source_code(span);
181        let filename = span_to_filename(span);
182        let renderer = Renderer::styled();
183
184        for bug in bugs.values() {
185            if are_spans_in_same_file(span, bug.span) {
186                let format_local_info = |id: usize| -> String {
187                    if id >= body.local_decls().len() {
188                        return format!("UNKNWON(_{}) in {}", id, fn_name.as_str());
189                    }
190                    let local = Local::from_usize(id);
191                    let name_opt = get_variable_name(body, id);
192                    let decl_span = body.local_decls[local].source_info.span;
193                    let location = format!(
194                        "{}:{}",
195                        span_to_filename(decl_span),
196                        span_to_line_number(decl_span)
197                    );
198                    match name_opt {
199                        Some(name) => format!("_{}({}, {})", id, name, location),
200                        None => format!("_{}(_, {})", id, location),
201                    }
202                };
203
204                let format_bb_info = |bb_id: usize| -> String {
205                    let bb_span = get_basic_block_span(body, bb_id);
206                    let location = format!(
207                        "{}:{}",
208                        span_to_filename(bb_span),
209                        span_to_line_number(bb_span)
210                    );
211                    format!("BB{}({})", bb_id, location)
212                };
213
214                let drop_bb = if let Some(bb) = bug.drop_spot.bb {
215                    format_bb_info(bb)
216                } else {
217                    String::from("NA")
218                };
219                let drop_local = if let Some(local) = bug.drop_spot.local {
220                    format_local_info(local)
221                } else {
222                    String::from("NA")
223                };
224                let trigger_bb = if let Some(bb) = bug.trigger_info.bb {
225                    format_bb_info(bb)
226                } else {
227                    String::from("NA")
228                };
229                let trigger_local = if let Some(local) = bug.trigger_info.local {
230                    format_local_info(local)
231                } else {
232                    String::from("NA")
233                };
234
235                let detail =
236                    detail_formatter(bug, &drop_local, &trigger_local, &drop_bb, &trigger_bb);
237
238                let mut snippet = Snippet::source(&code_source)
239                    .line_start(span_to_line_number(span))
240                    .origin(&filename)
241                    .fold(false);
242
243                snippet = snippet.annotation(
244                    Level::Warning
245                        .span(relative_pos_range(span, bug.span))
246                        .label(&detail),
247                );
248
249                let message = Level::Warning.title(title).snippet(snippet);
250
251                rap_warn!("{}", renderer.render(message));
252            }
253        }
254    }
255}
256
257fn df_uaf_detail(
258    bug: &TyBug,
259    drop_local: &str,
260    trigger_local: &str,
261    drop_bb: &str,
262    trigger_bb: &str,
263) -> String {
264    let line = format!(
265        "{} line {}.",
266        span_to_filename(bug.span),
267        span_to_line_number(bug.span)
268    );
269    match bug.bug_type {
270        BugType::UseAfterFreeAndDoubleFree => format!(
271            "Double free / Use-after-free (confidence {}%): Location in file {}\n    | MIR detail: Value {} and {} are alias.\n    | MIR detail: {} is dropped at {}; {} is dropped at {}.",
272            bug.confidence,
273            line,
274            drop_local,
275            trigger_local,
276            drop_local,
277            drop_bb,
278            trigger_local,
279            trigger_bb
280        ),
281        BugType::UseAfterFree => format!(
282            "Use-after-free (confidence {}%): Location in file {}\n    | MIR detail: Value {} and {} are alias.\n    | MIR detail: {} is dropped at {}; {} is used at {}.",
283            bug.confidence,
284            line,
285            drop_local,
286            trigger_local,
287            drop_local,
288            drop_bb,
289            trigger_local,
290            trigger_bb
291        ),
292        _ => format!(
293            "Double free (confidence {}%): Location in file {}\n    | MIR detail: Value {} and {} are alias.\n    | MIR detail: {} is dropped at {}; {} is dropped at {}.",
294            bug.confidence,
295            line,
296            drop_local,
297            trigger_local,
298            drop_local,
299            drop_bb,
300            trigger_local,
301            trigger_bb
302        ),
303    }
304}
305
306fn dp_detail(
307    bug: &TyBug,
308    drop_local: &str,
309    trigger_local: &str,
310    drop_bb: &str,
311    _trigger_bb: &str,
312) -> String {
313    format!(
314        "Dangling pointer (confidence {}%): Location in file {} line {}.\n    | MIR detail: Value {} and {} are alias.\n    | MIR detail: {} is dropped at {}; {} became dangling.",
315        bug.confidence,
316        span_to_filename(bug.span),
317        span_to_line_number(bug.span),
318        drop_local,
319        trigger_local,
320        drop_local,
321        drop_bb,
322        trigger_local,
323    )
324}