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
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions clang/docs/analyzer/checkers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,34 @@ 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.

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

.. code-block:: c

void test1() {
if (setuid(getuid()) == -1) {
/* handle error condition */
}
if (setgid(getgid()) == -1) { // warn
/* handle error condition */
}
}

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.

.. _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)">,
steakhal marked this conversation as resolved.
Show resolved Hide resolved
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 {

class SetgidSetuidOrderChecker
: public Checker<check::PostCall, check::DeadSymbols, eval::Assume> {
const BugType BT_WrongRevocationOrder{
this, "Possible wrong order of privilege revocation"};
steakhal marked this conversation as resolved.
Show resolved Hide resolved

const CallDescription SetuidDesc{{"setuid"}, 1};
const CallDescription SetgidDesc{{"setgid"}, 1};
Copy link
Contributor

Choose a reason for hiding this comment

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

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

const CallDescription SeteuidDesc{{"seteuid"}, 1};
const CallDescription SetegidDesc{{"setegid"}, 1};
const CallDescription SetreuidDesc{{"setreuid"}, 2};
const CallDescription SetregidDesc{{"setregid"}, 2};
const CallDescription SetresuidDesc{{"setresuid"}, 3};
const CallDescription SetresgidDesc{{"setresgid"}, 3};
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use the MatchAs argument of the CallDescription constructor, which was recently introduced by my changes. In this case you'd probably use the matching mode CDM::CLibrary.

Without this these call descriptions would match even a C++ method if it has the right name and number of arguments.


public:
SetgidSetuidOrderChecker() {}
steakhal marked this conversation as resolved.
Show resolved Hide resolved

void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const;
ProgramStateRef evalAssume(ProgramStateRef State, SVal Cond,
bool Assumption) const;

private:
ProgramStateRef processSetuid(ProgramStateRef State, const CallEvent &Call,
CheckerContext &C) const;
ProgramStateRef processSetgid(ProgramStateRef State, const CallEvent &Call,
CheckerContext &C) const;
ProgramStateRef 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(llvm::StringRef FnName,
const CallEvent &Call) const;
void emitReport(ProgramStateRef State, CheckerContext &C) const;
};

enum SetPrivilegeFunctionKind { Irrelevant, Setuid, Setgid };
Copy link
Contributor

@NagyDonat NagyDonat May 8, 2024

Choose a reason for hiding this comment

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

Perhaps place this enum declaration above the checker class. (When I was looking for its definition, I automatically scrolled to the top, and I was surprised that I didn't see it there.)

(This is minor bikeshedding, feel free to ignore it if you feel that it's better to keep this declaration here.)


} // 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).
REGISTER_TRAIT_WITH_PROGRAMSTATE(LastSetuidCallSVal, SymbolRef)

void SetgidSetuidOrderChecker::checkPostCall(const CallEvent &Call,
CheckerContext &C) const {
ProgramStateRef State = C.getState();
if (SetuidDesc.matches(Call)) {
State = processSetuid(State, Call, C);
} else if (SetgidDesc.matches(Call)) {
State = processSetgid(State, Call, C);
} else if (matchesAny(Call, SeteuidDesc, SetegidDesc, SetreuidDesc,
SetregidDesc, SetresuidDesc, SetresgidDesc)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

matchesAny is a nice tool, but it would be even better to use a CallDescriptionSet and its contains method, because then you wouldn't need to introduce individual variable names for these call descriptions.

State = processOther(State, Call, C);
}
if (State)
C.addTransition(State);
}

void SetgidSetuidOrderChecker::checkDeadSymbols(SymbolReaper &SymReaper,
CheckerContext &C) const {
ProgramStateRef State = C.getState();

SymbolRef LastSetuidSym = State->get<LastSetuidCallSVal>();
if (!LastSetuidSym)
return;

if (!SymReaper.isDead(LastSetuidSym))
return;

State = State->set<LastSetuidCallSVal>(SymbolRef{});
steakhal marked this conversation as resolved.
Show resolved Hide resolved
C.addTransition(State, C.getPredecessor());
steakhal marked this conversation as resolved.
Show resolved Hide resolved
}

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 -1.
auto FailVal =
SVB.evalBinOpNN(State, BO_EQ, nonloc::SymbolVal(LastSetuidSym),
SVB.makeIntVal(-1, false), SVB.getConditionType())
steakhal marked this conversation as resolved.
Show resolved Hide resolved
.getAs<DefinedOrUnknownSVal>();
if (!FailVal)
return State;
auto IsFailBranch = State->assume(*FailVal);
steakhal marked this conversation as resolved.
Show resolved Hide resolved
if (IsFailBranch.first && !IsFailBranch.second) {
// This is the 'setuid(getuid())' == -1 case.
// On this branch we do not want to emit warning.
State = State->set<LastSetuidCallSVal>(SymbolRef{});
State = State->set<LastSetPrivilegeCall>(Irrelevant);
}
return State;
}

ProgramStateRef SetgidSetuidOrderChecker::processSetuid(
ProgramStateRef State, const CallEvent &Call, CheckerContext &C) const {
bool IsSetuidWithGetuid = isFunctionCalledInArg("getuid", Call);
if (State->get<LastSetPrivilegeCall>() != Setgid && IsSetuidWithGetuid) {
State = State->set<LastSetuidCallSVal>(Call.getReturnValue().getAsSymbol());
return State->set<LastSetPrivilegeCall>(Setuid);
}
State = State->set<LastSetuidCallSVal>(SymbolRef{});
return State->set<LastSetPrivilegeCall>(Irrelevant);
}

ProgramStateRef SetgidSetuidOrderChecker::processSetgid(
ProgramStateRef State, const CallEvent &Call, CheckerContext &C) const {
bool IsSetgidWithGetgid = isFunctionCalledInArg("getgid", Call);
State = State->set<LastSetuidCallSVal>(SymbolRef{});
if (State->get<LastSetPrivilegeCall>() == Setuid) {
if (IsSetgidWithGetgid) {
State = State->set<LastSetPrivilegeCall>(Irrelevant);
emitReport(State, C);
// return nullptr to prevent adding transition with the returned state
return nullptr;
}
return State->set<LastSetPrivilegeCall>(Irrelevant);
} else
return State->set<LastSetPrivilegeCall>(IsSetgidWithGetgid ? Setgid
: Irrelevant);
steakhal marked this conversation as resolved.
Show resolved Hide resolved
}

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

bool SetgidSetuidOrderChecker::isFunctionCalledInArg(
llvm::StringRef FnName, const CallEvent &Call) const {
if (const auto *CE = dyn_cast<CallExpr>(Call.getArgExpr(0)))
steakhal marked this conversation as resolved.
Show resolved Hide resolved
if (const FunctionDecl *FuncD = CE->getDirectCallee())
return FuncD->isGlobal() &&
FuncD->getASTContext().getSourceManager().isInSystemHeader(
FuncD->getCanonicalDecl()->getLocation()) &&
FuncD->getNumParams() == 0 && FuncD->getNameAsString() == FnName;
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using CallDescription::matchesAsWritten(const CallExpr &CE) with the matching mode CDM::CLibrary. It is a bit more permissive than your checks (e.g. it accepts a function that wasn't declared in a system header), but I think it is a good "sane default" heuristic for recognizing C library functions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I did not observe that CallDescription can be used with CallExpr, it is much simple now. The considered functions are moved into the test file instead of using a new header only for this checker (it was added because system header requirement).

return false;
}

void SetgidSetuidOrderChecker::emitReport(ProgramStateRef State,
CheckerContext &C) const {
if (ExplodedNode *N = C.generateNonFatalErrorNode(State)) {
StringRef Msg = "A 'setgid(getgid())' call following a 'setuid(getuid())' "
Copy link
Contributor

Choose a reason for hiding this comment

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

StringRef has a subclass StringLiteral that's optimized for compile-time constant strings (this is llvm::StringLiteral -- while clang::StringLiteral is an unrelated AST node class). It would be better to use that (or perhaps const char * if you prefer a lower-level style).

"call is likely to fail; Probably order of these "
"statements was reversed mistakenly";
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"call is likely to fail; Probably order of these "
"statements was reversed mistakenly";
"call is likely to fail; this order is probably a mistake";

C.emitReport(std::make_unique<PathSensitiveBugReport>(
BT_WrongRevocationOrder, Msg, N));
Copy link
Contributor

Choose a reason for hiding this comment

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

I was expecting a visitor highlighting the previous setuid call.
Without a visitor, or note tag, the call wouldn't be mentioned in the report.

Copy link
Contributor

@steakhal steakhal May 14, 2024

Choose a reason for hiding this comment

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

I was expecting a visitor highlighting the previous setuid call.
Without a visitor, or note tag, the call wouldn't be mentioned in the report.

I think the lack of visitors or note tags was not resolved.

}
}

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

bool ento::shouldRegisterSetgidSetuidOrderChecker(const CheckerManager &mgr) {
return true;
}
15 changes: 15 additions & 0 deletions clang/test/Analysis/Inputs/system-header-simulator-setgid-setuid.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#pragma clang system_header

typedef int uid_t;
typedef int gid_t;
int setuid(uid_t);
int setgid(gid_t);
int seteuid(uid_t);
int setegid(gid_t);
int setreuid(uid_t, uid_t);
int setregid(gid_t, gid_t);
int setresuid(uid_t, uid_t, uid_t);
int setresgid(gid_t, gid_t, gid_t);

uid_t getuid();
gid_t getgid();
Loading
Loading