Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[clang][analyzer] Add checker 'security.SetgidSetuidOrder' #91445

Merged
merged 10 commits into from
May 22, 2024
41 changes: 41 additions & 0 deletions clang/docs/analyzer/checkers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,47 @@ security.insecureAPI.DeprecatedOrUnsafeBufferHandling (C)
strncpy(buf, "a", 1); // warn
}

security.SetgidSetuidOrder (C)
""""""""""""""""""""""""""""""
Comment on lines +1182 to +1183
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give that the alpha.unix.chroot checker does something similar, I wonder if this checker should share the same parent package with that one to aid discoverability.
WDYT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the new checker into unix, or move the chroot checker into security (or alpha.security)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a preference, I was just asking.
I just felt this is a natural question, and you seem to care about alpha checkers and documentation/discoverability, and this seemed like a subject to discuss. @NagyDonat WDYT?

When dropping user-level and group-level privileges in a program by using
``setuid`` and ``setgid`` calls, it is important to reset the group-level
privileges (with ``setgid``) first. Function ``setgid`` will likely fail if
the superuser privileges are already dropped.

The checker checks for sequences of ``setuid(getuid())`` and
``setgid(getgid())`` calls (in this order). If such a sequence is found and
there is no other privilege-changing function call (``seteuid``, ``setreuid``,
``setresuid`` and the GID versions of these) in between, a warning is
generated. The checker finds only exactly ``setuid(getuid())`` calls (and the
GID versions), not for example if the result of ``getuid()`` is stored in a
variable.

.. code-block:: c

