wowlab_tidy/languages/rust/rules/safety/
unsafe_impl_send.rs1use crate::{
2 Example, FileCtx, Violation,
3 infra::{helpers, parse, scanner},
4 violation,
5};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9 Example {
10 label: "unsafe impl Send without safety comment",
11 code: "struct Foo(*mut u8);\nunsafe impl Send for Foo {}",
12 pass: false,
13 },
14 Example {
15 label: "unsafe impl Sync without safety comment",
16 code: "struct Foo(*mut u8);\nunsafe impl Sync for Foo {}",
17 pass: false,
18 },
19 Example {
20 label: "unsafe impl Send with safety comment",
21 code: "struct Foo(*mut u8);\n// SAFETY: the pointer is owned and never shared across threads\nunsafe impl Send for Foo {}",
22 pass: true,
23 },
24 Example {
25 label: "generic blanket impl flagged even with safety comment",
26 code: "struct Wrapper<T>(T);\n// SAFETY: trust me\nunsafe impl<T> Send for Wrapper<T> {}",
27 pass: false,
28 },
29 Example {
30 label: "generic blanket Sync with bounds",
31 code: "struct Wrapper<T>(T);\nunsafe impl<T: Copy> Sync for Wrapper<T> {}",
32 pass: false,
33 },
34 Example {
35 label: "other unsafe trait impl",
36 code: "struct Chunk([u8; 8]);\n// SAFETY: all-zero bit pattern is valid\nunsafe impl Zeroable for Chunk {}",
37 pass: true,
38 },
39 Example {
40 label: "mention in string literal",
41 code: r#"let s = "unsafe impl Send for Foo";"#,
42 pass: true,
43 },
44 Example {
45 label: "mention in comment",
46 code: "// unsafe impl Send for Foo",
47 pass: true,
48 },
49];
50
51crate::line_rule!(
52 unsafe_impl_send,
53 "Flag `unsafe impl Send`/`Sync` without a `// SAFETY:` comment, and any generic (blanket) form.",
54 "Bypassing Send/Sync bounds is the canonical unsoundness footgun, and a blanket impl over all T cannot be proven safe (M-UNSAFE, M-UNSOUND).",
55 High,
56);
57
58const GENERIC_MSG: &str = "blanket `unsafe impl<..> Send/Sync` over a generic type is almost \
59 always unsound — implement for concrete types instead (M-UNSOUND)";
60const MISSING_COMMENT_MSG: &str =
61 "`unsafe impl Send/Sync` without a `// SAFETY:` comment on a preceding line (M-UNSAFE)";
62
63fn check_unsafe_impl_send(ctx: &FileCtx<'_>) -> Vec<Violation> {
64 let mut out = Vec::new();
65
66 for (index, line) in ctx.lines.iter().enumerate() {
67 if parse::is_comment(line.trim()) {
68 continue;
69 }
70
71 let code = scanner::code_only(line);
72 let Some(generic) = find_unsafe_send_impl(&code) else {
73 continue;
74 };
75 let lineno = index + 1;
76
77 if generic {
78 out.push(violation(ctx.rel, lineno, GENERIC_MSG));
79 } else if !helpers::has_preceding_comment(ctx.lines, lineno, &["SAFETY:"]) {
80 out.push(violation(ctx.rel, lineno, MISSING_COMMENT_MSG));
81 }
82 }
83
84 out
85}
86
87fn find_unsafe_send_impl(code: &str) -> Option<bool> {
88 let (_, after) = code.split_once("unsafe impl")?;
89 let mut rest = after.trim_start();
90 let generic = rest.starts_with('<');
91
92 if generic {
93 rest = skip_generics(rest)?;
94 }
95
96 let trait_name = ["Send", "Sync"].into_iter().find(|t| rest.starts_with(t))?;
97 let tail = rest
98 .strip_prefix(trait_name)?
99 .trim_start()
100 .strip_prefix("for")?;
101
102 (tail.is_empty() || tail.starts_with(char::is_whitespace)).then_some(generic)
103}
104
105fn skip_generics(s: &str) -> Option<&str> {
106 let mut depth: usize = 0;
107
108 for (i, ch) in s.char_indices() {
109 match ch {
110 '<' => depth += 1,
111 '>' => {
112 depth = depth.checked_sub(1)?;
113
114 if depth == 0 {
115 return s.get(i + 1..).map(str::trim_start);
116 }
117 }
118 _ => {}
119 }
120 }
121
122 None
123}
124
125crate::tidy_test!(check_unsafe_impl_send, {
126 crate::example_tests!(EXAMPLES, check_unsafe_impl_send);
127});