Skip to main content

hir_ty/diagnostics/
unsafe_check.rs

1//! Provides validations for unsafe code. Currently checks if unsafe functions are missing
2//! unsafe blocks.
3
4use std::mem;
5
6use either::Either;
7use hir_def::{
8    AdtId, CallableDefId, DefWithBodyId, ExpressionStoreOwnerId, FieldId, FunctionId, GenericDefId,
9    VariantId,
10    expr_store::{Body, ExpressionStore, path::Path},
11    hir::{AsmOperand, Expr, ExprId, ExprOrPatId, InlineAsmKind, Pat, PatId, Statement, UnaryOp},
12    resolver::{HasResolver, ResolveValueResult, Resolver, ValueNs},
13    signatures::{FunctionSignature, StaticFlags, StaticSignature},
14    type_ref::Rawness,
15};
16use rustc_type_ir::inherent::IntoKind;
17use span::Edition;
18
19use crate::{
20    InferenceResult, TargetFeatures,
21    db::HirDatabase,
22    next_solver::{CallableIdWrapper, TyKind, abi::Safety},
23    utils::{TargetFeatureIsSafeInTarget, is_fn_unsafe_to_call, target_feature_is_safe_in_target},
24};
25
26#[derive(Debug, Default)]
27pub struct MissingUnsafeResult {
28    pub unsafe_exprs: Vec<(ExprOrPatId, UnsafetyReason)>,
29    /// If `fn_is_unsafe` is false, `unsafe_exprs` are hard errors. If true, they're `unsafe_op_in_unsafe_fn`.
30    pub fn_is_unsafe: bool,
31    pub deprecated_safe_calls: Vec<ExprId>,
32}
33
34pub fn missing_unsafe(db: &dyn HirDatabase, def: DefWithBodyId) -> MissingUnsafeResult {
35    let _p = tracing::info_span!("missing_unsafe").entered();
36
37    let is_unsafe = match def {
38        DefWithBodyId::FunctionId(it) => FunctionSignature::of(db, it).is_unsafe(),
39        DefWithBodyId::StaticId(_) | DefWithBodyId::ConstId(_) | DefWithBodyId::VariantId(_) => {
40            false
41        }
42    };
43
44    let mut res = MissingUnsafeResult { fn_is_unsafe: is_unsafe, ..MissingUnsafeResult::default() };
45    let body = Body::of(db, def);
46    let infer = InferenceResult::of(db, def);
47    let mut callback = |diag| match diag {
48        UnsafeDiagnostic::UnsafeOperation { node, inside_unsafe_block, reason } => {
49            if inside_unsafe_block == InsideUnsafeBlock::No {
50                res.unsafe_exprs.push((node, reason));
51            }
52        }
53        UnsafeDiagnostic::DeprecatedSafe2024 { node, inside_unsafe_block } => {
54            if inside_unsafe_block == InsideUnsafeBlock::No {
55                res.deprecated_safe_calls.push(node)
56            }
57        }
58    };
59    let mut visitor = UnsafeVisitor::new(db, infer, body, def.into(), &mut callback);
60    visitor.walk_expr(body.root_expr());
61
62    if !is_unsafe {
63        // Unsafety in function parameter patterns (that can only be union destructuring)
64        // cannot be inserted into an unsafe block, so even with `unsafe_op_in_unsafe_fn`
65        // it is turned off for unsafe functions.
66        for param in &body.params {
67            visitor.walk_pat(param.formal);
68        }
69    }
70
71    res
72}
73
74#[derive(Debug, Clone, Copy)]
75pub enum UnsafetyReason {
76    UnionField,
77    UnsafeFnCall,
78    InlineAsm,
79    RawPtrDeref,
80    MutableStatic,
81    ExternStatic,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum InsideUnsafeBlock {
86    No,
87    Yes,
88}
89
90#[derive(Debug)]
91enum UnsafeDiagnostic {
92    UnsafeOperation {
93        node: ExprOrPatId,
94        inside_unsafe_block: InsideUnsafeBlock,
95        reason: UnsafetyReason,
96    },
97    /// A lint.
98    DeprecatedSafe2024 { node: ExprId, inside_unsafe_block: InsideUnsafeBlock },
99}
100
101pub fn unsafe_operations_for_body(
102    db: &dyn HirDatabase,
103    infer: &InferenceResult<'_>,
104    def: DefWithBodyId,
105    body: &Body,
106    callback: &mut dyn FnMut(ExprOrPatId),
107) {
108    let mut visitor_callback = |diag| {
109        if let UnsafeDiagnostic::UnsafeOperation { node, .. } = diag {
110            callback(node);
111        }
112    };
113    let mut visitor = UnsafeVisitor::new(db, infer, body, def.into(), &mut visitor_callback);
114    visitor.walk_expr(body.root_expr());
115    for param in &body.params {
116        visitor.walk_pat(param.formal);
117    }
118}
119
120pub fn unsafe_operations(
121    db: &dyn HirDatabase,
122    infer: &InferenceResult<'_>,
123    def: ExpressionStoreOwnerId,
124    body: &ExpressionStore,
125    current: ExprId,
126    callback: &mut dyn FnMut(ExprOrPatId, InsideUnsafeBlock),
127) {
128    let mut visitor_callback = |diag| {
129        if let UnsafeDiagnostic::UnsafeOperation { inside_unsafe_block, node, .. } = diag {
130            callback(node, inside_unsafe_block);
131        }
132    };
133    let mut visitor = UnsafeVisitor::new(db, infer, body, def, &mut visitor_callback);
134    _ = visitor.resolver.update_to_inner_scope(db, def, current);
135    visitor.walk_expr(current);
136}
137
138struct UnsafeVisitor<'db> {
139    db: &'db dyn HirDatabase,
140    infer: &'db InferenceResult<'db>,
141    body: &'db ExpressionStore,
142    resolver: Resolver<'db>,
143    def: ExpressionStoreOwnerId,
144    inside_unsafe_block: InsideUnsafeBlock,
145    inside_assignment: bool,
146    inside_union_destructure: bool,
147    callback: &'db mut dyn FnMut(UnsafeDiagnostic),
148    def_target_features: TargetFeatures<'db>,
149    // FIXME: This needs to be the edition of the span of each call.
150    edition: Edition,
151    /// On some targets (WASM), calling safe functions with `#[target_feature]` is always safe, even when
152    /// the target feature is not enabled. This flag encodes that.
153    target_feature_is_safe: TargetFeatureIsSafeInTarget,
154}
155
156impl<'db> UnsafeVisitor<'db> {
157    fn new(
158        db: &'db dyn HirDatabase,
159        infer: &'db InferenceResult<'db>,
160        body: &'db ExpressionStore,
161        def: ExpressionStoreOwnerId,
162        unsafe_expr_cb: &'db mut dyn FnMut(UnsafeDiagnostic),
163    ) -> Self {
164        let resolver = def.resolver(db);
165        let def_target_features = match def {
166            ExpressionStoreOwnerId::Body(DefWithBodyId::FunctionId(func))
167            | ExpressionStoreOwnerId::Signature(GenericDefId::FunctionId(func)) => {
168                TargetFeatures::from_fn(db, func)
169            }
170            _ => TargetFeatures::default(),
171        };
172        let krate = resolver.krate();
173        let edition = krate.data(db).edition;
174        let target_feature_is_safe = match &krate.workspace_data(db).target {
175            Ok(target) => target_feature_is_safe_in_target(target),
176            Err(_) => TargetFeatureIsSafeInTarget::No,
177        };
178        Self {
179            db,
180            infer,
181            body,
182            resolver,
183            def,
184            inside_unsafe_block: InsideUnsafeBlock::No,
185            inside_assignment: false,
186            inside_union_destructure: false,
187            callback: unsafe_expr_cb,
188            def_target_features,
189            edition,
190            target_feature_is_safe,
191        }
192    }
193
194    fn on_unsafe_op(&mut self, node: ExprOrPatId, reason: UnsafetyReason) {
195        (self.callback)(UnsafeDiagnostic::UnsafeOperation {
196            node,
197            inside_unsafe_block: self.inside_unsafe_block,
198            reason,
199        });
200    }
201
202    fn check_call(&mut self, node: ExprId, func: FunctionId) {
203        let unsafety = is_fn_unsafe_to_call(
204            self.db,
205            func,
206            &self.def_target_features,
207            self.edition,
208            self.target_feature_is_safe,
209        );
210        match unsafety {
211            crate::utils::Unsafety::Safe => {}
212            crate::utils::Unsafety::Unsafe => {
213                self.on_unsafe_op(node.into(), UnsafetyReason::UnsafeFnCall)
214            }
215            crate::utils::Unsafety::DeprecatedSafe2024 => {
216                (self.callback)(UnsafeDiagnostic::DeprecatedSafe2024 {
217                    node,
218                    inside_unsafe_block: self.inside_unsafe_block,
219                })
220            }
221        }
222    }
223
224    fn with_inside_unsafe_block<R>(
225        &mut self,
226        inside_unsafe_block: InsideUnsafeBlock,
227        f: impl FnOnce(&mut Self) -> R,
228    ) -> R {
229        let old = mem::replace(&mut self.inside_unsafe_block, inside_unsafe_block);
230        let result = f(self);
231        self.inside_unsafe_block = old;
232        result
233    }
234
235    fn walk_pats_top(&mut self, pats: impl Iterator<Item = PatId>, parent_expr: ExprId) {
236        let guard = self.resolver.update_to_inner_scope(self.db, self.def, parent_expr);
237        pats.for_each(|pat| self.walk_pat(pat));
238        self.resolver.reset_to_guard(guard);
239    }
240
241    fn walk_pat(&mut self, current: PatId) {
242        let pat = &self.body[current];
243
244        if self.inside_union_destructure {
245            match pat {
246                Pat::Tuple { .. }
247                | Pat::Record { .. }
248                | Pat::Range { .. }
249                | Pat::Slice { .. }
250                | Pat::Path(..)
251                | Pat::Lit(..)
252                | Pat::Bind { .. }
253                | Pat::TupleStruct { .. }
254                | Pat::Ref { .. }
255                | Pat::Box { .. }
256                | Pat::Deref { .. }
257                | Pat::Expr(..)
258                | Pat::ConstBlock(..)
259                | Pat::NotNull => self.on_unsafe_op(current.into(), UnsafetyReason::UnionField),
260                // `Or` only wraps other patterns, and `Missing`/`Wild` do not constitute a read.
261                Pat::Missing | Pat::Rest | Pat::Wild | Pat::Or(_) => {}
262            }
263        }
264
265        match pat {
266            Pat::Record { .. } => {
267                if let Some((AdtId::UnionId(_), _)) = self.infer.pat_ty(current).as_adt() {
268                    let old_inside_union_destructure =
269                        mem::replace(&mut self.inside_union_destructure, true);
270                    self.body.walk_pats_shallow(current, |pat| self.walk_pat(pat));
271                    self.inside_union_destructure = old_inside_union_destructure;
272                    return;
273                }
274            }
275            Pat::Path(path) => self.mark_unsafe_path(current.into(), path),
276            &Pat::ConstBlock(expr) => {
277                let old_inside_assignment = mem::replace(&mut self.inside_assignment, false);
278                self.walk_expr(expr);
279                self.inside_assignment = old_inside_assignment;
280            }
281            &Pat::Expr(expr) => self.walk_expr(expr),
282            _ => {}
283        }
284
285        self.body.walk_pats_shallow(current, |pat| self.walk_pat(pat));
286    }
287
288    fn walk_expr(&mut self, current: ExprId) {
289        let expr = &self.body[current];
290        let inside_assignment = mem::replace(&mut self.inside_assignment, false);
291        match expr {
292            &Expr::Call { callee, .. } => {
293                let callee = self.infer.expr_ty(callee);
294                if let TyKind::FnDef(CallableIdWrapper(CallableDefId::FunctionId(func)), _) =
295                    callee.kind()
296                {
297                    self.check_call(current, func);
298                }
299                if let TyKind::FnPtr(_, hdr) = callee.kind()
300                    && hdr.safety() == Safety::Unsafe
301                {
302                    self.on_unsafe_op(current.into(), UnsafetyReason::UnsafeFnCall);
303                }
304            }
305            Expr::Path(path) => {
306                let guard = self.resolver.update_to_inner_scope(self.db, self.def, current);
307                self.mark_unsafe_path(current.into(), path);
308                self.resolver.reset_to_guard(guard);
309            }
310            Expr::Ref { expr, rawness: Rawness::RawPtr, mutability: _ } => {
311                match self.body[*expr] {
312                    // Do not report unsafe for `addr_of[_mut]!(EXTERN_OR_MUT_STATIC)`,
313                    // see https://github.com/rust-lang/rust/pull/125834.
314                    Expr::Path(_) => return,
315                    // https://github.com/rust-lang/rust/pull/129248
316                    // Taking a raw ref to a deref place expr is always safe.
317                    Expr::UnaryOp { expr, op: UnaryOp::Deref } => {
318                        self.body
319                            .walk_child_exprs_without_pats(expr, |child| self.walk_expr(child));
320
321                        return;
322                    }
323                    _ => (),
324                }
325
326                let mut peeled = *expr;
327                while let Expr::Field { expr: lhs, .. } = &self.body[peeled] {
328                    if let Some(Either::Left(FieldId { parent: VariantId::UnionId(_), .. })) =
329                        self.infer.field_resolution(peeled)
330                    {
331                        peeled = *lhs;
332                    } else {
333                        break;
334                    }
335                }
336
337                // Walk the peeled expression (the LHS of the union field chain)
338                self.walk_expr(peeled);
339                // Return so we don't recurse directly onto the union field access(es)
340                return;
341            }
342            Expr::MethodCall { .. } => {
343                if let Some((func, _)) = self.infer.method_resolution(current) {
344                    self.check_call(current, func);
345                }
346            }
347            Expr::UnaryOp { expr, op: UnaryOp::Deref } => {
348                if let TyKind::RawPtr(..) = self.infer.expr_ty(*expr).kind() {
349                    self.on_unsafe_op(current.into(), UnsafetyReason::RawPtrDeref);
350                }
351            }
352            &Expr::Assignment { target, value: _ } => {
353                let old_inside_assignment = mem::replace(&mut self.inside_assignment, true);
354                self.walk_pats_top(std::iter::once(target), current);
355                self.inside_assignment = old_inside_assignment;
356            }
357            Expr::InlineAsm(asm) => {
358                if asm.kind == InlineAsmKind::Asm {
359                    // `naked_asm!()` requires `unsafe` on the attribute (`#[unsafe(naked)]`),
360                    // and `global_asm!()` doesn't require it at all.
361                    self.on_unsafe_op(current.into(), UnsafetyReason::InlineAsm);
362                }
363
364                asm.operands.iter().for_each(|(_, op)| match op {
365                    AsmOperand::In { expr, .. }
366                    | AsmOperand::Out { expr: Some(expr), .. }
367                    | AsmOperand::InOut { expr, .. }
368                    | AsmOperand::Const(expr) => self.walk_expr(*expr),
369                    AsmOperand::SplitInOut { in_expr, out_expr, .. } => {
370                        self.walk_expr(*in_expr);
371                        if let Some(out_expr) = out_expr {
372                            self.walk_expr(*out_expr);
373                        }
374                    }
375                    AsmOperand::Out { expr: None, .. } | AsmOperand::Sym(_) => (),
376                    AsmOperand::Label(expr) => {
377                        // Inline asm labels are considered safe even when inside unsafe blocks.
378                        self.with_inside_unsafe_block(InsideUnsafeBlock::No, |this| {
379                            this.walk_expr(*expr)
380                        });
381                    }
382                });
383                return;
384            }
385            // rustc allows union assignment to propagate through field accesses and casts.
386            Expr::Cast { .. } => self.inside_assignment = inside_assignment,
387            Expr::Field { .. } => {
388                self.inside_assignment = inside_assignment;
389                if !inside_assignment
390                    && let Some(Either::Left(FieldId { parent: VariantId::UnionId(_), .. })) =
391                        self.infer.field_resolution(current)
392                {
393                    self.on_unsafe_op(current.into(), UnsafetyReason::UnionField);
394                }
395            }
396            Expr::Unsafe { statements, .. } => {
397                self.with_inside_unsafe_block(InsideUnsafeBlock::Yes, |this| {
398                    this.walk_pats_top(
399                        statements.iter().filter_map(|statement| match statement {
400                            &Statement::Let { pat, .. } => Some(pat),
401                            _ => None,
402                        }),
403                        current,
404                    );
405                    this.body.walk_child_exprs_without_pats(current, |child| this.walk_expr(child));
406                });
407                return;
408            }
409            Expr::Block { statements, .. } => {
410                self.walk_pats_top(
411                    statements.iter().filter_map(|statement| match statement {
412                        &Statement::Let { pat, .. } => Some(pat),
413                        _ => None,
414                    }),
415                    current,
416                );
417            }
418            Expr::Match { arms, .. } => {
419                self.walk_pats_top(arms.iter().map(|arm| arm.pat), current);
420            }
421            &Expr::Let { pat, .. } => {
422                self.walk_pats_top(std::iter::once(pat), current);
423            }
424            Expr::Closure { args, .. } => {
425                self.walk_pats_top(args.iter().copied(), current);
426            }
427            _ => {}
428        }
429
430        self.body.walk_child_exprs_without_pats(current, |child| self.walk_expr(child));
431    }
432
433    fn mark_unsafe_path(&mut self, node: ExprOrPatId, path: &Path) {
434        let hygiene = self.body.expr_or_pat_path_hygiene(node);
435        let value_or_partial = self.resolver.resolve_path_in_value_ns(self.db, path, hygiene);
436        if let Some(ResolveValueResult::ValueNs(ValueNs::StaticId(id))) = value_or_partial {
437            let static_data = StaticSignature::of(self.db, id);
438            if static_data.flags.contains(StaticFlags::MUTABLE) {
439                self.on_unsafe_op(node, UnsafetyReason::MutableStatic);
440            } else if static_data.flags.contains(StaticFlags::EXTERN)
441                && !static_data.flags.contains(StaticFlags::EXPLICIT_SAFE)
442            {
443                self.on_unsafe_op(node, UnsafetyReason::ExternStatic);
444            }
445        }
446    }
447}