Make MTRServerAttribute threadsafe. (#31970)

* Make MTRServerAttribute threadsafe.

If two API clients are both touching the same instance of MTRServerAttribute on
different threads, we should handle that correctly.

* Address review comments.
diff --git a/src/darwin/Framework/CHIP/MTRUnfairLock.h b/src/darwin/Framework/CHIP/MTRUnfairLock.h
new file mode 100644
index 0000000..c046ee6
--- /dev/null
+++ b/src/darwin/Framework/CHIP/MTRUnfairLock.h
@@ -0,0 +1,37 @@
+/**
+ *    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.
+ */
+
+/**
+ * RAII wrapper around os_unfair_lock.
+ */
+
+#import <os/lock.h>
+
+#include <mutex>
+
+template <>
+class std::lock_guard<os_unfair_lock>
+{
+public:
+    explicit lock_guard(os_unfair_lock & lock) : mLock(lock) { os_unfair_lock_lock(&mLock); }
+    ~lock_guard() { os_unfair_lock_unlock(&mLock); }
+
+    lock_guard(const lock_guard &)     = delete;
+    void operator=(const lock_guard &) = delete;
+
+private:
+    os_unfair_lock & mLock;
+};
diff --git a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute.mm b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute.mm
index 5a69c10..6b9ced0 100644
--- a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute.mm
+++ b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute.mm
@@ -20,7 +20,9 @@
 #import "MTRLogging_Internal.h"
 #import "MTRServerAttribute_Internal.h"
 #import "MTRServerEndpoint_Internal.h"
+#import "MTRUnfairLock.h"
 #import "NSDataSpanConversion.h"
+
 #import <Matter/MTRServerAttribute.h>
 
 #include <app/reporting/reporting.h>
@@ -32,7 +34,14 @@
 
 MTR_DIRECT_MEMBERS
 @implementation MTRServerAttribute {
+    // _lock always protects access to _deviceController, _value, and
+    // _parentCluster.  _serializedValue is protected when we are modifying it
+    // directly while we have no _deviceController.  Once we have one,
+    // _serializedValue is only modified on the Matter thread.
+    os_unfair_lock _lock;
     MTRDeviceController * __weak _deviceController;
+    NSDictionary<NSString *, id> * _value;
+    app::ConcreteClusterPath _parentCluster;
 }
 
 - (nullable instancetype)initAttributeWithID:(NSNumber *)attributeID initialValue:(NSDictionary<NSString *, id> *)value requiredReadPrivilege:(MTRAccessControlEntryPrivilege)requiredReadPrivilege writable:(BOOL)writable
@@ -66,6 +75,7 @@
         return nil;
     }
 
+    _lock = OS_UNFAIR_LOCK_INIT;
     _attributeID = attributeID;
     _requiredReadPrivilege = requiredReadPrivilege;
     _writable = writable;
@@ -121,7 +131,10 @@
         }
     }
 
-    // We serialized properly, so should be good to go on the value.
+    // We serialized properly, so should be good to go on the value.  Lock
+    // around our ivar accesses.
+    std::lock_guard lock(_lock);
+
     _value = [value copy];
 
     MTRDeviceController * deviceController = _deviceController;
@@ -147,8 +160,16 @@
     return YES;
 }
 
