352 lines
12 KiB
C++
352 lines
12 KiB
C++
// Socket Memory Pressure Test Suite
|
|
//
|
|
// This test suite reproduces socket crashes under memory pressure and concurrent operations.
|
|
// It targets the SIGSEGV crash that occurs when:
|
|
// 1. Socket connections are closed during idle timeout
|
|
// 2. Multiple concurrent connections are being processed
|
|
// 3. Memory is constrained (simulated via rapid connect/disconnect cycles)
|
|
// 4. Socket handles are accessed after closure (fd=UINT64_MAX in crash logs)
|
|
|
|
#define CATCH_CONFIG_MAIN
|
|
#include <catch2/catch_session.hpp>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <atomic>
|
|
#include <filesystem>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include "test_async_helpers.h"
|
|
#include "test_daemon_harness.h"
|
|
#include "test_helpers_catch2.h"
|
|
#include <yams/daemon/client/daemon_client.h>
|
|
#include <yams/daemon/ipc/ipc_protocol.h>
|
|
|
|
using namespace yams::daemon;
|
|
using namespace yams::test;
|
|
using namespace std::chrono_literals;
|
|
|
|
namespace {
|
|
|
|
// Helper to create a client with specific timeout configuration
|
|
DaemonClient createTestClient(const std::filesystem::path& socketPath,
|
|
std::chrono::milliseconds readTimeout = 500ms) {
|
|
ClientConfig config;
|
|
config.socketPath = socketPath;
|
|
config.connectTimeout = 2s;
|
|
config.requestTimeout = readTimeout;
|
|
config.autoStart = false;
|
|
config.enableChunkedResponses = false;
|
|
return DaemonClient(config);
|
|
}
|
|
|
|
// Helper to connect with retry logic for GlobalIOContext stabilization
|
|
bool connectWithRetry(DaemonClient& client, int maxRetries = 3,
|
|
std::chrono::milliseconds retryDelay = 200ms) {
|
|
// Initial delay for GlobalIOContext threads to stabilize
|
|
std::this_thread::sleep_for(200ms);
|
|
|
|
for (int attempt = 0; attempt < maxRetries; ++attempt) {
|
|
auto result = yams::cli::run_sync(client.connect(), 5s);
|
|
if (result.has_value()) {
|
|
return true;
|
|
}
|
|
std::this_thread::sleep_for(retryDelay);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void startHarnessWithRetry(DaemonHarness& harness, int maxRetries = 3,
|
|
std::chrono::milliseconds retryDelay = 250ms) {
|
|
for (int attempt = 0; attempt < maxRetries; ++attempt) {
|
|
if (harness.start(30s)) {
|
|
return;
|
|
}
|
|
harness.stop();
|
|
std::this_thread::sleep_for(retryDelay);
|
|
}
|
|
|
|
SKIP("Skipping socket memory-pressure section due to daemon startup instability");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Socket memory pressure - rapid connect/disconnect cycles",
|
|
"[daemon][socket][memory-pressure][integration]") {
|
|
SKIP_DAEMON_TEST_ON_WINDOWS();
|
|
|
|
DaemonHarness harness;
|
|
startHarnessWithRetry(harness);
|
|
|
|
std::atomic<int> successfulOps{0};
|
|
std::atomic<int> failedOps{0};
|
|
std::atomic<bool> shouldStop{false};
|
|
|
|
// Spawn multiple threads that rapidly connect, idle, and disconnect
|
|
constexpr int numThreads = 4;
|
|
std::vector<std::thread> workers;
|
|
|
|
for (int i = 0; i < numThreads; ++i) {
|
|
workers.emplace_back([&, threadId = i]() {
|
|
int localSuccess = 0;
|
|
int localFailed = 0;
|
|
|
|
for (int attempt = 0; attempt < 10 && !shouldStop.load(); ++attempt) {
|
|
try {
|
|
auto client = createTestClient(harness.socketPath(), 500ms);
|
|
|
|
// Connect
|
|
auto connResult = yams::cli::run_sync(client.connect(), 2s);
|
|
if (!connResult.has_value()) {
|
|
localFailed++;
|
|
continue;
|
|
}
|
|
|
|
// Let connection idle (triggers idle timeout path)
|
|
std::this_thread::sleep_for(600ms);
|
|
|
|
// Try to send request on potentially timed-out connection
|
|
auto statusResult = yams::cli::run_sync(client.status(), 1s);
|
|
|
|
if (statusResult.has_value()) {
|
|
localSuccess++;
|
|
} else {
|
|
localFailed++;
|
|
}
|
|
|
|
// Disconnect
|
|
client.disconnect();
|
|
|
|
} catch (const std::exception& e) {
|
|
INFO("Thread " << threadId << " exception: " << e.what());
|
|
localFailed++;
|
|
}
|
|
|
|
// Brief pause between cycles
|
|
std::this_thread::sleep_for(50ms);
|
|
}
|
|
|
|
successfulOps.fetch_add(localSuccess);
|
|
failedOps.fetch_add(localFailed);
|
|
});
|
|
}
|
|
|
|
// Let workers run
|
|
for (auto& worker : workers) {
|
|
worker.join();
|
|
}
|
|
|
|
INFO("Successful operations: " << successfulOps.load());
|
|
INFO("Failed operations: " << failedOps.load());
|
|
|
|
// Test passes if daemon didn't crash (no SIGSEGV)
|
|
// Some failures are expected due to timeouts, but daemon should remain stable
|
|
REQUIRE(successfulOps.load() > 0);
|
|
}
|
|
|
|
TEST_CASE("Socket memory pressure - concurrent idle connections",
|
|
"[daemon][socket][memory-pressure][integration]") {
|
|
SKIP_DAEMON_TEST_ON_WINDOWS();
|
|
|
|
ScopedEnvVar ipcTimeoutEnv{"YAMS_IPC_TIMEOUT_MS", std::string{"1000"}};
|
|
ScopedEnvVar maxIdleEnv{"YAMS_MAX_IDLE_TIMEOUTS", std::string{"3"}};
|
|
|
|
DaemonHarness harness;
|
|
startHarnessWithRetry(harness);
|
|
|
|
// Initial delay for GlobalIOContext threads to stabilize
|
|
std::this_thread::sleep_for(200ms);
|
|
|
|
constexpr int numConnections = 8;
|
|
std::vector<std::unique_ptr<DaemonClient>> clients;
|
|
|
|
// Create multiple connections with retry logic for first connection
|
|
for (int i = 0; i < numConnections; ++i) {
|
|
auto client = std::make_unique<DaemonClient>(createTestClient(harness.socketPath()));
|
|
|
|
// First connection may need retries due to GlobalIOContext timing
|
|
bool connected = false;
|
|
for (int attempt = 0; attempt < 3; ++attempt) {
|
|
auto connResult = yams::cli::run_sync(client->connect(), 2s);
|
|
if (connResult.has_value()) {
|
|
connected = true;
|
|
break;
|
|
}
|
|
std::this_thread::sleep_for(200ms);
|
|
}
|
|
REQUIRE(connected);
|
|
clients.push_back(std::move(client));
|
|
}
|
|
|
|
// Let all connections idle past timeout threshold
|
|
// (3x 1s read timeout via env = ~3s)
|
|
INFO("Idling connections to trigger timeout cleanup...");
|
|
std::this_thread::sleep_for(4s);
|
|
|
|
// Try to use connections after idle timeout
|
|
int successfulReconnects = 0;
|
|
for (size_t i = 0; i < clients.size(); ++i) {
|
|
try {
|
|
// Connection may have been closed by idle timeout
|
|
auto statusResult = yams::cli::run_sync(clients[i]->status(), 1s);
|
|
|
|
if (!statusResult.has_value()) {
|
|
// Expected: connection was closed, try to reconnect
|
|
auto reconnResult = yams::cli::run_sync(clients[i]->connect(), 2s);
|
|
if (reconnResult.has_value()) {
|
|
successfulReconnects++;
|
|
}
|
|
}
|
|
} catch (const std::exception& e) {
|
|
INFO("Client " << i << " error: " << e.what());
|
|
}
|
|
}
|
|
|
|
INFO("Successfully reconnected " << successfulReconnects << " out of " << clients.size()
|
|
<< " clients");
|
|
|
|
// Cleanup
|
|
for (auto& client : clients) {
|
|
client->disconnect();
|
|
}
|
|
|
|
// Test passes if daemon didn't crash
|
|
REQUIRE(true);
|
|
}
|
|
|
|
TEST_CASE("Socket memory pressure - handle validity after close",
|
|
"[daemon][socket][memory-pressure][integration]") {
|
|
SKIP_DAEMON_TEST_ON_WINDOWS();
|
|
|
|
DaemonHarness harness;
|
|
startHarnessWithRetry(harness);
|
|
|
|
auto client = createTestClient(harness.socketPath());
|
|
REQUIRE(connectWithRetry(client));
|
|
|
|
// Send a request that will complete
|
|
auto statusResult = yams::cli::run_sync(client.status(), 2s);
|
|
REQUIRE(statusResult.has_value());
|
|
|
|
// Force disconnect
|
|
client.disconnect();
|
|
|
|
// Try to use after disconnect - may auto-reconnect or fail, but must not crash
|
|
// The key safety property is graceful handling (no SIGSEGV/use-after-free)
|
|
auto statusResult2 = yams::cli::run_sync(client.status(), 1s);
|
|
// Either succeeds (auto-reconnect) or fails gracefully - both are valid
|
|
(void)statusResult2;
|
|
|
|
// Daemon should still be responsive
|
|
auto client2 = createTestClient(harness.socketPath());
|
|
REQUIRE(connectWithRetry(client2));
|
|
}
|
|
|
|
TEST_CASE("Socket memory pressure - native_handle safety",
|
|
"[daemon][socket][memory-pressure][crash]") {
|
|
SKIP_DAEMON_TEST_ON_WINDOWS();
|
|
|
|
ScopedEnvVar ipcTimeoutEnv{"YAMS_IPC_TIMEOUT_MS", std::string{"1000"}};
|
|
ScopedEnvVar maxIdleEnv{"YAMS_MAX_IDLE_TIMEOUTS", std::string{"3"}};
|
|
|
|
DaemonHarness harness;
|
|
startHarnessWithRetry(harness);
|
|
|
|
SECTION("native_handle after socket close returns invalid value") {
|
|
auto client = createTestClient(harness.socketPath());
|
|
REQUIRE(connectWithRetry(client));
|
|
|
|
client.disconnect();
|
|
|
|
// After disconnect, internal socket is closed
|
|
// Accessing native_handle on a closed socket should be safe
|
|
// This tests the exact scenario from the crash log where fd=18446744073709551615
|
|
// The daemon should not dereference the socket handle after closure
|
|
|
|
// Re-establish connection to verify daemon didn't crash
|
|
// Use retry logic since reconnecting after disconnect may need time
|
|
REQUIRE(connectWithRetry(client));
|
|
}
|
|
|
|
SECTION("logging socket fd during idle timeout doesn't crash") {
|
|
auto client = createTestClient(harness.socketPath(), 1s);
|
|
REQUIRE(connectWithRetry(client));
|
|
|
|
// Let connection idle to trigger the timeout path that logs the fd
|
|
// From crash log line 371: "Closing idle connection after {} consecutive read timeouts
|
|
// (fd={})"
|
|
std::this_thread::sleep_for(4s);
|
|
|
|
// Try to reconnect - daemon should be stable
|
|
// Use retry logic since reconnecting after idle timeout may need time
|
|
REQUIRE(connectWithRetry(client));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Socket memory pressure - stress test with mixed operations",
|
|
"[daemon][socket][memory-pressure][stress]") {
|
|
SKIP_DAEMON_TEST_ON_WINDOWS();
|
|
|
|
DaemonHarness harness;
|
|
startHarnessWithRetry(harness);
|
|
|
|
std::atomic<int> totalOps{0};
|
|
std::atomic<int> crashes{0};
|
|
std::atomic<bool> shouldStop{false};
|
|
|
|
constexpr int numThreads = 6;
|
|
constexpr int opsPerThread = 20;
|
|
std::vector<std::thread> workers;
|
|
|
|
for (int i = 0; i < numThreads; ++i) {
|
|
workers.emplace_back([&, threadId = i]() {
|
|
for (int op = 0; op < opsPerThread && !shouldStop.load(); ++op) {
|
|
try {
|
|
auto client = createTestClient(harness.socketPath(), 800ms);
|
|
|
|
// Connect
|
|
auto connResult = yams::cli::run_sync(client.connect(), 2s);
|
|
if (!connResult.has_value()) {
|
|
continue;
|
|
}
|
|
|
|
// Mix of operations
|
|
switch (op % 3) {
|
|
case 0:
|
|
// Quick status check
|
|
{
|
|
yams::cli::run_sync(client.status(), 1s);
|
|
}
|
|
break;
|
|
case 1:
|
|
// Idle timeout path
|
|
std::this_thread::sleep_for(1s);
|
|
break;
|
|
case 2:
|
|
// Rapid reconnect
|
|
client.disconnect();
|
|
yams::cli::run_sync(client.connect(), 2s);
|
|
break;
|
|
}
|
|
|
|
totalOps.fetch_add(1);
|
|
|
|
} catch (const std::exception& e) {
|
|
crashes.fetch_add(1);
|
|
INFO("Thread " << threadId << " op " << op << " exception: " << e.what());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& worker : workers) {
|
|
worker.join();
|
|
}
|
|
|
|
INFO("Total operations completed: " << totalOps.load());
|
|
INFO("Exceptions caught: " << crashes.load());
|
|
|
|
// Test passes if daemon remained stable (no SIGSEGV)
|
|
// We expect some exceptions due to timeouts, but no crashes
|
|
REQUIRE(crashes.load() < totalOps.load());
|
|
}
|