Skip to content

Commit 4c6f313

Browse files
authored
[analyzer] [MallocChecker] suspect all release functions as candidate for suppression (#104599)
Current MalloChecker logic suppresses FP caused by refcounting only for C++ destructors. The same pattern occurs a lot in C in objects with intrusive refcounting. See #104229 for code example. To extend current logic to C, suspect all release functions as candidate for suppression. Closes: #104229
1 parent b592917 commit 4c6f313

File tree

3 files changed

+188
-36
lines changed

3 files changed

+188
-36
lines changed

clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp

Lines changed: 62 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -940,16 +940,16 @@ class MallocBugVisitor final : public BugReporterVisitor {
940940
// A symbol from when the primary region should have been reallocated.
941941
SymbolRef FailedReallocSymbol;
942942

943-
// A C++ destructor stack frame in which memory was released. Used for
943+
// A release function stack frame in which memory was released. Used for
944944
// miscellaneous false positive suppression.
945-
const StackFrameContext *ReleaseDestructorLC;
945+
const StackFrameContext *ReleaseFunctionLC;
946946

947947
bool IsLeak;
948948

949949
public:
950950
MallocBugVisitor(SymbolRef S, bool isLeak = false)
951951
: Sym(S), Mode(Normal), FailedReallocSymbol(nullptr),
952-
ReleaseDestructorLC(nullptr), IsLeak(isLeak) {}
952+
ReleaseFunctionLC(nullptr), IsLeak(isLeak) {}
953953

954954
static void *getTag() {
955955
static int Tag = 0;
@@ -3653,21 +3653,25 @@ PathDiagnosticPieceRef MallocBugVisitor::VisitNode(const ExplodedNode *N,
36533653

36543654
const LocationContext *CurrentLC = N->getLocationContext();
36553655

3656-
// If we find an atomic fetch_add or fetch_sub within the destructor in which
3657-
// the pointer was released (before the release), this is likely a destructor
3658-
// of a shared pointer.
3656+
// If we find an atomic fetch_add or fetch_sub within the function in which
3657+
// the pointer was released (before the release), this is likely a release
3658+
// point of reference-counted object (like shared pointer).
3659+
//
36593660
// Because we don't model atomics, and also because we don't know that the
36603661
// original reference count is positive, we should not report use-after-frees
3661-
// on objects deleted in such destructors. This can probably be improved
3662+
// on objects deleted in such functions. This can probably be improved
36623663
// through better shared pointer modeling.
3663-
if (ReleaseDestructorLC && (ReleaseDestructorLC == CurrentLC ||
3664-
ReleaseDestructorLC->isParentOf(CurrentLC))) {
3664+
if (ReleaseFunctionLC && (ReleaseFunctionLC == CurrentLC ||
3665+
ReleaseFunctionLC->isParentOf(CurrentLC))) {
36653666
if (const auto *AE = dyn_cast<AtomicExpr>(S)) {
36663667
// Check for manual use of atomic builtins.
36673668
AtomicExpr::AtomicOp Op = AE->getOp();
36683669
if (Op == AtomicExpr::AO__c11_atomic_fetch_add ||
36693670
Op == AtomicExpr::AO__c11_atomic_fetch_sub) {
36703671
BR.markInvalid(getTag(), S);
3672+
// After report is considered invalid there is no need to proceed
3673+
// futher.
3674+
return nullptr;
36713675
}
36723676
} else if (const auto *CE = dyn_cast<CallExpr>(S)) {
36733677
// Check for `std::atomic` and such. This covers both regular method calls
@@ -3679,6 +3683,9 @@ PathDiagnosticPieceRef MallocBugVisitor::VisitNode(const ExplodedNode *N,
36793683
// "__atomic_base" or something.
36803684
if (StringRef(RD->getNameAsString()).contains("atomic")) {
36813685
BR.markInvalid(getTag(), S);
3686+
// After report is considered invalid there is no need to proceed
3687+
// futher.
3688+
return nullptr;
36823689
}
36833690
}
36843691
}
@@ -3750,35 +3757,54 @@ PathDiagnosticPieceRef MallocBugVisitor::VisitNode(const ExplodedNode *N,
37503757
return nullptr;
37513758
}
37523759

3753-
// See if we're releasing memory while inlining a destructor
3754-
// (or one of its callees). This turns on various common
3755-
// false positive suppressions.
3756-
bool FoundAnyDestructor = false;
3757-
for (const LocationContext *LC = CurrentLC; LC; LC = LC->getParent()) {
3758-
if (const auto *DD = dyn_cast<CXXDestructorDecl>(LC->getDecl())) {
3759-
if (isReferenceCountingPointerDestructor(DD)) {
3760-
// This immediately looks like a reference-counting destructor.
3761-
// We're bad at guessing the original reference count of the object,
3762-
// so suppress the report for now.
3763-
BR.markInvalid(getTag(), DD);
3764-
} else if (!FoundAnyDestructor) {
3765-
assert(!ReleaseDestructorLC &&
3766-
"There can be only one release point!");
3767-
// Suspect that it's a reference counting pointer destructor.
3768-
// On one of the next nodes might find out that it has atomic
3769-
// reference counting operations within it (see the code above),
3770-
// and if so, we'd conclude that it likely is a reference counting
3771-
// pointer destructor.
3772-
ReleaseDestructorLC = LC->getStackFrame();
3773-
// It is unlikely that releasing memory is delegated to a destructor
3774-
// inside a destructor of a shared pointer, because it's fairly hard
3775-
// to pass the information that the pointer indeed needs to be
3776-
// released into it. So we're only interested in the innermost
3777-
// destructor.
3778-
FoundAnyDestructor = true;
3760+
// Save the first destructor/function as release point.
3761+
assert(!ReleaseFunctionLC && "There should be only one release point");
3762+
ReleaseFunctionLC = CurrentLC->getStackFrame();
3763+
3764+
// See if we're releasing memory while inlining a destructor that
3765+
// decrement reference counters (or one of its callees).
3766+
// This turns on various common false positive suppressions.
3767+
for (const LocationContext *LC = CurrentLC; LC; LC = LC->getParent()) {
3768+
if (const auto *DD = dyn_cast<CXXDestructorDecl>(LC->getDecl())) {
3769+
if (isReferenceCountingPointerDestructor(DD)) {
3770+
// This immediately looks like a reference-counting destructor.
3771+
// We're bad at guessing the original reference count of the
3772+
// object, so suppress the report for now.
3773+
BR.markInvalid(getTag(), DD);
3774+
3775+
// After report is considered invalid there is no need to proceed
3776+
// futher.
3777+
return nullptr;
3778+
}
3779+
3780+
// Switch suspection to outer destructor to catch patterns like:
3781+
// (note that class name is distorted to bypass
3782+
// isReferenceCountingPointerDestructor() logic)
3783+
//
3784+
// SmartPointr::~SmartPointr() {
3785+
// if (refcount.fetch_sub(1) == 1)
3786+
// release_resources();
3787+
// }
3788+
// void SmartPointr::release_resources() {
3789+
// free(buffer);
3790+
// }
3791+
//
3792+
// This way ReleaseFunctionLC will point to outermost destructor and
3793+
// it would be possible to catch wider range of FP.
3794+
//
3795+
// NOTE: it would be great to support smth like that in C, since
3796+
// currently patterns like following won't be supressed:
3797+
//
3798+
// void doFree(struct Data *data) { free(data); }
3799+
// void putData(struct Data *data)
3800+
// {
3801+
// if (refPut(data))
3802+
// doFree(data);
3803+
// }
3804+
ReleaseFunctionLC = LC->getStackFrame();
37793805
}
37803806
}
3781-
}
3807+
37823808
} else if (isRelinquished(RSCurr, RSPrev, S)) {
37833809
Msg = "Memory ownership is transferred";
37843810
StackHint = std::make_unique<StackHintGeneratorForSymbol>(Sym, "");

clang/test/Analysis/NewDelete-atomics.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,32 @@ class DifferentlyNamed {
9797
T *getPtr() const { return Ptr; } // no-warning
9898
};
9999

100+
// Also IntrusivePtr with different name and outline release in destructor
101+
template <typename T>
102+
class DifferentlyNamedOutlineRelease {
103+
T *Ptr;
104+
105+
public:
106+
DifferentlyNamedOutlineRelease(T *Ptr) : Ptr(Ptr) {
107+
Ptr->incRef();
108+
}
109+
110+
DifferentlyNamedOutlineRelease(const DifferentlyNamedOutlineRelease &Other) : Ptr(Other.Ptr) {
111+
Ptr->incRef();
112+
}
113+
114+
void releasePtr(void) {
115+
delete Ptr;
116+
}
117+
118+
~DifferentlyNamedOutlineRelease() {
119+
if (Ptr->decRef() == 1)
120+
releasePtr();
121+
}
122+
123+
T *getPtr() const { return Ptr; } // no-warning
124+
};
125+
100126
void testDestroyLocalRefPtr() {
101127
IntrusivePtr<RawObj> p1(new RawObj());
102128
{
@@ -176,3 +202,23 @@ void testDestroyLocalRefPtrWithAtomicsDifferentlyNamed(
176202
// p1 still maintains ownership. The object is not deleted.
177203
p1.getPtr()->foo(); // no-warning
178204
}
205+
206+
void testDestroyLocalRefPtrWithOutlineRelease() {
207+
DifferentlyNamedOutlineRelease <RawObj> p1(new RawObj());
208+
{
209+
DifferentlyNamedOutlineRelease <RawObj> p2(p1);
210+
}
211+
212+
// p1 still maintains ownership. The object is not deleted.
213+
p1.getPtr()->foo(); // no-warning
214+
}
215+
216+
void testDestroySymbolicRefPtrWithOutlineRelease(
217+
const DifferentlyNamedOutlineRelease<RawObj> &p1) {
218+
{
219+
DifferentlyNamedOutlineRelease <RawObj> p2(p1);
220+
}
221+
222+
// p1 still maintains ownership. The object is not deleted.
223+
p1.getPtr()->foo(); // no-warning
224+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix.Malloc -verify %s
2+
//
3+
4+
typedef __SIZE_TYPE__ size_t;
5+
6+
typedef enum memory_order {
7+
memory_order_relaxed = __ATOMIC_RELAXED,
8+
} memory_order;
9+
10+
void *calloc(size_t, size_t);
11+
void free(void *);
12+
13+
struct SomeData {
14+
int i;
15+
_Atomic int ref;
16+
};
17+
18+
static struct SomeData *alloc_data(void)
19+
{
20+
struct SomeData *data = calloc(sizeof(*data), 1);
21+
22+
__c11_atomic_store(&data->ref, 2, memory_order_relaxed);
23+
return data;
24+
}
25+
26+
static void put_data(struct SomeData *data)
27+
{
28+
if (__c11_atomic_fetch_sub(&data->ref, 1, memory_order_relaxed) == 1)
29+
free(data);
30+
}
31+
32+
static int dec_refcounter(struct SomeData *data)
33+
{
34+
return __c11_atomic_fetch_sub(&data->ref, 1, memory_order_relaxed) == 1;
35+
}
36+
37+
static void put_data_nested(struct SomeData *data)
38+
{
39+
if (dec_refcounter(data))
40+
free(data);
41+
}
42+
43+
static void put_data_uncond(struct SomeData *data)
44+
{
45+
free(data);
46+
}
47+
48+
static void put_data_unrelated_atomic(struct SomeData *data)
49+
{
50+
free(data);
51+
__c11_atomic_fetch_sub(&data->ref, 1, memory_order_relaxed);
52+
}
53+
54+
void test_no_uaf(void)
55+
{
56+
struct SomeData *data = alloc_data();
57+
put_data(data);
58+
data->i += 1; // no warning
59+
}
60+
61+
void test_no_uaf_nested(void)
62+
{
63+
struct SomeData *data = alloc_data();
64+
put_data_nested(data);
65+
data->i += 1; // no warning
66+
}
67+
68+
void test_uaf(void)
69+
{
70+
struct SomeData *data = alloc_data();
71+
put_data_uncond(data);
72+
data->i += 1; // expected-warning{{Use of memory after it is freed}}
73+
}
74+
75+
void test_no_uaf_atomic_after(void)
76+
{
77+
struct SomeData *data = alloc_data();
78+
put_data_unrelated_atomic(data);
79+
data->i += 1; // expected-warning{{Use of memory after it is freed}}
80+
}

0 commit comments

Comments
 (0)