Skip to content

Commit 67c6ad6

Browse files
[clang][analyzer] Model allocation behavior or getdelim/geline (#83138)
`getdelim` and `getline` may free, allocate, or re-allocate the input buffer, ensuring its size is enough to hold the incoming line, the delimiter, and the null terminator. `*lineptr` must be a valid argument to `free`, which means it can be either 1. `NULL`, in which case these functions perform an allocation equivalent to a call to `malloc` even on failure. 2. A pointer returned by the `malloc` family of functions. Other pointers are UB (`alloca`, a pointer to a static, to a stack variable, etc.)
1 parent 6540f16 commit 67c6ad6

File tree

5 files changed

+182
-6
lines changed

5 files changed

+182
-6
lines changed

clang/include/clang/StaticAnalyzer/Core/PathSensitive/CheckerHelpers.h

+6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
#ifndef LLVM_CLANG_STATICANALYZER_CORE_PATHSENSITIVE_CHECKERHELPERS_H
1414
#define LLVM_CLANG_STATICANALYZER_CORE_PATHSENSITIVE_CHECKERHELPERS_H
1515

16+
#include "ProgramState_Fwd.h"
17+
#include "SVals.h"
18+
1619
#include "clang/AST/OperationKinds.h"
1720
#include "clang/AST/Stmt.h"
1821
#include "clang/Basic/OperatorKinds.h"
@@ -110,6 +113,9 @@ class OperatorKind {
110113
OperatorKind operationKindFromOverloadedOperator(OverloadedOperatorKind OOK,
111114
bool IsBinary);
112115

116+
std::optional<DefinedSVal> getPointeeDefVal(SVal PtrSVal,
117+
ProgramStateRef State);
118+
113119
} // namespace ento
114120

115121
} // namespace clang

clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp

+71-6
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,8 @@ class MallocChecker
382382
CHECK_FN(checkGMemdup)
383383
CHECK_FN(checkGMallocN)
384384
CHECK_FN(checkGMallocN0)
385+
CHECK_FN(preGetdelim)
386+
CHECK_FN(checkGetdelim)
385387
CHECK_FN(checkReallocN)
386388
CHECK_FN(checkOwnershipAttr)
387389

@@ -391,6 +393,11 @@ class MallocChecker
391393
using CheckFn = std::function<void(const MallocChecker *,
392394
const CallEvent &Call, CheckerContext &C)>;
393395

396+
const CallDescriptionMap<CheckFn> PreFnMap{
397+
{{{"getline"}, 3}, &MallocChecker::preGetdelim},
398+
{{{"getdelim"}, 4}, &MallocChecker::preGetdelim},
399+
};
400+
394401
const CallDescriptionMap<CheckFn> FreeingMemFnMap{
395402
{{{"free"}, 1}, &MallocChecker::checkFree},
396403
{{{"if_freenameindex"}, 1}, &MallocChecker::checkIfFreeNameIndex},
@@ -439,6 +446,8 @@ class MallocChecker
439446
std::bind(&MallocChecker::checkRealloc, _1, _2, _3, false)},
440447
{{{"g_realloc_n"}, 3}, &MallocChecker::checkReallocN},
441448
{{{"g_try_realloc_n"}, 3}, &MallocChecker::checkReallocN},
449+
{{{"getline"}, 3}, &MallocChecker::checkGetdelim},
450+
{{{"getdelim"}, 4}, &MallocChecker::checkGetdelim},
442451
};
443452

444453
bool isMemCall(const CallEvent &Call) const;
@@ -588,11 +597,14 @@ class MallocChecker
588597
/// }
589598
/// \param [in] ReturnsNullOnFailure Whether the memory deallocation function
590599
/// we're modeling returns with Null on failure.
600+
/// \param [in] ArgValOpt Optional value to use for the argument instead of
601+
/// the one obtained from ArgExpr.
591602
/// \returns The ProgramState right after deallocation.
592603
[[nodiscard]] ProgramStateRef
593604
FreeMemAux(CheckerContext &C, const Expr *ArgExpr, const CallEvent &Call,
594605
ProgramStateRef State, bool Hold, bool &IsKnownToBeAllocated,
595-
AllocationFamily Family, bool ReturnsNullOnFailure = false) const;
606+
AllocationFamily Family, bool ReturnsNullOnFailure = false,
607+
std::optional<SVal> ArgValOpt = {}) const;
596608

