blob: 3786f708c584239fcf28f8529043b134a6e55e27 [file]
/*
*
* Copyright (c) 2026 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 <gtest/gtest.h>
#include <pw_unit_test/framework.h>
#include <lib/support/CodeUtils.h>
#include <system/SystemConfig.h>
#include <system/SystemError.h>
#include <system/SystemLayer.h>
#include <system/SystemLayerImpl.h>
#if CHIP_SYSTEM_CONFIG_USE_DISPATCH && CHIP_SYSTEM_CONFIG_USE_SOCKETS
#include <atomic>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <mach-o/dyld.h>
#include <poll.h>
#include <signal.h>
#include <spawn.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
extern char ** environ;
using namespace chip;
using namespace chip::System;
namespace {
constexpr char kStrictChildEnv[] = "CHIP_TEST_DISPATCH_SOCKET_WATCH_STRICT_CHILD";
enum class Watch
{
kRead,
kWrite,
kReadWrite,
};
void NoopSocketCallback(SocketEvents, intptr_t) {}
void CountingSocketCallback(SocketEvents, intptr_t data)
{
reinterpret_cast<std::atomic<int> *>(data)->fetch_add(1);
}
int OpenDescriptorCount()
{
int count = 0;
for (int fd = 0; fd < getdtablesize(); fd++)
{
count += (::fcntl(fd, F_GETFD) != -1) ? 1 : 0;
}
return count;
}
template <typename F>
void RunOn(dispatch_queue_t queue, F && f)
{
dispatch_sync_f(queue, &f, [](void * context) { (*static_cast<F *>(context))(); });
}
CHIP_ERROR StartWatch(LayerImplDispatch & layer, dispatch_queue_t queue, int fd, Watch watch, SocketWatchToken & token,
SocketWatchCallback callback = NoopSocketCallback, intptr_t data = 0)
{
CHIP_ERROR err = CHIP_NO_ERROR;
RunOn(queue, [&] {
err = layer.StartWatchingSocket(fd, &token);
VerifyOrReturn(err == CHIP_NO_ERROR);
err = layer.SetCallback(token, callback, data);
VerifyOrReturn(err == CHIP_NO_ERROR);
if (watch != Watch::kWrite)
{
err = layer.RequestCallbackOnPendingRead(token);
VerifyOrReturn(err == CHIP_NO_ERROR);
}
if (watch != Watch::kRead)
{
err = layer.RequestCallbackOnPendingWrite(token);
}
});
return err;
}
// Endpoint teardown order: StopWatchingSocket, then close() in the same queue block.
// Returns true once the peer observes EOF, i.e. no descriptor for the socket remains open.
bool StopThenCloseReleasesSocket(LayerImplDispatch & layer, dispatch_queue_t queue, Watch watch)
{
int fds[2];
VerifyOrReturnValue(::socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0, false);
SocketWatchToken token;
bool released = false;
if (StartWatch(layer, queue, fds[0], watch, token) == CHIP_NO_ERROR)
{
RunOn(queue, [&] {
TEMPORARY_RETURN_IGNORED layer.StopWatchingSocket(&token);
::close(fds[0]);
});
char buf[8];
pollfd peer = { fds[1], POLLIN, 0 };
released = ::poll(&peer, 1, 1000) == 1 && ::recv(fds[1], buf, sizeof(buf), MSG_DONTWAIT) == 0;
}
else
{
::close(fds[0]);
}
::close(fds[1]);
return released;
}
bool RunStopThenCloseCycles(LayerImplDispatch & layer, dispatch_queue_t queue)
{
for (int i = 0; i < 60; i++)
{
VerifyOrReturnValue(StopThenCloseReleasesSocket(layer, queue, static_cast<Watch>(i % 3)), false);
}
return true;
}
// Runs before main() in the child spawned by StopThenCloseDoesNotAbortUnderStrictDispatch, so no other
// test in the binary executes under LIBDISPATCH_STRICT.
[[maybe_unused]] const bool gStrictChildRan = [] {
if (getenv(kStrictChildEnv) == nullptr)
{
return false;
}
LayerImplDispatch layer;
dispatch_queue_t queue = dispatch_queue_create("org.csa-iot.matter.test.socketwatch.strict", DISPATCH_QUEUE_SERIAL);
bool ok = layer.Init() == CHIP_NO_ERROR;
layer.SetDispatchQueue(queue);
ok = ok && RunStopThenCloseCycles(layer, queue);
RunOn(queue, [&] { layer.Shutdown(); });
_exit(ok ? 0 : 1);
}();
class TestSystemDispatchSocketWatch : public ::testing::Test
{
protected:
void SetUp() override
{
ASSERT_EQ(mLayer.Init(), CHIP_NO_ERROR);
mQueue = dispatch_queue_create("org.csa-iot.matter.test.socketwatch", DISPATCH_QUEUE_SERIAL);
ASSERT_NE(mQueue, nullptr);
mLayer.SetDispatchQueue(mQueue);
}
void TearDown() override
{
VerifyOrReturn(mQueue != nullptr);
RunOn(mQueue, [this] { mLayer.Shutdown(); });
}
LayerImplDispatch mLayer;
dispatch_queue_t mQueue = nullptr;
};
TEST_F(TestSystemDispatchSocketWatch, StopThenCloseCyclesReleaseSockets)
{
EXPECT_TRUE(RunStopThenCloseCycles(mLayer, mQueue));
}
TEST_F(TestSystemDispatchSocketWatch, StopThenCloseDoesNotAbortUnderStrictDispatch)
{
char path[4096];
uint32_t pathSize = sizeof(path);
ASSERT_EQ(_NSGetExecutablePath(path, &pathSize), 0);
std::vector<char *> env;
char strict[] = "LIBDISPATCH_STRICT=1";
char child[] = "CHIP_TEST_DISPATCH_SOCKET_WATCH_STRICT_CHILD=1";
for (char ** e = environ; *e != nullptr; e++)
{
if (strncmp(*e, "LIBDISPATCH_STRICT=", strlen("LIBDISPATCH_STRICT=")) != 0 &&
strncmp(*e, kStrictChildEnv, strlen(kStrictChildEnv)) != 0)
{
env.push_back(*e);
}
}
env.push_back(strict);
env.push_back(child);
env.push_back(nullptr);
posix_spawn_file_actions_t actions;
ASSERT_EQ(posix_spawn_file_actions_init(&actions), 0);
ASSERT_EQ(posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, "/dev/null", O_WRONLY, 0), 0);
ASSERT_EQ(posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, "/dev/null", O_WRONLY, 0), 0);
char * argv[] = { path, nullptr };
pid_t pid;
int spawnErr = posix_spawn(&pid, path, &actions, nullptr, argv, env.data());
posix_spawn_file_actions_destroy(&actions);
ASSERT_EQ(spawnErr, 0);
int status = 0;
pid_t waited = 0;
for (int i = 0; i < 3000 && (waited = waitpid(pid, &status, WNOHANG)) == 0; i++)
{
usleep(10 * 1000);
}
if (waited == 0)
{
kill(pid, SIGKILL);
waitpid(pid, &status, 0);
FAIL() << "strict child did not exit within 30s";
}
ASSERT_EQ(waited, pid);
EXPECT_FALSE(WIFSIGNALED(status));
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
TEST_F(TestSystemDispatchSocketWatch, StopClosesOnlyPrivateDescriptors)
{
int fds[2];
ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0);
int noSigPipe = 1;
ASSERT_EQ(setsockopt(fds[1], SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe)), 0);
int baseline = OpenDescriptorCount();
SocketWatchToken token;
ASSERT_EQ(StartWatch(mLayer, mQueue, fds[0], Watch::kReadWrite, token), CHIP_NO_ERROR);
EXPECT_EQ(OpenDescriptorCount(), baseline + 2);
CHIP_ERROR err = CHIP_NO_ERROR;
RunOn(mQueue, [&] { err = mLayer.StopWatchingSocket(&token); });
ASSERT_EQ(err, CHIP_NO_ERROR);
for (int i = 0; i < 100 && OpenDescriptorCount() != baseline; i++)
{
usleep(10 * 1000);
}
EXPECT_EQ(OpenDescriptorCount(), baseline);
char out = 'x';
char in = 0;
EXPECT_EQ(::send(fds[1], &out, 1, 0), static_cast<ssize_t>(1));
EXPECT_EQ(::recv(fds[0], &in, 1, 0), static_cast<ssize_t>(1));
EXPECT_EQ(in, out);
::close(fds[0]);
::close(fds[1]);
}
TEST_F(TestSystemDispatchSocketWatch, DupFailureReturnsErrorAndLaterRequestSucceeds)
{
int fds[2];
ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0);
SocketWatchToken token;
std::atomic<int> callbacks{ 0 };
CHIP_ERROR err = CHIP_NO_ERROR;
RunOn(mQueue, [&] {
err = mLayer.StartWatchingSocket(fds[0], &token);
VerifyOrReturn(err == CHIP_NO_ERROR);
err = mLayer.SetCallback(token, CountingSocketCallback, reinterpret_cast<intptr_t>(&callbacks));
});
ASSERT_EQ(err, CHIP_NO_ERROR);
rlimit saved;
ASSERT_EQ(getrlimit(RLIMIT_NOFILE, &saved), 0);
rlimit lowered = saved;
lowered.rlim_cur = static_cast<rlim_t>(fds[1] + 1);
ASSERT_EQ(setrlimit(RLIMIT_NOFILE, &lowered), 0);
std::vector<int> fillers;
for (int fd; (fd = ::fcntl(fds[1], F_DUPFD, 0)) >= 0;)
{
fillers.push_back(fd);
}
CHIP_ERROR failedErr = CHIP_NO_ERROR;
RunOn(mQueue, [&] { failedErr = mLayer.RequestCallbackOnPendingWrite(token); });
for (int fd : fillers)
{
::close(fd);
}
ASSERT_EQ(setrlimit(RLIMIT_NOFILE, &saved), 0);
EXPECT_EQ(failedErr, CHIP_ERROR_POSIX(EMFILE));
usleep(50 * 1000);
EXPECT_EQ(callbacks.load(), 0);
RunOn(mQueue, [&] { err = mLayer.RequestCallbackOnPendingWrite(token); });
EXPECT_EQ(err, CHIP_NO_ERROR);
for (int i = 0; i < 100 && callbacks.load() == 0; i++)
{
usleep(10 * 1000);
}
EXPECT_GT(callbacks.load(), 0);
RunOn(mQueue, [&] {
err = mLayer.StopWatchingSocket(&token);
::close(fds[0]);
});
EXPECT_EQ(err, CHIP_NO_ERROR);
::close(fds[1]);
}
} // namespace
#endif // CHIP_SYSTEM_CONFIG_USE_DISPATCH && CHIP_SYSTEM_CONFIG_USE_SOCKETS