//===-- ChrootChecker.cpp - chroot usage checks ---------------------------===// // // 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 chroot checker, which checks improper use of chroot. // This is described by the SEI Cert C rule POS05-C. // The checker is a warning not a hard failure since it only checks for a // recommended rule. // //===----------------------------------------------------------------------===// #include "clang/AST/ASTContext.h" #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" #include "clang/StaticAnalyzer/Core/PathSensitive/SymbolManager.h" using namespace clang; using namespace ento; namespace { enum ChrootKind { NO_CHROOT, ROOT_CHANGED, ROOT_CHANGE_FAILED, JAIL_ENTERED }; } // namespace // Track chroot state changes for success, failure, state change // and "jail" REGISTER_TRAIT_WITH_PROGRAMSTATE(ChrootState, ChrootKind) namespace { // This checker checks improper use of chroot. // The state transitions // // -> ROOT_CHANGE_FAILED // | // NO_CHROOT ---chroot(path)--> ROOT_CHANGED ---chdir(/) --> JAIL_ENTERED // | | // ROOT_CHANGED<--chdir(..)-- JAIL_ENTERED<--chdir(..)-- // | | // bug<--foo()-- JAIL_ENTERED<--foo()-- // class ChrootChecker final : public Checker { public: bool evalCall(const CallEvent &Call, CheckerContext &C) const; void checkPreCall(const CallEvent &Call, CheckerContext &C) const; private: bool evalChroot(const CallEvent &Call, CheckerContext &C) const; bool evalChdir(const CallEvent &Call, CheckerContext &C) const; const BugType BreakJailBug{this, "Break out of jail"}; const CallDescription Chroot{CDM::CLibrary, {"chroot"}, 1}; const CallDescription Chdir{CDM::CLibrary, {"chdir"}, 1}; }; bool ChrootChecker::evalCall(const CallEvent &Call, CheckerContext &C) const { if (Chroot.matches(Call)) return evalChroot(Call, C); if (Chdir.matches(Call)) return evalChdir(Call, C); return false; } bool ChrootChecker::evalChroot(const CallEvent &Call, CheckerContext &C) const { BasicValueFactory &BVF = C.getSValBuilder().getBasicValueFactory(); const LocationContext *LCtx = C.getLocationContext(); ProgramStateRef State = C.getState(); const auto *CE = cast(Call.getOriginExpr()); const QualType IntTy = C.getASTContext().IntTy; SVal Zero = nonloc::ConcreteInt{BVF.getValue(0, IntTy)}; SVal Minus1 = nonloc::ConcreteInt{BVF.getValue(-1, IntTy)}; ProgramStateRef ChrootFailed = State->BindExpr(CE, LCtx, Minus1); C.addTransition(ChrootFailed->set(ROOT_CHANGE_FAILED)); ProgramStateRef ChrootSucceeded = State->BindExpr(CE, LCtx, Zero); C.addTransition(ChrootSucceeded->set(ROOT_CHANGED)); return true; } bool ChrootChecker::evalChdir(const CallEvent &Call, CheckerContext &C) const { ProgramStateRef State = C.getState(); // If there are no jail state, just return. if (State->get() == NO_CHROOT) return false; // After chdir("/"), enter the jail, set the enum value JAIL_ENTERED. SVal ArgVal = Call.getArgSVal(0); if (const MemRegion *R = ArgVal.getAsRegion()) { R = R->StripCasts(); if (const auto *StrRegion = dyn_cast(R)) { if (StrRegion->getStringLiteral()->getString() == "/") { C.addTransition(State->set(JAIL_ENTERED)); return true; } } } return false; } class ChrootInvocationVisitor final : public BugReporterVisitor { public: explicit ChrootInvocationVisitor(const CallDescription &Chroot) : Chroot{Chroot} {} PathDiagnosticPieceRef VisitNode(const ExplodedNode *N, BugReporterContext &BRC, PathSensitiveBugReport &BR) override { if (Satisfied) return nullptr; auto StmtP = N->getLocation().getAs(); if (!StmtP) return nullptr; const CallExpr *Call = StmtP->getStmtAs(); if (!Call) return nullptr; if (!Chroot.matchesAsWritten(*Call)) return nullptr; Satisfied = true; PathDiagnosticLocation Pos(Call, BRC.getSourceManager(), N->getLocationContext()); return std::make_shared(Pos, "chroot called here", /*addPosRange=*/true); } void Profile(llvm::FoldingSetNodeID &ID) const override { static bool Tag; ID.AddPointer(&Tag); } private: const CallDescription &Chroot; bool Satisfied = false; }; // Check the jail state before any function call except chroot and chdir(). void ChrootChecker::checkPreCall(const CallEvent &Call, CheckerContext &C) const { // Ignore chroot and chdir. if (matchesAny(Call, Chroot, Chdir)) return; // If jail state is not ROOT_CHANGED just return. if (C.getState()->get() != ROOT_CHANGED) return; // Generate bug report. ExplodedNode *Err = C.generateNonFatalErrorNode(C.getState(), C.getPredecessor()); if (!Err) return; auto R = std::make_unique( BreakJailBug, R"(No call of chdir("/") immediately after chroot)", Err); R->addVisitor(Chroot); C.emitReport(std::move(R)); } } // namespace void ento::registerChrootChecker(CheckerManager &Mgr) { Mgr.registerChecker(); } bool ento::shouldRegisterChrootChecker(const CheckerManager &) { return true; }