597609
// TODO: Needs some refactoring, as all other deallocation modeling
598610
// functions are suffering from out parameters and messy code due to how
@@ -1423,6 +1435,50 @@ void MallocChecker::checkGMallocN0(const CallEvent &Call,
14231435
C.addTransition(State);
14241436
}
14251437

1438+
void MallocChecker::preGetdelim(const CallEvent &Call,
1439+
CheckerContext &C) const {
1440+
if (!Call.isGlobalCFunction())
1441+
return;
1442+
1443+
ProgramStateRef State = C.getState();
1444+
const auto LinePtr = getPointeeDefVal(Call.getArgSVal(0), State);
1445+
if (!LinePtr)
1446+
return;
1447+
1448+
// FreeMemAux takes IsKnownToBeAllocated as an output parameter, and it will
1449+
// be true after the call if the symbol was registered by this checker.
1450+
// We do not need this value here, as FreeMemAux will take care
1451+
// of reporting any violation of the preconditions.
1452+
bool IsKnownToBeAllocated = false;
1453+
State = FreeMemAux(C, Call.getArgExpr(0), Call, State, false,
1454+
IsKnownToBeAllocated, AF_Malloc, false, LinePtr);
1455+
if (State)
1456+
C.addTransition(State);
1457+
}
1458+
1459+
void MallocChecker::checkGetdelim(const CallEvent &Call,
1460+
CheckerContext &C) const {
1461+
if (!Call.isGlobalCFunction())
1462+
return;
1463+
1464+
ProgramStateRef State = C.getState();
1465+
// Handle the post-conditions of getline and getdelim:
1466+
// Register the new conjured value as an allocated buffer.
1467+
const CallExpr *CE = dyn_cast_or_null<CallExpr>(Call.getOriginExpr());
1468+
if (!CE)
1469+
return;
1470+
1471+
SValBuilder &SVB = C.getSValBuilder();
1472+
1473+
const auto LinePtr = getPointeeDefVal(Call.getArgSVal(0), State);
1474+
const auto Size = getPointeeDefVal(Call.getArgSVal(1), State);
1475+
if (!LinePtr || !Size || !LinePtr->getAsRegion())
1476+
return;
1477+
1478+
State = setDynamicExtent(State, LinePtr->getAsRegion(), *Size, SVB);
1479+
C.addTransition(MallocUpdateRefState(C, CE, State, AF_Malloc, *LinePtr));
1480+
}
1481+
14261482
void MallocChecker::checkReallocN(const CallEvent &Call,
14271483
CheckerContext &C) const {
14281484
ProgramStateRef State = C.getState();
@@ -1895,15 +1951,17 @@ static void printExpectedDeallocName(raw_ostream &os, AllocationFamily Family) {
18951951
}
18961952
}
18971953

