Skip to content

Commit 11b97da

Browse files
authored
[clang][analyzer] Add checker 'security.SetgidSetuidOrder' (#91445)
1 parent 5d833c6 commit 11b97da

File tree

6 files changed

+573
-0
lines changed

6 files changed

+573
-0
lines changed

clang/docs/analyzer/checkers.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,47 @@ security.insecureAPI.DeprecatedOrUnsafeBufferHandling (C)
11791179
strncpy(buf, "a", 1); // warn
11801180
}
11811181
1182+
security.SetgidSetuidOrder (C)
1183+
""""""""""""""""""""""""""""""
1184+
When dropping user-level and group-level privileges in a program by using
1185+
``setuid`` and ``setgid`` calls, it is important to reset the group-level
1186+
privileges (with ``setgid``) first. Function ``setgid`` will likely fail if
1187+
the superuser privileges are already dropped.
1188+
1189+
The checker checks for sequences of ``setuid(getuid())`` and
1190+
``setgid(getgid())`` calls (in this order). If such a sequence is found and
1191+
there is no other privilege-changing function call (``seteuid``, ``setreuid``,
1192+
``setresuid`` and the GID versions of these) in between, a warning is
1193+
generated. The checker finds only exactly ``setuid(getuid())`` calls (and the
1194+
GID versions), not for example if the result of ``getuid()`` is stored in a
1195+
variable.
1196+
1197+
.. code-block:: c
1198+
1199+
void test1() {
1200+
// ...
1201+
// end of section with elevated privileges
1202+
// reset privileges (user and group) to normal user
1203+
if (setuid(getuid()) != 0) {
1204+
handle_error();
1205+
return;
1206+
}
1207+
if (setgid(getgid()) != 0) { // warning: A 'setgid(getgid())' call following a 'setuid(getuid())' call is likely to fail
1208+
handle_error();
1209+
return;
1210+
}
1211+
// user-ID and group-ID are reset to normal user now
1212+
// ...
1213+
}
1214+
1215+
In the code above the problem is that ``setuid(getuid())`` removes superuser
1216+
privileges before ``setgid(getgid())`` is called. To fix the problem the
1217+
``setgid(getgid())`` should be called first. Further attention is needed to
1218+
avoid code like ``setgid(getuid())`` (this checker does not detect bugs like
1219+
this) and always check the return value of these calls.
1220+
1221+
This check corresponds to SEI CERT Rule `POS36-C <https://wiki.sei.cmu.edu/confluence/display/c/POS36-C.+Observe+correct+revocation+order+while+relinquishing+privileges>`_.
1222+
11821223
.. _unix-checkers:
11831224
11841225
unix

clang/include/clang/StaticAnalyzer/Checkers/Checkers.td

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,11 @@ def FloatLoopCounter : Checker<"FloatLoopCounter">,
10111011
Dependencies<[SecuritySyntaxChecker]>,
10121012
Documentation<HasDocumentation>;
10131013

1014+
def SetgidSetuidOrderChecker : Checker<"SetgidSetuidOrder">,
1015+
HelpText<"Warn on possible reversed order of 'setgid(getgid()))' and "
1016+
"'setuid(getuid())' (CERT: POS36-C)">,
1017+
Documentation<HasDocumentation>;
1018+
10141019
} // end "security"
10151020

