Skip to content

Commit 5f5a84e

Browse files
[clang][analyzer] Clear ObjCMethodCall's cache between runs (#161327)
`lookupRuntimeDefinition` assumed that a process would handle only one TU. This is not true for unit tests, for instance. Multiple snippets of code get parsed, and their AST are unloaded each time. Since the cache relies on pointers as keys, if the same address happens to be reused between runs, the cache would return a stale pointer, potentially causing a segmentation fault. This is not that unlikely if the snippets are similar, which would trigger similar allocation patterns. CPP-4889
1 parent 0b0dcf8 commit 5f5a84e

File tree

3 files changed

+59
-15
lines changed

3 files changed

+59
-15
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1414,7 +1414,7 @@ class CallEventManager {
14141414
}
14151415

14161416
public:
1417-
CallEventManager(llvm::BumpPtrAllocator &alloc) : Alloc(alloc) {}
1417+
CallEventManager(llvm::BumpPtrAllocator &alloc);
14181418

14191419
/// Gets an outside caller given a callee context.
14201420
CallEventRef<> getCaller(const StackFrameContext *CalleeCtx,

clang/lib/StaticAnalyzer/Core/CallEvent.cpp

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,6 +1254,15 @@ template <> struct DenseMapInfo<PrivateMethodKey> {
12541254
};
12551255
} // end namespace llvm
12561256

1257+
// NOTE: This cache is a "global" variable, and it is cleared by
1258+
// CallEventManager's constructor so we do not keep old entries when
1259+
// loading/unloading ASTs. If we are worried about concurrency, we may need to
1260+
// revisit this someday. In terms of memory, this table stays around until clang
1261+
// quits, which also may be bad if we need to release memory.
1262+
using PrivateMethodCacheTy =
1263+
llvm::DenseMap<PrivateMethodKey, std::optional<const ObjCMethodDecl *>>;
1264+
static PrivateMethodCacheTy PrivateMethodCache;
1265+
12571266
static const ObjCMethodDecl *
12581267
lookupRuntimeDefinition(const ObjCInterfaceDecl *Interface,
12591268
Selector LookupSelector, bool InstanceMethod) {
@@ -1262,21 +1271,8 @@ lookupRuntimeDefinition(const ObjCInterfaceDecl *Interface,
12621271
// that repeated queries on the same ObjCIntefaceDecl and Selector
12631272
// don't incur the same cost. On some test cases, we can see the
12641273
// same query being issued thousands of times.
1265-
//
1266-
// NOTE: This cache is essentially a "global" variable, but it
1267-
// only gets lazily created when we get here. The value of the
1268-
// cache probably comes from it being global across ExprEngines,
1269-
// where the same queries may get issued. If we are worried about
1270-
// concurrency, or possibly loading/unloading ASTs, etc., we may
1271-
// need to revisit this someday. In terms of memory, this table
1272-
// stays around until clang quits, which also may be bad if we
1273-
// need to release memory.
1274-
using PrivateMethodCache =
1275-
llvm::DenseMap<PrivateMethodKey, std::optional<const ObjCMethodDecl *>>;
1276-
1277-
static PrivateMethodCache PMC;
12781274
std::optional<const ObjCMethodDecl *> &Val =
1279-
PMC[{Interface, LookupSelector, InstanceMethod}];
1275+
PrivateMethodCache[{Interface, LookupSelector, InstanceMethod}];
12801276

12811277
// Query lookupPrivateMethod() if the cache does not hit.
12821278
if (!Val) {
@@ -1422,6 +1418,13 @@ void ObjCMethodCall::getInitialStackFrameContents(
14221418
}
14231419
}
14241420

1421+
CallEventManager::CallEventManager(llvm::BumpPtrAllocator &alloc)
1422+
: Alloc(alloc) {
1423+
// Clear the method cache to avoid hits when multiple AST are loaded/unloaded
1424+
// within a single process. This can happen with unit tests, for instance.
1425+
PrivateMethodCache.clear();
1426+
}
1427+
14251428
CallEventRef<>
14261429
CallEventManager::getSimpleCall(const CallExpr *CE, ProgramStateRef State,
14271430
const LocationContext *LCtx,

clang/unittests/StaticAnalyzer/CallEventTest.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,47 @@ TEST(CXXDeallocatorCall, SimpleDestructor) {
8484
#endif
8585
}
8686

87+
TEST(PrivateMethodCache, NeverReturnDanglingPointersWithMultipleASTs) {
88+
// Each iteration will load and unload an AST multiple times. Since the code
89+
// is always the same, we increase the chance of hitting a bug in the private
90+
// method cache, returning a dangling pointer and crashing the process. If the
91+
// cache is properly cleared between runs, the test should pass.
92+
for (int I = 0; I < 100; ++I) {
93+
auto const *Code = R"(
94+
typedef __typeof(sizeof(int)) size_t;
95+
96+
extern void *malloc(size_t size);
97+
extern void *memcpy(void *dest, const void *src, size_t n);
98+
99+
@interface SomeMoreData {
100+
char const* _buffer;
101+
int _size;
102+
}
103+
@property(nonatomic, readonly) const char* buffer;
104+
@property(nonatomic) int size;
105+
106+
- (void)appendData:(SomeMoreData*)other;
107+
108+
@end
109+
110+
@implementation SomeMoreData
111+
@synthesize size = _size;
112+
@synthesize buffer = _buffer;
113+
114+
- (void)appendData:(SomeMoreData*)other {
115+
int const len = (_size + other.size); // implicit self._length
116+
char* d = malloc(sizeof(char) * len);
117+
memcpy(d + 20, other.buffer, len);
118+
}
119+
120+
@end
121+
)";
122+
std::string Diags;
123+
EXPECT_TRUE(runCheckerOnCodeWithArgs<addCXXDeallocatorChecker>(
124+
Code, {"-x", "objective-c", "-Wno-objc-root-class"}, Diags));
125+
}
126+
}
127+
87128
} // namespace
88129
} // namespace ento
89130
} // namespace clang

0 commit comments

Comments
 (0)