Ember compatibility layer - split out GlobalAttributeAccessInterface and the I/O buffer (#33396)

* Split out ember-compatibiltiy-functions with some shareable bits.

When looking to implement the ember/codegen data mode, some
functionality in ember-compatibility-functions needs sharing to
avoid extra copy&paste bloat:

- Global attribute handling via AAI split into a separate file
- Raw "data I/O" buffer split into a separate file

Moved privilege-storage and implemented a few more mock ember
methods.

* Also update linter

* Remove obsolete file

* Fix odd include

* Add files to Matter.xcodeproject

* Update src/app/util/ember-io-storage.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update ember-io-storage.h

Add additional comments.

* Restyle

---------

Co-authored-by: Andrei Litvin <andreilitvin@google.com>
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 7336213..4685948 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -119,6 +119,10 @@
                      --known-failure app/util/DataModelHandler.h \
                      --known-failure app/util/ember-compatibility-functions.cpp \
                      --known-failure app/util/ember-compatibility-functions.h \
+                     --known-failure app/util/ember-global-attribute-access-interface.cpp \
+                     --known-failure app/util/ember-global-attribute-access-interface.h \
+                     --known-failure app/util/ember-io-storage.cpp \
+                     --known-failure app/util/ember-io-storage.h \
                      --known-failure app/util/endpoint-config-api.h \
                      --known-failure app/util/generic-callbacks.h \
                      --known-failure app/util/generic-callback-stubs.cpp \
diff --git a/src/app/chip_data_model.cmake b/src/app/chip_data_model.cmake
index 5573eeb..2d4149b 100644
--- a/src/app/chip_data_model.cmake
+++ b/src/app/chip_data_model.cmake
@@ -144,6 +144,8 @@
         ${CHIP_APP_BASE_DIR}/icd/server/ICDConfigurationData.cpp
         ${CHIP_APP_BASE_DIR}/util/DataModelHandler.cpp
         ${CHIP_APP_BASE_DIR}/util/ember-compatibility-functions.cpp
+        ${CHIP_APP_BASE_DIR}/util/ember-global-attribute-access-interface.cpp
+        ${CHIP_APP_BASE_DIR}/util/ember-io-storage.cpp
         ${CHIP_APP_BASE_DIR}/util/generic-callback-stubs.cpp
         ${CHIP_APP_BASE_DIR}/util/privilege-storage.cpp
         ${CHIP_APP_BASE_DIR}/util/util.cpp
diff --git a/src/app/chip_data_model.gni b/src/app/chip_data_model.gni
index c8a30b1..a68d193 100644
--- a/src/app/chip_data_model.gni
+++ b/src/app/chip_data_model.gni
@@ -209,6 +209,8 @@
         "${_app_root}/util/attribute-storage.cpp",
         "${_app_root}/util/attribute-table.cpp",
         "${_app_root}/util/ember-compatibility-functions.cpp",
+        "${_app_root}/util/ember-global-attribute-access-interface.cpp",
+        "${_app_root}/util/ember-io-storage.cpp",
         "${_app_root}/util/util.cpp",
       ]
     }
diff --git a/src/app/tests/BUILD.gn b/src/app/tests/BUILD.gn
index 49196f2..df6737a 100644
--- a/src/app/tests/BUILD.gn
+++ b/src/app/tests/BUILD.gn
@@ -29,7 +29,6 @@
     "${chip_root}/src/app/reporting/tests/MockReportScheduler.cpp",
     "AppTestContext.cpp",
     "AppTestContext.h",
-    "integration/RequiredPrivilegeStubs.cpp",
   ]
 
   cflags = [ "-Wconversion" ]
diff --git a/src/app/tests/integration/BUILD.gn b/src/app/tests/integration/BUILD.gn
index da0db88..49aea90 100644
--- a/src/app/tests/integration/BUILD.gn
+++ b/src/app/tests/integration/BUILD.gn
@@ -38,7 +38,6 @@
 executable("chip-im-initiator") {
   sources = [
     "${chip_root}/src/app/reporting/tests/MockReportScheduler.cpp",
-    "RequiredPrivilegeStubs.cpp",
     "chip_im_initiator.cpp",
   ]
 
@@ -61,7 +60,6 @@
     "${chip_root}/src/app/reporting/tests/MockReportScheduler.cpp",
     "MockEvents.cpp",
     "MockEvents.h",
-    "RequiredPrivilegeStubs.cpp",
     "chip_im_responder.cpp",
   ]
 
diff --git a/src/app/util/ember-compatibility-functions.cpp b/src/app/util/ember-compatibility-functions.cpp
index e87e0f5..cc3185f 100644
--- a/src/app/util/ember-compatibility-functions.cpp
+++ b/src/app/util/ember-compatibility-functions.cpp
@@ -33,6 +33,8 @@
 #include <app/util/attribute-table-detail.h>
 #include <app/util/attribute-table.h>
 #include <app/util/config.h>
+#include <app/util/ember-global-attribute-access-interface.h>
+#include <app/util/ember-io-storage.h>
 #include <app/util/odd-sized-integers.h>
 #include <app/util/util.h>
 #include <lib/core/CHIPCore.h>
@@ -54,118 +56,18 @@
 using namespace chip;
 using namespace chip::app;
 using namespace chip::Access;
