Skip to main content

wowlab_tidy/languages/rust/rules/safety/
unsafe_without_ub_surface.rs

1use ra_ap_syntax::{
2    AstNode,
3    ast::{self, HasAttrs, HasName, UnaryOp},
4};
5
6use super::super::support::is_item_or_impl_fn;
7use crate::{AstCtx, Example, Violation};
8
9#[rustfmt::skip]
10const EXAMPLES: &[Example] = &[
11    Example {
12        label: "unsafe as danger marker",
13        code: "unsafe fn delete_everything(name: &str) { let _ = name; }",
14        pass: false,
15    },
16    Example {
17        label: "unsafe method without UB surface",
18        code: "struct S;\nimpl S {\n    pub unsafe fn clear(&mut self) {}\n}",
19        pass: false,
20    },
21    Example {
22        label: "raw pointer parameter",
23        code: "unsafe fn read_at(p: *const u8) -> u8 { *p }",
24        pass: true,
25    },
26    Example {
27        label: "NonNull parameter",
28        code: "unsafe fn touch(p: std::ptr::NonNull<u8>) { let _ = p; }",
29        pass: true,
30    },
31    Example {
32        label: "raw pointer return type",
33        code: "unsafe fn alloc_raw() -> *mut u8 { std::ptr::null_mut() }",
34        pass: true,
35    },
36    Example {
37        label: "unsafe block in body",
38        code: "unsafe fn call() { unsafe { std::ptr::null::<u8>().read(); } }",
39        pass: true,
40    },
41    Example {
42        label: "extern abi is exempt",
43        code: "unsafe extern \"C\" fn callback() {}",
44        pass: true,
45    },
46    Example {
47        label: "no_mangle is exempt",
48        code: "#[no_mangle]\npub unsafe fn hook() {}",
49        pass: true,
50    },
51    Example {
52        label: "safe fn",
53        code: "fn f(x: u32) -> u32 { x }",
54        pass: true,
55    },
56];
57
58crate::ast_rule!(
59    unsafe_without_ub_surface,
60    "Flag `unsafe fn` with no raw-pointer surface and no unsafe operations in the body.",
61    "`unsafe` may only mark undefined-behavior risk, not general danger — a fn without UB surface trains callers to ignore the keyword (M-UNSAFE-IMPLIES-UB).",
62    Low,
63);
64
65fn check_unsafe_without_ub_surface(ctx: &AstCtx<'_>) -> Vec<Violation> {
66    let unsafe_functions = ctx
67        .nodes::<ast::Fn>()
68        .filter(is_item_or_impl_fn)
69        .filter(|function| {
70            !ctx.is_in_test(function)
71                && function.unsafe_token().is_some()
72                && function.abi().is_none()
73                && !is_no_mangle(function)
74        });
75
76    unsafe_functions
77        .filter(|function| {
78            !signature_has_ub_surface(function)
79                && function
80                    .body()
81                    .is_none_or(|body| !body_has_ub_marker(&body))
82        })
83        .filter_map(|function| {
84            let name = function.name()?;
85
86            Some(ctx.violation(
87                &name,
88                format!(
89                    "unsafe fn `{}` has no UB surface (no raw pointers, no unsafe operations) — \
90                     `unsafe` marks UB risk, not general danger (M-UNSAFE-IMPLIES-UB)",
91                    name.text()
92                ),
93            ))
94        })
95        .collect()
96}
97
98fn is_no_mangle(function: &ast::Fn) -> bool {
99    function
100        .attrs()
101        .any(|attr| attr.syntax().text().to_string().contains("no_mangle"))
102}
103
104fn signature_has_ub_surface(function: &ast::Fn) -> bool {
105    let parameter_surface = function.param_list().is_some_and(|parameters| {
106        parameters
107            .self_param()
108            .and_then(|parameter| parameter.ty())
109            .is_some_and(|ty| type_has_ub_surface(&ty))
110            || parameters
111                .params()
112                .filter_map(|parameter| parameter.ty())
113                .any(|ty| type_has_ub_surface(&ty))
114    });
115
116    parameter_surface
117        || function
118            .ret_type()
119            .and_then(|ret| ret.ty())
120            .is_some_and(|ty| type_has_ub_surface(&ty))
121}
122
123fn type_has_ub_surface(ty: &ast::Type) -> bool {
124    let rendered = ty.syntax().text().to_string();
125
126    rendered.contains('*') || rendered.contains("NonNull")
127}
128
129fn body_has_ub_marker(block: &ast::BlockExpr) -> bool {
130    block
131        .syntax()
132        .descendants()
133        .filter_map(ast::PrefixExpr::cast)
134        .any(|expr| matches!(expr.op_kind(), Some(UnaryOp::Deref)))
135        || block
136            .syntax()
137            .descendants()
138            .filter_map(ast::BlockExpr::cast)
139            .any(|block| block.unsafe_token().is_some())
140}
141
142crate::tidy_ast_test!(check_unsafe_without_ub_surface, {
143    crate::example_tests!(EXAMPLES, check_unsafe_without_ub_surface);
144});