diff --git a/src/controller/python/test/test_scripts/base.py b/src/controller/python/test/test_scripts/base.py index ab02b4645e4cf1..19e3261822e3ee 100644 --- a/src/controller/python/test/test_scripts/base.py +++ b/src/controller/python/test/test_scripts/base.py @@ -370,25 +370,104 @@ async def TestCaseEviction(self, nodeid: int): # of eviction, just that allocation and CASE session establishment proceeds successfully on both # the controller and target. # - for x in range(minimumSupportedFabrics * minimumCASESessionsPerFabric * 3): + for x in range(minimumSupportedFabrics * minimumCASESessionsPerFabric * 2): self.devCtrl.CloseSession(nodeid) await self.devCtrl.ReadAttribute(nodeid, [(Clusters.Basic.Attributes.ClusterRevision)]) self.logger.info("Testing CASE defunct logic") # - # This tests establishing a subscription on a given CASE session, then mark it defunct (to simulate + # This tests establishes a subscription on a given CASE session, then marks it defunct (to simulate # encountering a transport timeout on the session). # - # At the max interval, we should still have a valid subscription. + # Then, we write to the attribute that was subscribed to from a *different* fabric and check to ensure we still get a report + # on the sub we established previously. Since it was just marked defunct, it should return back to being + # active and a report should get delivered. + # + sawValueChange = False + + def OnValueChange(path: Attribute.TypedAttributePath, transaction: Attribute.SubscriptionTransaction) -> None: + nonlocal sawValueChange + self.logger.info("Saw value change!") + if (path.AttributeType == Clusters.TestCluster.Attributes.Int8u and path.Path.EndpointId == 1): + sawValueChange = True + + self.logger.info("Testing CASE defunct logic") + + sub = await self.devCtrl.ReadAttribute(nodeid, [(Clusters.TestCluster.Attributes.Int8u)], reportInterval=(0, 1)) + sub.SetAttributeUpdateCallback(OnValueChange) + + # + # This marks the session defunct. # - sub = await self.devCtrl.ReadAttribute(nodeid, [(Clusters.Basic.Attributes.ClusterRevision)], reportInterval=(0, 2)) - await asyncio.sleep(2) self.devCtrl.CloseSession(nodeid) - await asyncio.sleep(4) + + # + # Now write the attribute from fabric2, give it some time before checking if the report + # was received. + # + await self.devCtrl2.WriteAttribute(nodeid, [(1, Clusters.TestCluster.Attributes.Int8u(4))]) + time.sleep(2) sub.Shutdown() + if sawValueChange is False: + self.logger.error("Didn't see value change in time, likely because sub got terminated due to unexpected session eviction!") + return False + + # + # In this test, we're going to setup a subscription on fabric1 through devCtl, then, constantly keep + # evicting sessions on fabric2 (devCtl2) by cycling through closing sessions followed by issuing a Read. This + # should result in evictions on the server on fabric2, but not affect any sessions on fabric1. To test this, + # we're going to setup a subscription to an attribute prior to the cycling reads, and check at the end of the + # test that it's still valid by writing to an attribute from a *different* fabric, and validating that we see + # the change on the established subscription. That proves that the session from fabric1 is still valid and untouched. + # + self.logger.info("Testing fabric-isolated CASE eviction") + + sawValueChange = False + sub = await self.devCtrl.ReadAttribute(nodeid, [(Clusters.TestCluster.Attributes.Int8u)], reportInterval=(0, 1)) + sub.SetAttributeUpdateCallback(OnValueChange) + + for x in range(minimumSupportedFabrics * minimumCASESessionsPerFabric * 2): + self.devCtrl2.CloseSession(nodeid) + await self.devCtrl2.ReadAttribute(nodeid, [(Clusters.Basic.Attributes.ClusterRevision)]) + + # + # Now write the attribute from fabric2, give it some time before checking if the report + # was received. + # + await self.devCtrl2.WriteAttribute(nodeid, [(1, Clusters.TestCluster.Attributes.Int8u(4))]) + time.sleep(2) + + sub.Shutdown() + + if sawValueChange is False: + self.logger.error("Didn't see value change in time, likely because sub got terminated due to other fabric (fabric1)") + return False + + # + # Do the same test again, but reversing the roles of fabric1 and fabric2. + # + self.logger.info("Testing fabric-isolated CASE eviction (reverse)") + + sawValueChange = False + sub = await self.devCtrl2.ReadAttribute(nodeid, [(Clusters.TestCluster.Attributes.Int8u)], reportInterval=(0, 1)) + sub.SetAttributeUpdateCallback(OnValueChange) + + for x in range(minimumSupportedFabrics * minimumCASESessionsPerFabric * 2): + self.devCtrl.CloseSession(nodeid) + await self.devCtrl.ReadAttribute(nodeid, [(Clusters.Basic.Attributes.ClusterRevision)]) + + await self.devCtrl.WriteAttribute(nodeid, [(1, Clusters.TestCluster.Attributes.Int8u(4))]) + time.sleep(2) + + sub.Shutdown() + + if sawValueChange is False: + self.logger.error("Didn't see value change in time, likely because sub got terminated due to other fabric (fabric2)") + return False + return True async def TestMultiFabric(self, ip: str, setuppin: int, nodeid: int): diff --git a/src/controller/python/test/test_scripts/mobile-device-test.py b/src/controller/python/test/test_scripts/mobile-device-test.py index 9ed69d9d5d19b3..7ea8173d15d031 100755 --- a/src/controller/python/test/test_scripts/mobile-device-test.py +++ b/src/controller/python/test/test_scripts/mobile-device-test.py @@ -76,24 +76,14 @@ def ethernet_commissioning(test: BaseTestHelper, discriminator: int, setup_pin: nodeid=device_nodeid), "Failed to finish key exchange") - # - # Run this before the MultiFabric test, since it will result in the resultant CASE session - # on fabric2 to be evicted (due to the stressful nature of that test) on the target. - # - # It doesn't actually evict the CASE session for fabric2 on the client, since we prioritize - # defunct sessions for eviction first, which means our CASE session on fabric2 remains preserved - # throughout the stress test. This results in a mis-match later. - # - # TODO: Once we implement fabric-adjusted LRU, we should see if this issue remains (it shouldn't) - # - logger.info("Testing CASE Eviction") - FailIfNot(asyncio.run(test.TestCaseEviction(device_nodeid)), "Failed TestCaseEviction") - ok = asyncio.run(test.TestMultiFabric(ip=address, setuppin=20202021, nodeid=1)) FailIfNot(ok, "Failed to commission multi-fabric") + logger.info("Testing CASE Eviction") + FailIfNot(asyncio.run(test.TestCaseEviction(device_nodeid)), "Failed TestCaseEviction") + logger.info("Testing closing sessions") FailIfNot(test.TestCloseSession(nodeid=device_nodeid), "Failed to close sessions") diff --git a/src/transport/SecureSession.h b/src/transport/SecureSession.h index 88239c8c741ca3..ed8eae3a400454 100644 --- a/src/transport/SecureSession.h +++ b/src/transport/SecureSession.h @@ -292,6 +292,8 @@ class SecureSession : public Session, public ReferenceCounted SecureSessionTable::CreateNewSecureSession(SecureSession // to run the eviction algorithm to get a free slot. We shall ALWAYS be guaranteed to evict // an existing session in the table in normal operating circumstances. // - if (mEntries.Allocated() < CHIP_CONFIG_SECURE_SESSION_POOL_SIZE) + if (mEntries.Allocated() < GetMaxSessionTableSize()) { allocated = mEntries.CreateObject(*this, secureSessionType, sessionId.Value()); } @@ -102,47 +102,92 @@ SecureSession * SecureSessionTable::EvictAndAllocate(uint16_t localSessionId, Se ChipLogProgress(SecureChannel, "Evicting a slot for session with LSID: %d, type: %u", localSessionId, (uint8_t) secureSessionType); - VerifyOrDie(mEntries.Allocated() <= CHIP_CONFIG_SECURE_SESSION_POOL_SIZE); + VerifyOrDie(mEntries.Allocated() <= GetMaxSessionTableSize()); // // Create a temporary list of objects each of which points to a session in the existing // session table, but are swappable. This allows them to then be used with a sorting algorithm // without affecting the sessions in the table itself. // - Platform::ScopedMemoryBufferWithSize sortableSessions; - sortableSessions.Calloc(mEntries.Allocated()); - if (!sortableSessions) - { - VerifyOrDieWithMsg(false, SecureChannel, "We couldn't allocate a session!"); - return nullptr; - } + // The size of this shouldn't place significant demands on the stack if using the default + // configuration for CHIP_CONFIG_SECURE_SESSION_POOL_SIZE (17). Each item is + // 8 bytes in size (on a 32-bit platform), and 16 bytes in size (on a 64-bit platform, + // including padding). + // + // Total size of this stack variable = 17 * 8 = 136bytes (32-bit platform), 272 bytes (64-bit platform). + // + // Even if the define is set to a large value, it's likely not so bad on the sort of platform setup + // that would have that sort of pool size. + // + // We need to sort (as opposed to just a linear search for the smallest/largest item) + // since it is possible that the candidate selected for eviction may not actually be + // released once marked for expiration (see comments below for more details). + // + // Consequently, we may need to walk the candidate list till we find one that is. + // Sorting provides a better overall performance model in this scheme. + // + // (#19967): Investigate doing linear search instead. + // + // + SortableSession sortableSessions[CHIP_CONFIG_SECURE_SESSION_POOL_SIZE]; + + unsigned int index = 0; + + // + // Compute two key stats for each session - the number of other sessions that + // match its fabric, as well as the number of other sessions that match its peer. + // + // This will be used by the session eviction algorithm later. + // + ForEachSession([&index, &sortableSessions, this](auto * session) { + sortableSessions[index].mSession = session; + sortableSessions[index].mNumMatchingOnFabric = 0; + sortableSessions[index].mNumMatchingOnPeer = 0; + + ForEachSession([session, index, &sortableSessions](auto * otherSession) { + if (session != otherSession) + { + if (session->GetFabricIndex() == otherSession->GetFabricIndex()) + { + sortableSessions[index].mNumMatchingOnFabric++; + + if (session->GetPeerNodeId() == otherSession->GetPeerNodeId()) + { + sortableSessions[index].mNumMatchingOnPeer++; + } + } + } + + return Loop::Continue; + }); - int index = 0; - ForEachSession([&index, &sortableSessions](auto session) { - sortableSessions.Get()[index].mSession = session; index++; return Loop::Continue; }); - auto sortableSessionSpan = Span(sortableSessions.Get(), mEntries.Allocated()); + auto sortableSessionSpan = Span(sortableSessions, mEntries.Allocated()); EvictionPolicyContext policyContext(sortableSessionSpan, sessionEvictionHint); DefaultEvictionPolicy(policyContext); ChipLogProgress(SecureChannel, "Sorted sessions for eviction..."); - auto numSessions = mEntries.Allocated(); + const auto numSessions = mEntries.Allocated(); #if CHIP_DETAIL_LOGGING ChipLogDetail(SecureChannel, "Sorted Eviction Candidates (ranked from best candidate to worst):"); - for (auto * session = sortableSessions.Get(); session != (sortableSessions.Get() + numSessions); session++) + for (auto * session = sortableSessions; session != (sortableSessions + numSessions); session++) { - ChipLogDetail(SecureChannel, "\t%ld: [%p] -- State: '%s', ActivityTime: %lu", - static_cast(session - sortableSessions.Get()), session->mSession, session->mSession->GetStateStr(), + ChipLogDetail(SecureChannel, + "\t%ld: [%p] -- Peer: [%u:" ChipLogFormatX64 + "] State: '%s', NumMatchingOnFabric: %d NumMatchingOnPeer: %d ActivityTime: %lu", + static_cast(session - sortableSessions), session->mSession, + session->mSession->GetPeer().GetFabricIndex(), ChipLogValueX64(session->mSession->GetPeer().GetNodeId()), + session->mSession->GetStateStr(), session->mNumMatchingOnFabric, session->mNumMatchingOnPeer, static_cast(session->mSession->GetLastActivityTime().count())); } #endif - for (auto * session = sortableSessions.Get(); session != (sortableSessions.Get() + numSessions); session++) + for (auto * session = sortableSessions; session != (sortableSessions + numSessions); session++) { if (session->mSession->IsPendingEviction()) { @@ -183,9 +228,55 @@ SecureSession * SecureSessionTable::EvictAndAllocate(uint16_t localSessionId, Se void SecureSessionTable::DefaultEvictionPolicy(EvictionPolicyContext & evictionContext) { - evictionContext.Sort([](const auto & a, const auto & b) { - int aStateScore = 0, bStateScore = 0; + // + // This implements a spec-compliant sorting policy that ensures both guarantees for sessions per-fabric as + // mandated by the spec as well as fairness in terms of selecting the most appropriate session to evict + // based on multiple criteria. + // + // See the description of this function in the header for more details on each sorting key below. + // + evictionContext.Sort([&evictionContext](const SortableSession & a, const SortableSession & b) -> bool { + // + // Sorting on Key1 + // + if (a.mNumMatchingOnFabric != b.mNumMatchingOnFabric) + { + return a.mNumMatchingOnFabric > b.mNumMatchingOnFabric; + } + + bool doesAMatchSessionHintFabric = + a.mSession->GetPeer().GetFabricIndex() == evictionContext.GetSessionEvictionHint().GetFabricIndex(); + bool doesBMatchSessionHintFabric = + b.mSession->GetPeer().GetFabricIndex() == evictionContext.GetSessionEvictionHint().GetFabricIndex(); + + // + // Sorting on Key2 + // + if (doesAMatchSessionHintFabric != doesBMatchSessionHintFabric) + { + return doesAMatchSessionHintFabric > doesBMatchSessionHintFabric; + } + + // + // Sorting on Key3 + // + if (a.mNumMatchingOnPeer != b.mNumMatchingOnPeer) + { + return a.mNumMatchingOnPeer > b.mNumMatchingOnPeer; + } + + int doesAMatchSessionHint = a.mSession->GetPeer() == evictionContext.GetSessionEvictionHint(); + int doesBMatchSessionHint = b.mSession->GetPeer() == evictionContext.GetSessionEvictionHint(); + // + // Sorting on Key4 + // + if (doesAMatchSessionHint != doesBMatchSessionHint) + { + return doesAMatchSessionHint > doesBMatchSessionHint; + } + + int aStateScore = 0, bStateScore = 0; auto assignStateScore = [](auto & score, const auto & session) { if (session.IsDefunct()) { @@ -204,7 +295,18 @@ void SecureSessionTable::DefaultEvictionPolicy(EvictionPolicyContext & evictionC assignStateScore(aStateScore, *a.mSession); assignStateScore(bStateScore, *b.mSession); - return ((aStateScore > bStateScore) ? true : (a->GetLastActivityTime() < b->GetLastActivityTime())); + // + // Sorting on Key5 + // + if (aStateScore != bStateScore) + { + return (aStateScore > bStateScore); + } + + // + // Sorting on Key6 + // + return (a->GetLastActivityTime() < b->GetLastActivityTime()); }); } diff --git a/src/transport/SecureSessionTable.h b/src/transport/SecureSessionTable.h index 0d66ad465d1a0a..c84fc94ea3a5d8 100644 --- a/src/transport/SecureSessionTable.h +++ b/src/transport/SecureSessionTable.h @@ -142,9 +142,19 @@ class SecureSessionTable } const Transport::SecureSession * operator->() const { return mSession; } + auto GetNumMatchingOnFabric() { return mNumMatchingOnFabric; } + auto GetNumMatchingOnPeer() { return mNumMatchingOnPeer; } private: SecureSession * mSession; + uint16_t mNumMatchingOnFabric; + uint16_t mNumMatchingOnPeer; + + static_assert(CHIP_CONFIG_SECURE_SESSION_POOL_SIZE <= std::numeric_limits::max(), + "mNumMatchingOnFabric must be able to count up to CHIP_CONFIG_SECURE_SESSION_POOL_SIZE!"); + static_assert(CHIP_CONFIG_SECURE_SESSION_POOL_SIZE <= std::numeric_limits::max(), + "mNumMatchingOnPeer must be able to count up to CHIP_CONFIG_SECURE_SESSION_POOL_SIZE!"); + friend class SecureSessionTable; }; @@ -192,14 +202,32 @@ class SecureSessionTable /** * - * This implements the following eviction policy: + * This implements an eviction policy by sorting sessions using the following sorting keys and selecting + * the session that is most ahead as the best candidate for eviction: + * + * - Key1: Sessions on fabrics that have more sessions in the table are placed ahead of sessions on fabrics + * with lesser sessions. We conclusively know that if a particular fabric has more sessions in the table + * than another, then that fabric is definitely over minimas (assuming a minimally sized session table + * conformant to spec minimas). + * + * Key2: Sessions that match the eviction hint's fabric are placed ahead of those that don't. This ensures that + * if Key1 is even (i.e two fabrics are tied in count), that you attempt to select sessions that match + * the eviction hint's fabric to ensure we evict sessions within the fabric that a new session might be about + * to be created within. This is essential to preventing cross-fabric denial of service possibilities. + * + * Key3: Sessions with a higher mNumMatchingOnPeer are placed ahead of those with a lower one. This ensures + * we pick sessions that have a higher number of duplicated sessions to a peer over those with lower since + * evicting a duplicated session will have less of an impact to that peer. + * + * Key4: Sessions whose target peer's ScopedNodeId matches the eviction hint are placed ahead of those who don't. This + * ensures that all things equal, a session that already exists to the peer is refreshed ahead of another to another peer. * - * - Sessions are sorted with their state as the primary sort key and activity time as the secondary - * sort key. - * - The primary sort key places defunct sessions ahead of active ones, ahead of anything else. - * - The secondary sort key places older sessions ahead of newer sessions. This ensures - * we're prioritizing reaping less active sessions over more recently active sessions (activity - * in either TX or RX). + * Key5: Sessions that are in defunct state are placed ahead of those in the active state, ahead of any other state. + * This ensures that we prioritize evicting defunct sessions (since they have been deemed non-functional anyways) + * over active, healthy ones, over those are currently in the process of establishment. + * + * Key6: Sessions that have a less recent activity time are placed ahead of those with a more recent activity time. This + * is the canonical sorting criteria for basic LRU. * */ void DefaultEvictionPolicy(EvictionPolicyContext & evictionContext); @@ -230,6 +258,21 @@ class SecureSessionTable bool mRunningEvictionLogic = false; ObjectPool mEntries; + + size_t GetMaxSessionTableSize() const + { +#if CONFIG_BUILD_FOR_HOST_UNIT_TEST + return mMaxSessionTableSize; +#else + return CHIP_CONFIG_SECURE_SESSION_POOL_SIZE; +#endif + } + +#if CONFIG_BUILD_FOR_HOST_UNIT_TEST + size_t mMaxSessionTableSize = CHIP_CONFIG_SECURE_SESSION_POOL_SIZE; + void SetMaxSessionTableSize(size_t size) { mMaxSessionTableSize = size; } +#endif + uint16_t mNextSessionId = 0; }; diff --git a/src/transport/tests/BUILD.gn b/src/transport/tests/BUILD.gn index 4fea0a0a66a1c3..85c74f988a750b 100644 --- a/src/transport/tests/BUILD.gn +++ b/src/transport/tests/BUILD.gn @@ -40,6 +40,11 @@ chip_test_suite("tests") { "TestSessionManager.cpp", ] + if (chip_device_platform != "mbed" && chip_device_platform != "efr32" && + chip_device_platform != "esp32" && chip_device_platform != "nrfconnect") { + test_sources += [ "TestSecureSessionTable.cpp" ] + } + cflags = [ "-Wconversion" ] public_deps = [ diff --git a/src/transport/tests/TestSecureSessionTable.cpp b/src/transport/tests/TestSecureSessionTable.cpp new file mode 100644 index 00000000000000..a7201fc5314760 --- /dev/null +++ b/src/transport/tests/TestSecureSessionTable.cpp @@ -0,0 +1,424 @@ +/* + * + * Copyright (c) 2020-2021 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. + */ + +/** + * @file + * This file implements unit tests for the SessionManager implementation. + */ + +#include "system/SystemClock.h" +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace chip { +namespace Transport { + +class TestSecureSessionTable +{ +public: + // + // This test specifically validates eviction of sessions in the session table + // with various scenarios based on the existing set of sessions in the table + // and a provided session eviction hint + // + static void ValidateSessionSorting(nlTestSuite * inSuite, void * inContext); + +private: + struct SessionParameters + { + ScopedNodeId mPeer; + System::Clock::Timestamp mLastActivityTime; + SecureSession::State mState; + }; + + // + // This listener lets us track which sessions get evicted by + // using a SessionHolderWithDelegate to get notified on session release. + // + class SessionNotificationListener : public SessionDelegate + { + public: + SessionNotificationListener(const SessionHandle & session) : mSessionHolder(*this) { mSessionHolder.Grab(session); } + + void OnSessionReleased() { mSessionReleased = true; } + + NewSessionHandlingPolicy GetNewSessionHandlingPolicy() { return NewSessionHandlingPolicy::kStayAtOldSession; } + + SessionHolderWithDelegate mSessionHolder; + bool mSessionReleased = false; + }; + + static constexpr FabricIndex kFabric1 = 1; + static constexpr FabricIndex kFabric2 = 2; + static constexpr FabricIndex kFabric3 = 3; + + // + // Allocates a new secure session given an eviction hint. The session that was evicted is compared against the provided + // evictedSessionIndex (which indexes into the provided SessionParameter table) to validate that it matches. + // + void AllocateSession(const ScopedNodeId & sessionEvictionHint, std::vector & sessionParameters, + uint16_t evictedSessionIndex); + + // + // Reset our internal SecureSessionTable list and create a new one given the provided parameters. + // + void CreateSessionTable(std::vector & sessionParams); + + nlTestSuite * mTestSuite; + Platform::UniquePtr mSessionTable; + std::vector> mSessionList; +}; + +void TestSecureSessionTable::AllocateSession(const ScopedNodeId & sessionEvictionHint, + std::vector & sessionParameters, uint16_t evictedSessionIndex) +{ + auto session = mSessionTable->CreateNewSecureSession(SecureSession::Type::kCASE, sessionEvictionHint); + NL_TEST_ASSERT(mTestSuite, session.HasValue()); + NL_TEST_ASSERT(mTestSuite, mSessionList[evictedSessionIndex].get()->mSessionReleased == true); +} + +void TestSecureSessionTable::CreateSessionTable(std::vector & sessionParams) +{ + mSessionList.clear(); + + mSessionTable = Platform::MakeUnique(); + NL_TEST_ASSERT(mTestSuite, mSessionTable.get() != nullptr); + + mSessionTable->Init(); + mSessionTable->SetMaxSessionTableSize(static_cast(sessionParams.size())); + + for (unsigned int i = 0; i < sessionParams.size(); i++) + { + auto session = mSessionTable->CreateNewSecureSession(SecureSession::Type::kCASE, ScopedNodeId()); + NL_TEST_ASSERT(mTestSuite, session.HasValue()); + + session.Value()->AsSecureSession()->Activate( + ScopedNodeId(1, sessionParams[i].mPeer.GetFabricIndex()), sessionParams[i].mPeer, CATValues(), static_cast(i), + ReliableMessageProtocolConfig(System::Clock::Milliseconds32(0), System::Clock::Milliseconds32(0))); + session.Value()->AsSecureSession()->mLastActivityTime = sessionParams[i].mLastActivityTime; + session.Value()->AsSecureSession()->mState = sessionParams[i].mState; + + mSessionList.push_back(Platform::MakeUnique(session.Value())); + } +} + +void TestSecureSessionTable::ValidateSessionSorting(nlTestSuite * inSuite, void * inContext) +{ + Platform::UniquePtr & _this = *static_cast *>(inContext); + _this->mTestSuite = inSuite; + + // + // This validates basic eviction. The table is full of sessions from Fabric1 from the same + // Node (2). Eviction should select the oldest session in the table (with timestamp 1) and evict that + // + { + ChipLogProgress(SecureChannel, "-------- Validating Basic Eviction (Matching Hint's Fabric) --------"); + + std::vector sessionParamList = { + { { 2, kFabric1 }, System::Clock::Timestamp(9), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(3), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(7), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(1), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + }; + + _this->CreateSessionTable(sessionParamList); + _this->AllocateSession(ScopedNodeId(2, kFabric1), sessionParamList, 4); + } + + // + // This validates basic eviction, with the sessionHint indicating a request from a different fabric than + // those in the table. Nothing changes from the example above since the sessions in the table are over minima, + // so it will just reap the oldest session in the table (with timestamp 1 again). + // + // + { + ChipLogProgress(SecureChannel, "-------- Validating Basic Eviction (No Match for Hint's Fabric) --------"); + + std::vector sessionParamList = { + { { 2, kFabric1 }, System::Clock::Timestamp(9), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(3), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(7), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(1), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + }; + + _this->CreateSessionTable(sessionParamList); + _this->AllocateSession(ScopedNodeId(2, kFabric2), sessionParamList, 4); + } + + // + // This validates evicting an over-minima fabric from the session table where there + // are sessions from two fabrics, Fabric1 and Fabric2. + // + // Fabric1 has 2 sessions, and Fabric2 has 4 sessions. Fabric2 will be selected since + // it has more sessions than Fabric2. + // + // Within that set, there are more sessions to Node 2 than others, so the oldest one + // in that set (timestamp 3) will be selected. + // + { + ChipLogProgress(SecureChannel, "-------- Over-minima Fabric Eviction ---------"); + + std::vector sessionParamList = { + { { 2, kFabric1 }, System::Clock::Timestamp(9), SecureSession::State::kActive }, + { { 2, kFabric2 }, System::Clock::Timestamp(3), SecureSession::State::kActive }, + { { 1, kFabric1 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + { { 2, kFabric2 }, System::Clock::Timestamp(7), SecureSession::State::kActive }, + { { 3, kFabric2 }, System::Clock::Timestamp(1), SecureSession::State::kActive }, + { { 4, kFabric2 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + }; + + _this->CreateSessionTable(sessionParamList); + _this->AllocateSession(ScopedNodeId(2, kFabric1), sessionParamList, 1); + } + + // + // This validates evicting an over-minima fabric from the session table where there + // are sessions from two fabrics, Fabric1 and Fabric2. + // + // Fabric1 has 2 sessions, and Fabric2 has 3 sessions. Fabric2 will be selected since + // it has more sessions than Fabric2. + // + // Within that set, there are more sessions to Node 2 than others, except one session + // is in the pairing state. So the active one will be selected instead. + // + { + ChipLogProgress(SecureChannel, "-------- Over-minima Fabric Eviction (State) ---------"); + + std::vector sessionParamList = { + { { 2, kFabric1 }, System::Clock::Timestamp(9), SecureSession::State::kActive }, + { { 2, kFabric2 }, System::Clock::Timestamp(3), SecureSession::State::kEstablishing }, + { { 1, kFabric1 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + { { 2, kFabric2 }, System::Clock::Timestamp(7), SecureSession::State::kActive }, + { { 3, kFabric2 }, System::Clock::Timestamp(1), SecureSession::State::kActive }, + { { 4, kFabric2 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + }; + + _this->CreateSessionTable(sessionParamList); + _this->AllocateSession(ScopedNodeId(2, kFabric1), sessionParamList, 3); + } + + // + // This validates evicting an over-minima fabric from the session table where there + // are sessions from two fabrics, Fabric1 and Fabric2. + // + // Fabric1 has 2 sessions, and Fabric2 has 4 sessions. Fabric2 will be selected since + // it has more sessions than Fabric1. + // + // Within that set, there are equal sessions to each node, so the session with the + // older timestamp will be selected. + // + { + ChipLogProgress(SecureChannel, "-------- Over-minima Fabric Eviction ---------"); + + std::vector sessionParamList = { + { { 2, kFabric1 }, System::Clock::Timestamp(9), SecureSession::State::kActive }, + { { 1, kFabric2 }, System::Clock::Timestamp(3), SecureSession::State::kActive }, + { { 1, kFabric1 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + { { 2, kFabric2 }, System::Clock::Timestamp(7), SecureSession::State::kActive }, + { { 3, kFabric2 }, System::Clock::Timestamp(1), SecureSession::State::kActive }, + { { 4, kFabric2 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + }; + + _this->CreateSessionTable(sessionParamList); + _this->AllocateSession(ScopedNodeId(2, kFabric1), sessionParamList, 4); + } + + // + // This validates evicting from a table with equally loaded fabrics. In this scenario, + // bias is given to the fabric that matches that of the eviction hint. + // + // There are more sessions to Node 2 in that fabric, so despite there be a match to + // Node 3 in the table, the older session to Node 2 will be evicted. + // + { + ChipLogProgress(SecureChannel, "-------- Equal Fabrics Eviction (Un-equal # Sessions / Node) ---------"); + + std::vector sessionParamList = { + { { 2, kFabric1 }, System::Clock::Timestamp(9), SecureSession::State::kActive }, + { { 1, kFabric2 }, System::Clock::Timestamp(3), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + { { 3, kFabric1 }, System::Clock::Timestamp(7), SecureSession::State::kActive }, + { { 3, kFabric2 }, System::Clock::Timestamp(1), SecureSession::State::kActive }, + { { 4, kFabric2 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + }; + + _this->CreateSessionTable(sessionParamList); + _this->AllocateSession(ScopedNodeId(3, kFabric1), sessionParamList, 2); + } + + // + // This validates evicting from a table with equally loaded fabrics. In this scenario, + // bias is given to the fabric that matches that of the eviction hint. + // + // There are equal sessions to Node 2 as well as Node 3 in that fabric, so the Node + // that matches the session eviction hint will be selected, and in that, the older session. + // + { + ChipLogProgress(SecureChannel, + "-------- Equal Fabrics Eviction (Equal # Sessions to Nodes, Hint Match On Fabric & Node) ---------"); + + std::vector sessionParamList = { + { { 1, kFabric1 }, System::Clock::Timestamp(9), SecureSession::State::kActive }, + { { 1, kFabric2 }, System::Clock::Timestamp(3), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + { { 3, kFabric1 }, System::Clock::Timestamp(7), SecureSession::State::kActive }, + { { 3, kFabric2 }, System::Clock::Timestamp(1), SecureSession::State::kActive }, + { { 4, kFabric2 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + }; + + _this->CreateSessionTable(sessionParamList); + _this->AllocateSession(ScopedNodeId(3, kFabric1), sessionParamList, 3); + } + + // + // Similar to above, except that the eviction hint matches a given fabric (kFabric1) in the + // session table, but not any nodes. In this case, the oldest session in that fabric is selected + // for eviction from the table. + // + { + ChipLogProgress(SecureChannel, + "-------- Equal Fabrics Eviction (Equal # of Sessions to Nodes, Hint Match on Fabric ONLY) ---------"); + + std::vector sessionParamList = { + { { 1, kFabric1 }, System::Clock::Timestamp(9), SecureSession::State::kActive }, + { { 1, kFabric2 }, System::Clock::Timestamp(3), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + { { 3, kFabric1 }, System::Clock::Timestamp(7), SecureSession::State::kActive }, + { { 3, kFabric2 }, System::Clock::Timestamp(1), SecureSession::State::kActive }, + { { 4, kFabric2 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + }; + + _this->CreateSessionTable(sessionParamList); + _this->AllocateSession(ScopedNodeId(4, kFabric1), sessionParamList, 2); + } + + // + // Similar to above, except the eviction hint does not match any fabric in the session table. + // Given all fabrics are within minimas, the oldest session is then selected. + // + { + ChipLogProgress(SecureChannel, "-------- Equal Fabrics Eviction (Equal # of Sessions to Nodes, No Hint Match) ---------"); + + std::vector sessionParamList = { + { { 1, kFabric1 }, System::Clock::Timestamp(9), SecureSession::State::kActive }, + { { 1, kFabric2 }, System::Clock::Timestamp(3), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + { { 3, kFabric1 }, System::Clock::Timestamp(7), SecureSession::State::kActive }, + { { 3, kFabric2 }, System::Clock::Timestamp(1), SecureSession::State::kActive }, + { { 4, kFabric2 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + }; + + _this->CreateSessionTable(sessionParamList); + _this->AllocateSession(ScopedNodeId(4, kFabric3), sessionParamList, 4); + } + + // + // Similar to above, except the oldest session happens to not be an active one. Instead, + // select the next oldest active session. + // + { + ChipLogProgress( + SecureChannel, + "-------- Equal Fabrics Eviction (Equal # of Sessions to Nodes, No Hint Match, In-active Session) ---------"); + + std::vector sessionParamList = { + { { 1, kFabric1 }, System::Clock::Timestamp(9), SecureSession::State::kActive }, + { { 1, kFabric2 }, System::Clock::Timestamp(3), SecureSession::State::kActive }, + { { 2, kFabric1 }, System::Clock::Timestamp(3), SecureSession::State::kActive }, + { { 3, kFabric1 }, System::Clock::Timestamp(7), SecureSession::State::kActive }, + { { 3, kFabric2 }, System::Clock::Timestamp(1), SecureSession::State::kEstablishing }, + { { 4, kFabric2 }, System::Clock::Timestamp(2), SecureSession::State::kActive }, + }; + + _this->CreateSessionTable(sessionParamList); + _this->AllocateSession(ScopedNodeId(4, kFabric3), sessionParamList, 5); + } +} + +Platform::UniquePtr gTestSecureSessionTable; + +} // namespace Transport +} // namespace chip + +// Test Suite + +namespace { + +/** + * Test Suite that lists all the test functions. + */ +// clang-format off +const nlTest sTests[] = +{ + NL_TEST_DEF("Validate Session Sorting (Over Minima)", chip::Transport::TestSecureSessionTable::ValidateSessionSorting), + NL_TEST_SENTINEL() +}; +// clang-format on + +int Initialize(void * apSuite) +{ + VerifyOrReturnError(chip::Platform::MemoryInit() == CHIP_NO_ERROR, FAILURE); + chip::Transport::gTestSecureSessionTable = chip::Platform::MakeUnique(); + return SUCCESS; +} + +int Finalize(void * aContext) +{ + chip::Transport::gTestSecureSessionTable.reset(); + chip::Platform::MemoryShutdown(); + return SUCCESS; +} + +// clang-format off +nlTestSuite sSuite = +{ + "TestSecureSessionTable", + &sTests[0], + Initialize, + Finalize +}; +// clang-format on + +} // namespace + +/** + * Main + */ +int SecureSessionTableTest() +{ + // Run test suit against one context + nlTestRunner(&sSuite, &chip::Transport::gTestSecureSessionTable); + + int r = (nlTestRunnerStats(&sSuite)); + return r; +} + +CHIP_REGISTER_TEST_SUITE(SecureSessionTableTest);