blob: f77c6e565ead616d47e325b7efea180712a33a1f [file]
/*
*
* Copyright (c) 2026 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
* Regression tests for src/inet/TCPEndPointImplLwIP.cpp.
*
* Built for ESP32 / FreeRTOS+LwIP so the test runs under the
* esp32-qemu-tests CI image.
*/
#include <pw_unit_test/framework.h>
#include <lwip/opt.h>
#include <lwip/tcp.h>
#include <inet/IPAddress.h>
#include <inet/InetConfig.h>
#include <inet/TCPEndPoint.h>
#include <inet/TCPEndPointImplLwIP.h>
#include <lib/core/CHIPError.h>
#include <lib/core/StringBuilderAdapters.h>
#include <lib/support/CHIPMem.h>
#include <platform/CHIPDeviceLayer.h>
#include <platform/PlatformManager.h>
#if INET_CONFIG_ENABLE_TCP_ENDPOINT
namespace chip {
namespace Inet {
// Friend hook into TCPEndPoint (mState) and TCPEndPointImplLwIP (mTCP) so the
// test can simulate the post-LwIPHandleError state without performing a real
// TCP three-way handshake. The matching `friend class TCPTest;` declarations
// live in TCPEndPoint.h and TCPEndPointImplLwIP.h.
class TCPTest
{
public:
// Drive the endpoint into the exact race window that the fix targets:
// mState is still kConnected (so IsConnected() passes the gate at the top
// of every affected method) but mTCP has already been cleared by
// LwIPHandleError running on the LwIP TCP/IP task.
static void SimulatePCBClearedWhileConnected(const TCPEndPointHandle & ep)
{
auto * impl = static_cast<TCPEndPointImplLwIP *>(ep.operator->());
impl->mState = TCPEndPoint::State::kConnected;
impl->mTCP = nullptr;
}
// Restore mState to kReady so the destructor's Free() path traverses a
// straightforward kReady->kClosed transition. mTCP is still nullptr, so
// DoCloseImpl() is a no-op either way; this just makes teardown intent
// explicit.
static void ResetToReadyForTeardown(const TCPEndPointHandle & ep)
{
auto * impl = static_cast<TCPEndPointImplLwIP *>(ep.operator->());
impl->mState = TCPEndPoint::State::kReady;
}
// DriveSendingImpl() is private on TCPEndPointImplLwIP; expose it through
// the friend so the test can invoke it directly without the surrounding
// DriveSending() wrapper that would otherwise call DoClose() on error.
static CHIP_ERROR CallDriveSendingImpl(const TCPEndPointHandle & ep)
{
auto * impl = static_cast<TCPEndPointImplLwIP *>(ep.operator->());
return impl->DriveSendingImpl();
}
};
} // namespace Inet
} // namespace chip
namespace {
using namespace chip;
using namespace chip::Inet;
class TestTCPEndPointLwIP : public ::testing::Test
{
public:
static void SetUpTestSuite()
{
ASSERT_EQ(Platform::MemoryInit(), CHIP_NO_ERROR);
// PlatformMgr().InitChipStack() brings up esp_netif (and therefore the
// LwIP TCP/IP thread + LWIP_TCPIP_CORE_LOCKING mutex used by
// RunOnTCPIPRet) and initializes the Inet TCPEndPointManager.
ASSERT_EQ(DeviceLayer::PlatformMgr().InitChipStack(), CHIP_NO_ERROR);
}
static void TearDownTestSuite()
{
DeviceLayer::PlatformMgr().Shutdown();
Platform::MemoryShutdown();
}
};
// Regression test for "[Inet] Fix LwIP TCP endpoint against PCB races".
//
// Background
// ----------
// mTCP is shared between the chip stack thread and the LwIP TCP/IP task.
// LwIPHandleError() runs on the TCP/IP task and clears mTCP synchronously when
// LwIP frees the PCB. Pre-fix, GetPeerInfo / GetLocalInfo / EnableNoDelay /
// EnableKeepAlive / DisableKeepAlive / AckReceive / DriveSendingImpl
// null-checked mTCP on the chip stack thread before scheduling the dereference
// onto the TCP/IP task, leaving a TOCTOU window in which the lambda would
// dereference a freed PCB and crash.
//
// Post-fix, the null-check moved inside RunOnTCPIPRet() so the check and the
// dereference both happen under the LwIP core lock, and each method reports
// CHIP_ERROR_CONNECTION_ABORTED when mTCP is gone.
//
// Test strategy
// -------------
// Reproduce the exact race state without needing a real peer: drive the
// endpoint into kConnected (so IsConnected() passes the gate at the top of
// each method) but leave mTCP null (the post-LwIPHandleError state, before
// the chip-stack-thread HandleError() lambda has run). Pre-fix this would
// dereference null inside the LwIP-task lambda; post-fix every method must
// cleanly return CHIP_ERROR_CONNECTION_ABORTED.
TEST_F(TestTCPEndPointLwIP, AbortedAccessReturnsConnectionAborted)
{
DeviceLayer::StackLock lock;
TCPEndPointHandle ep;
ASSERT_EQ(DeviceLayer::TCPEndPointManager()->NewEndPoint(ep), CHIP_NO_ERROR);
ASSERT_FALSE(ep.IsNull());
TCPTest::SimulatePCBClearedWhileConnected(ep);
ASSERT_TRUE(ep->IsConnected());
IPAddress addr;
uint16_t port = 0;
EXPECT_EQ(ep->GetPeerInfo(&addr, &port), CHIP_ERROR_CONNECTION_ABORTED);
EXPECT_EQ(ep->GetLocalInfo(&addr, &port), CHIP_ERROR_CONNECTION_ABORTED);
EXPECT_EQ(ep->EnableNoDelay(), CHIP_ERROR_CONNECTION_ABORTED);
EXPECT_EQ(ep->AckReceive(1), CHIP_ERROR_CONNECTION_ABORTED);
#if LWIP_TCP_KEEPALIVE
// When LwIP is built without keepalive support these methods short-circuit
// to CHIP_ERROR_NOT_IMPLEMENTED before reaching the (un)fixed code path,
// so they only carry signal when keepalive is compiled in.
EXPECT_EQ(ep->EnableKeepAlive(/*interval=*/10, /*timeoutCount=*/3), CHIP_ERROR_CONNECTION_ABORTED);
EXPECT_EQ(ep->DisableKeepAlive(), CHIP_ERROR_CONNECTION_ABORTED);
#endif // LWIP_TCP_KEEPALIVE
// DriveSendingImpl() does not gate on IsConnected(); it always interacts
// with the PCB. With mTCP null, the very first RunOnTCPIPRet() inside it
// (the tcp_sndbuf() call) is the one that used to crash.
EXPECT_EQ(TCPTest::CallDriveSendingImpl(ep), CHIP_ERROR_CONNECTION_ABORTED);
TCPTest::ResetToReadyForTeardown(ep);
ep.Release();
}
} // namespace
#endif // INET_CONFIG_ENABLE_TCP_ENDPOINT