Skip to content

[alpha.webkit.RetainPtrCtorAdoptChecker] Support adopt(cast(copy(~)) #132316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 10, 2025

Conversation

rniwa
Copy link
Contributor

@rniwa rniwa commented Mar 21, 2025

This PR adds the support for recognizing calling adoptCF/adoptNS on the result of a cast operation on the return value of a function which creates NS or CF types. It also fixes a bug that we weren't reporting memory leaks when CF types are created without ever calling RetainPtr's constructor, adoptCF, or adoptNS.

To do this, this PR adds a new mechanism to report a memory leak whenever create or copy CF functions are invoked unless this CallExpr has already been visited while validating a call to adoptCF. Also added an early exit when isOwned returns IsOwnedResult::Skip due to an unresolved template argument.

This PR adds the support for recognizing calling adoptCF/adoptNS on the result of a cast
operation on the return value of a function which creates NS or CF types. It also fixes
a bug that we weren't reporting memory leaks when CF types are created without ever
calling RetainPtr's constructor, adoptCF, or adoptNS.

To do this, this PR adds a new mechanism to report a memory leak whenever create or copy
CF functions are invoked unless this CallExpr has already been visited while validating
a call to adoptCF. Also added an early exit when isOwned returns IsOwnedResult::Skip due
to an unresolved template argument.
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:static analyzer labels Mar 21, 2025
@llvmbot
Copy link
Member

llvmbot commented Mar 21, 2025

@llvm/pr-subscribers-clang-static-analyzer-1

@llvm/pr-subscribers-clang

Author: Ryosuke Niwa (rniwa)

Changes

This PR adds the support for recognizing calling adoptCF/adoptNS on the result of a cast operation on the return value of a function which creates NS or CF types. It also fixes a bug that we weren't reporting memory leaks when CF types are created without ever calling RetainPtr's constructor, adoptCF, or adoptNS.

To do this, this PR adds a new mechanism to report a memory leak whenever create or copy CF functions are invoked unless this CallExpr has already been visited while validating a call to adoptCF. Also added an early exit when isOwned returns IsOwnedResult::Skip due to an unresolved template argument.


Full diff: https://github.com/llvm/llvm-project/pull/132316.diff

4 Files Affected:

  • (modified) clang/lib/StaticAnalyzer/Checkers/WebKit/RetainPtrCtorAdoptChecker.cpp (+56-14)
  • (modified) clang/test/Analysis/Checkers/WebKit/objc-mock-types.h (+70-2)
  • (modified) clang/test/Analysis/Checkers/WebKit/retain-ptr-ctor-adopt-use-arc.mm (+7-2)
  • (modified) clang/test/Analysis/Checkers/WebKit/retain-ptr-ctor-adopt-use.mm (+7-2)
diff --git a/clang/lib/StaticAnalyzer/Checkers/WebKit/RetainPtrCtorAdoptChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/WebKit/RetainPtrCtorAdoptChecker.cpp
index 4ce3262e90e13..2d9620cc8ee5e 100644
--- a/clang/lib/StaticAnalyzer/Checkers/WebKit/RetainPtrCtorAdoptChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/WebKit/RetainPtrCtorAdoptChecker.cpp
@@ -34,6 +34,7 @@ class RetainPtrCtorAdoptChecker
   mutable BugReporter *BR;
   mutable std::unique_ptr<RetainSummaryManager> Summaries;
   mutable llvm::DenseSet<const ValueDecl *> CreateOrCopyOutArguments;
+  mutable llvm::DenseSet<const Expr *> CreateOrCopyFnCall;
   mutable RetainTypeChecker RTC;
 
 public:
@@ -119,7 +120,7 @@ class RetainPtrCtorAdoptChecker
       return;
 
     if (!isAdoptFn(F) || !CE->getNumArgs()) {
-      rememberOutArguments(CE, F);
+      checkCreateOrCopyFunction(CE, F, DeclWithIssue);
       return;
     }
 
@@ -128,24 +129,30 @@ class RetainPtrCtorAdoptChecker
     auto Name = safeGetName(F);
     if (Result == IsOwnedResult::Unknown)
       Result = IsOwnedResult::NotOwned;
-    if (Result == IsOwnedResult::NotOwned && !isAllocInit(Arg) &&
-        !isCreateOrCopy(Arg)) {
-      if (auto *DRE = dyn_cast<DeclRefExpr>(Arg)) {
-        if (CreateOrCopyOutArguments.contains(DRE->getDecl()))
-          return;
-      }
-      if (RTC.isARCEnabled() && isAdoptNS(F))
-        reportUseAfterFree(Name, CE, DeclWithIssue, "when ARC is disabled");
-      else
-        reportUseAfterFree(Name, CE, DeclWithIssue);
+    if (isAllocInit(Arg) || isCreateOrCopy(Arg)) {
+      CreateOrCopyFnCall.insert(Arg); // Avoid double reporting.
+      return;
+    }
+    if (Result == IsOwnedResult::Owned || Result == IsOwnedResult::Skip)
+      return;
+
+    if (auto *DRE = dyn_cast<DeclRefExpr>(Arg)) {
+      if (CreateOrCopyOutArguments.contains(DRE->getDecl()))
+        return;
     }
+    if (RTC.isARCEnabled() && isAdoptNS(F))
+      reportUseAfterFree(Name, CE, DeclWithIssue, "when ARC is disabled");
+    else
+      reportUseAfterFree(Name, CE, DeclWithIssue);
   }
 
-  void rememberOutArguments(const CallExpr *CE,
-                            const FunctionDecl *Callee) const {
+  void checkCreateOrCopyFunction(const CallExpr *CE,
+                                 const FunctionDecl *Callee,
+                                 const Decl *DeclWithIssue) const {
     if (!isCreateOrCopyFunction(Callee))
       return;
 
+    bool hasOutArgument = false;
     unsigned ArgCount = CE->getNumArgs();
     for (unsigned ArgIndex = 0; ArgIndex < ArgCount; ++ArgIndex) {
       auto *Arg = CE->getArg(ArgIndex)->IgnoreParenCasts();
@@ -164,7 +171,10 @@ class RetainPtrCtorAdoptChecker
       if (!Decl)
         continue;
       CreateOrCopyOutArguments.insert(Decl);
+      hasOutArgument = true;
     }
+    if (!hasOutArgument && !CreateOrCopyFnCall.contains(CE))
+      reportLeak(CE, DeclWithIssue);
   }
 
   void visitConstructExpr(const CXXConstructExpr *CE,
@@ -190,6 +200,13 @@ class RetainPtrCtorAdoptChecker
     std::string Name = "RetainPtr constructor";
     auto *Arg = CE->getArg(0)->IgnoreParenCasts();
     auto Result = isOwned(Arg);
+
+    if (isCreateOrCopy(Arg))
+      CreateOrCopyFnCall.insert(Arg); // Avoid double reporting.
+    
+    if (Result == IsOwnedResult::Skip)
+      return;
+
     if (Result == IsOwnedResult::Unknown)
       Result = IsOwnedResult::NotOwned;
     if (Result == IsOwnedResult::Owned)
@@ -303,11 +320,22 @@ class RetainPtrCtorAdoptChecker
         if (auto *Callee = CE->getDirectCallee()) {
           if (isAdoptFn(Callee))
             return IsOwnedResult::NotOwned;
-          if (safeGetName(Callee) == "__builtin___CFStringMakeConstantString")
+          auto Name = safeGetName(Callee);
+          if (Name == "__builtin___CFStringMakeConstantString")
             return IsOwnedResult::NotOwned;
+          if ((Name == "checked_cf_cast" || Name == "dynamic_cf_cast" ||
+               Name == "checked_objc_cast" || Name == "dynamic_objc_cast") &&
+              CE->getNumArgs() == 1) {
+            E = CE->getArg(0)->IgnoreParenCasts();
+            continue;
+          }
           auto RetType = Callee->getReturnType();
           if (isRetainPtrType(RetType))
             return IsOwnedResult::NotOwned;
+          if (isCreateOrCopyFunction(Callee)) {
+            CreateOrCopyFnCall.insert(CE);
+            return IsOwnedResult::Owned;
+          }
         } else if (auto *CalleeExpr = CE->getCallee()) {
           if (isa<CXXDependentScopeMemberExpr>(CalleeExpr))
             return IsOwnedResult::Skip; // Wait for instantiation.
@@ -371,6 +399,20 @@ class RetainPtrCtorAdoptChecker
     Report->setDeclWithIssue(DeclWithIssue);
     BR->emitReport(std::move(Report));
   }
+
+  void reportLeak(const CallExpr *CE, const Decl *DeclWithIssue) const {
+    SmallString<100> Buf;
+    llvm::raw_svector_ostream Os(Buf);
+
+    Os << "The return value is +1 and results in a memory leak.";
+
+    PathDiagnosticLocation BSLoc(CE->getSourceRange().getBegin(),
+                                 BR->getSourceManager());
+    auto Report = std::make_unique<BasicBugReport>(Bug, Os.str(), BSLoc);
+    Report->addRange(CE->getSourceRange());
+    Report->setDeclWithIssue(DeclWithIssue);
+    BR->emitReport(std::move(Report));
+  }
 };
 } // namespace
 
diff --git a/clang/test/Analysis/Checkers/WebKit/objc-mock-types.h b/clang/test/Analysis/Checkers/WebKit/objc-mock-types.h
index 5bd265596a0b4..ab5e820b2c636 100644
--- a/clang/test/Analysis/Checkers/WebKit/objc-mock-types.h
+++ b/clang/test/Analysis/Checkers/WebKit/objc-mock-types.h
@@ -21,6 +21,11 @@ typedef struct CF_BRIDGED_MUTABLE_TYPE(CFRunLoopRef) __CFRunLoop * CFRunLoopRef;
 
 extern const CFAllocatorRef kCFAllocatorDefault;
 typedef struct _NSZone NSZone;
+typedef unsigned long CFTypeID;
+
+CFTypeID CFGetTypeID(CFTypeRef cf);
+
+CFTypeID CFArrayGetTypeID(void);
 CFMutableArrayRef CFArrayCreateMutable(CFAllocatorRef allocator, CFIndex capacity);
 extern void CFArrayAppendValue(CFMutableArrayRef theArray, const void *value);
 CFArrayRef CFArrayCreate(CFAllocatorRef allocator, const void **values, CFIndex numValues);
@@ -29,6 +34,7 @@ CFIndex CFArrayGetCount(CFArrayRef theArray);
 typedef const struct CF_BRIDGED_TYPE(NSDictionary) __CFDictionary * CFDictionaryRef;
 typedef struct CF_BRIDGED_MUTABLE_TYPE(NSMutableDictionary) __CFDictionary * CFMutableDictionaryRef;
 
+CFTypeID CFDictionaryGetTypeID(void);
 CFDictionaryRef CFDictionaryCreate(CFAllocatorRef allocator, const void **keys, const void **values, CFIndex numValues);
 CFDictionaryRef CFDictionaryCreateCopy(CFAllocatorRef allocator, CFDictionaryRef theDict);
 CFDictionaryRef CFDictionaryCreateMutableCopy(CFAllocatorRef allocator, CFIndex capacity, CFDictionaryRef theDict);
@@ -135,6 +141,8 @@ __attribute__((objc_root_class))
 
 namespace WTF {
 
+void WTFCrash(void);
+
 template<typename T> class RetainPtr;
 template<typename T> RetainPtr<T> adoptNS(T*);
 template<typename T> RetainPtr<T> adoptCF(T);
@@ -265,19 +273,79 @@ template<typename T> inline RetainPtr<T> retainPtr(T* ptr)
 
 inline NSObject *bridge_cast(CFTypeRef object)
 {
-    return (__bridge NSObject *)object;
+  return (__bridge NSObject *)object;
 }
 
 inline CFTypeRef bridge_cast(NSObject *object)
 {
-    return (__bridge CFTypeRef)object;
+  return (__bridge CFTypeRef)object;
+}
+
+template <typename> struct CFTypeTrait;
+
+// Use dynamic_cf_cast<> instead of checked_cf_cast<> when actively checking CF types,
+// similar to dynamic_cast<> in C++. Be sure to include a nullptr check.
+
+template<typename T> T dynamic_cf_cast(CFTypeRef object)
+{
+  if (!object)
+    return nullptr;
+
+  if (CFGetTypeID(object) != CFTypeTrait<T>::typeID())
+    return nullptr;
+
+  return static_cast<T>(const_cast<CF_BRIDGED_TYPE(id) void*>(object));
+}
+
+template<typename T, typename U> RetainPtr<T> dynamic_cf_cast(RetainPtr<U>&& object)
+{
+  if (!object)
+    return nullptr;
+
+  if (CFGetTypeID(object.get()) != CFTypeTrait<T>::typeID())
+    return nullptr;
+
+  return adoptCF(static_cast<T>(const_cast<CF_BRIDGED_TYPE(id) void*>(object.leakRef())));
 }
 
+// Use checked_cf_cast<> instead of dynamic_cf_cast<> when a specific CF type is required.
+
+template<typename T> T checked_cf_cast(CFTypeRef object)
+{
+  if (!object)
+    return nullptr;
+
+  if (CFGetTypeID(object) != CFTypeTrait<T>::typeID())
+    WTFCrash();
+
+  return static_cast<T>(const_cast<CF_BRIDGED_TYPE(id) void*>(object));
 }
 
+} // namespace WTF
+
+#define WTF_DECLARE_CF_TYPE_TRAIT(ClassName) \
+template <> \
+struct WTF::CFTypeTrait<ClassName##Ref> { \
+    static inline CFTypeID typeID(void) { return ClassName##GetTypeID(); } \
+};
+
+WTF_DECLARE_CF_TYPE_TRAIT(CFArray);
+WTF_DECLARE_CF_TYPE_TRAIT(CFDictionary);
+
+#define WTF_DECLARE_CF_MUTABLE_TYPE_TRAIT(ClassName, MutableClassName) \
+template <> \
+struct WTF::CFTypeTrait<MutableClassName##Ref> { \
+    static inline CFTypeID typeID(void) { return ClassName##GetTypeID(); } \
+};
+
+WTF_DECLARE_CF_MUTABLE_TYPE_TRAIT(CFArray, CFMutableArray);
+WTF_DECLARE_CF_MUTABLE_TYPE_TRAIT(CFDictionary, CFMutableDictionary);
+
 using WTF::RetainPtr;
 using WTF::adoptNS;
 using WTF::adoptCF;
 using WTF::retainPtr;
 using WTF::downcast;
 using WTF::bridge_cast;
+using WTF::dynamic_cf_cast;
+using WTF::checked_cf_cast;
\ No newline at end of file
diff --git a/clang/test/Analysis/Checkers/WebKit/retain-ptr-ctor-adopt-use-arc.mm b/clang/test/Analysis/Checkers/WebKit/retain-ptr-ctor-adopt-use-arc.mm
index dd7208a534ea1..c2a93358b4cdd 100644
--- a/clang/test/Analysis/Checkers/WebKit/retain-ptr-ctor-adopt-use-arc.mm
+++ b/clang/test/Analysis/Checkers/WebKit/retain-ptr-ctor-adopt-use-arc.mm
@@ -3,6 +3,8 @@
 
 #include "objc-mock-types.h"
 
+CFTypeRef CFCopyArray(CFArrayRef);
+
 void basic_correct() {
   auto ns1 = adoptNS([SomeObj alloc]);
   auto ns2 = adoptNS([[SomeObj alloc] init]);
@@ -12,6 +14,7 @@ void basic_correct() {
   auto ns6 = retainPtr([ns3 next]);
   CFMutableArrayRef cf1 = adoptCF(CFArrayCreateMutable(kCFAllocatorDefault, 10));
   auto cf2 = adoptCF(SecTaskCreateFromSelf(kCFAllocatorDefault));
+  auto cf3 = adoptCF(checked_cf_cast<CFArrayRef>(CFCopyArray(cf1)));
 }
 
 CFMutableArrayRef provide_cf();
@@ -27,6 +30,8 @@ void basic_wrong() {
   // expected-warning@-1{{Incorrect use of adoptCF. The argument is +0 and results in an use-after-free [alpha.webkit.RetainPtrCtorAdoptChecker]}}
   RetainPtr<CFTypeRef> cf3 = SecTaskCreateFromSelf(kCFAllocatorDefault);
   // expected-warning@-1{{Incorrect use of RetainPtr constructor. The argument is +1 and results in a memory leak [alpha.webkit.RetainPtrCtorAdoptChecker]}}
+  CFCopyArray(cf1);
+  // expected-warning@-1{{The return value is +1 and results in a memory leak [alpha.webkit.RetainPtrCtorAdoptChecker]}}
 }
 
 RetainPtr<CVPixelBufferRef> cf_out_argument() {
@@ -68,7 +73,7 @@ void adopt_retainptr() {
 
 class MemberInit {
 public:
-  MemberInit(CFMutableArrayRef array, NSString *str, CFRunLoopRef runLoop)
+  MemberInit(RetainPtr<CFMutableArrayRef>&& array, NSString *str, CFRunLoopRef runLoop)
     : m_array(array)
     , m_str(str)
     , m_runLoop(runLoop)
@@ -80,7 +85,7 @@ void adopt_retainptr() {
   RetainPtr<CFRunLoopRef> m_runLoop;
 };
 void create_member_init() {
-  MemberInit init { CFArrayCreateMutable(kCFAllocatorDefault, 10), @"hello", CFRunLoopGetCurrent() };
+  MemberInit init { adoptCF(CFArrayCreateMutable(kCFAllocatorDefault, 10)), @"hello", CFRunLoopGetCurrent() };
 }
 
 RetainPtr<CFStringRef> cfstr() {
diff --git a/clang/test/Analysis/Checkers/WebKit/retain-ptr-ctor-adopt-use.mm b/clang/test/Analysis/Checkers/WebKit/retain-ptr-ctor-adopt-use.mm
index 79e0bdb7c577b..c6d2c1fddb9e8 100644
--- a/clang/test/Analysis/Checkers/WebKit/retain-ptr-ctor-adopt-use.mm
+++ b/clang/test/Analysis/Checkers/WebKit/retain-ptr-ctor-adopt-use.mm
@@ -3,6 +3,8 @@
 
 #include "objc-mock-types.h"
 
+CFTypeRef CFCopyArray(CFArrayRef);
+
 void basic_correct() {
   auto ns1 = adoptNS([SomeObj alloc]);
   auto ns2 = adoptNS([[SomeObj alloc] init]);
@@ -12,6 +14,7 @@ void basic_correct() {
   auto ns6 = retainPtr([ns3 next]);
   CFMutableArrayRef cf1 = adoptCF(CFArrayCreateMutable(kCFAllocatorDefault, 10));
   auto cf2 = adoptCF(SecTaskCreateFromSelf(kCFAllocatorDefault));
+  auto cf3 = adoptCF(checked_cf_cast<CFArrayRef>(CFCopyArray(cf1)));
 }
 
 CFMutableArrayRef provide_cf();
@@ -27,6 +30,8 @@ void basic_wrong() {
   // expected-warning@-1{{Incorrect use of adoptCF. The argument is +0 and results in an use-after-free [alpha.webkit.RetainPtrCtorAdoptChecker]}}
   RetainPtr<CFTypeRef> cf3 = SecTaskCreateFromSelf(kCFAllocatorDefault);
   // expected-warning@-1{{Incorrect use of RetainPtr constructor. The argument is +1 and results in a memory leak [alpha.webkit.RetainPtrCtorAdoptChecker]}}
+  CFCopyArray(cf1);
+  // expected-warning@-1{{The return value is +1 and results in a memory leak [alpha.webkit.RetainPtrCtorAdoptChecker]}}
 }
 
 RetainPtr<CVPixelBufferRef> cf_out_argument() {
@@ -68,7 +73,7 @@ void adopt_retainptr() {
 
 class MemberInit {
 public:
-  MemberInit(CFMutableArrayRef array, NSString *str, CFRunLoopRef runLoop)
+  MemberInit(RetainPtr<CFMutableArrayRef>&& array, NSString *str, CFRunLoopRef runLoop)
     : m_array(array)
     , m_str(str)
     , m_runLoop(runLoop)
@@ -80,7 +85,7 @@ void adopt_retainptr() {
   RetainPtr<CFRunLoopRef> m_runLoop;
 };
 void create_member_init() {
-  MemberInit init { CFArrayCreateMutable(kCFAllocatorDefault, 10), @"hello", CFRunLoopGetCurrent() };
+  MemberInit init { adoptCF(CFArrayCreateMutable(kCFAllocatorDefault, 10)), @"hello", CFRunLoopGetCurrent() };
 }
 
 RetainPtr<CFStringRef> cfstr() {

Copy link

github-actions bot commented Mar 21, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@rniwa rniwa requested a review from t-rasmud March 21, 2025 01:16
rniwa added 2 commits March 21, 2025 20:43
…warning for non-retained types.

This was caused by the checkCreateOrCopyFunction not checking RTC.isUnretained. Fixed this bug by
adding the check. This in turn revealed a bug that we weren't properly recognizing CFTypeRef as
the pointee type is "void" and therefore does not have a corresponding Record object. Added a new
dense set, RecordlessTypes, to discover these types. We only use as a fallback since looping over
ElaboratedType and checking its existence in the set is slower than just checking the presence of
the canonical type in the CFPointees set.

Finally, skip the analyses of smart pointer template classes in RawPtrRefCallArgsChecker and
RawPtrRefLocalVarsChecker now that we properly recognize CFTypeRef as a CF type and this type
is used internally inside the mock RetainPtr.
Copy link
Contributor

@t-rasmud t-rasmud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@rniwa
Copy link
Contributor Author

rniwa commented Apr 10, 2025

Thanks for the review!

@rniwa rniwa merged commit c26d097 into llvm:main Apr 10, 2025
11 checks passed
@rniwa rniwa deleted the fix-adopt-cast-create branch April 10, 2025 22:26
rniwa added a commit to rniwa/llvm-project that referenced this pull request Apr 11, 2025
…lvm#132316)

This PR adds the support for recognizing calling adoptCF/adoptNS on the
result of a cast operation on the return value of a function which
creates NS or CF types. It also fixes a bug that we weren't reporting
memory leaks when CF types are created without ever calling RetainPtr's
constructor, adoptCF, or adoptNS.

To do this, this PR adds a new mechanism to report a memory leak
whenever create or copy CF functions are invoked unless this CallExpr
has already been visited while validating a call to adoptCF. Also added
an early exit when isOwned returns IsOwnedResult::Skip due to an
unresolved template argument.
var-const pushed a commit to ldionne/llvm-project that referenced this pull request Apr 17, 2025
…lvm#132316)

This PR adds the support for recognizing calling adoptCF/adoptNS on the
result of a cast operation on the return value of a function which
creates NS or CF types. It also fixes a bug that we weren't reporting
memory leaks when CF types are created without ever calling RetainPtr's
constructor, adoptCF, or adoptNS.

To do this, this PR adds a new mechanism to report a memory leak
whenever create or copy CF functions are invoked unless this CallExpr
has already been visited while validating a call to adoptCF. Also added
an early exit when isOwned returns IsOwnedResult::Skip due to an
unresolved template argument.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:static analyzer clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants