Skip to content

Commit d6d84b5

Browse files
authored
[analyzer] Handle builtin functions in MallocChecker (#88416)
This commit ensures that the `CallDescription`s in `MallocChecker` are matched with the mode `CDM::CLibrary`, so: - they don't match methods or functions within user-defined namespaces; - they also match builtin variants of these functions (if any), so the checker can model `__builtin_alloca()` like `alloca()`. This change fixes #81597. New tests were added to verify that `std::malloc` and `std::free` (from `<cstdlib>`) are modeled, but a method that's named e.g. `free` isn't confused with the memory release function. The responsibility for modeling `__builtin_alloca` and `__builtin_alloca_with_align` was moved from `BuiltinFunctionChecker` to `MallocChecker`, to avoid buggy interactions between the checkers and ensure that the builtin and non-builtin variants are handled by exactly the same logic. This change might be a step backwards for the users who don't have `unix.Malloc` enabled; but I suspect that `__builtin_alloca()` is so rare that it would be a waste of time to implement backwards compatibility for them. There were several test files that relied on `__builtin_alloca()` calls to get an `AllocaRegion`, these were modified to enable `unix.Malloc`. One of these files (cxx-uninitialized-object-ptr-ref.cpp) had some tests that relied on the fact that `malloc()` was treated as a "black box" in them, these were updated to use `calloc()` (to get initialized memory) and `free()` (to avoid memory leak reports). While I was developing this change, I found a very suspicious assert in `MallocChecker`. As it isn't blocking the goals of this commit, I just marked it with a FIXME, but I'll try to investigate and fix it in a follow-up change.
1 parent 66cf995 commit d6d84b5

File tree

11 files changed

+125
-75
lines changed

11 files changed

+125
-75
lines changed

clang/lib/StaticAnalyzer/Checkers/BuiltinFunctionChecker.cpp

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
//
77
//===----------------------------------------------------------------------===//
88
//
9-
// This checker evaluates clang builtin functions.
9+
// This checker evaluates "standalone" clang builtin functions that are not
10+
// just special-cased variants of well-known non-builtin functions.
11+
// Builtin functions like __builtin_memcpy and __builtin_alloca should be
12+
// evaluated by the same checker that handles their non-builtin variant to
13+
// ensure that the two variants are handled consistently.
1014
//
1115
//===----------------------------------------------------------------------===//
1216

@@ -80,25 +84,6 @@ bool BuiltinFunctionChecker::evalCall(const CallEvent &Call,
8084
return true;
8185
}
8286

83-
case Builtin::BI__builtin_alloca_with_align:
84-
case Builtin::BI__builtin_alloca: {
85-
SValBuilder &SVB = C.getSValBuilder();
86-
const loc::MemRegionVal R =
87-
SVB.getAllocaRegionVal(CE, C.getLocationContext(), C.blockCount());
88-
89-
// Set the extent of the region in bytes. This enables us to use the SVal
90-
// of the argument directly. If we saved the extent in bits, it'd be more
91-
// difficult to reason about values like symbol*8.
92-
auto Size = Call.getArgSVal(0);
93-
if (auto DefSize = Size.getAs<DefinedOrUnknownSVal>()) {
94-
// This `getAs()` is mostly paranoia, because core.CallAndMessage reports
95-
// undefined function arguments (unless it's disabled somehow).
96-
state = setDynamicExtent(state, R.getRegion(), *DefSize, SVB);
97-
}
98-
C.addTransition(state->BindExpr(CE, LCtx, R));
99-
return true;
100-
}
101-
10287
case Builtin::BI__builtin_dynamic_object_size:
10388
case Builtin::BI__builtin_object_size:
10489
case Builtin::BI__builtin_constant_p: {

clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp

Lines changed: 49 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,11 @@ class MallocChecker
401401
};
402402

403403
const CallDescriptionMap<CheckFn> FreeingMemFnMap{
404-
{{{"free"}, 1}, &MallocChecker::checkFree},
405-
{{{"if_freenameindex"}, 1}, &MallocChecker::checkIfFreeNameIndex},
406-
{{{"kfree"}, 1}, &MallocChecker::checkFree},
407-
{{{"g_free"}, 1}, &MallocChecker::checkFree},
404+
{{CDM::CLibrary, {"free"}, 1}, &MallocChecker::checkFree},
405+
{{CDM::CLibrary, {"if_freenameindex"}, 1},
406+
&MallocChecker::checkIfFreeNameIndex},
407+
{{CDM::CLibrary, {"kfree"}, 1}, &MallocChecker::checkFree},
408+
{{CDM::CLibrary, {"g_free"}, 1}, &MallocChecker::checkFree},
408409
};
409410

410411
bool isFreeingCall(const CallEvent &Call) const;
@@ -413,41 +414,46 @@ class MallocChecker
413414
friend class NoOwnershipChangeVisitor;
414415

415416
CallDescriptionMap<CheckFn> AllocatingMemFnMap{
416-
{{{"alloca"}, 1}, &MallocChecker::checkAlloca},
417-
{{{"_alloca"}, 1}, &MallocChecker::checkAlloca},
418-
{{{"malloc"}, 1}, &MallocChecker::checkBasicAlloc},
419-
{{{"malloc"}, 3}, &MallocChecker::checkKernelMalloc},
420-
{{{"calloc"}, 2}, &MallocChecker::checkCalloc},
421-
{{{"valloc"}, 1}, &MallocChecker::checkBasicAlloc},
417+
{{CDM::CLibrary, {"alloca"}, 1}, &MallocChecker::checkAlloca},
418+
{{CDM::CLibrary, {"_alloca"}, 1}, &MallocChecker::checkAlloca},
419+
// The line for "alloca" also covers "__builtin_alloca", but the
420+
// _with_align variant must be listed separately because it takes an
421+
// extra argument:
422+
{{CDM::CLibrary, {"__builtin_alloca_with_align"}, 2},
423+
&MallocChecker::checkAlloca},
424+
{{CDM::CLibrary, {"malloc"}, 1}, &MallocChecker::checkBasicAlloc},
425+
{{CDM::CLibrary, {"malloc"}, 3}, &MallocChecker::checkKernelMalloc},
426+
{{CDM::CLibrary, {"calloc"}, 2}, &MallocChecker::checkCalloc},
427+
{{CDM::CLibrary, {"valloc"}, 1}, &MallocChecker::checkBasicAlloc},
422428
{{CDM::CLibrary, {"strndup"}, 2}, &MallocChecker::checkStrdup},
423429
{{CDM::CLibrary, {"strdup"}, 1}, &MallocChecker::checkStrdup},
424-
{{{"_strdup"}, 1}, &MallocChecker::checkStrdup},
425-
{{{"kmalloc"}, 2}, &MallocChecker::checkKernelMalloc},
426-
{{{"if_nameindex"}, 1}, &MallocChecker::checkIfNameIndex},
430+
{{CDM::CLibrary, {"_strdup"}, 1}, &MallocChecker::checkStrdup},
431+
{{CDM::CLibrary, {"kmalloc"}, 2}, &MallocChecker::checkKernelMalloc},
432+
{{CDM::CLibrary, {"if_nameindex"}, 1}, &MallocChecker::checkIfNameIndex},
427433
{{CDM::CLibrary, {"wcsdup"}, 1}, &MallocChecker::checkStrdup},
428434
{{CDM::CLibrary, {"_wcsdup"}, 1}, &MallocChecker::checkStrdup},
429-
{{{"g_malloc"}, 1}, &MallocChecker::checkBasicAlloc},
430-
{{{"g_malloc0"}, 1}, &MallocChecker::checkGMalloc0},
431-
{{{"g_try_malloc"}, 1}, &MallocChecker::checkBasicAlloc},
432-
{{{"g_try_malloc0"}, 1}, &MallocChecker::checkGMalloc0},
433-
{{{"g_memdup"}, 2}, &MallocChecker::checkGMemdup},
434-
{{{"g_malloc_n"}, 2}, &MallocChecker::checkGMallocN},
435-
{{{"g_malloc0_n"}, 2}, &MallocChecker::checkGMallocN0},
436-
{{{"g_try_malloc_n"}, 2}, &MallocChecker::checkGMallocN},
437-
{{{"g_try_malloc0_n"}, 2}, &MallocChecker::checkGMallocN0},
435+
{{CDM::CLibrary, {"g_malloc"}, 1}, &MallocChecker::checkBasicAlloc},
436+
{{CDM::CLibrary, {"g_malloc0"}, 1}, &MallocChecker::checkGMalloc0},
437+
{{CDM::CLibrary, {"g_try_malloc"}, 1}, &MallocChecker::checkBasicAlloc},
438+
{{CDM::CLibrary, {"g_try_malloc0"}, 1}, &MallocChecker::checkGMalloc0},
439+
{{CDM::CLibrary, {"g_memdup"}, 2}, &MallocChecker::checkGMemdup},
440+
{{CDM::CLibrary, {"g_malloc_n"}, 2}, &MallocChecker::checkGMallocN},
441+
{{CDM::CLibrary, {"g_malloc0_n"}, 2}, &MallocChecker::checkGMallocN0},
442+
{{CDM::CLibrary, {"g_try_malloc_n"}, 2}, &MallocChecker::checkGMallocN},
443+
{{CDM::CLibrary, {"g_try_malloc0_n"}, 2}, &MallocChecker::checkGMallocN0},
438444
};
439445

440446
CallDescriptionMap<CheckFn> ReallocatingMemFnMap{
441-
{{{"realloc"}, 2},
447+
{{CDM::CLibrary, {"realloc"}, 2},
442448
std::bind(&MallocChecker::checkRealloc, _1, _2, _3, false)},
443-
{{{"reallocf"}, 2},
449+
{{CDM::CLibrary, {"reallocf"}, 2},
444450
std::bind(&MallocChecker::checkRealloc, _1, _2, _3, true)},
445-
{{{"g_realloc"}, 2},
451+
{{CDM::CLibrary, {"g_realloc"}, 2},
446452
std::bind(&MallocChecker::checkRealloc, _1, _2, _3, false)},
447-
{{{"g_try_realloc"}, 2},
453+
{{CDM::CLibrary, {"g_try_realloc"}, 2},
448454
std::bind(&MallocChecker::checkRealloc, _1, _2, _3, false)},
449-
{{{"g_realloc_n"}, 3}, &MallocChecker::checkReallocN},
450-
{{{"g_try_realloc_n"}, 3}, &MallocChecker::checkReallocN},
455+
{{CDM::CLibrary, {"g_realloc_n"}, 3}, &MallocChecker::checkReallocN},
456+
{{CDM::CLibrary, {"g_try_realloc_n"}, 3}, &MallocChecker::checkReallocN},
451457

452458
// NOTE: the following CallDescription also matches the C++ standard
453459
// library function std::getline(); the callback will filter it out.
@@ -1259,9 +1265,6 @@ static bool isStandardRealloc(const CallEvent &Call) {
12591265
assert(FD);
12601266
ASTContext &AC = FD->getASTContext();
12611267

1262-
if (isa<CXXMethodDecl>(FD))
1263-
return false;
1264-
12651268
return FD->getDeclaredReturnType().getDesugaredType(AC) == AC.VoidPtrTy &&
12661269
FD->getParamDecl(0)->getType().getDesugaredType(AC) == AC.VoidPtrTy &&
12671270
FD->getParamDecl(1)->getType().getDesugaredType(AC) ==
@@ -1273,9 +1276,6 @@ static bool isGRealloc(const CallEvent &Call) {
12731276
assert(FD);
12741277
ASTContext &AC = FD->getASTContext();
12751278

1276-
if (isa<CXXMethodDecl>(FD))
1277-
return false;
1278-
12791279
return FD->getDeclaredReturnType().getDesugaredType(AC) == AC.VoidPtrTy &&
12801280
FD->getParamDecl(0)->getType().getDesugaredType(AC) == AC.VoidPtrTy &&
12811281
FD->getParamDecl(1)->getType().getDesugaredType(AC) ==
@@ -1284,14 +1284,14 @@ static bool isGRealloc(const CallEvent &Call) {
12841284

12851285
void MallocChecker::checkRealloc(const CallEvent &Call, CheckerContext &C,
12861286
bool ShouldFreeOnFail) const {
1287-
// HACK: CallDescription currently recognizes non-standard realloc functions
1288-
// as standard because it doesn't check the type, or wether its a non-method
1289-
// function. This should be solved by making CallDescription smarter.
1290-
// Mind that this came from a bug report, and all other functions suffer from
1291-
// this.
1292-
// https://bugs.llvm.org/show_bug.cgi?id=46253
1287+
// Ignore calls to functions whose type does not match the expected type of
1288+
// either the standard realloc or g_realloc from GLib.
1289+
// FIXME: Should we perform this kind of checking consistently for each
1290+
// function? If yes, then perhaps extend the `CallDescription` interface to
1291+
// handle this.
12931292
if (!isStandardRealloc(Call) && !isGRealloc(Call))
12941293
return;
1294+
12951295
ProgramStateRef State = C.getState();
12961296
State = ReallocMemAux(C, Call, ShouldFreeOnFail, State, AF_Malloc);
12971297
State = ProcessZeroAllocCheck(Call, 1, State);
@@ -1842,9 +1842,18 @@ static ProgramStateRef MallocUpdateRefState(CheckerContext &C, const Expr *E,
18421842
return nullptr;
18431843

18441844
SymbolRef Sym = RetVal->getAsLocSymbol();
1845+
18451846
// This is a return value of a function that was not inlined, such as malloc()
18461847
// or new(). We've checked that in the caller. Therefore, it must be a symbol.
18471848
assert(Sym);
1849+
// FIXME: In theory this assertion should fail for `alloca()` calls (because
1850+
// `AllocaRegion`s are not symbolic); but in practice this does not happen.
1851+
// As the current code appears to work correctly, I'm not touching this issue
1852+
// now, but it would be good to investigate and clarify this.
1853+
// Also note that perhaps the special `AllocaRegion` should be replaced by
1854+
// `SymbolicRegion` (or turned into a subclass of `SymbolicRegion`) to enable
1855+
// proper tracking of memory allocated by `alloca()` -- and after that change
1856+
// this assertion would become valid again.
18481857

18491858
// Set the symbol's state to Allocated.
18501859
return State->set<RegionState>(Sym, RefState::getAllocated(Family, E));

clang/test/Analysis/Inputs/system-header-simulator-cxx.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,7 @@ using ostream = basic_ostream<char>;
11061106
extern std::ostream cout;
11071107

11081108
ostream &operator<<(ostream &, const string &);
1109+
11091110
#if __cplusplus >= 202002L
11101111
template <class T>
11111112
ostream &operator<<(ostream &, const std::unique_ptr<T> &);
@@ -1122,11 +1123,12 @@ istream &getline(istream &, string &, char);
11221123
istream &getline(istream &, string &);
11231124
} // namespace std
11241125

1125-
#ifdef TEST_INLINABLE_ALLOCATORS
11261126
namespace std {
11271127
void *malloc(size_t);
11281128
void free(void *);
1129-
}
1129+
} // namespace std
1130+
1131+
#ifdef TEST_INLINABLE_ALLOCATORS
11301132
void* operator new(std::size_t size, const std::nothrow_t&) throw() { return std::malloc(size); }
11311133
void* operator new[](std::size_t size, const std::nothrow_t&) throw() { return std::malloc(size); }
11321134
void operator delete(void* ptr, const std::nothrow_t&) throw() { std::free(ptr); }

clang/test/Analysis/cxx-uninitialized-object-ptr-ref.cpp

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
// RUN: %clang_analyze_cc1 -analyzer-checker=core,optin.cplusplus.UninitializedObject \
1+
// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix.Malloc,optin.cplusplus.UninitializedObject \
22
// RUN: -analyzer-config optin.cplusplus.UninitializedObject:Pedantic=true -DPEDANTIC \
33
// RUN: -analyzer-config optin.cplusplus.UninitializedObject:CheckPointeeInitialization=true \
44
// RUN: -std=c++11 -verify %s
55

6-
// RUN: %clang_analyze_cc1 -analyzer-checker=core,optin.cplusplus.UninitializedObject \
6+
// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix.Malloc,optin.cplusplus.UninitializedObject \
77
// RUN: -analyzer-config optin.cplusplus.UninitializedObject:CheckPointeeInitialization=true \
88
// RUN: -std=c++11 -verify %s
99

@@ -316,7 +316,10 @@ void fCyclicPointerTest2() {
316316

317317
// Void pointer tests are mainly no-crash tests.
318318

319-
void *malloc(int size);
319+
typedef __typeof(sizeof(int)) size_t;
320+
321+
void *calloc(size_t nmemb, size_t size);
322+
void free(void *p);
320323

321324
class VoidPointerTest1 {
322325
void *vptr;
@@ -328,8 +331,9 @@ class VoidPointerTest1 {
328331
};
329332

330333
void fVoidPointerTest1() {
331-
void *vptr = malloc(sizeof(int));
334+
void *vptr = calloc(1, sizeof(int));
332335
VoidPointerTest1(vptr, char());
336+
free(vptr);
333337
}
334338

335339
class VoidPointerTest2 {
@@ -342,8 +346,9 @@ class VoidPointerTest2 {
342346
};
343347

344348
void fVoidPointerTest2() {
345-
void *vptr = malloc(sizeof(int));
349+
void *vptr = calloc(1, sizeof(int));
346350
VoidPointerTest2(&vptr, char());
351+
free(vptr);
347352
}
348353

349354
class VoidPointerRRefTest1 {
@@ -359,8 +364,9 @@ upon returning to the caller. This will be a dangling reference}}
359364
};
360365

361366
void fVoidPointerRRefTest1() {
362-
void *vptr = malloc(sizeof(int));
367+
void *vptr = calloc(1, sizeof(int));
363368
VoidPointerRRefTest1(vptr, char());
369+
free(vptr);
364370
}
365371

366372
class VoidPointerRRefTest2 {
@@ -376,8 +382,9 @@ upon returning to the caller. This will be a dangling reference}}
376382
};
377383

378384
void fVoidPointerRRefTest2() {
379-
void *vptr = malloc(sizeof(int));
385+
void *vptr = calloc(1, sizeof(int));
380386
VoidPointerRRefTest2(&vptr, char());
387+
free(vptr);
381388
}
382389

383390
class VoidPointerLRefTest {
@@ -393,8 +400,9 @@ upon returning to the caller. This will be a dangling reference}}
393400
};
394401

395402
void fVoidPointerLRefTest() {
396-
void *vptr = malloc(sizeof(int));
403+
void *vptr = calloc(1, sizeof(int));
397404
VoidPointerLRefTest(vptr, char());
405+
free(vptr);
398406
}
399407

400408
struct CyclicVoidPointerTest {

clang/test/Analysis/exercise-ps.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// RUN: %clang_analyze_cc1 %s -verify -Wno-error=implicit-function-declaration \
2-
// RUN: -analyzer-checker=core \
2+
// RUN: -analyzer-checker=core,unix.Malloc \
33
// RUN: -analyzer-config core.CallAndMessage:ArgPointeeInitializedness=true
44
//
55
// Just exercise the analyzer on code that has at one point caused issues

clang/test/Analysis/explain-svals.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// RUN: %clang_analyze_cc1 -triple i386-apple-darwin10 -verify %s \
2-
// RUN: -analyzer-checker=core.builtin \
32
// RUN: -analyzer-checker=debug.ExprInspection \
43
// RUN: -analyzer-checker=unix.cstring \
4+
// RUN: -analyzer-checker=unix.Malloc \
55
// RUN: -analyzer-config display-checker-name=false
66

77
typedef unsigned long size_t;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix.Malloc -verify -analyzer-output=text %s
2+
3+
// This file tests that unix.Malloc can handle C++ code where e.g. malloc and
4+
// free are declared within the namespace 'std' by the header <cstdlib>.
5+
6+
#include "Inputs/system-header-simulator-cxx.h"
7+
8+
void leak() {
9+
int *p = static_cast<int*>(std::malloc(sizeof(int))); // expected-note{{Memory is allocated}}
10+
} // expected-warning{{Potential leak of memory pointed to by 'p'}}
11+
// expected-note@-1{{Potential leak of memory pointed to by 'p'}}
12+
13+
void no_leak() {
14+
int *p = static_cast<int*>(std::malloc(sizeof(int)));
15+
std::free(p); // no-warning
16+
}
17+
18+
void invalid_free() {
19+
int i;
20+
int *p = &i;
21+
//expected-note@+2{{Argument to free() is the address of the local variable 'i', which is not memory allocated by malloc()}}
22+
//expected-warning@+1{{Argument to free() is the address of the local variable 'i', which is not memory allocated by malloc()}}
23+
std::free(p);
24+
}

clang/test/Analysis/malloc.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,17 @@ void allocaFree(void) {
740740
free(p); // expected-warning {{Memory allocated by alloca() should not be deallocated}}
741741
}
742742

743+
void allocaFreeBuiltin(void) {
744+
int *p = __builtin_alloca(sizeof(int));
745+
free(p); // expected-warning {{Memory allocated by alloca() should not be deallocated}}
746+
}
747+
748+
void allocaFreeBuiltinAlign(void) {
749+
int *p = __builtin_alloca_with_align(sizeof(int), 64);
750+
free(p); // expected-warning {{Memory allocated by alloca() should not be deallocated}}
751+
}
752+
753+
743754
int* mallocEscapeRet(void) {
744755
int *p = malloc(12);
745756
return p; // no warning

clang/test/Analysis/malloc.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,14 @@ void *realloc(void **ptr, size_t size) { realloc(ptr, size); } // no-crash
214214
namespace pr46253_paramty2{
215215
void *realloc(void *ptr, int size) { realloc(ptr, size); } // no-crash
216216
} // namespace pr46253_paramty2
217+
218+
namespace pr81597 {
219+
struct S {};
220+
struct T {
221+
void free(const S& s);
222+
};
223+
void f(T& t) {
224+
S s;
225+
t.free(s); // no-warning: This is not the free you are looking for...
226+
}
227+
} // namespace pr81597

clang/test/Analysis/stack-addr-ps.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %clang_analyze_cc1 -analyzer-checker=core -fblocks -verify %s
1+
// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix.Malloc -fblocks -verify %s
22

33
int* f1(void) {
44
int x = 0;

clang/test/Analysis/stackaddrleak.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// RUN: %clang_analyze_cc1 -analyzer-checker=core -verify -std=c99 -Dbool=_Bool -Wno-bool-conversion %s
2-
// RUN: %clang_analyze_cc1 -analyzer-checker=core -verify -x c++ -Wno-bool-conversion %s
1+
// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix.Malloc -verify -std=c99 -Dbool=_Bool -Wno-bool-conversion %s
2+
// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix.Malloc -verify -x c++ -Wno-bool-conversion %s
33

44
typedef __INTPTR_TYPE__ intptr_t;
55
char const *p;

0 commit comments

Comments
 (0)