glove/tests/control/wallet_status_managed_session_test.cpp
0xkite 95617c966b
feat(control): add wallet status managed session owner
Wallet status managed session: exact current session/plan/policy
revalidation, owned trusted-clock zero/rollback/deadline/freshness
checks, and bounded 64-KiB SIGPIPE-safe Unix-socket framing over
the inherited channel. Fail-closed on missing readiness or malformed
observation. No wallet secrets or mutation authority.

Layer 4 of the reviewed Glove wallet-status stack; includes the
wallet_status_managed_session unit suite.

Validation: dev CTest, ASan/UBSan, TSan, and libFuzzer 10,000 units
on the final stacked tree (see workflow184 layer records).
2026-08-29 09:48:03 -06:00

556 lines
22 KiB
C++

#include "wallet_status_managed_session.hpp"
#include <fcntl.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#include <array>
#include <atomic>
#include <cerrno>
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <mutex>
#include <new>
#include <string>
#include <thread>
#include <vector>
namespace allocation_failure {
std::atomic<bool> next{false};
} // namespace allocation_failure
void* operator new(std::size_t size) {
if (allocation_failure::next.exchange(false)) {
throw std::bad_alloc{};
}
if (void* memory = std::malloc(size); memory != nullptr) {
return memory;
}
throw std::bad_alloc{};
}
void operator delete(void* memory) noexcept {
std::free(memory);
}
void operator delete(void* memory, std::size_t /*size*/) noexcept {
std::free(memory);
}
namespace {
#define REQUIRE(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "REQUIRE failed: %s @ %s:%d\n", #condition, __FILE__, __LINE__); \
return 1; \
} \
} while (false)
using steady_time = std::chrono::steady_clock::time_point;
struct reentrant_close_state {
std::mutex mutex;
std::condition_variable condition;
glove::control::wallet_status_managed_session* managed = nullptr;
bool authority_entered = false;
bool permit_reentrant_close = false;
};
[[nodiscard]] auto session() -> glove::control::session_record {
return {
.schema_version = 1,
.session_id = "synthetic-wallet-session",
.controller_plan_digest = std::string(64U, 'a'),
.plan_content_digest = std::string(64U, 'b'),
.state = glove::control::session_state::running,
.policy_revision = 1,
.expires_at_ms = 20'000U,
.created_at_ms = 1'000U,
};
}
[[nodiscard]] auto policy() -> glove::control::wallet_status_bridge_policy {
return {
.schema_version = 1,
.tool_policy_id = "synthetic-wallet-status",
.wallet_server_alias = "synthetic-wallet-host",
.wallet_server_node_digest = std::string(64U, 'c'),
.allowed_chain_ids = {8453U},
.maximum_status_age_ms = 5'000U,
};
}
[[nodiscard]] auto binding() -> glove::control::wallet_status_plan_binding {
const auto current = session();
return {
.session_id = current.session_id,
.controller_plan_digest = current.controller_plan_digest,
.plan_content_digest = current.plan_content_digest,
.policy_revision = current.policy_revision,
.expires_at_ms = current.expires_at_ms,
.generation = 1U,
};
}
class fixed_clock final : public glove::control::wallet_status_clock {
public:
[[nodiscard]] auto now() noexcept -> glove::control::wallet_status_time override {
return {.unix_time_ms = 10'000U, .monotonic_time = std::chrono::steady_clock::now()};
}
};
class plan_authority final : public glove::control::wallet_status_plan_authority {
public:
explicit plan_authority(std::shared_ptr<reentrant_close_state> close_state = {})
: close_state_(std::move(close_state)) {}
[[nodiscard]] auto snapshot(steady_time /*deadline*/) const
-> glove::control::wallet_status_authority_result<
glove::control::wallet_status_plan_snapshot> override {
if (close_state_) {
glove::control::wallet_status_managed_session* managed = nullptr;
{
std::unique_lock lock{close_state_->mutex};
close_state_->authority_entered = true;
close_state_->condition.notify_all();
close_state_->condition.wait(lock, [this] {
return close_state_->permit_reentrant_close;
});
managed = close_state_->managed;
}
managed->close();
}
return glove::control::wallet_status_plan_snapshot{
.binding = binding(),
.session = session(),
.policy = policy(),
.adapter_command_digest = std::string(64U, 'd'),
};
}
private:
std::shared_ptr<reentrant_close_state> close_state_;
};
class readiness_authority final : public glove::control::wallet_status_readiness_authority {
public:
[[nodiscard]] auto snapshot(
const glove::control::wallet_status_plan_binding& current, steady_time /*deadline*/
) const
-> glove::control::wallet_status_authority_result<
glove::control::wallet_status_readiness_snapshot> override {
return glove::control::wallet_status_readiness_snapshot{
.binding = current,
.audit_generation = 1U,
.journal_generation = 1U,
};
}
};
class adapter_authority final : public glove::control::wallet_status_adapter_authority {
public:
[[nodiscard]] auto snapshot(
const glove::control::wallet_status_plan_binding& current, steady_time /*deadline*/
) const
-> glove::control::wallet_status_authority_result<
glove::control::wallet_status_adapter_snapshot> override {
return glove::control::wallet_status_adapter_snapshot{
.binding = current,
.adapter_command_digest = std::string(64U, 'd'),
};
}
};
class server_authority final : public glove::control::wallet_status_server_authority {
public:
[[nodiscard]] auto snapshot(
const glove::control::wallet_status_plan_binding& current, steady_time /*deadline*/
) const
-> glove::control::wallet_status_authority_result<
glove::control::wallet_status_server_snapshot> override {
return glove::control::wallet_status_server_snapshot{
.binding = current,
.wallet_server_node_digest = std::string(64U, 'c'),
.observation = {
.connected = true,
.observed_at_ms = 9'000U,
.wallet_server_alias = "synthetic-wallet-host",
.allowed_chain_ids = {8453U},
},
};
}
};
[[nodiscard]] auto bridge(std::shared_ptr<reentrant_close_state> close_state = {})
-> std::unique_ptr<glove::control::wallet_status_bridge> {
return std::make_unique<glove::control::wallet_status_bridge>(
std::make_unique<plan_authority>(std::move(close_state)),
std::make_unique<readiness_authority>(),
std::make_unique<adapter_authority>(),
std::make_unique<server_authority>(),
std::make_unique<fixed_clock>()
);
}
[[nodiscard]] auto options(
std::chrono::milliseconds request_timeout = std::chrono::seconds{1},
std::chrono::milliseconds session_lifetime = std::chrono::seconds{5}
) -> glove::control::wallet_status_managed_session_options {
return {
.enabled = true,
.request_timeout = request_timeout,
.session_deadline = std::chrono::steady_clock::now() + session_lifetime,
.expected_peer_uid = ::geteuid(),
};
}
[[nodiscard]] auto composed(
std::chrono::milliseconds request_timeout = std::chrono::seconds{1},
std::chrono::milliseconds session_lifetime = std::chrono::seconds{5}
) -> std::expected<std::unique_ptr<glove::control::wallet_status_managed_session>, std::string> {
return glove::control::wallet_status_managed_session::create(
bridge(), options(request_timeout, session_lifetime)
);
}
[[nodiscard]] auto wait_closed(
const glove::control::wallet_status_managed_session& managed,
std::chrono::milliseconds timeout = std::chrono::milliseconds{500}
) -> bool {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (!managed.closed() && std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds{2});
}
return managed.closed();
}
[[nodiscard]] auto open_descriptor_count() -> int {
int count = 0;
const auto limit = ::getdtablesize();
for (int descriptor = 0; descriptor < limit; ++descriptor) {
errno = 0;
if (::fcntl(descriptor, F_GETFD) >= 0 || errno != EBADF) {
++count;
}
}
return count;
}
[[nodiscard]] auto write_raw(int descriptor, const unsigned char* data, std::size_t size) -> bool {
std::size_t offset = 0;
while (offset < size) {
const auto count = ::write(descriptor, data + offset, size - offset);
if (count > 0) {
offset += static_cast<std::size_t>(count);
continue;
}
if (count < 0 && errno == EINTR) {
continue;
}
return false;
}
return true;
}
[[nodiscard]] auto run() -> int {
using namespace glove::control;
auto disabled_options = options();
disabled_options.enabled = false;
REQUIRE(!wallet_status_managed_session::create(bridge(), disabled_options).has_value());
REQUIRE(!wallet_status_managed_session::create(nullptr, options()).has_value());
auto wrong_owner = options();
wrong_owner.expected_peer_uid = ::geteuid() + 1U;
REQUIRE(!wallet_status_managed_session::create(bridge(), wrong_owner).has_value());
REQUIRE(
!wallet_status_managed_session::create(bridge(), options(std::chrono::milliseconds::zero()))
.has_value()
);
REQUIRE(!wallet_status_managed_session::create(bridge(), options(std::chrono::seconds{6}))
.has_value());
REQUIRE(!wallet_status_managed_session::create(
bridge(), options(std::chrono::seconds{1}, std::chrono::milliseconds{-1})
)
.has_value());
auto allocation_bridge = bridge();
const auto allocation_options = options();
const auto descriptors_before_allocation_failure = open_descriptor_count();
bool allocation_failed = false;
allocation_failure::next.store(true);
try {
(void)wallet_status_managed_session::create(
std::move(allocation_bridge), allocation_options
);
} catch (const std::bad_alloc&) {
allocation_failed = true;
}
allocation_failure::next.store(false);
REQUIRE(allocation_failed);
REQUIRE(open_descriptor_count() == descriptors_before_allocation_failure);
const auto descriptors_before_untransferred_expiry = open_descriptor_count();
auto untransferred_expiry = composed(std::chrono::seconds{1}, std::chrono::milliseconds{50});
REQUIRE(untransferred_expiry.has_value());
REQUIRE((*untransferred_expiry)->start().has_value());
REQUIRE(wait_closed(**untransferred_expiry));
REQUIRE(open_descriptor_count() == descriptors_before_untransferred_expiry);
(*untransferred_expiry)->close();
const auto descriptors_before_worker_allocation_failure = open_descriptor_count();
auto worker_allocation_failure = composed();
REQUIRE(worker_allocation_failure.has_value());
auto worker_allocation_guest = (*worker_allocation_failure)->take_guest_descriptor();
REQUIRE(worker_allocation_guest.has_value());
REQUIRE((*worker_allocation_failure)->start().has_value());
constexpr std::array<unsigned char, 2> worker_header_prefix{0x00U, 0x00U};
constexpr std::array<unsigned char, 2> worker_header_suffix{0x00U, 0x40U};
REQUIRE(write_raw(
*worker_allocation_guest, worker_header_prefix.data(), worker_header_prefix.size()
));
allocation_failure::next.store(true);
REQUIRE(write_raw(
*worker_allocation_guest, worker_header_suffix.data(), worker_header_suffix.size()
));
REQUIRE(wait_closed(**worker_allocation_failure));
allocation_failure::next.store(false);
REQUIRE(open_descriptor_count() == descriptors_before_worker_allocation_failure + 1);
(*worker_allocation_failure)->close();
REQUIRE(::close(*worker_allocation_guest) == 0);
REQUIRE(open_descriptor_count() == descriptors_before_worker_allocation_failure);
std::array<int, 2> sentinel{-1, -1};
REQUIRE(::pipe(sentinel.data()) == 0);
auto live = composed();
REQUIRE(live.has_value());
auto guest = (*live)->take_guest_descriptor();
REQUIRE(guest.has_value());
REQUIRE(!(*live)->take_guest_descriptor().has_value());
struct stat guest_status{};
int socket_type = 0;
socklen_t socket_type_length = sizeof(socket_type);
sockaddr_storage local{};
sockaddr_storage peer{};
socklen_t local_length = sizeof(local);
socklen_t peer_length = sizeof(peer);
REQUIRE(::fstat(*guest, &guest_status) == 0);
REQUIRE(S_ISSOCK(guest_status.st_mode));
REQUIRE(guest_status.st_uid == ::geteuid());
REQUIRE((::fcntl(*guest, F_GETFD) & FD_CLOEXEC) != 0);
REQUIRE((::fcntl(*guest, F_GETFL) & O_NONBLOCK) != 0);
REQUIRE(::getsockopt(*guest, SOL_SOCKET, SO_TYPE, &socket_type, &socket_type_length) == 0);
REQUIRE(socket_type == SOCK_STREAM);
REQUIRE(::getsockname(*guest, reinterpret_cast<sockaddr*>(&local), &local_length) == 0);
REQUIRE(::getpeername(*guest, reinterpret_cast<sockaddr*>(&peer), &peer_length) == 0);
REQUIRE(local.ss_family == AF_UNIX);
REQUIRE(peer.ss_family == AF_UNIX);
REQUIRE((*live)->start().has_value());
REQUIRE(!(*live)->start().has_value());
auto request = encode_wallet_status_request("managed-status-1", 1'000U);
REQUIRE(request.has_value());
for (int round = 0; round < 2; ++round) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds{1};
REQUIRE(write_wallet_status_frame(*guest, *request, deadline).has_value());
auto response = read_wallet_status_frame(*guest, deadline);
REQUIRE(response.has_value());
auto decoded = decode_wallet_status_response(*response);
REQUIRE(decoded.has_value());
REQUIRE(decoded->available_actions == std::vector<std::string>{"status"});
REQUIRE(decoded->mutating_actions.empty());
REQUIRE(response->find("private_key") == std::string::npos);
REQUIRE(response->find(session().controller_plan_digest) == std::string::npos);
}
const auto close_started = std::chrono::steady_clock::now();
(*live)->close();
(*live)->close();
REQUIRE((*live)->closed());
REQUIRE(std::chrono::steady_clock::now() - close_started < std::chrono::milliseconds{500});
REQUIRE(::fcntl(sentinel[0], F_GETFD) >= 0);
REQUIRE(::fcntl(sentinel[1], F_GETFD) >= 0);
REQUIRE(::close(*guest) == 0);
REQUIRE(::close(sentinel[0]) == 0);
REQUIRE(::close(sentinel[1]) == 0);
auto partial_header = composed(std::chrono::milliseconds{200});
REQUIRE(partial_header.has_value());
auto partial_header_guest = (*partial_header)->take_guest_descriptor();
REQUIRE(partial_header_guest.has_value());
REQUIRE((*partial_header)->start().has_value());
constexpr std::array<unsigned char, 2> short_header{0x00U, 0x00U};
REQUIRE(write_raw(*partial_header_guest, short_header.data(), short_header.size()));
REQUIRE(::shutdown(*partial_header_guest, SHUT_WR) == 0);
REQUIRE(wait_closed(**partial_header));
(*partial_header)->close();
REQUIRE(::close(*partial_header_guest) == 0);
auto partial_body = composed(std::chrono::milliseconds{200});
REQUIRE(partial_body.has_value());
auto partial_body_guest = (*partial_body)->take_guest_descriptor();
REQUIRE(partial_body_guest.has_value());
REQUIRE((*partial_body)->start().has_value());
constexpr std::array<unsigned char, 6> short_body{0x00U, 0x00U, 0x00U, 0x08U, 'x', 'y'};
REQUIRE(write_raw(*partial_body_guest, short_body.data(), short_body.size()));
REQUIRE(::shutdown(*partial_body_guest, SHUT_WR) == 0);
REQUIRE(wait_closed(**partial_body));
(*partial_body)->close();
REQUIRE(::close(*partial_body_guest) == 0);
for (const auto header : {
std::array<unsigned char, 4>{0x00U, 0x00U, 0x00U, 0x00U},
std::array<unsigned char, 4>{0x00U, 0x01U, 0x00U, 0x01U},
}) {
auto invalid_frame = composed();
REQUIRE(invalid_frame.has_value());
auto invalid_guest = (*invalid_frame)->take_guest_descriptor();
REQUIRE(invalid_guest.has_value());
REQUIRE((*invalid_frame)->start().has_value());
REQUIRE(write_raw(*invalid_guest, header.data(), header.size()));
REQUIRE(wait_closed(**invalid_frame));
(*invalid_frame)->close();
REQUIRE(::close(*invalid_guest) == 0);
}
auto malformed = composed();
REQUIRE(malformed.has_value());
auto malformed_guest = (*malformed)->take_guest_descriptor();
REQUIRE(malformed_guest.has_value());
REQUIRE((*malformed)->start().has_value());
const auto malformed_deadline = std::chrono::steady_clock::now() + std::chrono::seconds{1};
REQUIRE(write_wallet_status_frame(*malformed_guest, "{", malformed_deadline).has_value());
REQUIRE(wait_closed(**malformed));
(*malformed)->close();
REQUIRE(::close(*malformed_guest) == 0);
auto timed_out = composed(std::chrono::milliseconds{50});
REQUIRE(timed_out.has_value());
auto timed_out_guest = (*timed_out)->take_guest_descriptor();
REQUIRE(timed_out_guest.has_value());
REQUIRE((*timed_out)->start().has_value());
constexpr unsigned char one_byte = 0x00U;
REQUIRE(write_raw(*timed_out_guest, &one_byte, 1U));
REQUIRE(wait_closed(**timed_out));
(*timed_out)->close();
REQUIRE(::close(*timed_out_guest) == 0);
auto cancelled = composed();
REQUIRE(cancelled.has_value());
auto cancelled_guest = (*cancelled)->take_guest_descriptor();
REQUIRE(cancelled_guest.has_value());
REQUIRE((*cancelled)->start().has_value());
REQUIRE(write_raw(*cancelled_guest, &one_byte, 1U));
const auto cancel_started = std::chrono::steady_clock::now();
(*cancelled)->close();
REQUIRE(std::chrono::steady_clock::now() - cancel_started < std::chrono::milliseconds{500});
REQUIRE((*cancelled)->closed());
REQUIRE(::close(*cancelled_guest) == 0);
auto peer_exit = composed();
REQUIRE(peer_exit.has_value());
auto peer_exit_guest = (*peer_exit)->take_guest_descriptor();
REQUIRE(peer_exit_guest.has_value());
REQUIRE((*peer_exit)->start().has_value());
REQUIRE(::close(*peer_exit_guest) == 0);
REQUIRE(wait_closed(**peer_exit));
(*peer_exit)->close();
auto response_peer_exit = composed();
REQUIRE(response_peer_exit.has_value());
auto response_peer_exit_guest = (*response_peer_exit)->take_guest_descriptor();
REQUIRE(response_peer_exit_guest.has_value());
REQUIRE((*response_peer_exit)->start().has_value());
const auto response_peer_exit_deadline =
std::chrono::steady_clock::now() + std::chrono::seconds{1};
REQUIRE(
write_wallet_status_frame(*response_peer_exit_guest, *request, response_peer_exit_deadline)
.has_value()
);
REQUIRE(::close(*response_peer_exit_guest) == 0);
REQUIRE(wait_closed(**response_peer_exit));
(*response_peer_exit)->close();
const auto descriptors_before_concurrent_close = open_descriptor_count();
auto concurrent_close = composed();
REQUIRE(concurrent_close.has_value());
REQUIRE((*concurrent_close)->start().has_value());
std::vector<std::thread> closers;
closers.reserve(8U);
for (int index = 0; index < 8; ++index) {
closers.emplace_back([&concurrent_close] { (*concurrent_close)->close(); });
}
for (auto& closer : closers) {
closer.join();
}
REQUIRE((*concurrent_close)->closed());
REQUIRE(open_descriptor_count() == descriptors_before_concurrent_close);
auto reentrant_state = std::make_shared<reentrant_close_state>();
auto reentrant_close =
wallet_status_managed_session::create(bridge(reentrant_state), options());
REQUIRE(reentrant_close.has_value());
{
const std::scoped_lock lock{reentrant_state->mutex};
reentrant_state->managed = reentrant_close->get();
}
auto reentrant_guest = (*reentrant_close)->take_guest_descriptor();
REQUIRE(reentrant_guest.has_value());
REQUIRE((*reentrant_close)->start().has_value());
const auto reentrant_deadline = std::chrono::steady_clock::now() + std::chrono::seconds{1};
REQUIRE(write_wallet_status_frame(*reentrant_guest, *request, reentrant_deadline).has_value());
bool authority_entered = false;
{
std::unique_lock lock{reentrant_state->mutex};
authority_entered = reentrant_state->condition.wait_for(
lock, std::chrono::milliseconds{500}, [&reentrant_state] {
return reentrant_state->authority_entered;
}
);
if (!authority_entered) {
reentrant_state->permit_reentrant_close = true;
}
}
if (!authority_entered) {
reentrant_state->condition.notify_all();
(*reentrant_close)->close();
(void)::close(*reentrant_guest);
REQUIRE(authority_entered);
}
std::thread external_closer{[&reentrant_close] { (*reentrant_close)->close(); }};
pollfd reentrant_event{.fd = *reentrant_guest, .events = POLLIN, .revents = 0};
const auto reentrant_poll = ::poll(&reentrant_event, 1, 500);
{
const std::scoped_lock lock{reentrant_state->mutex};
reentrant_state->permit_reentrant_close = true;
}
reentrant_state->condition.notify_all();
external_closer.join();
REQUIRE(reentrant_poll > 0);
REQUIRE((reentrant_event.revents & (POLLIN | POLLHUP)) != 0);
REQUIRE((*reentrant_close)->closed());
REQUIRE(::close(*reentrant_guest) == 0);
auto session_expiry = composed(std::chrono::seconds{1}, std::chrono::milliseconds{50});
REQUIRE(session_expiry.has_value());
auto expiring_guest = (*session_expiry)->take_guest_descriptor();
REQUIRE(expiring_guest.has_value());
REQUIRE((*session_expiry)->start().has_value());
REQUIRE(wait_closed(**session_expiry));
(*session_expiry)->close();
REQUIRE(::close(*expiring_guest) == 0);
return 0;
}
} // namespace
int main() {
return run();
}