+using namespace chip::app::Compatibility;
+using namespace chip::app::Compatibility::Internal;
 
 namespace chip {
 namespace app {
-namespace Compatibility {
 namespace {
-// On some apps, ATTRIBUTE_LARGEST can as small as 3, making compiler unhappy since data[kAttributeReadBufferSize] cannot hold
-// uint64_t. Make kAttributeReadBufferSize at least 8 so it can fit all basic types.
-constexpr size_t kAttributeReadBufferSize = (ATTRIBUTE_LARGEST >= 8 ? ATTRIBUTE_LARGEST : 8);
-
-// BasicType maps the type to basic int(8|16|32|64)(s|u) types.
-EmberAfAttributeType BaseType(EmberAfAttributeType type)
-{
-    switch (type)
-    {
-    case ZCL_ACTION_ID_ATTRIBUTE_TYPE:  // Action Id
-    case ZCL_FABRIC_IDX_ATTRIBUTE_TYPE: // Fabric Index
-    case ZCL_BITMAP8_ATTRIBUTE_TYPE:    // 8-bit bitmap
-    case ZCL_ENUM8_ATTRIBUTE_TYPE:      // 8-bit enumeration
-    case ZCL_STATUS_ATTRIBUTE_TYPE:     // Status Code
-    case ZCL_PERCENT_ATTRIBUTE_TYPE:    // Percentage
-        static_assert(std::is_same<chip::Percent, uint8_t>::value,
-                      "chip::Percent is expected to be uint8_t, change this when necessary");
-        return ZCL_INT8U_ATTRIBUTE_TYPE;
-
-    case ZCL_ENDPOINT_NO_ATTRIBUTE_TYPE:   // Endpoint Number
-    case ZCL_GROUP_ID_ATTRIBUTE_TYPE:      // Group Id
-    case ZCL_VENDOR_ID_ATTRIBUTE_TYPE:     // Vendor Id
-    case ZCL_ENUM16_ATTRIBUTE_TYPE:        // 16-bit enumeration
-    case ZCL_BITMAP16_ATTRIBUTE_TYPE:      // 16-bit bitmap
-    case ZCL_PERCENT100THS_ATTRIBUTE_TYPE: // 100ths of a percent
-        static_assert(std::is_same<chip::EndpointId, uint16_t>::value,
-                      "chip::EndpointId is expected to be uint16_t, change this when necessary");
-        static_assert(std::is_same<chip::GroupId, uint16_t>::value,
-                      "chip::GroupId is expected to be uint16_t, change this when necessary");
-        static_assert(std::is_same<chip::Percent100ths, uint16_t>::value,
-                      "chip::Percent100ths is expected to be uint16_t, change this when necessary");
-        return ZCL_INT16U_ATTRIBUTE_TYPE;
-
-    case ZCL_CLUSTER_ID_ATTRIBUTE_TYPE: // Cluster Id
-    case ZCL_ATTRIB_ID_ATTRIBUTE_TYPE:  // Attribute Id
-    case ZCL_FIELD_ID_ATTRIBUTE_TYPE:   // Field Id
-    case ZCL_EVENT_ID_ATTRIBUTE_TYPE:   // Event Id
-    case ZCL_COMMAND_ID_ATTRIBUTE_TYPE: // Command Id
-    case ZCL_TRANS_ID_ATTRIBUTE_TYPE:   // Transaction Id
-    case ZCL_DEVTYPE_ID_ATTRIBUTE_TYPE: // Device Type Id
-    case ZCL_DATA_VER_ATTRIBUTE_TYPE:   // Data Version
-    case ZCL_BITMAP32_ATTRIBUTE_TYPE:   // 32-bit bitmap
-    case ZCL_EPOCH_S_ATTRIBUTE_TYPE:    // Epoch Seconds
-    case ZCL_ELAPSED_S_ATTRIBUTE_TYPE:  // Elapsed Seconds
-        static_assert(std::is_same<chip::ClusterId, uint32_t>::value,
-                      "chip::Cluster is expected to be uint32_t, change this when necessary");
-        static_assert(std::is_same<chip::AttributeId, uint32_t>::value,
-                      "chip::AttributeId is expected to be uint32_t, change this when necessary");
-        static_assert(std::is_same<chip::AttributeId, uint32_t>::value,
-                      "chip::AttributeId is expected to be uint32_t, change this when necessary");
-        static_assert(std::is_same<chip::EventId, uint32_t>::value,
-                      "chip::EventId is expected to be uint32_t, change this when necessary");
-        static_assert(std::is_same<chip::CommandId, uint32_t>::value,
-                      "chip::CommandId is expected to be uint32_t, change this when necessary");
-        static_assert(std::is_same<chip::TransactionId, uint32_t>::value,
-                      "chip::TransactionId is expected to be uint32_t, change this when necessary");
-        static_assert(std::is_same<chip::DeviceTypeId, uint32_t>::value,
-                      "chip::DeviceTypeId is expected to be uint32_t, change this when necessary");
-        static_assert(std::is_same<chip::DataVersion, uint32_t>::value,
-                      "chip::DataVersion is expected to be uint32_t, change this when necessary");
-        return ZCL_INT32U_ATTRIBUTE_TYPE;
-
-    case ZCL_AMPERAGE_MA_ATTRIBUTE_TYPE: // Amperage milliamps
-    case ZCL_ENERGY_MWH_ATTRIBUTE_TYPE:  // Energy milliwatt-hours
-    case ZCL_POWER_MW_ATTRIBUTE_TYPE:    // Power milliwatts
-    case ZCL_VOLTAGE_MV_ATTRIBUTE_TYPE:  // Voltage millivolts
-        return ZCL_INT64S_ATTRIBUTE_TYPE;
-
-    case ZCL_EVENT_NO_ATTRIBUTE_TYPE:   // Event Number
-    case ZCL_FABRIC_ID_ATTRIBUTE_TYPE:  // Fabric Id
-    case ZCL_NODE_ID_ATTRIBUTE_TYPE:    // Node Id
-    case ZCL_BITMAP64_ATTRIBUTE_TYPE:   // 64-bit bitmap
-    case ZCL_EPOCH_US_ATTRIBUTE_TYPE:   // Epoch Microseconds
-    case ZCL_POSIX_MS_ATTRIBUTE_TYPE:   // POSIX Milliseconds
-    case ZCL_SYSTIME_MS_ATTRIBUTE_TYPE: // System time Milliseconds
-    case ZCL_SYSTIME_US_ATTRIBUTE_TYPE: // System time Microseconds
-        static_assert(std::is_same<chip::EventNumber, uint64_t>::value,
-                      "chip::EventNumber is expected to be uint64_t, change this when necessary");
-        static_assert(std::is_same<chip::FabricId, uint64_t>::value,
-                      "chip::FabricId is expected to be uint64_t, change this when necessary");
-        static_assert(std::is_same<chip::NodeId, uint64_t>::value,
-                      "chip::NodeId is expected to be uint64_t, change this when necessary");
-        return ZCL_INT64U_ATTRIBUTE_TYPE;
-
-    case ZCL_TEMPERATURE_ATTRIBUTE_TYPE: // Temperature
-        return ZCL_INT16S_ATTRIBUTE_TYPE;
-
-    default:
-        return type;
-    }
-}
-
-} // namespace
-
-} // namespace Compatibility
-
-using namespace chip::app::Compatibility;
-
-namespace {
-// Common buffer for ReadSingleClusterData & WriteSingleClusterData
-uint8_t attributeData[kAttributeReadBufferSize];
 
 template <typename T>
 CHIP_ERROR attributeBufferToNumericTlvData(TLV::TLVWriter & writer, bool isNullable)
 {
     typename NumericAttributeTraits<T>::StorageType value;
-    memcpy(&value, attributeData, sizeof(value));
+    memcpy(&value, gEmberAttributeIOBufferSpan.data(), sizeof(value));
     TLV::Tag tag = TLV::ContextTag(AttributeDataIB::Tag::kData);
     if (isNullable && NumericAttributeTraits<T>::IsNullValue(value))
     {
@@ -285,143 +187,6 @@
     return aAttributeReports.EncodeAttributeStatus(aPath, StatusIB(aStatus));
 }
 
-// This reader should never actually be registered; we do manual dispatch to it
-// for the one attribute it handles.
-class MandatoryGlobalAttributeReader : public AttributeAccessInterface
-{
-public:
-    MandatoryGlobalAttributeReader(const EmberAfCluster * aCluster) :
-        AttributeAccessInterface(MakeOptional(kInvalidEndpointId), kInvalidClusterId), mCluster(aCluster)
-    {}
-
-protected:
-    const EmberAfCluster * mCluster;
-};
-
-class GlobalAttributeReader : public MandatoryGlobalAttributeReader
-{
-public:
-    GlobalAttributeReader(const EmberAfCluster * aCluster) : MandatoryGlobalAttributeReader(aCluster) {}
-
-    CHIP_ERROR Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) override;
-
-private:
-    typedef CHIP_ERROR (CommandHandlerInterface::*CommandListEnumerator)(const ConcreteClusterPath & cluster,
-                                                                         CommandHandlerInterface::CommandIdCallback callback,
-                                                                         void * context);
-    static CHIP_ERROR EncodeCommandList(const ConcreteClusterPath & aClusterPath, AttributeValueEncoder & aEncoder,
-                                        CommandListEnumerator aEnumerator, const CommandId * aClusterCommandList);
-};
-
-CHIP_ERROR GlobalAttributeReader::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder)
-{
-    using namespace Clusters::Globals::Attributes;
-    switch (aPath.mAttributeId)
-    {
-    case AttributeList::Id:
-        return aEncoder.EncodeList([this](const auto & encoder) {
-            const size_t count     = mCluster->attributeCount;
-            bool addedExtraGlobals = false;
-            for (size_t i = 0; i < count; ++i)
-            {
-                AttributeId id              = mCluster->attributes[i].attributeId;
-                constexpr auto lastGlobalId = GlobalAttributesNotInMetadata[ArraySize(GlobalAttributesNotInMetadata) - 1];
-#if CHIP_CONFIG_ENABLE_EVENTLIST_ATTRIBUTE
-                // The GlobalAttributesNotInMetadata shouldn't have any gaps in their ids here.
-                static_assert(lastGlobalId - GlobalAttributesNotInMetadata[0] == ArraySize(GlobalAttributesNotInMetadata) - 1,
-                              "Ids in GlobalAttributesNotInMetadata not consecutive");
-#else
-                // If EventList is not supported. The GlobalAttributesNotInMetadata is missing one id here.
-                static_assert(lastGlobalId - GlobalAttributesNotInMetadata[0] == ArraySize(GlobalAttributesNotInMetadata),
-                              "Ids in GlobalAttributesNotInMetadata not consecutive (except EventList)");
-#endif // CHIP_CONFIG_ENABLE_EVENTLIST_ATTRIBUTE
-                if (!addedExtraGlobals && id > lastGlobalId)
-                {
-                    for (const auto & globalId : GlobalAttributesNotInMetadata)
-                    {
-                        ReturnErrorOnFailure(encoder.Encode(globalId));
-                    }
-                    addedExtraGlobals = true;
-                }
-                ReturnErrorOnFailure(encoder.Encode(id));
-            }
-            if (!addedExtraGlobals)
-            {
-                for (const auto & globalId : GlobalAttributesNotInMetadata)
-                {
-                    ReturnErrorOnFailure(encoder.Encode(globalId));
-                }
-            }
-            return CHIP_NO_ERROR;
-        });
-#if CHIP_CONFIG_ENABLE_EVENTLIST_ATTRIBUTE
-    case EventList::Id:
-        return aEncoder.EncodeList([this](const auto & encoder) {
-            for (size_t i = 0; i < mCluster->eventCount; ++i)
-            {
-                ReturnErrorOnFailure(encoder.Encode(mCluster->eventList[i]));
-            }
-            return CHIP_NO_ERROR;
-        });
-#endif // CHIP_CONFIG_ENABLE_EVENTLIST_ATTRIBUTE
-    case AcceptedCommandList::Id:
-        return EncodeCommandList(aPath, aEncoder, &CommandHandlerInterface::EnumerateAcceptedCommands,
-                                 mCluster->acceptedCommandList);
-    case GeneratedCommandList::Id:
-        return EncodeCommandList(aPath, aEncoder, &CommandHandlerInterface::EnumerateGeneratedCommands,
-                                 mCluster->generatedCommandList);
-    default:
-        // This function is only called if attributeCluster is non-null in
-        // ReadSingleClusterData, which only happens for attributes listed in
-        // GlobalAttributesNotInMetadata.  If we reach this code, someone added
-        // a global attribute to that list but not the above switch.
-        VerifyOrDieWithMsg(false, DataManagement, "Unexpected global attribute: " ChipLogFormatMEI,
-                           ChipLogValueMEI(aPath.mAttributeId));
-        return CHIP_NO_ERROR;
-    }
-}
-
-CHIP_ERROR GlobalAttributeReader::EncodeCommandList(const ConcreteClusterPath & aClusterPath, AttributeValueEncoder & aEncoder,
-                                                    GlobalAttributeReader::CommandListEnumerator aEnumerator,
-                                                    const CommandId * aClusterCommandList)
-{
-    return aEncoder.EncodeList([&](const auto & encoder) {
-        auto * commandHandler =
-            InteractionModelEngine::GetInstance()->FindCommandHandler(aClusterPath.mEndpointId, aClusterPath.mClusterId);
-        if (commandHandler)
-        {
-            struct Context
-            {
-                decltype(encoder) & commandIdEncoder;
-                CHIP_ERROR err;
-            } context{ encoder, CHIP_NO_ERROR };
-            CHIP_ERROR err = (commandHandler->*aEnumerator)(
-                aClusterPath,
-                [](CommandId command, void * closure) -> Loop {
-                    auto * ctx = static_cast<Context *>(closure);
-                    ctx->err   = ctx->commandIdEncoder.Encode(command);
-                    if (ctx->err != CHIP_NO_ERROR)
-                    {
-                        return Loop::Break;
-                    }
-                    return Loop::Continue;
-                },
-                &context);
-            if (err != CHIP_ERROR_NOT_IMPLEMENTED)
-            {
-                return context.err;
-            }
-            // Else fall through to the list in aClusterCommandList.
-        }
-
-        for (const CommandId * cmd = aClusterCommandList; cmd != nullptr && *cmd != kInvalidCommandId; cmd++)
-        {
-            ReturnErrorOnFailure(encoder.Encode(*cmd));
-        }
-        return CHIP_NO_ERROR;
-    });
-}
-
 // Helper function for trying to read an attribute value via an
 // AttributeAccessInterface.  On failure, the read has failed.  On success, the
 // aTriedEncode outparam is set to whether the AttributeAccessInterface tried to encode a value.
@@ -603,7 +368,8 @@
     record.endpoint    = aPath.mEndpointId;
     record.clusterId   = aPath.mClusterId;
     record.attributeId = aPath.mAttributeId;
-    Status status      = emAfReadOrWriteAttribute(&record, &attributeMetadata, attributeData, sizeof(attributeData),
+    Status status      = emAfReadOrWriteAttribute(&record, &attributeMetadata, gEmberAttributeIOBufferSpan.data(),
+                                                  static_cast<uint16_t>(gEmberAttributeIOBufferSpan.size()),
                                                   /* write = */ false);
 
     if (status == Status::Success)
@@ -613,7 +379,7 @@
         TLV::TLVWriter * writer            = attributeDataIBBuilder.GetWriter();
         VerifyOrReturnError(writer != nullptr, CHIP_NO_ERROR);
         TLV::Tag tag = TLV::ContextTag(AttributeDataIB::Tag::kData);
-        switch (BaseType(attributeType))
+        switch (AttributeBaseType(attributeType))
         {
         case ZCL_NO_DATA_ATTRIBUTE_TYPE: // No data
             ReturnErrorOnFailure(writer->PutNull(tag));
@@ -719,8 +485,8 @@
         }
         case ZCL_CHAR_STRING_ATTRIBUTE_TYPE: // Char string
         {
-            char * actualData  = reinterpret_cast<char *>(attributeData + 1);
-            uint8_t dataLength = attributeData[0];
+            char * actualData  = reinterpret_cast<char *>(gEmberAttributeIOBufferSpan.data() + 1);
+            uint8_t dataLength = gEmberAttributeIOBufferSpan[0];
             if (dataLength == 0xFF)
             {
                 if (isNullable)
@@ -739,9 +505,10 @@
             break;
         }
         case ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE: {
-            char * actualData = reinterpret_cast<char *>(attributeData + 2); // The pascal string contains 2 bytes length
+            char * actualData =
+                reinterpret_cast<char *>(gEmberAttributeIOBufferSpan.data() + 2); // The pascal string contains 2 bytes length
             uint16_t dataLength;
-            memcpy(&dataLength, attributeData, sizeof(dataLength));
+            memcpy(&dataLength, gEmberAttributeIOBufferSpan.data(), sizeof(dataLength));
             if (dataLength == 0xFFFF)
             {
                 if (isNullable)
@@ -761,8 +528,8 @@
         }
         case ZCL_OCTET_STRING_ATTRIBUTE_TYPE: // Octet string
         {
-            uint8_t * actualData = attributeData + 1;
-            uint8_t dataLength   = attributeData[0];
+            uint8_t * actualData = gEmberAttributeIOBufferSpan.data() + 1;
+            uint8_t dataLength   = gEmberAttributeIOBufferSpan[0];
             if (dataLength == 0xFF)
             {
                 if (isNullable)
@@ -781,9 +548,9 @@
             break;
         }
         case ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE: {
-            uint8_t * actualData = attributeData + 2; // The pascal string contains 2 bytes length
+            uint8_t * actualData = gEmberAttributeIOBufferSpan.data() + 2; // The pascal string contains 2 bytes length
             uint16_t dataLength;
-            memcpy(&dataLength, attributeData, sizeof(dataLength));
+            memcpy(&dataLength, gEmberAttributeIOBufferSpan.data(), sizeof(dataLength));
             if (dataLength == 0xFFFF)
             {
                 if (isNullable)
@@ -821,7 +588,8 @@
 CHIP_ERROR numericTlvDataToAttributeBuffer(TLV::TLVReader & aReader, bool isNullable, uint16_t & dataLen)
 {
     typename NumericAttributeTraits<T>::StorageType value;
-    static_assert(sizeof(value) <= sizeof(attributeData), "Value cannot fit into attribute data");
+    VerifyOrDie(sizeof(value) <= gEmberAttributeIOBufferSpan.size());
+
     if (isNullable && aReader.GetType() == TLV::kTLVType_Null)
     {
         NumericAttributeTraits<T>::SetNull(value);
@@ -834,7 +602,7 @@
         NumericAttributeTraits<T>::WorkingToStorage(val, value);
     }
     dataLen = sizeof(value);
-    memcpy(attributeData, &value, sizeof(value));
+    memcpy(gEmberAttributeIOBufferSpan.data(), &value, sizeof(value));
     return CHIP_NO_ERROR;
 }
 
@@ -847,7 +615,7 @@
     {
         // Null is represented by an 0xFF or 0xFFFF length, respectively.
         len = std::numeric_limits<T>::max();
-        memcpy(&attributeData[0], &len, sizeof(len));
+        memcpy(gEmberAttributeIOBufferSpan.data(), &len, sizeof(len));
         dataLen = sizeof(len);
     }
     else
@@ -859,10 +627,10 @@
         ReturnErrorOnFailure(aReader.GetDataPtr(data));
         len = static_cast<T>(aReader.GetLength());
         VerifyOrReturnError(len != std::numeric_limits<T>::max(), CHIP_ERROR_MESSAGE_TOO_LONG);
-        VerifyOrReturnError(len + sizeof(len) /* length at the beginning of data */ <= sizeof(attributeData),
+        VerifyOrReturnError(len + sizeof(len) /* length at the beginning of data */ <= gEmberAttributeIOBufferSpan.size(),
                             CHIP_ERROR_MESSAGE_TOO_LONG);
-        memcpy(&attributeData[0], &len, sizeof(len));
-        memcpy(&attributeData[sizeof(len)], data, len);
+        memcpy(gEmberAttributeIOBufferSpan.data(), &len, sizeof(len));
+        memcpy(gEmberAttributeIOBufferSpan.data() + sizeof(len), data, len);
         dataLen = static_cast<uint16_t>(len + sizeof(len));
     }
     return CHIP_NO_ERROR;
@@ -870,7 +638,7 @@
 
 CHIP_ERROR prepareWriteData(const EmberAfAttributeMetadata * attributeMetadata, TLV::TLVReader & aReader, uint16_t & dataLen)
 {
-    EmberAfAttributeType expectedType = BaseType(attributeMetadata->attributeType);
+    EmberAfAttributeType expectedType = AttributeBaseType(attributeMetadata->attributeType);
     bool isNullable                   = attributeMetadata->IsNullable();
     switch (expectedType)
     {
@@ -1031,8 +799,8 @@
         return apWriteHandler->AddStatus(aPath, Protocols::InteractionModel::Status::InvalidValue);
     }
 
-    auto status = emAfWriteAttributeExternal(aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId, attributeData,
-                                             attributeMetadata->attributeType);
+    auto status = emAfWriteAttributeExternal(aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId,
+                                             gEmberAttributeIOBufferSpan.data(), attributeMetadata->attributeType);
     return apWriteHandler->AddStatus(aPath, status);
 }
 
diff --git a/src/app/util/ember-global-attribute-access-interface.cpp b/src/app/util/ember-global-attribute-access-interface.cpp
new file mode 100644
index 0000000..327ab09
--- /dev/null
+++ b/src/app/util/ember-global-attribute-access-interface.cpp
@@ -0,0 +1,136 @@
+/*
+ *    Copyright (c) 2021-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 <app/util/ember-global-attribute-access-interface.h>
+
+#include <app/GlobalAttributes.h>
+#include <app/InteractionModelEngine.h>
+
+namespace chip {
+namespace app {
+namespace Compatibility {
+
+CHIP_ERROR GlobalAttributeReader::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder)
+{
+    using namespace Clusters::Globals::Attributes;
+    switch (aPath.mAttributeId)
+    {
+    case AttributeList::Id:
+        return aEncoder.EncodeList([this](const auto & encoder) {
+            const size_t count     = mCluster->attributeCount;
+            bool addedExtraGlobals = false;
+            for (size_t i = 0; i < count; ++i)
+            {
+                AttributeId id              = mCluster->attributes[i].attributeId;
+                constexpr auto lastGlobalId = GlobalAttributesNotInMetadata[ArraySize(GlobalAttributesNotInMetadata) - 1];
+#if CHIP_CONFIG_ENABLE_EVENTLIST_ATTRIBUTE
+                // The GlobalAttributesNotInMetadata shouldn't have any gaps in their ids here.
+                static_assert(lastGlobalId - GlobalAttributesNotInMetadata[0] == ArraySize(GlobalAttributesNotInMetadata) - 1,
+                              "Ids in GlobalAttributesNotInMetadata not consecutive");
+#else
+                // If EventList is not supported. The GlobalAttributesNotInMetadata is missing one id here.
+                static_assert(lastGlobalId - GlobalAttributesNotInMetadata[0] == ArraySize(GlobalAttributesNotInMetadata),
+                              "Ids in GlobalAttributesNotInMetadata not consecutive (except EventList)");
+#endif // CHIP_CONFIG_ENABLE_EVENTLIST_ATTRIBUTE
+                if (!addedExtraGlobals && id > lastGlobalId)
+                {
+                    for (const auto & globalId : GlobalAttributesNotInMetadata)
+                    {
+                        ReturnErrorOnFailure(encoder.Encode(globalId));
+                    }
+                    addedExtraGlobals = true;
+                }
+                ReturnErrorOnFailure(encoder.Encode(id));
+            }
+            if (!addedExtraGlobals)
+            {
+                for (const auto & globalId : GlobalAttributesNotInMetadata)
+                {
+                    ReturnErrorOnFailure(encoder.Encode(globalId));
+                }
+            }
+            return CHIP_NO_ERROR;
+        });
+#if CHIP_CONFIG_ENABLE_EVENTLIST_ATTRIBUTE
+    case EventList::Id:
+        return aEncoder.EncodeList([this](const auto & encoder) {
+            for (size_t i = 0; i < mCluster->eventCount; ++i)
+            {
+                ReturnErrorOnFailure(encoder.Encode(mCluster->eventList[i]));
+            }
+            return CHIP_NO_ERROR;
+        });
+#endif // CHIP_CONFIG_ENABLE_EVENTLIST_ATTRIBUTE
+    case AcceptedCommandList::Id:
+        return EncodeCommandList(aPath, aEncoder, &CommandHandlerInterface::EnumerateAcceptedCommands,
+                                 mCluster->acceptedCommandList);
+    case GeneratedCommandList::Id:
+        return EncodeCommandList(aPath, aEncoder, &CommandHandlerInterface::EnumerateGeneratedCommands,
+                                 mCluster->generatedCommandList);
+    default:
+        // This function is only called if attributeCluster is non-null in
+        // ReadSingleClusterData, which only happens for attributes listed in
+        // GlobalAttributesNotInMetadata.  If we reach this code, someone added
+        // a global attribute to that list but not the above switch.
+        VerifyOrDieWithMsg(false, DataManagement, "Unexpected global attribute: " ChipLogFormatMEI,
+                           ChipLogValueMEI(aPath.mAttributeId));
+        return CHIP_NO_ERROR;
+    }
+}
+
+CHIP_ERROR GlobalAttributeReader::EncodeCommandList(const ConcreteClusterPath & aClusterPath, AttributeValueEncoder & aEncoder,
+                                                    GlobalAttributeReader::CommandListEnumerator aEnumerator,
+                                                    const CommandId * aClusterCommandList)
+{
+    return aEncoder.EncodeList([&](const auto & encoder) {
+        auto * commandHandler =
+            InteractionModelEngine::GetInstance()->FindCommandHandler(aClusterPath.mEndpointId, aClusterPath.mClusterId);
+        if (commandHandler)
+        {
+            struct Context
+            {
+                decltype(encoder) & commandIdEncoder;
+                CHIP_ERROR err;
+            } context{ encoder, CHIP_NO_ERROR };
+            CHIP_ERROR err = (commandHandler->*aEnumerator)(
+                aClusterPath,
+                [](CommandId command, void * closure) -> Loop {
+                    auto * ctx = static_cast<Context *>(closure);
+                    ctx->err   = ctx->commandIdEncoder.Encode(command);
+                    if (ctx->err != CHIP_NO_ERROR)
+                    {
+                        return Loop::Break;
+                    }
+                    return Loop::Continue;
+                },
+                &context);
+            if (err != CHIP_ERROR_NOT_IMPLEMENTED)
+            {
+                return context.err;
+            }
+            // Else fall through to the list in aClusterCommandList.
+        }
+
+        for (const CommandId * cmd = aClusterCommandList; cmd != nullptr && *cmd != kInvalidCommandId; cmd++)
+        {
+            ReturnErrorOnFailure(encoder.Encode(*cmd));
+        }
+        return CHIP_NO_ERROR;
+    });
+}
+
+} // namespace Compatibility
+} // namespace app
+} // namespace chip
diff --git a/src/app/util/ember-global-attribute-access-interface.h b/src/app/util/ember-global-attribute-access-interface.h
new file mode 100644
index 0000000..d17e1b2
--- /dev/null
+++ b/src/app/util/ember-global-attribute-access-interface.h
@@ -0,0 +1,56 @@
+/*
+ *    Copyright (c) 2021-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.
+ */
+#pragma once
+
+#include <app/AttributeAccessInterface.h>
+#include <app/CommandHandlerInterface.h>
+#include <app/util/af-types.h>
+
+namespace chip {
+namespace app {
+namespace Compatibility {
+
+// This reader should never actually be registered; we do manual dispatch to it
+// for the one attribute it handles.
+class MandatoryGlobalAttributeReader : public AttributeAccessInterface
+{
+public:
+    MandatoryGlobalAttributeReader(const EmberAfCluster * aCluster) :
+        AttributeAccessInterface(MakeOptional(kInvalidEndpointId), kInvalidClusterId), mCluster(aCluster)
+    {}
+
+protected:
+    const EmberAfCluster * mCluster;
+};
+
+class GlobalAttributeReader : public MandatoryGlobalAttributeReader
+{
+public:
+    GlobalAttributeReader(const EmberAfCluster * aCluster) : MandatoryGlobalAttributeReader(aCluster) {}
+
+    CHIP_ERROR Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) override;
+
+private:
+    typedef CHIP_ERROR (CommandHandlerInterface::*CommandListEnumerator)(const ConcreteClusterPath & cluster,
+                                                                         CommandHandlerInterface::CommandIdCallback callback,
+                                                                         void * context);
+    static CHIP_ERROR EncodeCommandList(const ConcreteClusterPath & aClusterPath, AttributeValueEncoder & aEncoder,
+                                        CommandListEnumerator aEnumerator, const CommandId * aClusterCommandList);
+};
+
+} // namespace Compatibility
+} // namespace app
+} // namespace chip
diff --git a/src/app/util/ember-io-storage.cpp b/src/app/util/ember-io-storage.cpp
new file mode 100644
index 0000000..cc5eacf
--- /dev/null
+++ b/src/app/util/ember-io-storage.cpp
@@ -0,0 +1,126 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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 <app/util/ember-io-storage.h>
+
+#include <app-common/zap-generated/attribute-type.h>
+#include <zap-generated/endpoint_config.h>
+
+#include <cstddef>
+
+namespace chip {
+namespace app {
+namespace Compatibility {
+namespace Internal {
+
+// On some apps, ATTRIBUTE_LARGEST can as small as 3, making compiler unhappy since data[kAttributeReadBufferSize] cannot hold
+// uint64_t. Make kAttributeReadBufferSize at least 8 so it can fit all basic types.
+constexpr size_t kAttributeReadBufferSize = (ATTRIBUTE_LARGEST >= 8 ? ATTRIBUTE_LARGEST : 8);
+uint8_t attributeIOBuffer[kAttributeReadBufferSize];
+
+MutableByteSpan gEmberAttributeIOBufferSpan(attributeIOBuffer);
+
+EmberAfAttributeType AttributeBaseType(EmberAfAttributeType type)
+{
+    switch (type)
+    {
+    case ZCL_ACTION_ID_ATTRIBUTE_TYPE:  // Action Id
+    case ZCL_FABRIC_IDX_ATTRIBUTE_TYPE: // Fabric Index
+    case ZCL_BITMAP8_ATTRIBUTE_TYPE:    // 8-bit bitmap
+    case ZCL_ENUM8_ATTRIBUTE_TYPE:      // 8-bit enumeration
+    case ZCL_STATUS_ATTRIBUTE_TYPE:     // Status Code
+    case ZCL_PERCENT_ATTRIBUTE_TYPE:    // Percentage
+        static_assert(std::is_same<chip::Percent, uint8_t>::value,
+                      "chip::Percent is expected to be uint8_t, change this when necessary");
+        return ZCL_INT8U_ATTRIBUTE_TYPE;
+
+    case ZCL_ENDPOINT_NO_ATTRIBUTE_TYPE:   // Endpoint Number
+    case ZCL_GROUP_ID_ATTRIBUTE_TYPE:      // Group Id
+    case ZCL_VENDOR_ID_ATTRIBUTE_TYPE:     // Vendor Id
+    case ZCL_ENUM16_ATTRIBUTE_TYPE:        // 16-bit enumeration
+    case ZCL_BITMAP16_ATTRIBUTE_TYPE:      // 16-bit bitmap
+    case ZCL_PERCENT100THS_ATTRIBUTE_TYPE: // 100ths of a percent
+        static_assert(std::is_same<chip::EndpointId, uint16_t>::value,
+                      "chip::EndpointId is expected to be uint16_t, change this when necessary");
+        static_assert(std::is_same<chip::GroupId, uint16_t>::value,
+                      "chip::GroupId is expected to be uint16_t, change this when necessary");
+        static_assert(std::is_same<chip::Percent100ths, uint16_t>::value,
+                      "chip::Percent100ths is expected to be uint16_t, change this when necessary");
+        return ZCL_INT16U_ATTRIBUTE_TYPE;
+
+    case ZCL_CLUSTER_ID_ATTRIBUTE_TYPE: // Cluster Id
+    case ZCL_ATTRIB_ID_ATTRIBUTE_TYPE:  // Attribute Id
+    case ZCL_FIELD_ID_ATTRIBUTE_TYPE:   // Field Id
+    case ZCL_EVENT_ID_ATTRIBUTE_TYPE:   // Event Id
+    case ZCL_COMMAND_ID_ATTRIBUTE_TYPE: // Command Id
+    case ZCL_TRANS_ID_ATTRIBUTE_TYPE:   // Transaction Id
+    case ZCL_DEVTYPE_ID_ATTRIBUTE_TYPE: // Device Type Id
+    case ZCL_DATA_VER_ATTRIBUTE_TYPE:   // Data Version
+    case ZCL_BITMAP32_ATTRIBUTE_TYPE:   // 32-bit bitmap
+    case ZCL_EPOCH_S_ATTRIBUTE_TYPE:    // Epoch Seconds
+    case ZCL_ELAPSED_S_ATTRIBUTE_TYPE:  // Elapsed Seconds
+        static_assert(std::is_same<chip::ClusterId, uint32_t>::value,
+                      "chip::Cluster is expected to be uint32_t, change this when necessary");
+        static_assert(std::is_same<chip::AttributeId, uint32_t>::value,
+                      "chip::AttributeId is expected to be uint32_t, change this when necessary");
+        static_assert(std::is_same<chip::AttributeId, uint32_t>::value,
+                      "chip::AttributeId is expected to be uint32_t, change this when necessary");
+        static_assert(std::is_same<chip::EventId, uint32_t>::value,
+                      "chip::EventId is expected to be uint32_t, change this when necessary");
+        static_assert(std::is_same<chip::CommandId, uint32_t>::value,
+                      "chip::CommandId is expected to be uint32_t, change this when necessary");
+        static_assert(std::is_same<chip::TransactionId, uint32_t>::value,
+                      "chip::TransactionId is expected to be uint32_t, change this when necessary");
+        static_assert(std::is_same<chip::DeviceTypeId, uint32_t>::value,
+                      "chip::DeviceTypeId is expected to be uint32_t, change this when necessary");
+        static_assert(std::is_same<chip::DataVersion, uint32_t>::value,
+                      "chip::DataVersion is expected to be uint32_t, change this when necessary");
+        return ZCL_INT32U_ATTRIBUTE_TYPE;
+
+    case ZCL_AMPERAGE_MA_ATTRIBUTE_TYPE: // Amperage milliamps
+    case ZCL_ENERGY_MWH_ATTRIBUTE_TYPE:  // Energy milliwatt-hours
+    case ZCL_POWER_MW_ATTRIBUTE_TYPE:    // Power milliwatts
+    case ZCL_VOLTAGE_MV_ATTRIBUTE_TYPE:  // Voltage millivolts
+        return ZCL_INT64S_ATTRIBUTE_TYPE;
+
+    case ZCL_EVENT_NO_ATTRIBUTE_TYPE:   // Event Number
+    case ZCL_FABRIC_ID_ATTRIBUTE_TYPE:  // Fabric Id
+    case ZCL_NODE_ID_ATTRIBUTE_TYPE:    // Node Id
+    case ZCL_BITMAP64_ATTRIBUTE_TYPE:   // 64-bit bitmap
+    case ZCL_EPOCH_US_ATTRIBUTE_TYPE:   // Epoch Microseconds
+    case ZCL_POSIX_MS_ATTRIBUTE_TYPE:   // POSIX Milliseconds
+    case ZCL_SYSTIME_MS_ATTRIBUTE_TYPE: // System time Milliseconds
+    case ZCL_SYSTIME_US_ATTRIBUTE_TYPE: // System time Microseconds
+        static_assert(std::is_same<chip::EventNumber, uint64_t>::value,
+                      "chip::EventNumber is expected to be uint64_t, change this when necessary");
+        static_assert(std::is_same<chip::FabricId, uint64_t>::value,
+                      "chip::FabricId is expected to be uint64_t, change this when necessary");
+        static_assert(std::is_same<chip::NodeId, uint64_t>::value,
+                      "chip::NodeId is expected to be uint64_t, change this when necessary");
+        return ZCL_INT64U_ATTRIBUTE_TYPE;
+
+    case ZCL_TEMPERATURE_ATTRIBUTE_TYPE: // Temperature
+        return ZCL_INT16S_ATTRIBUTE_TYPE;
+
+    default:
+        return type;
+    }
+}
+
+} // namespace Internal
+} // namespace Compatibility
+} // namespace app
+} // namespace chip
diff --git a/src/app/util/ember-io-storage.h b/src/app/util/ember-io-storage.h
new file mode 100644
index 0000000..4297bc7
--- /dev/null
+++ b/src/app/util/ember-io-storage.h
@@ -0,0 +1,55 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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.
+ */
+#pragma once
+
+#include <cstdint>
+
+#include <app/util/attribute-metadata.h>
+#include <lib/support/Span.h>
+
+namespace chip {
+namespace app {
+namespace Compatibility {
+namespace Internal {
+
+/// A buffer guaranteed to be sized sufficiently large to contain any individual value from
+/// the ember attribute/data store (i.e. a buffer that can be used to read ember data into
+/// or as a temporary buffer to place data before asking ember to store it).
+///
+/// This buffer is intended to be used for calls to `emAfReadOrWriteAttribute` and
+/// `emAfWriteAttributeExternal`: it is sufficiently sized to be able to handle any
+/// max-sized data that ember is aware of.
+extern MutableByteSpan gEmberAttributeIOBufferSpan;
+
+/// Maps an attribute type that is not an integer but can be represented as an integer to the
+/// corresponding basic int(8|16|32|64)(s|u) type
+///
+/// For example:
+///    ZCL_ENUM8_ATTRIBUTE_TYPE maps to ZCL_INT8U_ATTRIBUTE_TYPE
+///    ZCL_VENDOR_ID_ATTRIBUTE_TYPE maps to ZCL_INT16U_ATTRIBUTE_TYPE
+///    ZCL_BITMAP32_ATTRIBUTE_TYPE maps to ZCL_INT32U_ATTRIBUTE_TYPE
+///    ZCL_VOLTAGE_MV_ATTRIBUTE_TYPE maps to ZCL_INT64S_ATTRIBUTE_TYPE
+///    ...
+///
+/// If the `type` cannot be mapped to a basic type (or is already a basic type) its value
+/// is returned unchanged.
+EmberAfAttributeType AttributeBaseType(EmberAfAttributeType type);
+
+} // namespace Internal
+} // namespace Compatibility
+} // namespace app
+} // namespace chip
diff --git a/src/app/util/mock/BUILD.gn b/src/app/util/mock/BUILD.gn
index da44fb8..b63c49e 100644
--- a/src/app/util/mock/BUILD.gn
+++ b/src/app/util/mock/BUILD.gn
@@ -25,6 +25,7 @@
     "MockNodeConfig.cpp",
     "MockNodeConfig.h",
     "attribute-storage.cpp",
+    "privilege-storage.cpp",
   ]
 
   public_deps = [
diff --git a/src/app/util/mock/attribute-storage.cpp b/src/app/util/mock/attribute-storage.cpp
index 2293a48..e4c965e 100644
--- a/src/app/util/mock/attribute-storage.cpp
+++ b/src/app/util/mock/attribute-storage.cpp
@@ -152,6 +152,50 @@
     return static_cast<uint8_t>(endpoint->clusters.size());
 }
 
+const EmberAfAttributeMetadata * emberAfLocateAttributeMetadata(EndpointId endpointId, ClusterId clusterId, AttributeId attributeId)
+{
+    auto ep = GetMockNodeConfig().endpointById(endpointId);
+    VerifyOrReturnValue(ep != nullptr, nullptr);
+
+    auto cluster = ep->clusterById(clusterId);
+    VerifyOrReturnValue(cluster != nullptr, nullptr);
+
+    auto attr = cluster->attributeById(attributeId);
+    VerifyOrReturnValue(attr != nullptr, nullptr);
+
+    return &attr->attributeMetaData;
+}
+
+const EmberAfCluster * emberAfFindClusterInType(const EmberAfEndpointType * endpointType, ClusterId clusterId,
+                                                EmberAfClusterMask mask, uint8_t * index)
+{
+    // This is a copy & paste implementation from ember attribute storage
+    // TODO: this hard-codes ember logic and is duplicated code.
+    uint8_t scopedIndex = 0;
+
+    for (uint8_t i = 0; i < endpointType->clusterCount; i++)
+    {
+        const EmberAfCluster * cluster = &(endpointType->cluster[i]);
+
+        if (mask == 0 || ((cluster->mask & mask) != 0))
+        {
+            if (cluster->clusterId == clusterId)
+            {
+                if (index)
+                {
+                    *index = scopedIndex;
+                }
+
+                return cluster;
+            }
+
+            scopedIndex++;
+        }
+    }
+
+    return nullptr;
+}
+
 uint8_t emberAfClusterCount(chip::EndpointId endpoint, bool server)
 {
     return (server) ? emberAfGetClusterCountForEndpoint(endpoint) : 0;
@@ -414,6 +458,7 @@
     mockConfig = &config;
 }
 
+/// Resets the mock attribute storage to the default configuration.
 void ResetMockNodeConfig()
 {
     mockConfig = nullptr;
diff --git a/src/app/util/mock/include/zap-generated/endpoint_config.h b/src/app/util/mock/include/zap-generated/endpoint_config.h
index 33d1e87..620e6e2 100644
--- a/src/app/util/mock/include/zap-generated/endpoint_config.h
+++ b/src/app/util/mock/include/zap-generated/endpoint_config.h
@@ -1,2 +1,4 @@
 // Number of fixed endpoints
 #define FIXED_ENDPOINT_COUNT (3)
+
+#define ATTRIBUTE_LARGEST (1003)
diff --git a/src/app/tests/integration/RequiredPrivilegeStubs.cpp b/src/app/util/mock/privilege-storage.cpp
similarity index 81%
rename from src/app/tests/integration/RequiredPrivilegeStubs.cpp
rename to src/app/util/mock/privilege-storage.cpp
index 2cc2305..26ff896 100644
--- a/src/app/tests/integration/RequiredPrivilegeStubs.cpp
+++ b/src/app/util/mock/privilege-storage.cpp
@@ -1,6 +1,5 @@
-/*
- *
- *    Copyright (c) 2022 Project CHIP Authors
+/**
+ *    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.
@@ -14,8 +13,12 @@
  *    See the License for the specific language governing permissions and
  *    limitations under the License.
  */
+#include <access/Privilege.h>
+#include <lib/core/DataModelTypes.h>
 
-#include <app/util/privilege-storage.h>
+// Privilege mocks here are MUCH more strict so that
+// testing code can generally validatate access without something
+// being permissive like kView.
 
 chip::Access::Privilege MatterGetAccessPrivilegeForReadAttribute(chip::ClusterId cluster, chip::AttributeId attribute)
 {
diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
index 4cb4edf..36aa65d 100644
--- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
+++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
@@ -364,6 +364,10 @@
 		B4FCD5732B611EB300832859 /* MTRDiagnosticLogsDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = B4C8E6B32B3453AD00FCD54D /* MTRDiagnosticLogsDownloader.h */; };
 		BA09EB43247477BA00605257 /* libCHIP.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BA09EB3F2474762900605257 /* libCHIP.a */; };
 		D4772A46285AE98400383630 /* MTRClusterConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = D4772A45285AE98300383630 /* MTRClusterConstants.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		E04AC67D2BEEA17F00BA409B /* ember-io-storage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E04AC67B2BEEA17F00BA409B /* ember-io-storage.cpp */; };
+		E04AC67E2BEEA17F00BA409B /* ember-io-storage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E04AC67B2BEEA17F00BA409B /* ember-io-storage.cpp */; };
+		E04AC67F2BEEA17F00BA409B /* ember-global-attribute-access-interface.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E04AC67C2BEEA17F00BA409B /* ember-global-attribute-access-interface.cpp */; };
+		E04AC6802BEEA17F00BA409B /* ember-global-attribute-access-interface.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E04AC67C2BEEA17F00BA409B /* ember-global-attribute-access-interface.cpp */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -788,6 +792,8 @@
 		D437613F285BDC0D0051FEA2 /* MTRTestKeys.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRTestKeys.h; sourceTree = "<group>"; };
 		D4376140285BDC0D0051FEA2 /* MTRTestStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRTestStorage.h; sourceTree = "<group>"; };
 		D4772A45285AE98300383630 /* MTRClusterConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRClusterConstants.h; sourceTree = "<group>"; };
+		E04AC67B2BEEA17F00BA409B /* ember-io-storage.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = "ember-io-storage.cpp"; path = "util/ember-io-storage.cpp"; sourceTree = "<group>"; };
+		E04AC67C2BEEA17F00BA409B /* ember-global-attribute-access-interface.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = "ember-global-attribute-access-interface.cpp"; path = "util/ember-global-attribute-access-interface.cpp"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -1033,6 +1039,8 @@
 				514C79F22B62ED5500DD6D7B /* attribute-storage.cpp */,
 				514C79EF2B62ADDA00DD6D7B /* descriptor.cpp */,
 				514C79EC2B62ADCD00DD6D7B /* ember-compatibility-functions.cpp */,
+				E04AC67C2BEEA17F00BA409B /* ember-global-attribute-access-interface.cpp */,
+				E04AC67B2BEEA17F00BA409B /* ember-io-storage.cpp */,
 				516415FE2B6B132200D5CE11 /* DataModelHandler.cpp */,
 				514C79F52B62F0B900DD6D7B /* util.cpp */,
 				514C79FB2B62F94C00DD6D7B /* ota-provider.cpp */,
@@ -1845,6 +1853,7 @@
 				B45373FE2A9FEC4F00807602 /* unix-fds.c in Sources */,
 				B45374002A9FEC4F00807602 /* unix-init.c in Sources */,
 				B45373FF2A9FEC4F00807602 /* unix-misc.c in Sources */,
+				E04AC67E2BEEA17F00BA409B /* ember-io-storage.cpp in Sources */,
 				B45373FD2A9FEC4F00807602 /* unix-pipe.c in Sources */,
 				B45373FB2A9FEC4F00807602 /* unix-service.c in Sources */,
 				B45374012A9FEC4F00807602 /* unix-sockets.c in Sources */,
@@ -1891,6 +1900,7 @@
 				037C3DB62991BD5000B7EEE2 /* ModelCommandBridge.mm in Sources */,
 				516411322B6BF75700E67C05 /* MTRIMDispatch.mm in Sources */,
 				037C3DB42991BD5000B7EEE2 /* DeviceControllerDelegateBridge.mm in Sources */,
+				E04AC6802BEEA17F00BA409B /* ember-global-attribute-access-interface.cpp in Sources */,
 				039547012992D461006D42A8 /* generic-callback-stubs.cpp in Sources */,
 				514C79F12B62ADDA00DD6D7B /* descriptor.cpp in Sources */,
 			);
@@ -1926,6 +1936,7 @@
 				7534F12828BFF20300390851 /* MTRDeviceAttestationDelegate.mm in Sources */,
 				B4C8E6B72B3453AD00FCD54D /* MTRDiagnosticLogsDownloader.mm in Sources */,
 				2C5EEEF7268A85C400CAE3D3 /* MTRDeviceConnectionBridge.mm in Sources */,
+				E04AC67D2BEEA17F00BA409B /* ember-io-storage.cpp in Sources */,
 				51B22C262740CB32008D5055 /* MTRStructsObjc.mm in Sources */,
 				2C222AD1255C620600E446B9 /* MTRBaseDevice.mm in Sources */,
 				1EC3238D271999E2002A8BF0 /* cluster-objects.cpp in Sources */,
@@ -1967,6 +1978,7 @@
 				5178E67E2AE098210069DF72 /* MTRCommandTimedCheck.mm in Sources */,
 				7596A84928762783004DAE0E /* MTRAsyncCallbackWorkQueue.mm in Sources */,
 				B2E0D7B9245B0B5C003C5B48 /* MTRSetupPayload.mm in Sources */,
+				E04AC67F2BEEA17F00BA409B /* ember-global-attribute-access-interface.cpp in Sources */,
 				B2E0D7B6245B0B5C003C5B48 /* MTRManualSetupPayloadParser.mm in Sources */,
 				7596A85528788557004DAE0E /* MTRClusters.mm in Sources */,
 				88EBF8CF27FABDD500686BC1 /* MTRDeviceAttestationDelegateBridge.mm in Sources */,