Fix reference cycle between controller and data store in Matter.framework. (#33263)

* Fix reference cycle between controller and data store in Matter.framework.

Also adds some leak detection to unit tests in CI that would have caught this.

* Address review comment.

* Disable the leak checks for now, because we are getting unrelated positives.
diff --git a/.github/workflows/darwin.yaml b/.github/workflows/darwin.yaml
index dcc025b..62a8d08 100644
--- a/.github/workflows/darwin.yaml
+++ b/.github/workflows/darwin.yaml
@@ -83,6 +83,9 @@
                           -enableUndefinedBehaviorSanitizer YES
                     - flavor: tsan
                       arguments: -enableThreadSanitizer YES
+                    # "leaks" does not seem to be very compatible with asan or tsan
+                    - flavor: leaks
+                      defines: ENABLE_LEAK_DETECTION=1
         steps:
             - name: Checkout
               uses: actions/checkout@v4
diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm
index 38f0bbc..1f515f2 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm
+++ b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm
@@ -22,6 +22,7 @@
 
 #include <lib/core/CASEAuthTag.h>
 #include <lib/core/NodeId.h>
+#include <lib/support/CodeUtils.h>
 #include <lib/support/SafeInt.h>
 
 // FIXME: Are these good key strings? https://github.com/project-chip/connectedhomeip/issues/28973
@@ -106,7 +107,8 @@
 @implementation MTRDeviceControllerDataStore {
     id<MTRDeviceControllerStorageDelegate> _storageDelegate;
     dispatch_queue_t _storageDelegateQueue;
-    MTRDeviceController * _controller;
+    // Controller owns us, so we have to make sure to not keep it alive.
+    __weak MTRDeviceController * _controller;
     // Array of nodes with resumption info, oldest-stored first.
     NSMutableArray<NSNumber *> * _nodesWithResumptionInfo;
 }
@@ -126,7 +128,9 @@
     __block id resumptionNodeList;
     dispatch_sync(_storageDelegateQueue, ^{
         @autoreleasepool {
-            resumptionNodeList = [_storageDelegate controller:_controller
+            // NOTE: controller, not our weak ref, since we know it's still
+            // valid under this sync dispatch.
+            resumptionNodeList = [_storageDelegate controller:controller
                                                   valueForKey:sResumptionNodeListKey
                                                 securityLevel:MTRStorageSecurityLevelSecure
                                                   sharingType:MTRStorageSharingTypeNotShared];
@@ -154,9 +158,12 @@
 - (void)fetchAttributeDataForAllDevices:(MTRDeviceControllerDataStoreClusterDataHandler)clusterDataHandler
 {
     __block NSDictionary<NSString *, id> * dataStoreSecureLocalValues = nil;
+    MTRDeviceController * controller = _controller;
+    VerifyOrReturn(controller != nil); // No way to call delegate without controller.
+
     dispatch_sync(_storageDelegateQueue, ^{
         if ([self->_storageDelegate respondsToSelector:@selector(valuesForController:securityLevel:sharingType:)]) {
-            dataStoreSecureLocalValues = [self->_storageDelegate valuesForController:self->_controller securityLevel:MTRStorageSecurityLevelSecure sharingType:MTRStorageSharingTypeNotShared];
+            dataStoreSecureLocalValues = [self->_storageDelegate valuesForController:controller securityLevel:MTRStorageSecurityLevelSecure sharingType:MTRStorageSharingTypeNotShared];
         }
     });
 
@@ -177,24 +184,27 @@
 
 - (void)storeResumptionInfo:(MTRCASESessionResumptionInfo *)resumptionInfo
 {
+    MTRDeviceController * controller = _controller;
+    VerifyOrReturn(controller != nil); // No way to call delegate without controller.
+
     auto * oldInfo = [self findResumptionInfoByNodeID:resumptionInfo.nodeID];
     dispatch_sync(_storageDelegateQueue, ^{
         if (oldInfo != nil) {
             // Remove old resumption id key.  No need to do that for the
             // node id, because we are about to overwrite it.
-            [_storageDelegate controller:_controller
+            [_storageDelegate controller:controller
                        removeValueForKey:ResumptionByResumptionIDKey(oldInfo.resumptionID)
                            securityLevel:MTRStorageSecurityLevelSecure
                              sharingType:MTRStorageSharingTypeNotShared];
             [_nodesWithResumptionInfo removeObject:resumptionInfo.nodeID];
         }
 
-        [_storageDelegate controller:_controller
+        [_storageDelegate controller:controller
                           storeValue:resumptionInfo
                               forKey:ResumptionByNodeIDKey(resumptionInfo.nodeID)
                        securityLevel:MTRStorageSecurityLevelSecure
                          sharingType:MTRStorageSharingTypeNotShared];
-        [_storageDelegate controller:_controller
+        [_storageDelegate controller:controller
                           storeValue:resumptionInfo
                               forKey:ResumptionByResumptionIDKey(resumptionInfo.resumptionID)
                        securityLevel:MTRStorageSecurityLevelSecure
@@ -202,7 +212,7 @@
 
         // Update our resumption info node list.
         [_nodesWithResumptionInfo addObject:resumptionInfo.nodeID];
-        [_storageDelegate controller:_controller
+        [_storageDelegate controller:controller
                           storeValue:[_nodesWithResumptionInfo copy]
                               forKey:sResumptionNodeListKey
                        securityLevel:MTRStorageSecurityLevelSecure
@@ -212,17 +222,20 @@
 
 - (void)clearAllResumptionInfo
 {
+    MTRDeviceController * controller = _controller;
+    VerifyOrReturn(controller != nil); // No way to call delegate without controller.
+
     // Can we do less dispatch?  We would need to have a version of
     // _findResumptionInfoWithKey that assumes we are already on the right queue.
     for (NSNumber * nodeID in _nodesWithResumptionInfo) {
         auto * oldInfo = [self findResumptionInfoByNodeID:nodeID];
         if (oldInfo != nil) {
             dispatch_sync(_storageDelegateQueue, ^{
-                [_storageDelegate controller:_controller
+                [_storageDelegate controller:controller
                            removeValueForKey:ResumptionByResumptionIDKey(oldInfo.resumptionID)
                                securityLevel:MTRStorageSecurityLevelSecure
                                  sharingType:MTRStorageSharingTypeNotShared];
-                [_storageDelegate controller:_controller
+                [_storageDelegate controller:controller
                            removeValueForKey:ResumptionByNodeIDKey(oldInfo.nodeID)
                                securityLevel:MTRStorageSecurityLevelSecure
                                  sharingType:MTRStorageSharingTypeNotShared];
@@ -235,9 +248,12 @@
 
 - (CHIP_ERROR)storeLastLocallyUsedNOC:(MTRCertificateTLVBytes)noc
 {
+    MTRDeviceController * controller = _controller;
+    VerifyOrReturnError(controller != nil, CHIP_ERROR_PERSISTED_STORAGE_FAILED); // No way to call delegate without controller.
+
     __block BOOL ok;
     dispatch_sync(_storageDelegateQueue, ^{
-        ok = [_storageDelegate controller:_controller
+        ok = [_storageDelegate controller:controller
                                storeValue:noc
                                    forKey:sLastLocallyUsedNOCKey
                             securityLevel:MTRStorageSecurityLevelSecure
@@ -248,10 +264,13 @@
 
 - (MTRCertificateTLVBytes _Nullable)fetchLastLocallyUsedNOC
 {
+    MTRDeviceController * controller = _controller;
+    VerifyOrReturnValue(controller != nil, nil); // No way to call delegate without controller.
+
     __block id data;
     dispatch_sync(_storageDelegateQueue, ^{
         @autoreleasepool {
-            data = [_storageDelegate controller:_controller
+            data = [_storageDelegate controller:controller
                                     valueForKey:sLastLocallyUsedNOCKey
                                   securityLevel:MTRStorageSecurityLevelSecure
                                     sharingType:MTRStorageSharingTypeNotShared];
@@ -271,6 +290,9 @@
 
 - (nullable MTRCASESessionResumptionInfo *)_findResumptionInfoWithKey:(nullable NSString *)key
 {
+    MTRDeviceController * controller = _controller;
+    VerifyOrReturnValue(controller != nil, nil); // No way to call delegate without controller.
+
     // key could be nil if [NSString stringWithFormat] returns nil for some reason.
     if (key == nil) {
         return nil;
@@ -279,7 +301,7 @@
     __block id resumptionInfo;
     dispatch_sync(_storageDelegateQueue, ^{
         @autoreleasepool {
-            resumptionInfo = [_storageDelegate controller:_controller
+            resumptionInfo = [_storageDelegate controller:controller
                                               valueForKey:key
                                             securityLevel:MTRStorageSecurityLevelSecure
                                               sharingType:MTRStorageSharingTypeNotShared];
@@ -318,9 +340,12 @@
 
 - (id)_fetchAttributeCacheValueForKey:(NSString *)key expectedClass:(Class)expectedClass;
 {
+    MTRDeviceController * controller = _controller;
+    VerifyOrReturnValue(controller != nil, nil); // No way to call delegate without controller.
+
     id data;
     @autoreleasepool {
-        data = [_storageDelegate controller:_controller
+        data = [_storageDelegate controller:controller
                                 valueForKey:key
                               securityLevel:MTRStorageSecurityLevelSecure
                                 sharingType:MTRStorageSharingTypeNotShared];
@@ -338,7 +363,10 @@
 
 - (BOOL)_storeAttributeCacheValue:(id)value forKey:(NSString *)key
 {
-    return [_storageDelegate controller:_controller
+    MTRDeviceController * controller = _controller;
+    VerifyOrReturnValue(controller != nil, NO); // No way to call delegate without controller.
+
+    return [_storageDelegate controller:controller
                              storeValue:value
                                  forKey:key
                           securityLevel:MTRStorageSecurityLevelSecure
@@ -347,7 +375,10 @@
 
 - (BOOL)_bulkStoreAttributeCacheValues:(NSDictionary<NSString *, id<NSSecureCoding>> *)values
 {
-    return [_storageDelegate controller:_controller
+    MTRDeviceController * controller = _controller;
+    VerifyOrReturnValue(controller != nil, NO); // No way to call delegate without controller.
+
+    return [_storageDelegate controller:controller
                             storeValues:values
                           securityLevel:MTRStorageSecurityLevelSecure
                             sharingType:MTRStorageSharingTypeNotShared];
@@ -355,7 +386,10 @@
 
 - (BOOL)_removeAttributeCacheValueForKey:(NSString *)key
 {
-    return [_storageDelegate controller:_controller
+    MTRDeviceController * controller = _controller;
+    VerifyOrReturnValue(controller != nil, NO); // No way to call delegate without controller.
+
+    return [_storageDelegate controller:controller
                       removeValueForKey:key
                           securityLevel:MTRStorageSecurityLevelSecure
                             sharingType:MTRStorageSharingTypeNotShared];
diff --git a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m
index cce918d..3c6ebfd 100644
--- a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m
@@ -16,14 +16,12 @@
 
 #import <Matter/Matter.h>
 
-// system dependencies
-#import <XCTest/XCTest.h>
-
 #import "MTRDeviceControllerLocalTestStorage.h"
 #import "MTRDeviceTestDelegate.h"
 #import "MTRDevice_Internal.h"
 #import "MTRErrorTestUtils.h"
 #import "MTRFabricInfoChecker.h"
+#import "MTRTestCase.h"
 #import "MTRTestDeclarations.h"
 #import "MTRTestKeys.h"
 #import "MTRTestPerControllerStorage.h"
@@ -177,7 +175,7 @@
 
 @end
 
-@interface MTRPerControllerStorageTests : XCTestCase
+@interface MTRPerControllerStorageTests : MTRTestCase
 @end
 
 @implementation MTRPerControllerStorageTests {
@@ -353,6 +351,8 @@
 
 - (void)test001_BasicControllerStartup
 {
+    self.detectLeaks = YES;
+
     __auto_type * factory = [MTRDeviceControllerFactory sharedInstance];
     XCTAssertNotNil(factory);
 
@@ -401,6 +401,8 @@
 
 - (void)test002_TryStartingTwoControllersWithSameNodeID
 {
+    self.detectLeaks = YES;
+
     __auto_type * rootKeys = [[MTRTestKeys alloc] init];
     XCTAssertNotNil(rootKeys);
 
diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.h b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.h
new file mode 100644
index 0000000..d12ea0a
--- /dev/null
+++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.h
@@ -0,0 +1,28 @@
+/**
+ *    Copyright (c) 2024 Project CHIP Authors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#import <XCTest/XCTest.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MTRTestCase : XCTestCase
+// It would be nice to do the leak-detection automatically, but running "leaks"
+// on every single sub-test is slow, and some of our tests seem to have leaks
+// outside Matter.framework.  So have it be opt-in for now, and improve later.
+@property (nonatomic) BOOL detectLeaks;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.mm b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.mm
new file mode 100644
index 0000000..b4cf92b
--- /dev/null
+++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestCase.mm
@@ -0,0 +1,44 @@
+/**
+ *    Copyright (c) 2024 Project CHIP Authors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#include <stdlib.h>
+#include <unistd.h>
+
+#import "MTRTestCase.h"
+
+@implementation MTRTestCase
+
+/**
+ * Unfortunately, doing this in "+ (void)tearDown" (the global suite teardown)
+ * does not trigger a test failure even if the XCTAssertEqual fails.
+ */
+- (void)tearDown
+{
+#if 0
+#if defined(ENABLE_LEAK_DETECTION) && ENABLE_LEAK_DETECTION
+    if (_detectLeaks) {
+        int pid = getpid();
+        __auto_type * cmd = [NSString stringWithFormat:@"leaks %d", pid];
+        int ret = system(cmd.UTF8String);
+        XCTAssertEqual(ret, 0, "LEAKS DETECTED");
+    }
+#endif
+#endif
+
+    [super tearDown];
+}
+
+@end
diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
index 03b0b10..be17129 100644
--- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
+++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
@@ -139,6 +139,7 @@
 		512431282BA0C8BF000BC136 /* SetMRPParametersCommand.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5124311C2BA0C09A000BC136 /* SetMRPParametersCommand.mm */; };
 		512431292BA0C8BF000BC136 /* ResetMRPParametersCommand.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5124311A2BA0C09A000BC136 /* ResetMRPParametersCommand.mm */; };
 		5129BCFD26A9EE3300122DDF /* MTRError.h in Headers */ = {isa = PBXBuildFile; fileRef = 5129BCFC26A9EE3300122DDF /* MTRError.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		5131BF662BE2E1B000D5D6BC /* MTRTestCase.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5131BF642BE2E1B000D5D6BC /* MTRTestCase.mm */; };
 		51339B1F2A0DA64D00C798C1 /* MTRCertificateValidityTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 51339B1E2A0DA64D00C798C1 /* MTRCertificateValidityTests.m */; };
 		5136661328067D550025EDAE /* MTRDeviceController_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 5136660F28067D540025EDAE /* MTRDeviceController_Internal.h */; };
 		5136661428067D550025EDAE /* MTRDeviceControllerFactory.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5136661028067D540025EDAE /* MTRDeviceControllerFactory.mm */; };
@@ -549,6 +550,8 @@
 		5124311B2BA0C09A000BC136 /* SetMRPParametersCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SetMRPParametersCommand.h; sourceTree = "<group>"; };
 		5124311C2BA0C09A000BC136 /* SetMRPParametersCommand.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SetMRPParametersCommand.mm; sourceTree = "<group>"; };
 		5129BCFC26A9EE3300122DDF /* MTRError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRError.h; sourceTree = "<group>"; };
+		5131BF642BE2E1B000D5D6BC /* MTRTestCase.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRTestCase.mm; sourceTree = "<group>"; };
+		5131BF652BE2E1B000D5D6BC /* MTRTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRTestCase.h; sourceTree = "<group>"; };
 		51339B1E2A0DA64D00C798C1 /* MTRCertificateValidityTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRCertificateValidityTests.m; sourceTree = "<group>"; };
 		5136660F28067D540025EDAE /* MTRDeviceController_Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRDeviceController_Internal.h; sourceTree = "<group>"; };
 		5136661028067D540025EDAE /* MTRDeviceControllerFactory.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRDeviceControllerFactory.mm; sourceTree = "<group>"; };
@@ -1119,6 +1122,8 @@
 		51189FC72A33ACE900184508 /* TestHelpers */ = {
 			isa = PBXGroup;
 			children = (
+				5131BF652BE2E1B000D5D6BC /* MTRTestCase.h */,
+				5131BF642BE2E1B000D5D6BC /* MTRTestCase.mm */,
 				D437613F285BDC0D0051FEA2 /* MTRTestKeys.h */,
 				51C8E3F72825CDB600D47D00 /* MTRTestKeys.m */,
 				D4376140285BDC0D0051FEA2 /* MTRTestStorage.h */,
@@ -1974,6 +1979,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				51742B4E29CB6B88009974FE /* MTRPairingTests.m in Sources */,
+				5131BF662BE2E1B000D5D6BC /* MTRTestCase.mm in Sources */,
 				51669AF02913204400F4AA36 /* MTRBackwardsCompatTests.m in Sources */,
 				51D10D2E2808E2CA00E8CA3D /* MTRTestStorage.m in Sources */,
 				51D0B12A2B61766F006E3511 /* MTRServerEndpointTests.m in Sources */,