1898-
ProgramStateRef MallocChecker::FreeMemAux(
1899-
CheckerContext &C, const Expr *ArgExpr, const CallEvent &Call,
1900-
ProgramStateRef State, bool Hold, bool &IsKnownToBeAllocated,
1901-
AllocationFamily Family, bool ReturnsNullOnFailure) const {
1954+
ProgramStateRef
1955+
MallocChecker::FreeMemAux(CheckerContext &C, const Expr *ArgExpr,
1956+
const CallEvent &Call, ProgramStateRef State,
1957+
bool Hold, bool &IsKnownToBeAllocated,
1958+
AllocationFamily Family, bool ReturnsNullOnFailure,
1959+
std::optional<SVal> ArgValOpt) const {
19021960

19031961
if (!State)
19041962
return nullptr;
19051963

1906-
SVal ArgVal = C.getSVal(ArgExpr);
1964+
SVal ArgVal = ArgValOpt.value_or(C.getSVal(ArgExpr));
19071965
if (!isa<DefinedOrUnknownSVal>(ArgVal))
19081966
return nullptr;
19091967
DefinedOrUnknownSVal location = ArgVal.castAs<DefinedOrUnknownSVal>();
@@ -2881,6 +2939,13 @@ void MallocChecker::checkPreCall(const CallEvent &Call,
28812939
return;
28822940
}
28832941

2942+
// We need to handle getline pre-conditions here before the pointed region
2943+
// gets invalidated by StreamChecker
2944+
if (const auto *PreFN = PreFnMap.lookup(Call)) {
2945+
(*PreFN)(this, Call, C);
2946+
return;
2947+
}
2948+
28842949
// We will check for double free in the post visit.
28852950
if (const AnyFunctionCall *FC = dyn_cast<AnyFunctionCall>(&Call)) {
28862951
const FunctionDecl *FD = FC->getDecl();

clang/lib/StaticAnalyzer/Core/CheckerHelpers.cpp

+9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "clang/AST/Decl.h"
1515
#include "clang/AST/Expr.h"
1616
#include "clang/Lex/Preprocessor.h"
17+
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h"
1718
#include <optional>
1819

1920
namespace clang {
@@ -182,5 +183,13 @@ OperatorKind operationKindFromOverloadedOperator(OverloadedOperatorKind OOK,
182183
}
183184
}
184185

186+
std::optional<DefinedSVal> getPointeeDefVal(SVal PtrSVal,
187+
ProgramStateRef State) {
188+
if (const auto *Ptr = PtrSVal.getAsRegion()) {
189+
return State->getSVal(Ptr).getAs<DefinedSVal>();
190+
}
191+
return std::nullopt;
192+
}
193+
185194
} // namespace ento
186195
} // namespace clang

clang/test/Analysis/Inputs/system-header-simulator-for-malloc.h

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ typedef __typeof(sizeof(int)) size_t;
99
void *malloc(size_t);
1010
void *calloc(size_t, size_t);
1111
void free(void *);
12+
void *alloca(size_t);
1213

1314

1415
#if __OBJC__

clang/test/Analysis/getline-alloc.c

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix,debug.ExprInspection -verify %s
2+
3+
// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix,alpha.unix,debug.ExprInspection -verify %s
4+
5+
#include "Inputs/system-header-simulator.h"
6+
#include "Inputs/system-header-simulator-for-malloc.h"
7+
8+
void test_getline_null_buffer() {
9+
FILE *F1 = tmpfile();
10+
if (!F1)
11+
return;
12+
char *buffer = NULL;
13+
size_t n = 0;
14+
if (getline(&buffer, &n, F1) > 0) {
15+
char c = buffer[0]; // ok
16+
}
17+
free(buffer);
18+
fclose(F1);
19+
}
20+
21+
void test_getline_malloc_buffer() {
22+
FILE *F1 = tmpfile();
23+
if (!F1)
24+
return;
25+
26+
size_t n = 10;
27+
char *buffer = malloc(n);
28+
char *ptr = buffer;
29+
30+
ssize_t r = getdelim(&buffer, &n, '\r', F1);
31+
// ptr may be dangling
32+
free(ptr); // expected-warning {{Attempt to free released memory}}
33+
free(buffer); // ok
34+
fclose(F1);
35+
}
36+
37+
void test_getline_alloca() {
38+
FILE *F1 = tmpfile();
39+
if (!F1)
40+
return;
41+
size_t n = 10;
42+
char *buffer = alloca(n);
43+
getline(&buffer, &n, F1); // expected-warning {{Memory allocated by alloca() should not be deallocated}}
44+
fclose(F1);
45+
}
46+
47+
void test_getline_invalid_ptr() {
48+
FILE *F1 = tmpfile();
49+
if (!F1)
50+
return;
51+
size_t n = 10;
52+
char *buffer = (char*)test_getline_invalid_ptr;
53+
getline(&buffer, &n, F1); // expected-warning {{Argument to getline() is the address of the function 'test_getline_invalid_ptr', which is not memory allocated by malloc()}}
54+
fclose(F1);
55+
}
56+
57+
void test_getline_leak() {
58+
FILE *F1 = tmpfile();
59+
if (!F1)
60+
return;
61+
62+
char *buffer = NULL;
63+
size_t n = 0;
64+
ssize_t read;
65+
66+
while ((read = getline(&buffer, &n, F1)) != -1) {
67+
printf("%s\n", buffer);
68+
}
69+
70+
fclose(F1); // expected-warning {{Potential memory leak}}
71+
}
72+
73+
void test_getline_stack() {
74+
size_t n = 10;
75+
char buffer[10];
76+
char *ptr = buffer;
77+
78+
FILE *F1 = tmpfile();
79+
if (!F1)
80+
return;
81+
82+
getline(&ptr, &n, F1); // expected-warning {{Argument to getline() is the address of the local variable 'buffer', which is not memory allocated by malloc()}}
83+
}
84+
85+
void test_getline_static() {
86+
static size_t n = 10;
87+
static char buffer[10];
88+
char *ptr = buffer;
89+
90+
FILE *F1 = tmpfile();
91+
if (!F1)
92+
return;
93+
94+
getline(&ptr, &n, F1); // expected-warning {{Argument to getline() is the address of the static variable 'buffer', which is not memory allocated by malloc()}}
95+
}

0 commit comments

Comments
 (0)