Skip to main content

forge_lint/sol/high/
unprotected_initializer.rs

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        // The effective runtime dispatch surface: most-derived overrides plus the inherited
43        // fallback/receive functions.
44        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
106/// True if `hit` matches an expression in `fid`'s body or, transitively, in the body of any
107/// internal function it calls.
108fn 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    // The predicate and dispatch context are fixed for the entire reachability check.
124    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
165/// The selected internal function in the analyzed contract's dispatch context.
166fn 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
187/// `x.delegatecall(..)` or `selfdestruct(..)`.
188fn 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
196/// An assignment, `delete`, `++`/`--` or `push`/`pop` whose target lives in contract storage.
197fn 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
209/// A state variable, or a member/index of an expression that denotes contract storage.
210fn 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}