+- (NSDictionary<NSString *, id> *)value
+{
+    std::lock_guard lock(_lock);
+    return [_value copy];
+}
+
 - (BOOL)associateWithController:(nullable MTRDeviceController *)controller
 {
+    std::lock_guard lock(_lock);
+
     MTRDeviceController * existingController = _deviceController;
     if (existingController != nil) {
 #if MTR_PER_CONTROLLER_STORAGE_ENABLED
@@ -167,7 +188,34 @@
 
 - (void)invalidate
 {
+    std::lock_guard lock(_lock);
+
     _deviceController = nil;
 }
 
+- (BOOL)addToCluster:(const app::ConcreteClusterPath &)cluster
+{
+    std::lock_guard lock(_lock);
+
+    if (_parentCluster.mClusterId != kInvalidClusterId) {
+        MTR_LOG_ERROR("Cannot add attribute to cluster " ChipLogFormatMEI "; already added to cluster " ChipLogFormatMEI, ChipLogValueMEI(cluster.mClusterId), ChipLogValueMEI(_parentCluster.mClusterId));
+        return NO;
+    }
+
+    _parentCluster = cluster;
+    return YES;
+}
+
+- (void)updateParentCluster:(const app::ConcreteClusterPath &)cluster
+{
+    std::lock_guard lock(_lock);
+    _parentCluster = cluster;
+}
+
+- (const chip::app::ConcreteClusterPath &)parentCluster
+{
+    std::lock_guard lock(_lock);
+    return _parentCluster;
+}
+
 @end
diff --git a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute_Internal.h b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute_Internal.h
index f6b423f..647be68 100644
--- a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute_Internal.h
+++ b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute_Internal.h
@@ -38,6 +38,19 @@
 - (void)invalidate;
 
 /**
+ * Add the attribute to a cluster with the given cluster path.  Will return NO
+ * if the attribute is already added to a cluster.
+ */
+- (BOOL)addToCluster:(const chip::app::ConcreteClusterPath &)cluster;
+
+/**
+ * Update the parent cluster path of the attribute.  Should only be done for
+ * attributes that are already added to a cluster, when the endpoint id needs to
+ * be updated.
+ */
+- (void)updateParentCluster:(const chip::app::ConcreteClusterPath &)cluster;
+
+/**
  * serializedValue is either an NSData or an NSArray<NSData *>, depending on
  * whether the attribute is list-typed.
  */
@@ -47,7 +60,7 @@
  * parentCluster will have kInvalidClusterId for the cluster ID until the
  * attribute is added to a cluster.
  */
-@property (nonatomic, assign) chip::app::ConcreteClusterPath parentCluster;
+@property (nonatomic, assign, readonly) const chip::app::ConcreteClusterPath & parentCluster;
 
 @end
 
diff --git a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerCluster.mm b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerCluster.mm
index 8de78a6..1d331c7 100644
--- a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerCluster.mm
+++ b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerCluster.mm
@@ -176,11 +176,6 @@
         return NO;
     }
 
-    if (attribute.parentCluster.mClusterId != kInvalidClusterId) {
-        MTR_LOG_ERROR("Cannot add attribute to cluster %llu; already added to cluster %" PRIu32, _clusterID.unsignedLongLongValue, attribute.parentCluster.mClusterId);
-        return NO;
-    }
-
     auto attributeID = attribute.attributeID.unsignedLongLongValue;
     if (attributeID == MTRAttributeIDTypeGlobalAttributeAttributeListID || attributeID == MTRAttributeIDTypeGlobalAttributeAcceptedCommandListID || attributeID == MTRAttributeIDTypeGlobalAttributeGeneratedCommandListID || attributeID == MTRAttributeIDTypeGlobalAttributeClusterRevisionID) {
         MTR_LOG_ERROR("Cannot add global attribute %llx on cluster %llx", attributeID, _clusterID.unsignedLongLongValue);
@@ -201,8 +196,11 @@
         }
     }
 
+    if (![attribute addToCluster:ConcreteClusterPath(_parentEndpoint, static_cast<ClusterId>(_clusterID.unsignedLongLongValue))]) {
+        return NO;
+    }
+
     [_attributes addObject:attribute];
-    attribute.parentCluster = ConcreteClusterPath(_parentEndpoint, static_cast<ClusterId>(_clusterID.unsignedLongLongValue));
     return YES;
 }
 
@@ -374,7 +372,7 @@
     // Update it on all the attributes, in case the attributes were added to us
     // before we were added to the endpoint.
     for (MTRServerAttribute * attr in _attributes) {
-        attr.parentCluster = ConcreteClusterPath(endpoint, static_cast<ClusterId>(_clusterID.unsignedLongLongValue));
+        [attr updateParentCluster:ConcreteClusterPath(endpoint, static_cast<ClusterId>(_clusterID.unsignedLongLongValue))];
     }
 }
 
diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
index 0683838..2abac83 100644
--- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
+++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
@@ -170,6 +170,7 @@
 		51565CB22A7AD77600469F18 /* MTRDeviceControllerDataStore.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51565CB02A7AD77600469F18 /* MTRDeviceControllerDataStore.mm */; };
 		51565CB42A7AD78D00469F18 /* MTRDeviceControllerStorageDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 51565CB32A7AD78D00469F18 /* MTRDeviceControllerStorageDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		51565CB62A7B0D6600469F18 /* MTRDeviceControllerParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 51565CB52A7B0D6600469F18 /* MTRDeviceControllerParameters.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		515BE4ED2B72C0C5000BC1FD /* MTRUnfairLock.h in Headers */ = {isa = PBXBuildFile; fileRef = 515BE4EC2B72C0C5000BC1FD /* MTRUnfairLock.h */; };
 		515C1C6F284F9FFB00A48F0C /* MTRFramework.mm in Sources */ = {isa = PBXBuildFile; fileRef = 515C1C6D284F9FFB00A48F0C /* MTRFramework.mm */; };
 		515C1C70284F9FFB00A48F0C /* MTRFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = 515C1C6E284F9FFB00A48F0C /* MTRFramework.h */; };
 		516411312B6BF70300E67C05 /* DataModelHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 516415FE2B6B132200D5CE11 /* DataModelHandler.cpp */; };
@@ -567,6 +568,7 @@
 		51565CB02A7AD77600469F18 /* MTRDeviceControllerDataStore.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRDeviceControllerDataStore.mm; sourceTree = "<group>"; };
 		51565CB32A7AD78D00469F18 /* MTRDeviceControllerStorageDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRDeviceControllerStorageDelegate.h; sourceTree = "<group>"; };
 		51565CB52A7B0D6600469F18 /* MTRDeviceControllerParameters.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRDeviceControllerParameters.h; sourceTree = "<group>"; };
+		515BE4EC2B72C0C5000BC1FD /* MTRUnfairLock.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRUnfairLock.h; sourceTree = "<group>"; };
 		515C1C6D284F9FFB00A48F0C /* MTRFramework.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRFramework.mm; sourceTree = "<group>"; };
 		515C1C6E284F9FFB00A48F0C /* MTRFramework.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRFramework.h; sourceTree = "<group>"; };
 		516415FA2B6ACA8300D5CE11 /* MTRServerAccessControl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRServerAccessControl.mm; sourceTree = "<group>"; };
@@ -1358,6 +1360,7 @@
 				5173A47229C0E2ED00F67F48 /* MTRFabricInfo_Internal.h */,
 				5173A47429C0E2ED00F67F48 /* MTRFabricInfo.h */,
 				5173A47329C0E2ED00F67F48 /* MTRFabricInfo.mm */,
+				515BE4EC2B72C0C5000BC1FD /* MTRUnfairLock.h */,
 				3D69867E29382E58007314E7 /* Resources */,
 			);
 			path = CHIP;
@@ -1622,6 +1625,7 @@
 				51E51FC0282AD37A00FC978D /* MTRDeviceControllerStartupParams_Internal.h in Headers */,
 				3DECCB702934AECD00585AEC /* MTRLogging.h in Headers */,
 				1E4D654E29C208DD00BC3478 /* MTRCommissionableBrowserResult.h in Headers */,
+				515BE4ED2B72C0C5000BC1FD /* MTRUnfairLock.h in Headers */,
 				998F286F26D55EC5001846C6 /* MTRP256KeypairBridge.h in Headers */,
 				2C222ADF255C811800E446B9 /* MTRBaseDevice_Internal.h in Headers */,
 				514C7A022B64223400DD6D7B /* MTRServerEndpoint_Internal.h in Headers */,