1 | //===-- SetgidSetuidOrderChecker.cpp - check privilege revocation calls ---===// |
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | // |
9 | // This file defines a checker to detect possible reversed order of privilege |
10 | // revocations when 'setgid' and 'setuid' is used. |
11 | // |
12 | //===----------------------------------------------------------------------===// |
13 | |
14 | #include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h" |
15 | #include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" |
16 | #include "clang/StaticAnalyzer/Core/Checker.h" |
17 | #include "clang/StaticAnalyzer/Core/CheckerManager.h" |
18 | #include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h" |
19 | #include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h" |
20 | #include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" |
21 | #include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h" |
22 | #include "clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h" |
23 | |
24 | using namespace clang; |
25 | using namespace ento; |
26 | |
27 | namespace { |
28 | |
29 | enum SetPrivilegeFunctionKind { Irrelevant, Setuid, Setgid }; |
30 | |
31 | class SetgidSetuidOrderChecker : public Checker<check::PostCall, eval::Assume> { |
32 | const BugType BT{this, "Possible wrong order of privilege revocation" }; |
33 | |
34 | const CallDescription SetuidDesc{CDM::CLibrary, {"setuid" }, 1}; |
35 | const CallDescription SetgidDesc{CDM::CLibrary, {"setgid" }, 1}; |
36 | |
37 | const CallDescription GetuidDesc{CDM::CLibrary, {"getuid" }, 0}; |
38 | const CallDescription GetgidDesc{CDM::CLibrary, {"getgid" }, 0}; |
39 | |
40 | const CallDescriptionSet OtherSetPrivilegeDesc{ |
41 | {CDM::CLibrary, {"seteuid" }, 1}, {CDM::CLibrary, {"setegid" }, 1}, |
42 | {CDM::CLibrary, {"setreuid" }, 2}, {CDM::CLibrary, {"setregid" }, 2}, |
43 | {CDM::CLibrary, {"setresuid" }, 3}, {CDM::CLibrary, {"setresgid" }, 3}}; |
44 | |
45 | public: |
46 | void checkPostCall(const CallEvent &Call, CheckerContext &C) const; |
47 | ProgramStateRef evalAssume(ProgramStateRef State, SVal Cond, |
48 | bool Assumption) const; |
49 | |
50 | private: |
51 | void processSetuid(ProgramStateRef State, const CallEvent &Call, |
52 | CheckerContext &C) const; |
53 | void processSetgid(ProgramStateRef State, const CallEvent &Call, |
54 | CheckerContext &C) const; |
55 | void processOther(ProgramStateRef State, const CallEvent &Call, |
56 | CheckerContext &C) const; |
57 | /// Check if a function like \c getuid or \c getgid is called directly from |
58 | /// the first argument of function called from \a Call. |
59 | bool isFunctionCalledInArg(const CallDescription &Desc, |
60 | const CallEvent &Call) const; |
61 | void emitReport(ProgramStateRef State, CheckerContext &C) const; |
62 | }; |
63 | |
64 | } // end anonymous namespace |
65 | |
66 | /// Store if there was a call to 'setuid(getuid())' or 'setgid(getgid())' not |
67 | /// followed by other different privilege-change functions. |
68 | /// If the value \c Setuid is stored and a 'setgid(getgid())' call is found we |
69 | /// have found the bug to be reported. Value \c Setgid is used too to prevent |
70 | /// warnings at a setgid-setuid-setgid sequence. |
71 | REGISTER_TRAIT_WITH_PROGRAMSTATE(LastSetPrivilegeCall, SetPrivilegeFunctionKind) |
72 | /// Store the symbol value of the last 'setuid(getuid())' call. This is used to |
73 | /// detect if the result is compared to -1 and avoid warnings on that branch |
74 | /// (which is the failure branch of the call), and for identification of note |
75 | /// tags. |
76 | REGISTER_TRAIT_WITH_PROGRAMSTATE(LastSetuidCallSVal, SymbolRef) |
77 | |
78 | void SetgidSetuidOrderChecker::checkPostCall(const CallEvent &Call, |
79 | CheckerContext &C) const { |
80 | ProgramStateRef State = C.getState(); |
81 | if (SetuidDesc.matches(Call)) { |
82 | processSetuid(State, Call, C); |
83 | } else if (SetgidDesc.matches(Call)) { |
84 | processSetgid(State, Call, C); |
85 | } else if (OtherSetPrivilegeDesc.contains(Call)) { |
86 | processOther(State, Call, C); |
87 | } |
88 | } |
89 | |
90 | ProgramStateRef SetgidSetuidOrderChecker::evalAssume(ProgramStateRef State, |
91 | SVal Cond, |
92 | bool Assumption) const { |
93 | SValBuilder &SVB = State->getStateManager().getSValBuilder(); |
94 | SymbolRef LastSetuidSym = State->get<LastSetuidCallSVal>(); |
95 | if (!LastSetuidSym) |
96 | return State; |
97 | |
98 | // Check if the most recent call to 'setuid(getuid())' is assumed to be != 0. |
99 | // It should be only -1 at failure, but we want to accept a "!= 0" check too. |
100 | // (But now an invalid failure check like "!= 1" will be recognized as correct |
101 | // too. The "invalid failure check" is a different bug that is not the scope |
102 | // of this checker.) |
103 | auto FailComparison = |
104 | SVB.evalBinOpNN(state: State, op: BO_NE, lhs: nonloc::SymbolVal(LastSetuidSym), |
105 | rhs: SVB.makeIntVal(integer: 0, /*isUnsigned=*/false), |
106 | resultTy: SVB.getConditionType()) |
107 | .getAs<DefinedOrUnknownSVal>(); |
108 | if (!FailComparison) |
109 | return State; |
110 | if (auto IsFailBranch = State->assume(Cond: *FailComparison); |
111 | IsFailBranch.first && !IsFailBranch.second) { |
112 | // This is the 'setuid(getuid())' != 0 case. |
113 | // On this branch we do not want to emit warning. |
114 | State = State->set<LastSetPrivilegeCall>(Irrelevant); |
115 | State = State->set<LastSetuidCallSVal>(SymbolRef{}); |
116 | } |
117 | return State; |
118 | } |
119 | |
120 | void SetgidSetuidOrderChecker::processSetuid(ProgramStateRef State, |
121 | const CallEvent &Call, |
122 | CheckerContext &C) const { |
123 | bool IsSetuidWithGetuid = isFunctionCalledInArg(Desc: GetuidDesc, Call); |
124 | if (State->get<LastSetPrivilegeCall>() != Setgid && IsSetuidWithGetuid) { |
125 | SymbolRef RetSym = Call.getReturnValue().getAsSymbol(); |
126 | State = State->set<LastSetPrivilegeCall>(Setuid); |
127 | State = State->set<LastSetuidCallSVal>(RetSym); |
128 | const NoteTag *Note = C.getNoteTag(Cb: [this, |
129 | RetSym](PathSensitiveBugReport &BR) { |
130 | if (!BR.isInteresting(sym: RetSym) || &BR.getBugType() != &this->BT) |
131 | return "" ; |
132 | return "Call to 'setuid' found here that removes superuser privileges" ; |
133 | }); |
134 | C.addTransition(State, Tag: Note); |
135 | return; |
136 | } |
137 | State = State->set<LastSetPrivilegeCall>(Irrelevant); |
138 | State = State->set<LastSetuidCallSVal>(SymbolRef{}); |
139 | C.addTransition(State); |
140 | } |
141 | |
142 | void SetgidSetuidOrderChecker::processSetgid(ProgramStateRef State, |
143 | const CallEvent &Call, |
144 | CheckerContext &C) const { |
145 | bool IsSetgidWithGetgid = isFunctionCalledInArg(Desc: GetgidDesc, Call); |
146 | if (State->get<LastSetPrivilegeCall>() == Setuid) { |
147 | if (IsSetgidWithGetgid) { |
148 | State = State->set<LastSetPrivilegeCall>(Irrelevant); |
149 | emitReport(State, C); |
150 | return; |
151 | } |
152 | State = State->set<LastSetPrivilegeCall>(Irrelevant); |
153 | } else { |
154 | State = State->set<LastSetPrivilegeCall>(IsSetgidWithGetgid ? Setgid |
155 | : Irrelevant); |
156 | } |
157 | State = State->set<LastSetuidCallSVal>(SymbolRef{}); |
158 | C.addTransition(State); |
159 | } |
160 | |
161 | void SetgidSetuidOrderChecker::processOther(ProgramStateRef State, |
162 | const CallEvent &Call, |
163 | CheckerContext &C) const { |
164 | State = State->set<LastSetuidCallSVal>(SymbolRef{}); |
165 | State = State->set<LastSetPrivilegeCall>(Irrelevant); |
166 | C.addTransition(State); |
167 | } |
168 | |
169 | bool SetgidSetuidOrderChecker::isFunctionCalledInArg( |
170 | const CallDescription &Desc, const CallEvent &Call) const { |
171 | if (const auto *CallInArg0 = |
172 | dyn_cast<CallExpr>(Val: Call.getArgExpr(Index: 0)->IgnoreParenImpCasts())) |
173 | return Desc.matchesAsWritten(CE: *CallInArg0); |
174 | return false; |
175 | } |
176 | |
177 | void SetgidSetuidOrderChecker::emitReport(ProgramStateRef State, |
178 | CheckerContext &C) const { |
179 | if (ExplodedNode *N = C.generateNonFatalErrorNode(State)) { |
180 | llvm::StringLiteral Msg = |
181 | "A 'setgid(getgid())' call following a 'setuid(getuid())' " |
182 | "call is likely to fail; probably the order of these " |
183 | "statements is wrong" ; |
184 | auto Report = std::make_unique<PathSensitiveBugReport>(args: BT, args&: Msg, args&: N); |
185 | Report->markInteresting(sym: State->get<LastSetuidCallSVal>()); |
186 | C.emitReport(R: std::move(Report)); |
187 | } |
188 | } |
189 | |
190 | void ento::registerSetgidSetuidOrderChecker(CheckerManager &mgr) { |
191 | mgr.registerChecker<SetgidSetuidOrderChecker>(); |
192 | } |
193 | |
194 | bool ento::shouldRegisterSetgidSetuidOrderChecker(const CheckerManager &mgr) { |
195 | return true; |
196 | } |
197 | |