https://github.com/rniwa updated https://github.com/llvm/llvm-project/pull/234682
>From 20e0034dad5ef6c75808a7b304c955a3a62df726 Mon Sep 17 00:00:00 2001
From: Ryosuke Niwa <rniwa at webkit.org>
Date: Thu, 22 Oct 2026 09:27:45 -0700
Subject: [PATCH] [alpha.webkit.UnretainedLocalVarsChecker] Treat a raw pointer
collection of a fast enumeration as the origin of its element
alpha.webkit.UnretainedLocalVarsChecker still reported the element variable
of an Objective-C fast enumeration when the collection was a raw pointer
local variable or parameter, as in "NSArray *array = obj.array;" followed by
"for (T *x in array)", even though the variable itself isn't reported. This
includes a nested fast enumeration whose collection is the element of the
enclosing one, and a range-based for loop over an Objective-C collection,
which is a fast enumeration as well.
The collection of a fast enumeration is evaluated before the loop body is
entered and keeps its elements alive. A raw pointer local variable or
parameter holding the collection is checked on its own, so treat it as a
guardian of the element as long as the loop body doesn't mutate it.
Also treat passing the address of an Objective-C object pointer guardian as
a mutation of the guardian as done for other raw pointers.
Authored with Claude Code.
---
.../WebKit/RawPtrRefLocalVarsChecker.cpp | 13 ++-
.../Checkers/WebKit/unretained-local-vars.mm | 96 +++++++++++++++++++
2 files changed, 108 insertions(+), 1 deletion(-)
diff --git a/clang/lib/StaticAnalyzer/Checkers/WebKit/RawPtrRefLocalVarsChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/WebKit/RawPtrRefLocalVarsChecker.cpp
index 88c70571d1f179..2e493b0a49758a 100644
--- a/clang/lib/StaticAnalyzer/Checkers/WebKit/RawPtrRefLocalVarsChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/WebKit/RawPtrRefLocalVarsChecker.cpp
@@ -149,7 +149,7 @@ struct GuardianVisitor : DynamicRecursiveASTVisitor {
auto *VarRef = dyn_cast<DeclRefExpr>(Arg);
if (!VarRef || VarRef->getDecl() != Guardian)
return false;
- if (GuardianIsRawPtrOrRef && !Guardian->getType()->isPointerType())
+ if (GuardianIsRawPtrOrRef && !Guardian->getType()->isAnyPointerType())
return false;
if (IsAddressOf) {
if (!ArgType->isPointerType())
@@ -536,6 +536,17 @@ class RawPtrRefLocalVarsChecker
return false;
QualType GuardianType = MaybeGuardian->getType();
+ // The collection of a fast enumeration is evaluated before the loop body is
+ // entered and keeps its elements alive. A raw pointer local variable or
+ // parameter holding the collection is checked on its own, so it only needs
+ // to be left untouched by the loop body.
+ if (auto *FCS = findObjCForCollectionStmtOfElement(V);
+ FCS && !GuardianType.isNull() && GuardianType->isAnyPointerType() &&
+ (MaybeGuardian->isLocalVarDecl() || isa<ParmVarDecl>(MaybeGuardian))) {
+ GuardianVisitor Visitor(MaybeGuardian, /*GuardianIsRawPtrOrRef=*/true);
+ return Visitor.TraverseStmt(const_cast<Stmt *>(FCS->getBody()));
+ }
+
if (!GuardianType.isNull()) {
if (auto *Record = GuardianType->getAsCXXRecordDecl()) {
if (MaybeGuardian->isLocalVarDecl() &&
diff --git a/clang/test/Analysis/Checkers/WebKit/unretained-local-vars.mm b/clang/test/Analysis/Checkers/WebKit/unretained-local-vars.mm
index acec4b36947e23..c2555a14ea2a49 100644
--- a/clang/test/Analysis/Checkers/WebKit/unretained-local-vars.mm
+++ b/clang/test/Analysis/Checkers/WebKit/unretained-local-vars.mm
@@ -574,9 +574,15 @@ void ns() {
}
+ at interface ArrayProvider : NSObject
+- (NSArray *)arrayValue;
+ at end
+
namespace fast_enumeration {
NSArray *provideArray();
+void provideArrayToOutArgument(NSArray **);
+void consumeString(NSString *);
void iterateLocal() {
RetainPtr<NSArray> array = provideArray();
@@ -670,8 +676,98 @@ void iterateRawLocal() {
NSArray *array = provideArray();
// expected-warning at -1{{Local variable 'array' is a raw pointer to RetainPtr-capable type 'NSArray' [alpha.webkit.UnretainedLocalVarsChecker]}}
for (SomeObj *obj in array)
+ [obj doWork];
+}
+
+void iterateRawLocalFromMessage(ArrayProvider *provider) {
+ NSArray *array = [provider arrayValue];
+ for (SomeObj *item in array)
+ [item doWork];
+}
+
+void iterateRawLocalFromMessageWithRangeBasedFor(ArrayProvider *provider) {
+ NSArray *array = [provider arrayValue];
+ for (SomeObj *item : array)
+ [item doWork];
+}
+
+void iterateRawLocalFromOutArgument() {
+ NSArray *array = nil;
+ provideArrayToOutArgument(&array);
+ for (SomeObj *obj in array)
+ [obj doWork];
+}
+
+void iterateRawLocalAssignedBeforeLoop(ArrayProvider *provider) {
+ NSArray *array = [provider arrayValue];
+ // expected-warning at -1{{Local variable 'array' is a raw pointer to RetainPtr-capable type 'NSArray' [alpha.webkit.UnretainedLocalVarsChecker]}}
+ array = provideArray();
+ for (SomeObj *item in array)
+ [item doWork];
+}
+
+void iterateRawLocalClearedInBody(ArrayProvider *provider) {
+ NSArray *array = [provider arrayValue];
+ for (SomeObj *item in array) {
+ // expected-warning at -1{{Local variable 'item' is a raw pointer to RetainPtr-capable type 'SomeObj' [alpha.webkit.UnretainedLocalVarsChecker]}}
+ array = nil;
+ [item doWork];
+ }
+}
+
+void iterateRawLocalOverwrittenViaOutArgumentInBody(ArrayProvider *provider) {
+ NSArray *array = [provider arrayValue];
+ for (SomeObj *item in array) {
+ // expected-warning at -1{{Local variable 'item' is a raw pointer to RetainPtr-capable type 'SomeObj' [alpha.webkit.UnretainedLocalVarsChecker]}}
+ provideArrayToOutArgument(&array);
+ [item doWork];
+ }
+}
+
+void iterateStaticRawLocal() {
+ static NSArray *array = @[ @"foo" ];
+ for (NSString *str in array)
+ consumeString(str);
+}
+
+void iterateParamWithIndirectCallOutsideBody(NSArray *array, void (*handler)()) {
+ handler();
+ for (SomeObj *obj in array)
+ [obj doWork];
+}
+
+void iterateParamOverwrittenViaOutArgumentInBody(NSArray *array) {
+ for (SomeObj *obj in array) {
// expected-warning at -1{{Local variable 'obj' is a raw pointer to RetainPtr-capable type 'SomeObj' [alpha.webkit.UnretainedLocalVarsChecker]}}
+ provideArrayToOutArgument(&array);
[obj doWork];
+ }
+}
+
+void iterateNested(NSArray *array) {
+ for (NSArray *inner in array) {
+ for (SomeObj *obj in inner)
+ [obj doWork];
+ }
+}
+
+void iterateNestedInLocal() {
+ RetainPtr<NSArray> array = provideArray();
+ for (NSArray *inner in array.get()) {
+ for (SomeObj *obj in inner)
+ [obj doWork];
+ }
+}
+
+void iterateNestedOuterElementReassignedInInnerBody(NSArray *array) {
+ for (NSArray *inner in array) {
+ // expected-warning at -1{{Local variable 'inner' is a raw pointer to RetainPtr-capable type 'NSArray' [alpha.webkit.UnretainedLocalVarsChecker]}}
+ for (SomeObj *obj in inner) {
+ // expected-warning at -1{{Local variable 'obj' is a raw pointer to RetainPtr-capable type 'SomeObj' [alpha.webkit.UnretainedLocalVarsChecker]}}
+ inner = provideArray();
+ [obj doWork];
+ }
+ }
}
} // namespace fast_enumeration