blob: 1725fae4b03198b6d513cb4b0c23b18361960ebd [file]
/*
*
* Copyright (c) 2025 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.
*/
// Tests that CASESessionManager::ReleaseSession(peerId) does not tear down an
// OperationalSessionSetup that is mid-handshake, while still releasing setups
// that are fully connected or uninitialized.
#include <pw_unit_test/framework.h>
#include <app/CASEClientPool.h>
#include <app/CASESessionManager.h>
#include <app/OperationalSessionSetup.h>
#include <app/OperationalSessionSetupPool.h>
#include <app/tests/AppTestContext.h>
#include <app/tests/CASESessionManagerTestAccess.h>
#include <lib/core/CHIPError.h>
#include <lib/core/ScopedNodeId.h>
#include <lib/support/CodeUtils.h>
using namespace chip;
using namespace chip::app;
namespace {
constexpr NodeId kTestNodeId = 0x123456789abcdefULL;
constexpr FabricIndex kFabricIndex = 1;
using OSSAccess = chip::TestOperationalSessionSetupAccess;
using MgrAccess = chip::TestCASESessionManagerAccess;
// Capacity-2 pools are plenty for these single-peer tests.
using TestSetupPool = OperationalSessionSetupPool<2>;
using TestClientPool = CASEClientPool<2>;
class TestCASESessionManagerReleaseSession : public chip::Testing::AppContext
{
protected:
// Allocate a session setup in the pool for kTestNodeId and force it into the
// given internal state. We force mPeerId directly so the setup is locatable
// by ReleaseSession(peerId) without standing up a full, validated CASE
// stack (the internal state is the only thing under test here).
template <size_t N, size_t M>
OperationalSessionSetup * AllocateSetup(OperationalSessionSetupPool<N> & pool, CASEClientPool<M> & clientPool,
OperationalSessionReleaseDelegate & releaseDelegate, const ScopedNodeId & peerId)
{
// Provide a valid exchange/session manager so the setup's destructor
// (which cancels timers via exchangeMgr->GetSessionManager()) is safe.
// The setup's reachable state is forced below, so full validation of
// the params is not required for this test.
CASEClientInitParams params;
params.sessionManager = &GetSecureSessionManager();
params.exchangeMgr = &GetExchangeManager();
auto * setup = pool.Allocate(params, &clientPool, peerId, &releaseDelegate);
if (setup != nullptr)
{
OSSAccess::SetPeerId(*setup, peerId);
}
return setup;
}
};
// A setup that is actively establishing a session must NOT be released by
// ReleaseSession(peerId) — destroying it mid-handshake would reset the retry
// state (attempt counter / busy backoff) and churn fresh Sigma1 attempts.
//
// Fail-without-fix: without the IsEstablishingSession() gate in
// CASESessionManager::ReleaseSession, the in-flight setup is destroyed and
// FindSessionSetup returns nullptr.
TEST_F(TestCASESessionManagerReleaseSession, EstablishingSetupIsNotReleased)
{
TestSetupPool pool;
TestClientPool clientPool;
CASESessionManager manager;
MgrAccess::SetSessionSetupPool(manager, &pool);
ScopedNodeId peerId(kTestNodeId, kFabricIndex);
auto * setup = AllocateSetup(pool, clientPool, manager, peerId);
ASSERT_NE(setup, nullptr);
OSSAccess::SetEstablishing(*setup);
ASSERT_TRUE(OSSAccess::IsEstablishing(*setup));
// Sanity: the setup is locatable by peer id before release.
ASSERT_NE(pool.FindSessionSetup(peerId, false), nullptr);
manager.ReleaseSession(peerId);
// The in-flight setup must survive.
EXPECT_NE(pool.FindSessionSetup(peerId, false), nullptr);
pool.ReleaseAllSessionSetupsForFabric(kFabricIndex);
}
// A fully-connected setup is not "establishing" and must be released.
TEST_F(TestCASESessionManagerReleaseSession, SecureConnectedSetupIsReleased)
{
TestSetupPool pool;
TestClientPool clientPool;
CASESessionManager manager;
MgrAccess::SetSessionSetupPool(manager, &pool);
ScopedNodeId peerId(kTestNodeId, kFabricIndex);
auto * setup = AllocateSetup(pool, clientPool, manager, peerId);
ASSERT_NE(setup, nullptr);
OSSAccess::SetSecureConnected(*setup);
ASSERT_FALSE(OSSAccess::IsEstablishing(*setup));
ASSERT_NE(pool.FindSessionSetup(peerId, false), nullptr);
manager.ReleaseSession(peerId);
// The connected setup must have been released.
EXPECT_EQ(pool.FindSessionSetup(peerId, false), nullptr);
}
// An uninitialized setup is not "establishing" and must be released.
TEST_F(TestCASESessionManagerReleaseSession, UninitializedSetupIsReleased)
{
TestSetupPool pool;
TestClientPool clientPool;
CASESessionManager manager;
MgrAccess::SetSessionSetupPool(manager, &pool);
ScopedNodeId peerId(kTestNodeId, kFabricIndex);
auto * setup = AllocateSetup(pool, clientPool, manager, peerId);
ASSERT_NE(setup, nullptr);
OSSAccess::SetUninitialized(*setup);
ASSERT_FALSE(OSSAccess::IsEstablishing(*setup));
ASSERT_NE(pool.FindSessionSetup(peerId, false), nullptr);
manager.ReleaseSession(peerId);
EXPECT_EQ(pool.FindSessionSetup(peerId, false), nullptr);
}
// Parameterised: each of the 5 establishing states must survive ReleaseSession.
TEST_F(TestCASESessionManagerReleaseSession, AllEstablishingStatesAreNotReleased)
{
using ForceFn = void (*)(OperationalSessionSetup &);
const ForceFn states[] = {
[](OperationalSessionSetup & s) { OSSAccess::SetNeedsAddress(s); },
[](OperationalSessionSetup & s) { OSSAccess::SetResolvingAddress(s); },
[](OperationalSessionSetup & s) { OSSAccess::SetHasAddress(s); },
[](OperationalSessionSetup & s) { OSSAccess::SetEstablishing(s); }, // Connecting
[](OperationalSessionSetup & s) { OSSAccess::SetWaitingForRetry(s); },
};
for (auto forceState : states)
{
OperationalSessionSetupPool<4> pool;
CASEClientPool<4> clientPool;
CASESessionManager manager;
MgrAccess::SetSessionSetupPool(manager, &pool);
ScopedNodeId peerId(kTestNodeId, kFabricIndex);
CASEClientInitParams params;
params.sessionManager = &GetSecureSessionManager();
params.exchangeMgr = &GetExchangeManager();
auto * setup = pool.Allocate(params, &clientPool, peerId, &manager);
ASSERT_NE(setup, nullptr);
OSSAccess::SetPeerId(*setup, peerId);
forceState(*setup);
ASSERT_TRUE(OSSAccess::IsEstablishing(*setup));
OperationalSessionSetup * before = setup;
manager.ReleaseSession(peerId);
OperationalSessionSetup * after = pool.FindSessionSetup(peerId, false);
ASSERT_NE(after, nullptr);
EXPECT_EQ(after, before);
pool.ReleaseAllSessionSetupsForFabric(kFabricIndex);
}
}
// Repeated ReleaseSession during establishment is idempotent (no double-release
// or state corruption from N back-to-back skipped-release calls).
TEST_F(TestCASESessionManagerReleaseSession, RepeatedReleaseDuringEstablishmentIsIdempotent)
{
OperationalSessionSetupPool<4> pool;
CASEClientPool<4> clientPool;
CASESessionManager manager;
MgrAccess::SetSessionSetupPool(manager, &pool);
ScopedNodeId peerId(kTestNodeId, kFabricIndex);
CASEClientInitParams params;
params.sessionManager = &GetSecureSessionManager();
params.exchangeMgr = &GetExchangeManager();
auto * setup = pool.Allocate(params, &clientPool, peerId, &manager);
ASSERT_NE(setup, nullptr);
OSSAccess::SetPeerId(*setup, peerId);
OSSAccess::SetWaitingForRetry(*setup);
OperationalSessionSetup * before = setup;
for (int i = 0; i < 5; ++i)
{
manager.ReleaseSession(peerId);
}
OperationalSessionSetup * after = pool.FindSessionSetup(peerId, false);
ASSERT_NE(after, nullptr);
EXPECT_EQ(after, before);
pool.ReleaseAllSessionSetupsForFabric(kFabricIndex);
}
// Establishing -> ReleaseSession (no-op) -> SecureConnected -> ReleaseSession
// (releases). Verifies the guard applies only during establishment and is correctly
// lifted once the handshake completes.
TEST_F(TestCASESessionManagerReleaseSession, ReleaseAfterEstablishmentCompletes)
{
OperationalSessionSetupPool<4> pool;
CASEClientPool<4> clientPool;
CASESessionManager manager;
MgrAccess::SetSessionSetupPool(manager, &pool);
ScopedNodeId peerId(kTestNodeId, kFabricIndex);
CASEClientInitParams params;
params.sessionManager = &GetSecureSessionManager();
params.exchangeMgr = &GetExchangeManager();
auto * setup = pool.Allocate(params, &clientPool, peerId, &manager);
ASSERT_NE(setup, nullptr);
OSSAccess::SetPeerId(*setup, peerId);
OSSAccess::SetEstablishing(*setup);
// First call: guarded, setup survives.
manager.ReleaseSession(peerId);
EXPECT_NE(pool.FindSessionSetup(peerId, false), nullptr);
// Transition to connected.
OSSAccess::SetSecureConnected(*setup);
// Second call: not establishing, setup is released.
manager.ReleaseSession(peerId);
EXPECT_EQ(pool.FindSessionSetup(peerId, false), nullptr);
}
// Integration-style: a CASE callback handler that calls
// manager.ReleaseSession(peerId) while the setup is mid-handshake must NOT
// destroy the in-flight setup. This exercises the realistic re-entry pattern
// where higher layers, on a callback (e.g. OnDeviceConnectionFailure dispatched
// by an enclosing component while another attempt is still establishing for the
// same peer), defensively call ReleaseSession on the manager.
//
// We model "called from a CASE callback" by issuing the ReleaseSession call
// from within a function-call frame that captures the manager and peerId,
// mimicking what an OnDeviceConnectionFailure / app-level handler would do.
TEST_F(TestCASESessionManagerReleaseSession, ReleaseFromCallbackDuringEstablishmentIsNoOp)
{
OperationalSessionSetupPool<4> pool;
CASEClientPool<4> clientPool;
CASESessionManager manager;
MgrAccess::SetSessionSetupPool(manager, &pool);
ScopedNodeId peerId(kTestNodeId, kFabricIndex);
auto * setup = AllocateSetup(pool, clientPool, manager, peerId);
ASSERT_NE(setup, nullptr);
OSSAccess::SetEstablishing(*setup);
ASSERT_TRUE(OSSAccess::IsEstablishing(*setup));
OperationalSessionSetup * before = setup;
// Simulate a CASE callback handler invoking ReleaseSession on the manager.
// The lambda body is exactly what a user-supplied OnDeviceConnectionFailure
// (or similar) handler would do: ask the manager to drop the session.
auto simulatedCaseCallback = [&]() { manager.ReleaseSession(peerId); };
simulatedCaseCallback();
// Setup must still be present and unchanged.
OperationalSessionSetup * after = pool.FindSessionSetup(peerId, false);
ASSERT_NE(after, nullptr);
EXPECT_EQ(after, before);
EXPECT_TRUE(OSSAccess::IsEstablishing(*after));
// A second callback-style release while still establishing is also a no-op.
simulatedCaseCallback();
after = pool.FindSessionSetup(peerId, false);
ASSERT_NE(after, nullptr);
EXPECT_EQ(after, before);
pool.ReleaseAllSessionSetupsForFabric(kFabricIndex);
}
// Asymmetry test: ReleaseSessionsForFabric (used on fabric removal) WILL tear
// down establishing setups, in contrast to ReleaseSession(peerId) which guards
// them. This is the documented contract in CASESessionManager::ReleaseSessionsForFabric --
// fabric removal is irreversible, so dangling references to a removed fabric are
// strictly worse than restarting an in-flight handshake.
//
// Fail-without-fix: if a future change ever extends the in-flight guard into
// the fabric-removal path, this test catches it.
TEST_F(TestCASESessionManagerReleaseSession, ReleaseSessionsForFabricTearsDownEstablishingSetups)
{
OperationalSessionSetupPool<4> pool;
CASEClientPool<4> clientPool;
CASESessionManager manager;
MgrAccess::SetSessionSetupPool(manager, &pool);
// Two setups on the same fabric, both mid-handshake, plus a third on a
// different fabric that must NOT be torn down.
ScopedNodeId peerOnFabric1A(kTestNodeId, kFabricIndex);
ScopedNodeId peerOnFabric1B(kTestNodeId + 1, kFabricIndex);
ScopedNodeId peerOnFabric2(kTestNodeId + 2, static_cast<FabricIndex>(kFabricIndex + 1));
auto * s1 = AllocateSetup(pool, clientPool, manager, peerOnFabric1A);
auto * s2 = AllocateSetup(pool, clientPool, manager, peerOnFabric1B);
auto * s3 = AllocateSetup(pool, clientPool, manager, peerOnFabric2);
ASSERT_NE(s1, nullptr);
ASSERT_NE(s2, nullptr);
ASSERT_NE(s3, nullptr);
OSSAccess::SetEstablishing(*s1); // Connecting
OSSAccess::SetWaitingForRetry(*s2); // also "establishing"
OSSAccess::SetEstablishing(*s3); // different fabric, should survive
ASSERT_TRUE(OSSAccess::IsEstablishing(*s1));
ASSERT_TRUE(OSSAccess::IsEstablishing(*s2));
ASSERT_TRUE(OSSAccess::IsEstablishing(*s3));
// Per-peer ReleaseSession is guarded -- sanity check the asymmetry first:
// both establishing peers on fabric 1 survive a per-peer release call.
manager.ReleaseSession(peerOnFabric1A);
manager.ReleaseSession(peerOnFabric1B);
EXPECT_NE(pool.FindSessionSetup(peerOnFabric1A, false), nullptr);
EXPECT_NE(pool.FindSessionSetup(peerOnFabric1B, false), nullptr);
// Now exercise the fabric-removal path: it must tear down both establishing
// setups on fabric 1, while leaving fabric 2's establishing setup alone.
manager.ReleaseSessionsForFabric(kFabricIndex);
EXPECT_EQ(pool.FindSessionSetup(peerOnFabric1A, false), nullptr);
EXPECT_EQ(pool.FindSessionSetup(peerOnFabric1B, false), nullptr);
EXPECT_NE(pool.FindSessionSetup(peerOnFabric2, false), nullptr);
pool.ReleaseAllSessionSetupsForFabric(static_cast<FabricIndex>(kFabricIndex + 1));
}
// ReleaseAllSessions (shutdown path) MUST tear down establishing setups -- the
// per-peer guard is intentionally bypassed at shutdown to avoid leaks.
TEST_F(TestCASESessionManagerReleaseSession, ShutdownReleasesEstablishingSetups)
{
OperationalSessionSetupPool<4> pool;
CASEClientPool<4> clientPool;
CASESessionManager manager;
MgrAccess::SetSessionSetupPool(manager, &pool);
ScopedNodeId peerId1(kTestNodeId, kFabricIndex);
ScopedNodeId peerId2(kTestNodeId + 1, kFabricIndex);
CASEClientInitParams params;
params.sessionManager = &GetSecureSessionManager();
params.exchangeMgr = &GetExchangeManager();
auto * s1 = pool.Allocate(params, &clientPool, peerId1, &manager);
auto * s2 = pool.Allocate(params, &clientPool, peerId2, &manager);
ASSERT_NE(s1, nullptr);
ASSERT_NE(s2, nullptr);
OSSAccess::SetPeerId(*s1, peerId1);
OSSAccess::SetPeerId(*s2, peerId2);
OSSAccess::SetEstablishing(*s1);
OSSAccess::SetWaitingForRetry(*s2);
ASSERT_TRUE(OSSAccess::IsEstablishing(*s1));
ASSERT_TRUE(OSSAccess::IsEstablishing(*s2));
manager.ReleaseAllSessions();
EXPECT_EQ(pool.FindSessionSetup(peerId1, false), nullptr);
EXPECT_EQ(pool.FindSessionSetup(peerId2, false), nullptr);
}
// ReleaseSession(peerId) for a peer that has no setup in the pool must be a
// safe no-op (the FindSessionSetup lookup returns nullptr and we must not
// crash, assert, or affect any other entries in the pool).
TEST_F(TestCASESessionManagerReleaseSession, ReleaseForNonExistentPeerIsSafeNoOp)
{
OperationalSessionSetupPool<4> pool;
CASEClientPool<4> clientPool;
CASESessionManager manager;
MgrAccess::SetSessionSetupPool(manager, &pool);
// Allocate an unrelated establishing setup that must survive the release
// of a different, non-existent peer.
ScopedNodeId existingPeer(kTestNodeId, kFabricIndex);
auto * setup = AllocateSetup(pool, clientPool, manager, existingPeer);
ASSERT_NE(setup, nullptr);
OSSAccess::SetEstablishing(*setup);
// Sanity-check the peer is not present.
ScopedNodeId missingPeer(kTestNodeId + 999, kFabricIndex);
ASSERT_EQ(pool.FindSessionSetup(missingPeer, false), nullptr);
// Must not crash; must not disturb the unrelated establishing setup.
manager.ReleaseSession(missingPeer);
EXPECT_NE(pool.FindSessionSetup(existingPeer, false), nullptr);
EXPECT_TRUE(OSSAccess::IsEstablishing(*setup));
pool.ReleaseAllSessionSetupsForFabric(kFabricIndex);
}
// ReleaseSession(peerId) for one fabric must NOT release a setup for the
// same NodeId on a *different* fabric. ScopedNodeId equality is (fabric,
// node); a regression that collapses to a node-only lookup would break the
// fabric-scoping contract and break multi-fabric controllers.
TEST_F(TestCASESessionManagerReleaseSession, ReleaseIsScopedByFabric)
{
OperationalSessionSetupPool<4> pool;
CASEClientPool<4> clientPool;
CASESessionManager manager;
MgrAccess::SetSessionSetupPool(manager, &pool);
ScopedNodeId peerOnFabric1(kTestNodeId, kFabricIndex);
ScopedNodeId peerOnFabric2(kTestNodeId, static_cast<FabricIndex>(kFabricIndex + 1));
auto * s1 = AllocateSetup(pool, clientPool, manager, peerOnFabric1);
auto * s2 = AllocateSetup(pool, clientPool, manager, peerOnFabric2);
ASSERT_NE(s1, nullptr);
ASSERT_NE(s2, nullptr);
// s1 is connected (and so would normally be released); s2 is mid-handshake
// on a different fabric. After releasing fabric-1's peer, fabric-2's
// setup must be untouched.
OSSAccess::SetSecureConnected(*s1);
OSSAccess::SetEstablishing(*s2);
OperationalSessionSetup * s2Before = s2;
manager.ReleaseSession(peerOnFabric1);
EXPECT_EQ(pool.FindSessionSetup(peerOnFabric1, false), nullptr);
OperationalSessionSetup * s2After = pool.FindSessionSetup(peerOnFabric2, false);
ASSERT_NE(s2After, nullptr);
EXPECT_EQ(s2After, s2Before);
EXPECT_TRUE(OSSAccess::IsEstablishing(*s2After));
pool.ReleaseAllSessionSetupsForFabric(static_cast<FabricIndex>(kFabricIndex + 1));
}
// Skipping the release of an establishing setup must leave the setup's
// internal state (mState, peer id) untouched. A regression that mutated
// state on the "skip" path (e.g. resetting attempt counters) would defeat
// the entire purpose of the guard.
TEST_F(TestCASESessionManagerReleaseSession, EstablishingSetupStateUnchangedAfterSkippedRelease)
{
OperationalSessionSetupPool<4> pool;
CASEClientPool<4> clientPool;
CASESessionManager manager;
MgrAccess::SetSessionSetupPool(manager, &pool);
ScopedNodeId peerId(kTestNodeId, kFabricIndex);
auto * setup = AllocateSetup(pool, clientPool, manager, peerId);
ASSERT_NE(setup, nullptr);
// Use WaitingForRetry: this is the state that holds the most retry/backoff
// bookkeeping (mAttemptsDone, mRequestedBusyDelay) in production; any
// regression that mutated state on the skip path would clobber it here.
OSSAccess::SetWaitingForRetry(*setup);
ASSERT_TRUE(OSSAccess::IsEstablishing(*setup));
manager.ReleaseSession(peerId);
OperationalSessionSetup * after = pool.FindSessionSetup(peerId, false);
ASSERT_NE(after, nullptr);
EXPECT_EQ(after, setup);
// Still establishing, still in the same state (lookup-by-peer-id still works).
EXPECT_TRUE(OSSAccess::IsEstablishing(*after));
pool.ReleaseAllSessionSetupsForFabric(kFabricIndex);
}
} // namespace