/* * 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 #include #include #include namespace chip { namespace Transport { Optional 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::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::Missing(); } (void) fabricIndex; } } SecureSession * result = mEntries.CreateObject(*this, secureSessionType, localSessionId, localNodeId, peerNodeId, peerCATs, peerSessionId, fabricIndex, config); return result != nullptr ? MakeOptional(*result) : Optional::Missing(); } Optional SecureSessionTable::CreateNewSecureSession(SecureSession::Type secureSessionType, ScopedNodeId sessionEvictionHint) { Optional rv = Optional::Missing(); SecureSession * allocated = nullptr; auto sessionId = FindUnusedSessionId(); VerifyOrReturnValue(sessionId.HasValue(), Optional::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::Missing()); rv = MakeOptional(*allocated); mNextSessionId = sessionId.Value() == kMaxSessionID ? static_cast(kUnsecuredSessionId + 1) : static_cast(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(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(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; 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 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(*result) : Optional::Missing(); } Optional 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(i + mNextSessionId); candidate_mask = 0; { uint16_t shift = static_cast(kUnsecuredSessionId - candidate_base); if (shift <= 63) { candidate_mask |= (1ULL << shift); // kUnsecuredSessionId is never available } } mEntries.ForEachActiveObject([&](auto session) { uint16_t shift = static_cast(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(candidate_base + offset); return MakeOptional(available); } return NullOptional; } } // namespace Transport } // namespace chip