| /* | 
 |  *    Copyright (c) 2021 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 "lib/support/CHIPMem.h" | 
 | #include "lib/support/CodeUtils.h" | 
 | #include "lib/support/ScopedBuffer.h" | 
 | #include <access/AuthMode.h> | 
 | #include <lib/support/Defer.h> | 
 | #include <transport/SecureSession.h> | 
 | #include <transport/SecureSessionTable.h> | 
 |  | 
 | namespace chip { | 
 | namespace Transport { | 
 |  | 
 | Optional<SessionHandle> SecureSessionTable::CreateNewSecureSessionForTest(SecureSession::Type secureSessionType, | 
 |                                                                           uint16_t localSessionId, NodeId localNodeId, | 
 |                                                                           NodeId peerNodeId, CATValues peerCATs, | 
 |                                                                           uint16_t peerSessionId, FabricIndex fabricIndex, | 
 |                                                                           const ReliableMessageProtocolConfig & config) | 
 | { | 
 |     if (secureSessionType == SecureSession::Type::kCASE) | 
 |     { | 
 |         if ((fabricIndex == kUndefinedFabricIndex) || (localNodeId == kUndefinedNodeId) || (peerNodeId == kUndefinedNodeId)) | 
 |         { | 
 |             return Optional<SessionHandle>::Missing(); | 
 |         } | 
 |     } | 
 |     else if (secureSessionType == SecureSession::Type::kPASE) | 
 |     { | 
 |         if ((fabricIndex != kUndefinedFabricIndex) || (localNodeId != kUndefinedNodeId) || (peerNodeId != kUndefinedNodeId)) | 
 |         { | 
 |             // TODO: This secure session type is infeasible! We must fix the tests | 
 |             if (false) | 
 |             { | 
 |                 return Optional<SessionHandle>::Missing(); | 
 |             } | 
 |  | 
 |             (void) fabricIndex; | 
 |         } | 
 |     } | 
 |  | 
 |     SecureSession * result = mEntries.CreateObject(*this, secureSessionType, localSessionId, localNodeId, peerNodeId, peerCATs, | 
 |                                                    peerSessionId, fabricIndex, config); | 
 |     return result != nullptr ? MakeOptional<SessionHandle>(*result) : Optional<SessionHandle>::Missing(); | 
 | } | 
 |  | 
 | Optional<SessionHandle> SecureSessionTable::CreateNewSecureSession(SecureSession::Type secureSessionType, | 
 |                                                                    ScopedNodeId sessionEvictionHint) | 
 | { | 
 |     Optional<SessionHandle> rv = Optional<SessionHandle>::Missing(); | 
 |     SecureSession * allocated  = nullptr; | 
 |  | 
 |     auto sessionId = FindUnusedSessionId(); | 
 |     VerifyOrReturnValue(sessionId.HasValue(), Optional<SessionHandle>::Missing()); | 
 |  | 
 |     // | 
 |     // We allocate a new session out of the pool if we have space in it. If we don't, we need | 
 |     // 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() < GetMaxSessionTableSize()) | 
 |     { | 
 |         allocated = mEntries.CreateObject(*this, secureSessionType, sessionId.Value()); | 
 |     } | 
 |     else | 
 |     { | 
 |         allocated = EvictAndAllocate(sessionId.Value(), secureSessionType, sessionEvictionHint); | 
 |     } | 
 |  | 
 |     VerifyOrReturnValue(allocated != nullptr, Optional<SessionHandle>::Missing()); | 
 |  | 
 |     rv             = MakeOptional<SessionHandle>(*allocated); | 
 |     mNextSessionId = sessionId.Value() == kMaxSessionID ? static_cast<uint16_t>(kUnsecuredSessionId + 1) | 
 |                                                         : static_cast<uint16_t>(sessionId.Value() + 1); | 
 |  | 
 |     return rv; | 
 | } | 
 |  | 
 | SecureSession * SecureSessionTable::EvictAndAllocate(uint16_t localSessionId, SecureSession::Type secureSessionType, | 
 |                                                      const ScopedNodeId & sessionEvictionHint) | 
 | { | 
 |     VerifyOrDieWithMsg(!mRunningEvictionLogic, SecureChannel, | 
 |                        "EvictAndAllocate isn't re-entrant, yet someone called us while we're already running"); | 
 |  | 
 |     mRunningEvictionLogic = true; | 
 |  | 
 |     auto cleanup = MakeDefer([this]() { mRunningEvictionLogic = false; }); | 
 |  | 
 |     ChipLogProgress(SecureChannel, "Evicting a slot for session with LSID: %d, type: %u", localSessionId, | 
 |                     (uint8_t) secureSessionType); | 
 |  | 
 |     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. | 
 |     // | 
 |     // 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; | 
 |         }); | 
 |  | 
 |         index++; | 
 |         return Loop::Continue; | 
 |     }); | 
 |  | 
 |     auto sortableSessionSpan = Span<SortableSession>(sortableSessions, mEntries.Allocated()); | 
 |     EvictionPolicyContext policyContext(sortableSessionSpan, sessionEvictionHint); | 
 |  | 
 |     DefaultEvictionPolicy(policyContext); | 
 |     ChipLogProgress(SecureChannel, "Sorted sessions for eviction..."); | 
 |  | 
 |     const auto numSessions = mEntries.Allocated(); | 
 |  | 
 | #if CHIP_DETAIL_LOGGING | 
 |     ChipLogDetail(SecureChannel, "Sorted Eviction Candidates (ranked from best candidate to worst):"); | 
 |     for (auto * session = sortableSessions; session != (sortableSessions + numSessions); session++) | 
 |     { | 
 |         ChipLogDetail(SecureChannel, | 
 |                       "\t%ld: [%p] -- Peer: [%u:" ChipLogFormatX64 | 
 |                       "] State: '%s', NumMatchingOnFabric: %d NumMatchingOnPeer: %d ActivityTime: %lu", | 
 |                       static_cast<long int>(session - sortableSessions), session->mSession, | 
 |                       session->mSession->GetPeer().GetFabricIndex(), ChipLogValueX64(session->mSession->GetPeer().GetNodeId()), | 
 |                       session->mSession->GetStateStr(), session->mNumMatchingOnFabric, session->mNumMatchingOnPeer, | 
 |                       static_cast<unsigned long>(session->mSession->GetLastActivityTime().count())); | 
 |     } | 
 | #endif | 
 |  | 
 |     for (auto * session = sortableSessions; session != (sortableSessions + numSessions); session++) | 
 |     { | 
 |         if (session->mSession->IsPendingEviction()) | 
 |         { | 
 |             continue; | 
 |         } | 
 |  | 
 |         ChipLogProgress(SecureChannel, "Candidate Session[%p] - Attempting to evict...", session->mSession); | 
 |  | 
 |         auto prevCount = mEntries.Allocated(); | 
 |  | 
 |         // | 
 |         // SessionHolders act like weak-refs on a session, but since they do still add to the ref-count of a SecureSession, we | 
 |         // cannot actually tell whether there are truly any strong-refs (SessionHandles) on this session because if we did, we'd | 
 |         // avoid evicting it since it's pointless to do so. | 
 |         // | 
 |         // However, we don't actually have SessionHolders implemented correctly as weak-refs, requiring us to go ahead and 'try' to | 
 |         // evict it, and see if it still remains in the table. If it does, we have to try the next one. If it doesn't, we know we've | 
 |         // earned a free spot. | 
 |         // | 
 |         // See #19495. | 
 |         // | 
 |         session->mSession->MarkForEviction(); | 
 |  | 
 |         auto newCount = mEntries.Allocated(); | 
 |  | 
 |         if (newCount < prevCount) | 
 |         { | 
 |             ChipLogProgress(SecureChannel, "Successfully evicted a session!"); | 
 |             auto * retSession = mEntries.CreateObject(*this, secureSessionType, localSessionId); | 
 |             VerifyOrDie(session != nullptr); | 
 |             return retSession; | 
 |         } | 
 |     } | 
 |  | 
 |     VerifyOrDieWithMsg(false, SecureChannel, "We couldn't find any session to evict at all, something's wrong!"); | 
 |     return nullptr; | 
 | } | 
 |  | 
 | void SecureSessionTable::DefaultEvictionPolicy(EvictionPolicyContext & evictionContext) | 
 | { | 
 |     // | 
 |     // 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; | 
 |         } | 
 |  | 
 |         // We have an evicton hint in two cases: | 
 |         // | 
 |         // 1) When we just established CASE as a responder, the hint is the node | 
 |         //    we just established CASE to. | 
 |         // 2) When starting to establish CASE as an initiator, the hint is the | 
 |         //    node we are going to establish CASE to. | 
 |         // | 
 |         // In case 2, we should not end up here if there is an active session to | 
 |         // the peer at all (because that session should have been used instead | 
 |         // of establishing a new one). | 
 |         // | 
 |         // In case 1, we know we have a session matching the hint, but we don't | 
 |         // want to pick that one for eviction, because we just established it. | 
 |         // So we should not consider a session as matching a hint if it's active | 
 |         // and is the only session to our peer. | 
 |         // | 
 |         // Checking for the "active" state in addition to the "only session to | 
 |         // peer" state allows us to prioritize evicting defuct sessions that | 
 |         // match the hint against other defunct sessions. | 
 |         auto sessionMatchesEvictionHint = [&evictionContext](const SortableSession & session) -> int { | 
 |             if (session.mSession->GetPeer() != evictionContext.GetSessionEvictionHint()) | 
 |             { | 
 |                 return false; | 
 |             } | 
 |             bool isOnlyActiveSessionToPeer = session.mSession->IsActiveSession() && session.mNumMatchingOnPeer == 0; | 
 |             return !isOnlyActiveSessionToPeer; | 
 |         }; | 
 |         int doesAMatchSessionHint = sessionMatchesEvictionHint(a); | 
 |         int doesBMatchSessionHint = sessionMatchesEvictionHint(b); | 
 |  | 
 |         // | 
 |         // Sorting on Key4 | 
 |         // | 
 |         if (doesAMatchSessionHint != doesBMatchSessionHint) | 
 |         { | 
 |             return doesAMatchSessionHint > doesBMatchSessionHint; | 
 |         } | 
 |  | 
 |         int aStateScore = 0, bStateScore = 0; | 
 |         auto assignStateScore = [](auto & score, const auto & session) { | 
 |             if (session.IsDefunct()) | 
 |             { | 
 |                 score = 2; | 
 |             } | 
 |             else if (session.IsActiveSession()) | 
 |             { | 
 |                 score = 1; | 
 |             } | 
 |             else | 
 |             { | 
 |                 score = 0; | 
 |             } | 
 |         }; | 
 |  | 
 |         assignStateScore(aStateScore, *a.mSession); | 
 |         assignStateScore(bStateScore, *b.mSession); | 
 |  | 
 |         // | 
 |         // Sorting on Key5 | 
 |         // | 
 |         if (aStateScore != bStateScore) | 
 |         { | 
 |             return (aStateScore > bStateScore); | 
 |         } | 
 |  | 
 |         // | 
 |         // Sorting on Key6 | 
 |         // | 
 |         return (a->GetLastActivityTime() < b->GetLastActivityTime()); | 
 |     }); | 
 | } | 
 |  | 
 | Optional<SessionHandle> SecureSessionTable::FindSecureSessionByLocalKey(uint16_t localSessionId) | 
 | { | 
 |     SecureSession * result = nullptr; | 
 |     mEntries.ForEachActiveObject([&](auto session) { | 
 |         if (session->GetLocalSessionId() == localSessionId) | 
 |         { | 
 |             result = session; | 
 |             return Loop::Break; | 
 |         } | 
 |         return Loop::Continue; | 
 |     }); | 
 |     return result != nullptr ? MakeOptional<SessionHandle>(*result) : Optional<SessionHandle>::Missing(); | 
 | } | 
 |  | 
 | Optional<uint16_t> SecureSessionTable::FindUnusedSessionId() | 
 | { | 
 |     uint16_t candidate_base = 0; | 
 |     uint64_t candidate_mask = 0; | 
 |     for (uint32_t i = 0; i <= kMaxSessionID; i += 64) | 
 |     { | 
 |         // candidate_base is the base session ID we are searching from. | 
 |         // We have a 64-bit mask anchored at this ID and iterate over the | 
 |         // whole session table, setting bits in the mask for in-use IDs. | 
 |         // If we can iterate through the entire session table and have | 
 |         // any bits clear in the mask, we have available session IDs. | 
 |         candidate_base = static_cast<uint16_t>(i + mNextSessionId); | 
 |         candidate_mask = 0; | 
 |         { | 
 |             uint16_t shift = static_cast<uint16_t>(kUnsecuredSessionId - candidate_base); | 
 |             if (shift <= 63) | 
 |             { | 
 |                 candidate_mask |= (1ULL << shift); // kUnsecuredSessionId is never available | 
 |             } | 
 |         } | 
 |         mEntries.ForEachActiveObject([&](auto session) { | 
 |             uint16_t shift = static_cast<uint16_t>(session->GetLocalSessionId() - candidate_base); | 
 |             if (shift <= 63) | 
 |             { | 
 |                 candidate_mask |= (1ULL << shift); | 
 |             } | 
 |             if (candidate_mask == UINT64_MAX) | 
 |             { | 
 |                 return Loop::Break; // No bits clear means this bucket is full. | 
 |             } | 
 |             return Loop::Continue; | 
 |         }); | 
 |         if (candidate_mask != UINT64_MAX) | 
 |         { | 
 |             break; // Any bit clear means we have an available ID in this bucket. | 
 |         } | 
 |     } | 
 |     if (candidate_mask != UINT64_MAX) | 
 |     { | 
 |         uint16_t offset = 0; | 
 |         while (candidate_mask & 1) | 
 |         { | 
 |             candidate_mask >>= 1; | 
 |             ++offset; | 
 |         } | 
 |         uint16_t available = static_cast<uint16_t>(candidate_base + offset); | 
 |         return MakeOptional<uint16_t>(available); | 
 |     } | 
 |  | 
 |     return NullOptional; | 
 | } | 
 |  | 
 | } // namespace Transport | 
 | } // namespace chip |