1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
|
//== MIGChecker.cpp - MIG calling convention checker ------------*- C++ -*--==//
//
// 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 MIGChecker, a Mach Interface Generator calling convention
// checker. Namely, in MIG callback implementation the following rules apply:
// - When a server routine returns an error code that represents success, it
// must take ownership of resources passed to it (and eventually release
// them).
// - Additionally, when returning success, all out-parameters must be
// initialized.
// - When it returns any other error code, it must not take ownership,
// because the message and its out-of-line parameters will be destroyed
// by the client that called the function.
// For now we only check the last rule, as its violations lead to dangerous
// use-after-free exploits.
//
//===----------------------------------------------------------------------===//
#include "clang/AST/Attr.h"
#include "clang/Analysis/AnyCall.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 <optional>
using namespace clang;
using namespace ento;
namespace {
class MIGChecker : public Checker<check::PostCall, check::PreStmt<ReturnStmt>,
check::EndFunction> {
BugType BT{this, "Use-after-free (MIG calling convention violation)",
categories::MemoryError};
// The checker knows that an out-of-line object is deallocated if it is
// passed as an argument to one of these functions. If this object is
// additionally an argument of a MIG routine, the checker keeps track of that
// information and issues a warning when an error is returned from the
// respective routine.
std::vector<std::pair<CallDescription, unsigned>> Deallocators = {
#define CALL(required_args, deallocated_arg, ...) \
{{{__VA_ARGS__}, required_args}, deallocated_arg}
// E.g., if the checker sees a C function 'vm_deallocate' that is
// defined on class 'IOUserClient' that has exactly 3 parameters, it knows
// that argument #1 (starting from 0, i.e. the second argument) is going
// to be consumed in the sense of the MIG consume-on-success convention.
CALL(3, 1, "vm_deallocate"),
CALL(3, 1, "mach_vm_deallocate"),
CALL(2, 0, "mig_deallocate"),
CALL(2, 1, "mach_port_deallocate"),
CALL(1, 0, "device_deallocate"),
CALL(1, 0, "iokit_remove_connect_reference"),
CALL(1, 0, "iokit_remove_reference"),
CALL(1, 0, "iokit_release_port"),
CALL(1, 0, "ipc_port_release"),
CALL(1, 0, "ipc_port_release_sonce"),
CALL(1, 0, "ipc_voucher_attr_control_release"),
CALL(1, 0, "ipc_voucher_release"),
CALL(1, 0, "lock_set_dereference"),
CALL(1, 0, "memory_object_control_deallocate"),
CALL(1, 0, "pset_deallocate"),
CALL(1, 0, "semaphore_dereference"),
CALL(1, 0, "space_deallocate"),
CALL(1, 0, "space_inspect_deallocate"),
CALL(1, 0, "task_deallocate"),
CALL(1, 0, "task_inspect_deallocate"),
CALL(1, 0, "task_name_deallocate"),
CALL(1, 0, "thread_deallocate"),
CALL(1, 0, "thread_inspect_deallocate"),
CALL(1, 0, "upl_deallocate"),
CALL(1, 0, "vm_map_deallocate"),
// E.g., if the checker sees a method 'releaseAsyncReference64()' that is
// defined on class 'IOUserClient' that takes exactly 1 argument, it knows
// that the argument is going to be consumed in the sense of the MIG
// consume-on-success convention.
CALL(1, 0, "IOUserClient", "releaseAsyncReference64"),
CALL(1, 0, "IOUserClient", "releaseNotificationPort"),
#undef CALL
};
CallDescription OsRefRetain{{"os_ref_retain"}, 1};
void checkReturnAux(const ReturnStmt *RS, CheckerContext &C) const;
public:
void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
// HACK: We're making two attempts to find the bug: checkEndFunction
// should normally be enough but it fails when the return value is a literal
// that never gets put into the Environment and ends of function with multiple
// returns get agglutinated across returns, preventing us from obtaining
// the return value. The problem is similar to https://reviews.llvm.org/D25326
// but now we step into it in the top-level function.
void checkPreStmt(const ReturnStmt *RS, CheckerContext &C) const {
checkReturnAux(RS, C);
}
void checkEndFunction(const ReturnStmt *RS, CheckerContext &C) const {
checkReturnAux(RS, C);
}
};
} // end anonymous namespace
// A flag that says that the programmer has called a MIG destructor
// for at least one parameter.
REGISTER_TRAIT_WITH_PROGRAMSTATE(ReleasedParameter, bool)
// A set of parameters for which the check is suppressed because
// reference counting is being performed.
REGISTER_SET_WITH_PROGRAMSTATE(RefCountedParameters, const ParmVarDecl *)
static const ParmVarDecl *getOriginParam(SVal V, CheckerContext &C,
bool IncludeBaseRegions = false) {
// TODO: We should most likely always include base regions here.
SymbolRef Sym = V.getAsSymbol(IncludeBaseRegions);
if (!Sym)
return nullptr;
// If we optimistically assume that the MIG routine never re-uses the storage
// that was passed to it as arguments when it invalidates it (but at most when
// it assigns to parameter variables directly), this procedure correctly
// determines if the value was loaded from the transitive closure of MIG
// routine arguments in the heap.
while (const MemRegion *MR = Sym->getOriginRegion()) {
const auto *VR = dyn_cast<VarRegion>(MR);
if (VR && VR->hasStackParametersStorage() &&
VR->getStackFrame()->inTopFrame())
return cast<ParmVarDecl>(VR->getDecl());
const SymbolicRegion *SR = MR->getSymbolicBase();
if (!SR)
return nullptr;
Sym = SR->getSymbol();
}
return nullptr;
}
static bool isInMIGCall(CheckerContext &C) {
const LocationContext *LC = C.getLocationContext();
assert(LC && "Unknown location context");
const StackFrameContext *SFC;
// Find the top frame.
while (LC) {
SFC = LC->getStackFrame();
LC = SFC->getParent();
}
const Decl *D = SFC->getDecl();
if (std::optional<AnyCall> AC = AnyCall::forDecl(D)) {
// Even though there's a Sema warning when the return type of an annotated
// function is not a kern_return_t, this warning isn't an error, so we need
// an extra check here.
// FIXME: AnyCall doesn't support blocks yet, so they remain unchecked
// for now.
if (!AC->getReturnType(C.getASTContext())
.getCanonicalType()->isSignedIntegerType())
return false;
}
if (D->hasAttr<MIGServerRoutineAttr>())
return true;
// See if there's an annotated method in the superclass.
if (const auto *MD = dyn_cast<CXXMethodDecl>(D))
for (const auto *OMD: MD->overridden_methods())
if (OMD->hasAttr<MIGServerRoutineAttr>())
return true;
return false;
}
void MIGChecker::checkPostCall(const CallEvent &Call, CheckerContext &C) const {
if (OsRefRetain.matches(Call)) {
// If the code is doing reference counting over the parameter,
// it opens up an opportunity for safely calling a destructor function.
// TODO: We should still check for over-releases.
if (const ParmVarDecl *PVD =
getOriginParam(Call.getArgSVal(0), C, /*IncludeBaseRegions=*/true)) {
// We never need to clean up the program state because these are
// top-level parameters anyway, so they're always live.
C.addTransition(C.getState()->add<RefCountedParameters>(PVD));
}
return;
}
if (!isInMIGCall(C))
return;
auto I = llvm::find_if(Deallocators,
[&](const std::pair<CallDescription, unsigned> &Item) {
return Item.first.matches(Call);
});
if (I == Deallocators.end())
return;
ProgramStateRef State = C.getState();
unsigned ArgIdx = I->second;
SVal Arg = Call.getArgSVal(ArgIdx);
const ParmVarDecl *PVD = getOriginParam(Arg, C);
if (!PVD || State->contains<RefCountedParameters>(PVD))
return;
const NoteTag *T =
C.getNoteTag([this, PVD](PathSensitiveBugReport &BR) -> std::string {
if (&BR.getBugType() != &BT)
return "";
SmallString<64> Str;
llvm::raw_svector_ostream OS(Str);
OS << "Value passed through parameter '" << PVD->getName()
<< "\' is deallocated";
return std::string(OS.str());
});
C.addTransition(State->set<ReleasedParameter>(true), T);
}
// Returns true if V can potentially represent a "successful" kern_return_t.
static bool mayBeSuccess(SVal V, CheckerContext &C) {
ProgramStateRef State = C.getState();
// Can V represent KERN_SUCCESS?
if (!State->isNull(V).isConstrainedFalse())
return true;
SValBuilder &SVB = C.getSValBuilder();
ASTContext &ACtx = C.getASTContext();
// Can V represent MIG_NO_REPLY?
static const int MigNoReply = -305;
V = SVB.evalEQ(C.getState(), V, SVB.makeIntVal(MigNoReply, ACtx.IntTy));
if (!State->isNull(V).isConstrainedTrue())
return true;
// If none of the above, it's definitely an error.
return false;
}
void MIGChecker::checkReturnAux(const ReturnStmt *RS, CheckerContext &C) const {
// It is very unlikely that a MIG callback will be called from anywhere
// within the project under analysis and the caller isn't itself a routine
// that follows the MIG calling convention. Therefore we're safe to believe
// that it's always the top frame that is of interest. There's a slight chance
// that the user would want to enforce the MIG calling convention upon
// a random routine in the middle of nowhere, but given that the convention is
// fairly weird and hard to follow in the first place, there's relatively
// little motivation to spread it this way.
if (!C.inTopFrame())
return;
if (!isInMIGCall(C))
return;
// We know that the function is non-void, but what if the return statement
// is not there in the code? It's not a compile error, we should not crash.
if (!RS)
return;
ProgramStateRef State = C.getState();
if (!State->get<ReleasedParameter>())
return;
SVal V = C.getSVal(RS);
if (mayBeSuccess(V, C))
return;
ExplodedNode *N = C.generateErrorNode();
if (!N)
return;
auto R = std::make_unique<PathSensitiveBugReport>(
BT,
"MIG callback fails with error after deallocating argument value. "
"This is a use-after-free vulnerability because the caller will try to "
"deallocate it again",
N);
R->addRange(RS->getSourceRange());
bugreporter::trackExpressionValue(
N, RS->getRetValue(), *R,
{bugreporter::TrackingKind::Thorough, /*EnableNullFPSuppression=*/false});
C.emitReport(std::move(R));
}
void ento::registerMIGChecker(CheckerManager &Mgr) {
Mgr.registerChecker<MIGChecker>();
}
bool ento::shouldRegisterMIGChecker(const CheckerManager &mgr) {
return true;
}
|