void test1() {
// ...
// end of section with elevated privileges
// reset privileges (user and group) to normal user
if (setuid(getuid()) != 0) {
handle_error();
return;
}
if (setgid(getgid()) != 0) { // warning: A 'setgid(getgid())' call following a 'setuid(getuid())' call is likely to fail
handle_error();
return;
}
// user-ID and group-ID are reset to normal user now
// ...
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the docs should explicitly mention how to fix/suppress this issue.
Like: One should call setgid(getgid()) first and then setuid(getuid()).
Maybe also mention to pay attention to not mix up the APIs so that user ID and group IDs are not swapped, etc. Also add a remark to not forget to check the return value of these APIs.

And this example looks suspiciously similar to the one present in the CERT docs.
Does it raise license and copy-right issues? If that's a concern, please rework the example.

In the code above the problem is that ``setuid(getuid())`` removes superuser
privileges before ``setgid(getgid())`` is called. To fix the problem the
``setgid(getgid())`` should be called first. Further attention is needed to
avoid code like ``setgid(getuid())`` (this checker does not detect bugs like
this).

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>`_.

.. _unix-checkers:

unix
Expand Down
5 changes: 5 additions & 0 deletions clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,11 @@ def FloatLoopCounter : Checker<"FloatLoopCounter">,
Dependencies<[SecuritySyntaxChecker]>,
Documentation<HasDocumentation>;

def SetgidSetuidOrderChecker : Checker<"SetgidSetuidOrder">,
HelpText<"Warn on possible reversed order of 'setgid(getgid()))' and "
"'setuid(getuid())' (CERT: POS36-C)">,
Documentation<HasDocumentation>;

} // end "security"

let ParentPackage = ENV in {
Expand Down
1 change: 1 addition & 0 deletions clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ add_clang_library(clangStaticAnalyzerCheckers
ReturnUndefChecker.cpp
ReturnValueChecker.cpp
RunLoopAutoreleaseLeakChecker.cpp
SetgidSetuidOrderChecker.cpp
SimpleStreamChecker.cpp
SmartPtrChecker.cpp
SmartPtrModeling.cpp
Expand Down
197 changes: 197 additions & 0 deletions clang/lib/StaticAnalyzer/Checkers/SetgidSetuidOrderChecker.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
//===-- SetgidSetuidOrderChecker.cpp - check privilege revocation calls ---===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file defines a checker to detect possible reversed order of privilege
// revocations when 'setgid' and 'setuid' is used.
//
//===----------------------------------------------------------------------===//

#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
#include "clang/StaticAnalyzer/Core/Checker.h"
#include "clang/StaticAnalyzer/Core/CheckerManager.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h"

using namespace clang;
using namespace ento;

namespace {

enum SetPrivilegeFunctionKind { Irrelevant, Setuid, Setgid };

class SetgidSetuidOrderChecker : public Checker<check::PostCall, eval::Assume> {
const BugType BT{this, "Possible wrong order of privilege revocation"};

const CallDescription SetuidDesc{CDM::CLibrary, {"setuid"}, 1};
const CallDescription SetgidDesc{CDM::CLibrary, {"setgid"}, 1};

const CallDescription GetuidDesc{CDM::CLibrary, {"getuid"}, 0};
const CallDescription GetgidDesc{CDM::CLibrary, {"getgid"}, 0};

const CallDescriptionSet OtherSetPrivilegeDesc{
{CDM::CLibrary, {"seteuid"}, 1}, {CDM::CLibrary, {"setegid"}, 1},
{CDM::CLibrary, {"setreuid"}, 2}, {CDM::CLibrary, {"setregid"}, 2},
{CDM::CLibrary, {"setresuid"}, 3}, {CDM::CLibrary, {"setresgid"}, 3}};

public:
void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
ProgramStateRef evalAssume(ProgramStateRef State, SVal Cond,
bool Assumption) const;
const BugType *getBT() const { return &BT; }
steakhal marked this conversation as resolved.
Show resolved Hide resolved

private:
void processSetuid(ProgramStateRef State, const CallEvent &Call,
CheckerContext &C) const;
void processSetgid(ProgramStateRef State, const CallEvent &Call,
CheckerContext &C) const;
void processOther(ProgramStateRef State, const CallEvent &Call,
CheckerContext &C) const;
/// Check if a function like \c getuid or \c getgid is called directly from
/// the first argument of function called from \a Call.
bool isFunctionCalledInArg(const CallDescription &Desc,
const CallEvent &Call) const;
void emitReport(ProgramStateRef State, CheckerContext &C) const;
};

} // end anonymous namespace

/// Store if there was a call to 'setuid(getuid())' or 'setgid(getgid())' not
/// followed by other different privilege-change functions.
/// If the value \c Setuid is stored and a 'setgid(getgid())' call is found we
/// have found the bug to be reported. Value \c Setgid is used too to prevent
/// warnings at a setgid-setuid-setgid sequence.
REGISTER_TRAIT_WITH_PROGRAMSTATE(LastSetPrivilegeCall, SetPrivilegeFunctionKind)
/// Store the symbol value of the last 'setuid(getuid())' call. This is used to
/// detect if the result is compared to -1 and avoid warnings on that branch
/// (which is the failure branch of the call), and for identification of note
/// tags.
REGISTER_TRAIT_WITH_PROGRAMSTATE(LastSetuidCallSVal, SymbolRef)

void SetgidSetuidOrderChecker::checkPostCall(const CallEvent &Call,
CheckerContext &C) const {
ProgramStateRef State = C.getState();
if (SetuidDesc.matches(Call)) {
processSetuid(State, Call, C);
} else if (SetgidDesc.matches(Call)) {
processSetgid(State, Call, C);
} else if (OtherSetPrivilegeDesc.contains(Call)) {
processOther(State, Call, C);
}
}

ProgramStateRef SetgidSetuidOrderChecker::evalAssume(ProgramStateRef State,
SVal Cond,
bool Assumption) const {
Comment on lines +90 to +92
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is very strange that this evalAssume callback doesn't use the parameters Cond and Assumption, but I see that this is apparently a normal thing that also happens in other checkers. Perhaps add a comment that explains this (e.g. something similar to the FIXME in RetainCountChecker if you agree with it).

@haoNoQ @Xazax-hun @steakhal Do you happen to have architectural insight about this situation? How did we end up with a callback where the arguments are mostly unused?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case it would be possible to examine what is inside Cond and check for (conj_$11{int, LC2, S1335, #1}) == -1 like things (if possible, the conj_$11 should be equal to the saved symbol of the setuid call), but the test with assumption is much more simple. And I see that the Cond is often just a simple 1 or 0 integer value, this looks interesting and not useful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@balazske Thanks for the explanation.

This question is still somewhat relevant as an architectural curiosity, but there is no need to change this part of this commit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it has limited usability. We eagerly fold expressions (see eagerly-assume), and it's really hard to recover meaningful context (if possible) from that callback. This holds back the users of this callback.

Now that I grepped for users, they are usually interested in if a symbol gets perfectly constrained; after which they do two things: cleanup some map and/or set some traits.

I think a callback for perfect constraints makes sense, but not for arbitrary assumptions.
On this note: The ExprEngine::processAssume is invoked once for every assume call. However, one assume call might lead to a sequence of re-assume calls, which could individually perfectly constrain a sequence of symbols (due to the merger eqclasses) like here.

SValBuilder &SVB = State->getStateManager().getSValBuilder();
SymbolRef LastSetuidSym = State->get<LastSetuidCallSVal>();
if (!LastSetuidSym)
return State;

// Check if the most recent call to 'setuid(getuid())' is assumed to be != 0.
// It should be only -1 at failure, but we want to accept a "!= 0" check too.
// (But now an invalid failure check like "!= 1" will be recognized as correct
// too. The "invalid failure check" is a different bug that is not the scope
// of this checker.)
auto FailComparison =
SVB.evalBinOpNN(State, BO_NE, nonloc::SymbolVal(LastSetuidSym),
SVB.makeIntVal(0, /*isUnsigned=*/false),
SVB.getConditionType())
.getAs<DefinedOrUnknownSVal>();
if (!FailComparison)
return State;
if (auto IsFailBranch = State->assume(*FailComparison);
IsFailBranch.first && !IsFailBranch.second) {
// This is the 'setuid(getuid())' != 0 case.
// On this branch we do not want to emit warning.
State = State->set<LastSetPrivilegeCall>(Irrelevant);
State = State->set<LastSetuidCallSVal>(SymbolRef{});
}
return State;
}

void SetgidSetuidOrderChecker::processSetuid(ProgramStateRef State,
const CallEvent &Call,
CheckerContext &C) const {
bool IsSetuidWithGetuid = isFunctionCalledInArg(GetuidDesc, Call);
if (State->get<LastSetPrivilegeCall>() != Setgid && IsSetuidWithGetuid) {
SymbolRef RetSym = Call.getReturnValue().getAsSymbol();
State = State->set<LastSetPrivilegeCall>(Setuid);
State = State->set<LastSetuidCallSVal>(RetSym);
const NoteTag *Note = C.getNoteTag([this,
RetSym](PathSensitiveBugReport &BR) {
if (!BR.isInteresting(RetSym) || &BR.getBugType() != this->getBT())
steakhal marked this conversation as resolved.
Show resolved Hide resolved
return "";
return "Call to 'setuid' found here that removes superuser privileges";
});
C.addTransition(State, Note);
return;
}
State = State->set<LastSetPrivilegeCall>(Irrelevant);
State = State->set<LastSetuidCallSVal>(SymbolRef{});
C.addTransition(State);
}

void SetgidSetuidOrderChecker::processSetgid(ProgramStateRef State,
const CallEvent &Call,
CheckerContext &C) const {
bool IsSetgidWithGetgid = isFunctionCalledInArg(GetgidDesc, Call);
if (State->get<LastSetPrivilegeCall>() == Setuid) {
if (IsSetgidWithGetgid) {
State = State->set<LastSetPrivilegeCall>(Irrelevant);
emitReport(State, C);
return;
}
State = State->set<LastSetPrivilegeCall>(Irrelevant);
} else {
State = State->set<LastSetPrivilegeCall>(IsSetgidWithGetgid ? Setgid
: Irrelevant);
}
State = State->set<LastSetuidCallSVal>(SymbolRef{});
C.addTransition(State);
}

void SetgidSetuidOrderChecker::processOther(ProgramStateRef State,
const CallEvent &Call,
CheckerContext &C) const {
State = State->set<LastSetuidCallSVal>(SymbolRef{});
State = State->set<LastSetPrivilegeCall>(Irrelevant);
C.addTransition(State);
}

bool SetgidSetuidOrderChecker::isFunctionCalledInArg(
const CallDescription &Desc, const CallEvent &Call) const {
if (const auto *CallInArg0 =
dyn_cast<CallExpr>(Call.getArgExpr(0)->IgnoreParenImpCasts()))
return Desc.matchesAsWritten(*CallInArg0);
return false;
}

void SetgidSetuidOrderChecker::emitReport(ProgramStateRef State,
CheckerContext &C) const {
if (ExplodedNode *N = C.generateNonFatalErrorNode(State)) {
llvm::StringLiteral Msg =
"A 'setgid(getgid())' call following a 'setuid(getuid())' "
"call is likely to fail; probably the order of these "
"statements is wrong";
auto Report = std::make_unique<PathSensitiveBugReport>(BT, Msg, N);
Report->markInteresting(State->get<LastSetuidCallSVal>());
C.emitReport(std::move(Report));
}
}

void ento::registerSetgidSetuidOrderChecker(CheckerManager &mgr) {
mgr.registerChecker<SetgidSetuidOrderChecker>();
}

bool ento::shouldRegisterSetgidSetuidOrderChecker(const CheckerManager &mgr) {
return true;
}
Loading
Loading