1//===-- ChrootChecker.cpp - chroot usage checks ---------------------------===//
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 chroot checker, which checks improper use of chroot.
10// This is described by the SEI Cert C rule POS05-C.
11// The checker is a warning not a hard failure since it only checks for a
12// recommended rule.
13//
14//===----------------------------------------------------------------------===//
15
16#include "clang/AST/ASTContext.h"
17#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
18#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
19#include "clang/StaticAnalyzer/Core/Checker.h"
20#include "clang/StaticAnalyzer/Core/CheckerManager.h"
21#include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h"
22#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
23#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
24#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h"
25#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h"
26#include "clang/StaticAnalyzer/Core/PathSensitive/SymbolManager.h"
27
28using namespace clang;
29using namespace ento;
30
31namespace {
32enum ChrootKind { NO_CHROOT, ROOT_CHANGED, ROOT_CHANGE_FAILED, JAIL_ENTERED };
33} // namespace
34
35// Track chroot state changes for success, failure, state change
36// and "jail"
37REGISTER_TRAIT_WITH_PROGRAMSTATE(ChrootState, ChrootKind)
38namespace {
39
40// This checker checks improper use of chroot.
41// The state transitions
42//
43// -> ROOT_CHANGE_FAILED
44// |
45// NO_CHROOT ---chroot(path)--> ROOT_CHANGED ---chdir(/) --> JAIL_ENTERED
46// | |
47// ROOT_CHANGED<--chdir(..)-- JAIL_ENTERED<--chdir(..)--
48// | |
49// bug<--foo()-- JAIL_ENTERED<--foo()--
50//
51class ChrootChecker final : public Checker<eval::Call, check::PreCall> {
52public:
53 bool evalCall(const CallEvent &Call, CheckerContext &C) const;
54 void checkPreCall(const CallEvent &Call, CheckerContext &C) const;
55
56private:
57 bool evalChroot(const CallEvent &Call, CheckerContext &C) const;
58 bool evalChdir(const CallEvent &Call, CheckerContext &C) const;
59
60 const BugType BreakJailBug{this, "Break out of jail"};
61 const CallDescription Chroot{CDM::CLibrary, {"chroot"}, 1};
62 const CallDescription Chdir{CDM::CLibrary, {"chdir"}, 1};
63};
64
65bool ChrootChecker::evalCall(const CallEvent &Call, CheckerContext &C) const {
66 if (Chroot.matches(Call))
67 return evalChroot(Call, C);
68
69 if (Chdir.matches(Call))
70 return evalChdir(Call, C);
71
72 return false;
73}
74
75bool ChrootChecker::evalChroot(const CallEvent &Call, CheckerContext &C) const {
76 BasicValueFactory &BVF = C.getSValBuilder().getBasicValueFactory();
77 const LocationContext *LCtx = C.getLocationContext();
78 ProgramStateRef State = C.getState();
79 const auto *CE = cast<CallExpr>(Val: Call.getOriginExpr());
80
81 const QualType IntTy = C.getASTContext().IntTy;
82 SVal Zero = nonloc::ConcreteInt{BVF.getValue(X: 0, T: IntTy)};
83 SVal Minus1 = nonloc::ConcreteInt{BVF.getValue(X: -1, T: IntTy)};
84
85 ProgramStateRef ChrootFailed = State->BindExpr(S: CE, LCtx, V: Minus1);
86 C.addTransition(State: ChrootFailed->set<ChrootState>(ROOT_CHANGE_FAILED));
87
88 ProgramStateRef ChrootSucceeded = State->BindExpr(S: CE, LCtx, V: Zero);
89 C.addTransition(State: ChrootSucceeded->set<ChrootState>(ROOT_CHANGED));
90 return true;
91}
92
93bool ChrootChecker::evalChdir(const CallEvent &Call, CheckerContext &C) const {
94 ProgramStateRef State = C.getState();
95
96 // If there are no jail state, just return.
97 if (State->get<ChrootState>() == NO_CHROOT)
98 return false;
99
100 // After chdir("/"), enter the jail, set the enum value JAIL_ENTERED.
101 SVal ArgVal = Call.getArgSVal(Index: 0);
102
103 if (const MemRegion *R = ArgVal.getAsRegion()) {
104 R = R->StripCasts();
105 if (const auto *StrRegion = dyn_cast<StringRegion>(Val: R)) {
106 if (StrRegion->getStringLiteral()->getString() == "/") {
107 C.addTransition(State: State->set<ChrootState>(JAIL_ENTERED));
108 return true;
109 }
110 }
111 }
112 return false;
113}
114
115class ChrootInvocationVisitor final : public BugReporterVisitor {
116public:
117 explicit ChrootInvocationVisitor(const CallDescription &Chroot)
118 : Chroot{Chroot} {}
119
120 PathDiagnosticPieceRef VisitNode(const ExplodedNode *N,
121 BugReporterContext &BRC,
122 PathSensitiveBugReport &BR) override {
123 if (Satisfied)
124 return nullptr;
125
126 auto StmtP = N->getLocation().getAs<StmtPoint>();
127 if (!StmtP)
128 return nullptr;
129
130 const CallExpr *Call = StmtP->getStmtAs<CallExpr>();
131 if (!Call)
132 return nullptr;
133
134 if (!Chroot.matchesAsWritten(CE: *Call))
135 return nullptr;
136
137 Satisfied = true;
138 PathDiagnosticLocation Pos(Call, BRC.getSourceManager(),
139 N->getLocationContext());
140 return std::make_shared<PathDiagnosticEventPiece>(args&: Pos, args: "chroot called here",
141 /*addPosRange=*/args: true);
142 }
143
144 void Profile(llvm::FoldingSetNodeID &ID) const override {
145 static bool Tag;
146 ID.AddPointer(Ptr: &Tag);
147 }
148
149private:
150 const CallDescription &Chroot;
151 bool Satisfied = false;
152};
153
154// Check the jail state before any function call except chroot and chdir().
155void ChrootChecker::checkPreCall(const CallEvent &Call,
156 CheckerContext &C) const {
157 // Ignore chroot and chdir.
158 if (matchesAny(Call, CD1: Chroot, CDs: Chdir))
159 return;
160
161 // If jail state is not ROOT_CHANGED just return.
162 if (C.getState()->get<ChrootState>() != ROOT_CHANGED)
163 return;
164
165 // Generate bug report.
166 ExplodedNode *Err =
167 C.generateNonFatalErrorNode(State: C.getState(), Pred: C.getPredecessor());
168 if (!Err)
169 return;
170
171 auto R = std::make_unique<PathSensitiveBugReport>(
172 args: BreakJailBug, args: R"(No call of chdir("/") immediately after chroot)", args&: Err);
173 R->addVisitor<ChrootInvocationVisitor>(ConstructorArgs: Chroot);
174 C.emitReport(R: std::move(R));
175}
176
177} // namespace
178
179void ento::registerChrootChecker(CheckerManager &Mgr) {
180 Mgr.registerChecker<ChrootChecker>();
181}
182
183bool ento::shouldRegisterChrootChecker(const CheckerManager &) { return true; }
184