wowlab_tidy/languages/rust/rules/safety/
unsafe_without_ub_surface.rs1use 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});