1use super::UnprotectedInitializer;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::{is_builtin, runtime_entry_points},
7 },
8};
9use alloy_primitives::map::HashSet;
10use solar::{
11 ast::{ContractKind, DataLocation},
12 interface::sym,
13 sema::{
14 Gcx,
15 builtins::Builtin,
16 hir::{self, ContractId, Expr, ExprKind, FunctionId, Visit},
17 ty::TyKind,
18 },
19};
20use std::ops::ControlFlow;
21
22declare_forge_lint!(
23 UNPROTECTED_INITIALIZER,
24 Severity::High,
25 "unprotected-initializer",
26 "upgradeable initializer is not protected against direct implementation calls"
27);
28
29impl<'gcx> LateLintPass<'gcx> for UnprotectedInitializer {
30 fn check_nested_contract(
31 &mut self,
32 ctx: &LintContext,
33 gcx: Gcx<'gcx>,
34 contract_id: ContractId,
35 ) {
36 let contract = gcx.hir.contract(contract_id);
37 if contract.kind != ContractKind::Contract || contract.linearization_failed() {
38 return;
39 }
40 let bases = contract.linearized_bases;
41
42 let entries = runtime_entry_points(gcx, contract_id);
45
46 let upgradeable = bases
47 .iter()
48 .any(|&cid| gcx.hir.contract(cid).name.as_str() == "Initializable")
49 || entries.iter().any(|&fid| has_initializer_modifier(&gcx.hir, gcx.hir.function(fid)));
50 if !upgradeable {
51 return;
52 }
53 let locked = bases.iter().filter_map(|&cid| gcx.hir.contract(cid).ctor).any(|ctor| {
54 reaches(gcx, bases, ctor, |expr| {
55 let ExprKind::Call(callee, ..) = &expr.kind else { return false };
56 if !gcx.type_of_expr(callee.peel_parens().id).is_some_and(
57 |ty| matches!(ty.kind, TyKind::Fn(function) if function.is_internal()),
58 ) {
59 return false;
60 }
61 gcx.resolved_function(callee).is_some_and(|fid| {
62 let func = gcx.hir.function(fid);
63 func.contract.is_some_and(|cid| bases.contains(&cid))
64 && func.name.is_some_and(|name| name.as_str() == "_disableInitializers")
65 })
66 })
67 });
68 if locked {
69 return;
70 }
71 let destructive = entries.iter().any(|&fid| {
72 !has_modifier_named(&gcx.hir, gcx.hir.function(fid), "onlyProxy")
73 && reaches(gcx, bases, fid, |expr| is_destructive_call(gcx, expr))
74 });
75 if !destructive {
76 return;
77 }
78
79 for fid in entries {
80 let func = gcx.hir.function(fid);
81 if func.is_part_of_external_interface()
82 && func.mutates_state()
83 && has_initializer_modifier(&gcx.hir, func)
84 && !has_modifier_named(&gcx.hir, func, "onlyProxy")
85 && reaches(gcx, bases, fid, |expr| writes_state(gcx, expr))
86 {
87 ctx.emit(&UNPROTECTED_INITIALIZER, func.name.map_or(func.span, |name| name.span));
88 }
89 }
90 }
91}
92
93fn has_initializer_modifier(hir: &hir::Hir<'_>, func: &hir::Function<'_>) -> bool {
94 has_modifier_named(hir, func, "initializer") || has_modifier_named(hir, func, "reinitializer")
95}
96
97fn has_modifier_named(hir: &hir::Hir<'_>, func: &hir::Function<'_>, name: &str) -> bool {
98 func.modifiers.iter().any(|modifier| {
99 modifier
100 .id
101 .as_function()
102 .is_some_and(|fid| hir.function(fid).name.is_some_and(|ident| ident.as_str() == name))
103 })
104}
105
106fn reaches<'gcx>(
109 gcx: Gcx<'gcx>,
110 bases: &'gcx [ContractId],
111 fid: FunctionId,
112 hit: impl FnMut(&'gcx Expr<'gcx>) -> bool,
113) -> bool {
114 Reach { gcx, bases, defining_contract: None, visited: HashSet::default(), hit }
115 .visit_function_body(fid)
116 .is_break()
117}
118
119struct Reach<'gcx, F> {
120 gcx: Gcx<'gcx>,
121 bases: &'gcx [ContractId],
122 defining_contract: Option<ContractId>,
123 visited: HashSet<FunctionId>,
125 hit: F,
126}
127
128impl<'gcx, F: FnMut(&'gcx Expr<'gcx>) -> bool> Reach<'gcx, F> {
129 fn visit_function_body(&mut self, fid: FunctionId) -> ControlFlow<()> {
130 if !self.visited.insert(fid) {
131 return ControlFlow::Continue(());
132 }
133 let Some(body) = self.gcx.hir.function(fid).body else {
134 return ControlFlow::Continue(());
135 };
136 let previous = self.defining_contract;
137 self.defining_contract = self.gcx.hir.function(fid).contract;
138 let result = body.stmts.iter().try_for_each(|stmt| self.visit_stmt(stmt));
139 self.defining_contract = previous;
140 result
141 }
142}
143
144impl<'gcx, F: FnMut(&'gcx Expr<'gcx>) -> bool> Visit<'gcx> for Reach<'gcx, F> {
145 type BreakValue = ();
146
147 fn hir(&self) -> &'gcx hir::Hir<'gcx> {
148 &self.gcx.hir
149 }
150
151 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<()> {
152 if (self.hit)(expr) {
153 return ControlFlow::Break(());
154 }
155 if let ExprKind::Call(callee, ..) = &expr.kind
156 && let Some(fid) =
157 internal_callee(self.gcx, callee, self.bases[0], self.defining_contract)
158 {
159 self.visit_function_body(fid)?;
160 }
161 self.walk_expr(expr)
162 }
163}
164
165fn internal_callee(
167 gcx: Gcx<'_>,
168 callee: &Expr<'_>,
169 contract: ContractId,
170 defining_contract: Option<ContractId>,
171) -> Option<FunctionId> {
172 let callee = callee.peel_parens();
173 let fid = gcx.resolved_function(callee)?;
174 let TyKind::Fn(function) = gcx.type_of_expr(callee.id)?.kind else { return None };
175 if !function.is_internal() {
176 return None;
177 }
178 Some(match &callee.kind {
179 ExprKind::Ident(_) => gcx.resolve_virtual_function(contract, fid),
180 ExprKind::Member(base, _) if is_builtin(gcx, base, sym::super_) => {
181 gcx.resolve_super_function(contract, defining_contract?, fid)
182 }
183 _ => fid,
184 })
185}
186
187fn is_destructive_call(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
189 let ExprKind::Call(callee, ..) = &expr.kind else { return false };
190 matches!(
191 gcx.resolved_builtin(callee),
192 Some(Builtin::AddressDelegatecall | Builtin::Selfdestruct)
193 )
194}
195
196fn writes_state(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
198 match &expr.kind {
199 ExprKind::Assign(lhs, ..) | ExprKind::Delete(lhs) => lhs_writes_state(gcx, lhs),
200 ExprKind::Unary(op, lhs) => op.kind.has_side_effects() && lhs_writes_state(gcx, lhs),
201 ExprKind::Call(callee, ..) => {
202 matches!(&callee.peel_parens().kind, ExprKind::Member(base, member)
203 if matches!(member.as_str(), "push" | "pop") && references_storage(gcx, base))
204 }
205 _ => false,
206 }
207}
208
209fn lhs_writes_state(gcx: Gcx<'_>, lhs: &Expr<'_>) -> bool {
211 match &lhs.peel_parens().kind {
212 ExprKind::Ident(_) => {
213 gcx.resolved_variable(lhs).is_some_and(|v| gcx.hir.variable(v).kind.is_state())
214 }
215 ExprKind::Index(base, _) | ExprKind::Slice(base, ..) | ExprKind::Member(base, _) => {
216 references_storage(gcx, base)
217 }
218 ExprKind::Tuple(elems) => elems.iter().flatten().any(|elem| lhs_writes_state(gcx, elem)),
219 _ => false,
220 }
221}
222
223fn references_storage(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
224 match &expr.peel_parens().kind {
225 ExprKind::Ident(_) => gcx.resolved_variable(expr).is_some_and(|v| {
226 let var = gcx.hir.variable(v);
227 var.kind.is_state() || var.data_location == Some(DataLocation::Storage)
228 }),
229 ExprKind::Index(base, _) | ExprKind::Slice(base, ..) | ExprKind::Member(base, _) => {
230 references_storage(gcx, base)
231 }
232 _ => gcx
233 .type_of_expr(expr.peel_parens().id)
234 .is_some_and(|ty| ty.loc() == Some(DataLocation::Storage)),
235 }
236}