10161021
let ParentPackage = ENV in {

clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ add_clang_library(clangStaticAnalyzerCheckers
103103
ReturnUndefChecker.cpp
104104
ReturnValueChecker.cpp
105105
RunLoopAutoreleaseLeakChecker.cpp
106+
SetgidSetuidOrderChecker.cpp
106107
SimpleStreamChecker.cpp
107108
SmartPtrChecker.cpp
108109
SmartPtrModeling.cpp
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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, BO_NE, nonloc::SymbolVal(LastSetuidSym),
105+
SVB.makeIntVal(0, /*isUnsigned=*/false),
106+
SVB.getConditionType())
107+
.getAs<DefinedOrUnknownSVal>();
108+
if (!FailComparison)
109+
return State;
110+
if (auto IsFailBranch = State->assume(*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(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([this,
129+
RetSym](PathSensitiveBugReport &BR) {
130+
if (!BR.isInteresting(RetSym) || &BR.getBugType() != &this->BT)
131+
return "";
132+
return "Call to 'setuid' found here that removes superuser privileges";
133+
});
134+
C.addTransition(State, 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(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>(Call.getArgExpr(0)->IgnoreParenImpCasts()))
173+
return Desc.matchesAsWritten(*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>(BT, Msg, N);
185+
Report->markInteresting(State->get<LastSetuidCallSVal>());
186+
C.emitReport(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+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// RUN: %clang_analyze_cc1 -analyzer-checker=core,security.SetgidSetuidOrder -analyzer-output=text -verify %s
2+
3+
typedef int uid_t;
4+
typedef int gid_t;
5+
6+
int setuid(uid_t);
7+
int setgid(gid_t);
8+
9+
uid_t getuid();
10+
gid_t getgid();
11+
12+
13+
14+
void test_note_1() {
15+
if (setuid(getuid()) == -1) // expected-note{{Assuming the condition is false}} \
16+
// expected-note{{Taking false branch}}
17+
return;
18+
if (setuid(getuid()) == -1) // expected-note{{Call to 'setuid' found here that removes superuser privileges}} \
19+
// expected-note{{Assuming the condition is false}} \
20+
// expected-note{{Taking false branch}}
21+
return;
22+
if (setgid(getgid()) == -1) // expected-warning{{A 'setgid(getgid())' call following a 'setuid(getuid())' call is likely to fail}} \
23+
// expected-note{{A 'setgid(getgid())' call following a 'setuid(getuid())' call is likely to fail}}
24+
return;
25+
}
26+
27+
void test_note_2() {
28+
if (setuid(getuid()) == -1) // expected-note{{Call to 'setuid' found here that removes superuser privileges}} \
29+
// expected-note 2 {{Assuming the condition is false}} \
30+
// expected-note 2 {{Taking false branch}}
31+
return;
32+
if (setgid(getgid()) == -1) // expected-warning{{A 'setgid(getgid())' call following a 'setuid(getuid())' call is likely to fail}} \
33+
// expected-note{{A 'setgid(getgid())' call following a 'setuid(getuid())' call is likely to fail}} \
34+
// expected-note{{Assuming the condition is false}} \
35+
// expected-note{{Taking false branch}}
36+
return;
37+
if (setuid(getuid()) == -1) // expected-note{{Call to 'setuid' found here that removes superuser privileges}} \
38+
// expected-note{{Assuming the condition is false}} \
39+
// expected-note{{Taking false branch}}
40+
return;
41+
if (setgid(getgid()) == -1) // expected-warning{{A 'setgid(getgid())' call following a 'setuid(getuid())' call is likely to fail}} \
42+
// expected-note{{A 'setgid(getgid())' call following a 'setuid(getuid())' call is likely to fail}}
43+
return;
44+
}
45+
46+
int f_setuid() {
47+
return setuid(getuid()); // expected-note{{Call to 'setuid' found here that removes superuser privileges}}
48+
}
49+
50+
int f_setgid() {
51+
return setgid(getgid()); // expected-warning{{A 'setgid(getgid())' call following a 'setuid(getuid())' call is likely to fail}} \
52+
// expected-note{{A 'setgid(getgid())' call following a 'setuid(getuid())' call is likely to fail}}
53+
}
54+
55+
void test_note_3() {
56+
if (f_setuid() == -1) // expected-note{{Assuming the condition is false}} \
57+
// expected-note{{Calling 'f_setuid'}} \
58+
// expected-note{{Returning from 'f_setuid'}} \
59+
// expected-note{{Taking false branch}}
60+
return;
61+
if (f_setgid() == -1) // expected-note{{Calling 'f_setgid'}}
62+
return;
63+
}
64+
65+
void test_note_4() {
66+
if (setuid(getuid()) == 0) { // expected-note{{Assuming the condition is true}} \
67+
// expected-note{{Call to 'setuid' found here that removes superuser privileges}} \
68+
// expected-note{{Taking true branch}}
69+
if (setgid(getgid()) == 0) { // expected-warning{{A 'setgid(getgid())' call following a 'setuid(getuid())' call is likely to fail}} \
70+
// expected-note{{A 'setgid(getgid())' call following a 'setuid(getuid())' call is likely to fail}}
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)