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 | |
28 | using namespace clang; |
29 | using namespace ento; |
30 | |
31 | namespace { |
32 | enum 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" |
37 | REGISTER_TRAIT_WITH_PROGRAMSTATE(ChrootState, ChrootKind) |
38 | namespace { |
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 | // |
51 | class ChrootChecker final : public Checker<eval::Call, check::PreCall> { |
52 | public: |
53 | bool evalCall(const CallEvent &Call, CheckerContext &C) const; |
54 | void checkPreCall(const CallEvent &Call, CheckerContext &C) const; |
55 | |
56 | private: |
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 | |
65 | bool 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 | |
75 | bool 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 | |
93 | bool 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 | |
115 | class ChrootInvocationVisitor final : public BugReporterVisitor { |
116 | public: |
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 | |
149 | private: |
150 | const CallDescription &Chroot; |
151 | bool Satisfied = false; |
152 | }; |
153 | |
154 | // Check the jail state before any function call except chroot and chdir(). |
155 | void 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 | |
179 | void ento::registerChrootChecker(CheckerManager &Mgr) { |
180 | Mgr.registerChecker<ChrootChecker>(); |
181 | } |
182 | |
183 | bool ento::shouldRegisterChrootChecker(const CheckerManager &) { return true; } |
184 | |