Introduce the framed guest_channel_transport with shared deadlines and std::jthread ownership, metadata-only retired-schema quarantine with fail-closed integrity, stateless frozen-catalog validators with capability pinned to zero, and the generic local-service proxy with descriptor-pinned endpoints, alias-only guest sockets, audit-gated delivery, and a Linux managed runtime capability derived only from a registry-bound operational factory. Sage vocabulary is confined to src/adapters/sage.
1286 lines
52 KiB
C++
1286 lines
52 KiB
C++
#include "glove/container/digest.hpp"
|
|
#include "glove/container/receipt_producer.hpp"
|
|
#include "glove/control/guest_channel.hpp"
|
|
#include "glove/control/observation_intent_unix_server.hpp"
|
|
#include "glove/control/session_registry.hpp"
|
|
#include "glove/supervisor/library_bundle.hpp"
|
|
#include "glove/supervisor/path_alias.hpp"
|
|
#include "glove/supervisor/session_plan.hpp"
|
|
|
|
#include <arpa/inet.h>
|
|
#include <fcntl.h>
|
|
#include <glaze/glaze.hpp>
|
|
#include <poll.h>
|
|
#include <sys/resource.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/un.h>
|
|
#include <unistd.h>
|
|
|
|
#include <algorithm>
|
|
#include <cerrno>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <optional>
|
|
#include <span>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <system_error>
|
|
#include <thread>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace wire_test {
|
|
|
|
struct enqueue_success {
|
|
std::uint8_t schema_version = 0;
|
|
std::string status;
|
|
std::uint64_t sequence = 0;
|
|
std::string intent_digest;
|
|
};
|
|
|
|
struct enqueue_error {
|
|
std::uint8_t schema_version = 0;
|
|
std::string code;
|
|
};
|
|
|
|
} // namespace wire_test
|
|
|
|
namespace {
|
|
|
|
#define REQUIRE(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::fprintf(stderr, "REQUIRE failed: %s @ %s:%d\n", #cond, __FILE__, __LINE__); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
constexpr std::string_view audit_key =
|
|
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
|
|
constexpr std::string_view controller_digest =
|
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
|
|
constexpr std::string_view channel_token =
|
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
|
constexpr std::string_view library_bundle =
|
|
R"({"schema_version":1,"source_library_ref":"bafy-test","source_manifest_digest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","entries":[]})";
|
|
|
|
static_assert(std::is_same_v<
|
|
decltype(glove::control::observation_intent_unix_server_config::expected_peer_uid),
|
|
std::uint32_t>);
|
|
|
|
auto write_owner_only(const std::filesystem::path& path, std::string_view value) -> bool {
|
|
std::ofstream output{path, std::ios::binary | std::ios::trunc};
|
|
output << value;
|
|
output.flush();
|
|
return output.good() && ::chmod(path.c_str(), 0600) == 0;
|
|
}
|
|
|
|
class temporary_directory {
|
|
public:
|
|
temporary_directory() {
|
|
std::string pattern = "/tmp/glove-observation-server-test-XXXXXX";
|
|
if (char* created = ::mkdtemp(pattern.data()); created != nullptr) {
|
|
root_ = created;
|
|
}
|
|
}
|
|
|
|
temporary_directory(const temporary_directory&) = delete;
|
|
auto operator=(const temporary_directory&) -> temporary_directory& = delete;
|
|
|
|
temporary_directory(temporary_directory&& other) noexcept : root_{std::move(other.root_)} {}
|
|
|
|
auto operator=(temporary_directory&& other) noexcept -> temporary_directory& {
|
|
if (this != &other) {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(root_, error);
|
|
root_ = std::move(other.root_);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
~temporary_directory() {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(root_, error);
|
|
}
|
|
|
|
[[nodiscard]] auto root() const -> const std::filesystem::path& { return root_; }
|
|
|
|
private:
|
|
std::filesystem::path root_;
|
|
};
|
|
|
|
class unique_fd {
|
|
public:
|
|
explicit unique_fd(int value = -1) noexcept : value_{value} {}
|
|
|
|
unique_fd(const unique_fd&) = delete;
|
|
auto operator=(const unique_fd&) -> unique_fd& = delete;
|
|
|
|
unique_fd(unique_fd&& other) noexcept : value_{std::exchange(other.value_, -1)} {}
|
|
|
|
auto operator=(unique_fd&&) -> unique_fd& = delete;
|
|
|
|
~unique_fd() {
|
|
if (value_ >= 0) {
|
|
::close(value_);
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] auto get() const noexcept -> int { return value_; }
|
|
|
|
private:
|
|
int value_;
|
|
};
|
|
|
|
auto library_bundle_digest() -> std::string {
|
|
const auto* bytes = reinterpret_cast<const unsigned char*>(library_bundle.data());
|
|
return glove::container::sha256_hex(std::span{bytes, library_bundle.size()}).value_or("");
|
|
}
|
|
|
|
auto launch_template() -> glove::supervisor::runtime_launch_template {
|
|
return {
|
|
.runtime_discovery = {},
|
|
.executable_path = "/usr/bin/true",
|
|
.executable_search_paths = {},
|
|
.arguments = {"--version"},
|
|
.environment = {"PATH=/usr/bin:/bin", "TERM=xterm-256color"},
|
|
.read_only_paths = {},
|
|
};
|
|
}
|
|
|
|
auto runtime_digest() -> std::string {
|
|
return glove::supervisor::runtime_launch_template_digest(launch_template()).value_or("");
|
|
}
|
|
|
|
auto valid_plan_at(std::uint64_t now_ms) -> std::string {
|
|
return R"({"schema_version":1,"runtime_id":"codex","runtime_template_id":"codex-safe","adapter_command_digest":")" +
|
|
runtime_digest() +
|
|
R"(","sandbox_backend":"linux_production","egress_policy_id":"no-network","tool_policy_id":"sage-readonly","path_grants":[{"alias":"workspace","access":"ephemeral_write","materialization":"copy","max_bytes":1048576,"ttl_secs":60,"cleanup_policy":"remove"}],"library_projections":[{"projection_id":"sage-core","content_digest":")" +
|
|
library_bundle_digest() +
|
|
R"(","destination_alias":"libraries"}],"secret_handles":["codex-token"],"limits":{"cpu_time_ms":1000,"memory_bytes":67108864,"pids":16,"wall_time_ms":2000,"disk_bytes":2097152,"terminal_output_bytes":1048576},"policy_revision":7,"expires_at_ms":)" +
|
|
std::to_string(now_ms + 120'000) + "}";
|
|
}
|
|
|
|
auto sage_guest_plan_at(std::uint64_t now_ms) -> std::string {
|
|
auto plan = valid_plan_at(now_ms);
|
|
for (const auto& [from, to] : {
|
|
std::pair{
|
|
std::string_view{"\"runtime_id\":\"codex\""},
|
|
std::string_view{"\"runtime_id\":\"sage-guest\""}
|
|
},
|
|
std::pair{
|
|
std::string_view{"\"runtime_template_id\":\"codex-safe\""},
|
|
std::string_view{"\"runtime_template_id\":\"sage-guest-safe\""}
|
|
},
|
|
std::pair{
|
|
std::string_view{"\"secret_handles\":[\"codex-token\"]"},
|
|
std::string_view{"\"secret_handles\":[]"}
|
|
},
|
|
}) {
|
|
const auto offset = plan.find(from);
|
|
if (offset != std::string::npos) {
|
|
plan.replace(offset, from.size(), to);
|
|
}
|
|
}
|
|
return plan;
|
|
}
|
|
|
|
// Registration example (test fixture): the host owns payload semantics.
|
|
auto test_channel_host() -> std::shared_ptr<const glove::control::channel_host> {
|
|
auto host = std::make_shared<glove::control::channel_host>();
|
|
if (!host->register_channel({
|
|
.schema_id = "test.observation.v1",
|
|
.body_validator =
|
|
[](const glove::control::glove_observation_body&) noexcept { return true; },
|
|
.bounds =
|
|
{
|
|
.max_items = 4'096,
|
|
.max_body_bytes = 8'192,
|
|
.max_ttl_ms = 600'000,
|
|
.max_skew_ms = 30'000,
|
|
},
|
|
}) ||
|
|
!host->freeze()) {
|
|
return {};
|
|
}
|
|
return host;
|
|
}
|
|
|
|
auto validator_for(const std::filesystem::path& source)
|
|
-> glove::supervisor::result<glove::supervisor::session_plan_validator> {
|
|
using namespace glove::supervisor;
|
|
auto paths = path_alias_registry::build({
|
|
path_alias_policy{
|
|
.alias = "workspace",
|
|
.host_path = std::filesystem::canonical(source).string(),
|
|
.target_path = "/workspace",
|
|
.max_ttl_secs = 120,
|
|
.access = {
|
|
path_access_policy{
|
|
.access = path_access::ephemeral_write,
|
|
.materialization = path_materialization::copy,
|
|
.create_policy = path_create_policy::empty_directory,
|
|
.cleanup_policy = path_cleanup_policy::remove,
|
|
.max_bytes = 2'097'152,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (!paths) {
|
|
return std::unexpected(paths.error());
|
|
}
|
|
return session_plan_validator::build(
|
|
session_plan_policy{
|
|
.revision = 7,
|
|
.max_plan_ttl_ms = 120'000,
|
|
.runtime_templates =
|
|
{
|
|
runtime_template_policy{
|
|
.runtime_template_id = "codex-safe",
|
|
.runtime_id = "codex",
|
|
.adapter_command_digest = runtime_digest(),
|
|
.backend = sandbox_backend::linux_production,
|
|
.allowed_path_aliases = {"workspace"},
|
|
.allowed_projection_destinations = {"libraries"},
|
|
.launch = launch_template(),
|
|
.adoption = std::nullopt,
|
|
},
|
|
runtime_template_policy{
|
|
.runtime_template_id = "sage-guest-safe",
|
|
.runtime_id = "sage-guest",
|
|
.adapter_command_digest = runtime_digest(),
|
|
.backend = sandbox_backend::linux_production,
|
|
.allowed_path_aliases = {"workspace"},
|
|
.allowed_projection_destinations = {"libraries"},
|
|
.launch = launch_template(),
|
|
.adoption = std::nullopt,
|
|
},
|
|
},
|
|
.library_projection_destinations =
|
|
{
|
|
library_projection_destination_policy{
|
|
.alias = "libraries",
|
|
.target_path = "/opt/sage/library-bundles",
|
|
},
|
|
},
|
|
.resource_profiles =
|
|
{
|
|
resource_limits{
|
|
.cpu_time_ms = 1'000,
|
|
.memory_bytes = 67'108'864,
|
|
.pids = 16,
|
|
.wall_time_ms = 2'000,
|
|
.disk_bytes = 2'097'152,
|
|
.terminal_output_bytes = 1'048'576,
|
|
},
|
|
},
|
|
.egress_policy_ids = {"no-network"},
|
|
.tool_policy_ids = {"sage-readonly"},
|
|
.secret_handles = {"codex-token"},
|
|
.egress_policies = {},
|
|
.secret_mounts = {},
|
|
},
|
|
std::move(*paths)
|
|
);
|
|
}
|
|
|
|
auto cgroup_identity(std::uint32_t pid) -> glove::control::linux_cgroup_recovery_identity {
|
|
return {
|
|
.schema_version = 1,
|
|
.device = 42,
|
|
.inode = 20'000U + pid,
|
|
};
|
|
}
|
|
|
|
auto filesystem_identity() -> glove::control::linux_filesystem_recovery_identity {
|
|
return {
|
|
.schema_version = 1,
|
|
.disk_limit_bytes = 2'097'152,
|
|
.partitions = {{.alias = "workspace", .quota_bytes = 1'048'576}},
|
|
};
|
|
}
|
|
|
|
auto process_identity(std::uint32_t pid) -> glove::control::linux_process_identity {
|
|
return {
|
|
.schema_version = 1,
|
|
.pid = pid,
|
|
.boot_id = "12345678-1234-1234-1234-123456789abc",
|
|
.start_time_ticks = 10'000U + pid,
|
|
.cgroup_device = 42,
|
|
.cgroup_inode = 20'000U + pid,
|
|
.cgroup_path_digest = std::string(64, 'd'),
|
|
};
|
|
}
|
|
|
|
struct running_session_fixture {
|
|
temporary_directory temp;
|
|
std::shared_ptr<glove::control::session_registry> registry;
|
|
std::shared_ptr<glove::container::receipt_audit_producer> producer;
|
|
std::string session_id;
|
|
std::string profile_digest;
|
|
std::string projection_digest;
|
|
std::uint64_t start_ms = 0;
|
|
|
|
running_session_fixture(const running_session_fixture&) = delete;
|
|
auto operator=(const running_session_fixture&) -> running_session_fixture& = delete;
|
|
running_session_fixture(running_session_fixture&&) = default;
|
|
auto operator=(running_session_fixture&&) -> running_session_fixture& = default;
|
|
running_session_fixture() = default;
|
|
};
|
|
|
|
auto open_running_session(std::uint64_t start_ms) -> std::optional<running_session_fixture> {
|
|
running_session_fixture fixture;
|
|
fixture.start_ms = start_ms;
|
|
if (fixture.temp.root().empty()) {
|
|
return std::nullopt;
|
|
}
|
|
const auto source = fixture.temp.root() / "source";
|
|
if (!std::filesystem::create_directory(source)) {
|
|
return std::nullopt;
|
|
}
|
|
auto validator = validator_for(source);
|
|
if (!validator) {
|
|
return std::nullopt;
|
|
}
|
|
auto shared_validator =
|
|
std::make_shared<const glove::supervisor::session_plan_validator>(std::move(*validator));
|
|
|
|
const auto bundle_root = fixture.temp.root() / "library-bundles";
|
|
if (!std::filesystem::create_directory(bundle_root)) {
|
|
return std::nullopt;
|
|
}
|
|
if (::chmod(bundle_root.c_str(), 0700) != 0) {
|
|
return std::nullopt;
|
|
}
|
|
const auto bundle_path = bundle_root / (library_bundle_digest() + ".json");
|
|
{
|
|
std::ofstream output{bundle_path, std::ios::binary};
|
|
output.write(library_bundle.data(), static_cast<std::streamsize>(library_bundle.size()));
|
|
}
|
|
if (::chmod(bundle_path.c_str(), 0600) != 0) {
|
|
return std::nullopt;
|
|
}
|
|
auto opened_bundle_store = glove::supervisor::library_bundle_store::open(bundle_root);
|
|
if (!opened_bundle_store) {
|
|
return std::nullopt;
|
|
}
|
|
auto shared_bundle_store = std::make_shared<const glove::supervisor::library_bundle_store>(
|
|
std::move(*opened_bundle_store)
|
|
);
|
|
|
|
const auto audit_key_path = fixture.temp.root() / "receipt.key";
|
|
{
|
|
std::ofstream output{audit_key_path, std::ios::binary | std::ios::trunc};
|
|
output << audit_key << '\n';
|
|
}
|
|
if (::chmod(audit_key_path.c_str(), 0600) != 0) {
|
|
return std::nullopt;
|
|
}
|
|
auto producer = glove::container::receipt_audit_producer::initialize({
|
|
.key_path = audit_key_path,
|
|
.journal_path = fixture.temp.root() / "receipts.journal",
|
|
});
|
|
if (!producer) {
|
|
return std::nullopt;
|
|
}
|
|
if (!(*producer)->acknowledge_bootstrap((*producer)->anchor()).has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
fixture.producer = *producer;
|
|
|
|
auto registry = glove::control::session_registry::open_or_create(
|
|
fixture.temp.root() / "sessions.journal",
|
|
shared_validator,
|
|
shared_bundle_store,
|
|
glove::control::default_session_registry_bytes,
|
|
test_channel_host()
|
|
);
|
|
if (!registry) {
|
|
return std::nullopt;
|
|
}
|
|
fixture.registry = std::move(*registry);
|
|
fixture.session_id = "observation-session";
|
|
fixture.profile_digest = std::string(64, 'a');
|
|
fixture.projection_digest = std::string(64, 'c');
|
|
|
|
auto created = fixture.registry->create(
|
|
fixture.session_id,
|
|
controller_digest,
|
|
sage_guest_plan_at(start_ms),
|
|
"create-observation-session",
|
|
start_ms
|
|
);
|
|
if (!created) {
|
|
return std::nullopt;
|
|
}
|
|
const glove::control::session_start_authorization authorization{
|
|
.schema_version = 1,
|
|
.authorization_id = "approval-observation-session",
|
|
.session_id = fixture.session_id,
|
|
.controller_plan_digest = std::string{controller_digest},
|
|
.plan_content_digest = created->plan_content_digest,
|
|
.approved_at_ms = start_ms + 1U,
|
|
.expires_at_ms = start_ms + 60'000U,
|
|
};
|
|
auto reserved = fixture.registry->reserve_start(
|
|
authorization, "reserve-observation-session", start_ms + 2U
|
|
);
|
|
if (!reserved) {
|
|
return std::nullopt;
|
|
}
|
|
const glove::control::session_execution_binding binding{
|
|
.schema_version = 1,
|
|
.session_id = fixture.session_id,
|
|
.controller_plan_digest = std::string{controller_digest},
|
|
.plan_content_digest = created->plan_content_digest,
|
|
.authorization_id = authorization.authorization_id,
|
|
.profile_digest = fixture.profile_digest,
|
|
.cgroup_identity = cgroup_identity(7001),
|
|
.filesystem_identity = filesystem_identity(),
|
|
};
|
|
auto reservation = fixture.producer->reserve_terminal(
|
|
binding.session_id, binding.controller_plan_digest, binding.profile_digest
|
|
);
|
|
if (!reservation) {
|
|
return std::nullopt;
|
|
}
|
|
auto starting = fixture.registry->mark_starting(
|
|
binding, *reservation, "starting-observation-session", start_ms + 3U
|
|
);
|
|
if (!starting) {
|
|
return std::nullopt;
|
|
}
|
|
const glove::control::session_running_commitment running{
|
|
.schema_version = 1,
|
|
.session_id = fixture.session_id,
|
|
.controller_plan_digest = binding.controller_plan_digest,
|
|
.plan_content_digest = binding.plan_content_digest,
|
|
.authorization_id = binding.authorization_id,
|
|
.profile_digest = fixture.profile_digest,
|
|
.process_identity = process_identity(7001),
|
|
.filesystem_identity = binding.filesystem_identity,
|
|
};
|
|
if (!fixture.registry
|
|
->mark_running(running, *reservation, "running-observation-session", start_ms + 4U)
|
|
.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
return std::optional<running_session_fixture>{std::move(fixture)};
|
|
}
|
|
|
|
auto make_enqueue_request(
|
|
std::string_view token,
|
|
std::string_view intent_id = "intent-1",
|
|
std::string_view observation = "guest-capability-inventory",
|
|
std::string_view value_digest =
|
|
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
|
std::uint64_t item_count = 4,
|
|
std::optional<std::string_view> extra_field = std::nullopt
|
|
) -> std::string {
|
|
std::string request =
|
|
std::string{"{\"schema_version\":1,\"channel_token\":\""} + std::string{token} +
|
|
"\",\"body\":{\"schema\":\"test.observation.v1\",\"intent_id\":\"" +
|
|
std::string{intent_id} + "\",\"observation\":\"" + std::string{observation} +
|
|
"\",\"value_digest\":\"" + std::string{value_digest} +
|
|
"\",\"item_count\":" + std::to_string(item_count) + "}";
|
|
if (extra_field) {
|
|
request += ",\"" + std::string{*extra_field} + "\":1";
|
|
}
|
|
request += "}";
|
|
return request;
|
|
}
|
|
|
|
auto write_exact(int descriptor, const void* input, std::size_t size) -> bool {
|
|
const auto* bytes = static_cast<const std::byte*>(input);
|
|
std::size_t written = 0;
|
|
while (written < size) {
|
|
const auto result = ::write(descriptor, bytes + written, size - written);
|
|
if (result < 0 && errno == EINTR) {
|
|
continue;
|
|
}
|
|
if (result <= 0) {
|
|
return false;
|
|
}
|
|
written += static_cast<std::size_t>(result);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
auto read_exact(int descriptor, void* output, std::size_t size) -> bool {
|
|
auto* bytes = static_cast<std::byte*>(output);
|
|
std::size_t consumed = 0;
|
|
while (consumed < size) {
|
|
const auto result = ::read(descriptor, bytes + consumed, size - consumed);
|
|
if (result < 0 && errno == EINTR) {
|
|
continue;
|
|
}
|
|
if (result <= 0) {
|
|
return false;
|
|
}
|
|
consumed += static_cast<std::size_t>(result);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
auto connect_to(const std::filesystem::path& socket_path) -> unique_fd {
|
|
#if defined(__linux__)
|
|
unique_fd descriptor{::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)};
|
|
#else
|
|
unique_fd descriptor{::socket(AF_UNIX, SOCK_STREAM, 0)};
|
|
if (descriptor.get() >= 0) {
|
|
(void)::fcntl(descriptor.get(), F_SETFD, FD_CLOEXEC);
|
|
}
|
|
#endif
|
|
if (descriptor.get() < 0) {
|
|
return unique_fd{};
|
|
}
|
|
#if defined(SO_NOSIGPIPE)
|
|
const int enabled = 1;
|
|
if (::setsockopt(descriptor.get(), SOL_SOCKET, SO_NOSIGPIPE, &enabled, sizeof(enabled)) != 0) {
|
|
return unique_fd{};
|
|
}
|
|
#endif
|
|
::sockaddr_un address{};
|
|
address.sun_family = AF_UNIX;
|
|
const auto value = socket_path.string();
|
|
if (value.size() >= sizeof(address.sun_path)) {
|
|
return unique_fd{};
|
|
}
|
|
std::memcpy(address.sun_path, value.c_str(), value.size() + 1U);
|
|
if (::connect(
|
|
descriptor.get(), reinterpret_cast<const ::sockaddr*>(&address), sizeof(address)
|
|
) != 0) {
|
|
return unique_fd{};
|
|
}
|
|
return descriptor;
|
|
}
|
|
|
|
// Locate and duplicate the private listening descriptor without adding a
|
|
// production native-handle API or creating a second public transport owner.
|
|
auto duplicate_listener_for(const std::filesystem::path& socket_path) -> unique_fd {
|
|
const auto expected = socket_path.string();
|
|
const long open_max = ::sysconf(_SC_OPEN_MAX);
|
|
const int upper_bound = static_cast<int>(std::clamp<long>(open_max, 0, 4'096));
|
|
for (int descriptor = 0; descriptor < upper_bound; ++descriptor) {
|
|
::sockaddr_un address{};
|
|
::socklen_t address_size = sizeof(address);
|
|
if (::getsockname(descriptor, reinterpret_cast<::sockaddr*>(&address), &address_size) !=
|
|
0 ||
|
|
address.sun_family != AF_UNIX || std::string_view{address.sun_path} != expected) {
|
|
continue;
|
|
}
|
|
return unique_fd{::dup(descriptor)};
|
|
}
|
|
return unique_fd{};
|
|
}
|
|
|
|
auto transact(const std::filesystem::path& socket_path, std::string_view frame)
|
|
-> std::optional<std::string> {
|
|
auto descriptor = connect_to(socket_path);
|
|
if (descriptor.get() < 0) {
|
|
return std::nullopt;
|
|
}
|
|
const auto size = htonl(static_cast<std::uint32_t>(frame.size()));
|
|
if (!write_exact(descriptor.get(), &size, sizeof(size))) {
|
|
return std::nullopt;
|
|
}
|
|
// An oversized-frame rejection may arrive before the peer drains the
|
|
// caller's body. Preserve the response even when that final write closes.
|
|
static_cast<void>(write_exact(descriptor.get(), frame.data(), frame.size()));
|
|
std::uint32_t response_size = 0;
|
|
if (!read_exact(descriptor.get(), &response_size, sizeof(response_size))) {
|
|
return std::nullopt;
|
|
}
|
|
const auto decoded_size = ntohl(response_size);
|
|
if (decoded_size == 0 || decoded_size > glove::control::max_observation_frame_bytes) {
|
|
return std::nullopt;
|
|
}
|
|
std::string response(decoded_size, '\0');
|
|
if (!read_exact(descriptor.get(), response.data(), response.size())) {
|
|
return std::nullopt;
|
|
}
|
|
return response;
|
|
}
|
|
|
|
auto decode_success(std::string_view frame) -> std::optional<wire_test::enqueue_success> {
|
|
wire_test::enqueue_success value{};
|
|
if (glz::read<glz::opts{.error_on_unknown_keys = true}>(value, frame)) {
|
|
return std::nullopt;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
auto decode_error(std::string_view frame) -> std::optional<wire_test::enqueue_error> {
|
|
wire_test::enqueue_error value{};
|
|
if (glz::read<glz::opts{.error_on_unknown_keys = true}>(value, frame)) {
|
|
return std::nullopt;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
auto response_is_redacted(std::string_view frame) -> bool {
|
|
return frame.find(channel_token) == std::string_view::npos &&
|
|
frame.find("\"channel_token\"") == std::string_view::npos &&
|
|
frame.find("observation-session") == std::string_view::npos &&
|
|
frame.find("secret_handles") == std::string_view::npos &&
|
|
frame.find("path_grants") == std::string_view::npos &&
|
|
frame.find("runtime_template_id") == std::string_view::npos &&
|
|
frame.find("intent-source") == std::string_view::npos;
|
|
}
|
|
|
|
auto server_config_for(
|
|
const running_session_fixture& fixture,
|
|
const std::filesystem::path& socket_path,
|
|
std::uint64_t session_expires_at_ms,
|
|
std::uint64_t io_timeout_ms = 100
|
|
) -> glove::control::observation_intent_unix_server_config {
|
|
return {
|
|
.socket_path = socket_path,
|
|
.sessions = fixture.registry.get(),
|
|
.session_id = fixture.session_id,
|
|
.runtime_id = "sage-guest",
|
|
.controller_plan_digest = std::string{controller_digest},
|
|
.profile_digest = fixture.profile_digest,
|
|
.projection_digest = fixture.projection_digest,
|
|
.policy_revision = 7,
|
|
.service_channel_id = "observation-session-observation-v1",
|
|
.channel_generation = 1,
|
|
.session_expires_at_ms = session_expires_at_ms,
|
|
.channel_token = std::string{channel_token},
|
|
.io_timeout_ms = io_timeout_ms,
|
|
.expected_peer_uid = ::geteuid(),
|
|
};
|
|
}
|
|
|
|
auto serve_in_background(
|
|
glove::control::observation_intent_unix_server& server,
|
|
std::size_t count,
|
|
std::optional<std::string>& error_out
|
|
) -> std::thread {
|
|
return std::thread{[&server, count, &error_out] {
|
|
for (std::size_t index = 0; index < count; ++index) {
|
|
auto served = server.serve_one_for(5'000);
|
|
if (!served) {
|
|
std::fprintf(stderr, "observation server failed: %s\n", served.error().c_str());
|
|
error_out = served.error();
|
|
return;
|
|
}
|
|
if (!*served) {
|
|
error_out = "accept deadline elapsed";
|
|
return;
|
|
}
|
|
}
|
|
}};
|
|
}
|
|
|
|
auto run() -> int {
|
|
using glove::control::guest_channel_transport_error;
|
|
using glove::control::guest_channel_transport_error_code;
|
|
|
|
const auto classified_send_failure = [](guest_channel_transport_error_code code) {
|
|
return glove::control::classify_observation_response_send(
|
|
std::unexpected(guest_channel_transport_error{.code = code, .message = "send failed"})
|
|
);
|
|
};
|
|
for (const auto code : {
|
|
guest_channel_transport_error_code::deadline_exceeded,
|
|
guest_channel_transport_error_code::disconnected,
|
|
guest_channel_transport_error_code::io,
|
|
}) {
|
|
auto classified = classified_send_failure(code);
|
|
REQUIRE(!classified.has_value());
|
|
REQUIRE(classified.error() == "send failed");
|
|
}
|
|
auto cancelled_send = classified_send_failure(guest_channel_transport_error_code::cancelled);
|
|
REQUIRE(cancelled_send.has_value());
|
|
REQUIRE(!*cancelled_send);
|
|
REQUIRE(glove::control::classify_observation_response_send({}).value_or(false));
|
|
|
|
// F5: transient accept-boundary errors are retryable; fatal config and
|
|
// descriptor-state errors are not.
|
|
REQUIRE(glove::control::observation_transient_accept_error(EINTR));
|
|
REQUIRE(glove::control::observation_transient_accept_error(EAGAIN));
|
|
REQUIRE(glove::control::observation_transient_accept_error(ECONNABORTED));
|
|
REQUIRE(glove::control::observation_transient_accept_error(EMFILE));
|
|
REQUIRE(glove::control::observation_transient_accept_error(ENFILE));
|
|
REQUIRE(glove::control::observation_transient_accept_error(ENOBUFS));
|
|
REQUIRE(glove::control::observation_transient_accept_error(ENOMEM));
|
|
REQUIRE(!glove::control::observation_transient_accept_error(EBADF));
|
|
REQUIRE(!glove::control::observation_transient_accept_error(EINVAL));
|
|
REQUIRE(!glove::control::observation_transient_accept_error(EACCES));
|
|
|
|
const auto now_ms = []() -> std::uint64_t {
|
|
using namespace std::chrono;
|
|
const auto ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch());
|
|
return ms.count() < 0 ? 0U : static_cast<std::uint64_t>(ms.count());
|
|
}();
|
|
const std::uint64_t start_ms = now_ms > 10'000U ? now_ms - 10'000U : 1U;
|
|
|
|
auto fixture = open_running_session(start_ms);
|
|
REQUIRE(fixture.has_value());
|
|
const auto socket_path = fixture->temp.root() / "observation.sock";
|
|
|
|
auto config = server_config_for(*fixture, socket_path, start_ms + 120'000U);
|
|
auto server = glove::control::observation_intent_unix_server::create(config);
|
|
REQUIRE(server.has_value());
|
|
REQUIRE((*server)->socket_path() == socket_path);
|
|
|
|
struct stat socket_status{};
|
|
REQUIRE(::lstat(socket_path.c_str(), &socket_status) == 0);
|
|
REQUIRE(S_ISSOCK(socket_status.st_mode));
|
|
REQUIRE(socket_status.st_uid == ::geteuid());
|
|
REQUIRE((socket_status.st_mode & 0777U) == 0600U);
|
|
|
|
std::optional<std::string> serve_error;
|
|
auto server_thread = serve_in_background(**server, 1, serve_error);
|
|
|
|
const auto request = make_enqueue_request(channel_token);
|
|
auto response = transact(socket_path, request);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(response.has_value());
|
|
REQUIRE(response_is_redacted(*response));
|
|
auto success = decode_success(*response);
|
|
REQUIRE(success.has_value());
|
|
REQUIRE(success->schema_version == 1);
|
|
REQUIRE(success->status == "queued");
|
|
REQUIRE(success->sequence != 0);
|
|
REQUIRE(success->intent_digest.size() == 64U);
|
|
const auto count_after_enqueue = fixture->registry->record_count();
|
|
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**server, 1, serve_error);
|
|
auto replay_response = transact(socket_path, request);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(replay_response.has_value());
|
|
auto replay_success = decode_success(*replay_response);
|
|
REQUIRE(replay_success.has_value());
|
|
REQUIRE(replay_success->sequence == success->sequence);
|
|
REQUIRE(replay_success->intent_digest == success->intent_digest);
|
|
REQUIRE(fixture->registry->record_count() == count_after_enqueue);
|
|
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**server, 1, serve_error);
|
|
auto conflict_response = transact(
|
|
socket_path,
|
|
make_enqueue_request(
|
|
channel_token, "intent-1", "guest-capability-inventory", std::string(64, 'e')
|
|
)
|
|
);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(conflict_response.has_value());
|
|
auto conflict_error = decode_error(*conflict_response);
|
|
REQUIRE(conflict_error.has_value());
|
|
REQUIRE(conflict_error->code == "idempotency_conflict");
|
|
REQUIRE(response_is_redacted(*conflict_response));
|
|
REQUIRE(fixture->registry->record_count() == count_after_enqueue);
|
|
|
|
const auto count_before_bad_token = fixture->registry->record_count();
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**server, 1, serve_error);
|
|
auto bad_token_response =
|
|
transact(socket_path, make_enqueue_request(std::string(64, 'f'), "intent-2"));
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(bad_token_response.has_value());
|
|
auto bad_token_error = decode_error(*bad_token_response);
|
|
REQUIRE(bad_token_error.has_value());
|
|
REQUIRE(bad_token_error->code == "unauthorized");
|
|
REQUIRE(fixture->registry->record_count() == count_before_bad_token);
|
|
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**server, 1, serve_error);
|
|
auto unknown_field_response = transact(
|
|
socket_path,
|
|
make_enqueue_request(
|
|
channel_token,
|
|
"intent-3",
|
|
"guest-capability-inventory",
|
|
std::string(64, 'b'),
|
|
4,
|
|
"issued_at_ms"
|
|
)
|
|
);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(unknown_field_response.has_value());
|
|
auto unknown_field_error = decode_error(*unknown_field_response);
|
|
REQUIRE(unknown_field_error.has_value());
|
|
REQUIRE(unknown_field_error->code == "invalid_request");
|
|
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**server, 1, serve_error);
|
|
auto missing_body_response = transact(
|
|
socket_path,
|
|
std::string{"{\"schema_version\":1,\"channel_token\":\""} + std::string{channel_token} +
|
|
"\"}"
|
|
);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(missing_body_response.has_value());
|
|
auto missing_body_error = decode_error(*missing_body_response);
|
|
REQUIRE(missing_body_error.has_value());
|
|
REQUIRE(missing_body_error->code == "invalid_request");
|
|
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**server, 1, serve_error);
|
|
const std::string oversized(glove::control::max_observation_frame_bytes + 1U, 'x');
|
|
auto oversized_response = transact(
|
|
socket_path,
|
|
std::string{"{\"schema_version\":1,\"channel_token\":\""} + std::string{channel_token} +
|
|
"\",\"body\":{\"schema\":\"test.observation.v1\",\"intent_id\":\"intent-4\","
|
|
"\"observation\":\"" +
|
|
oversized + "\",\"value_digest\":\"" + std::string(64, 'c') + "\",\"item_count\":1}}"
|
|
);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(oversized_response.has_value());
|
|
auto oversized_error = decode_error(*oversized_response);
|
|
REQUIRE(oversized_error.has_value());
|
|
REQUIRE(oversized_error->code == "invalid_request");
|
|
|
|
const auto count_before_resilience = fixture->registry->record_count();
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**server, 1, serve_error);
|
|
{
|
|
auto truncated_descriptor = connect_to(socket_path);
|
|
REQUIRE(truncated_descriptor.get() >= 0);
|
|
const std::uint32_t declared = htonl(128U);
|
|
REQUIRE(write_exact(truncated_descriptor.get(), &declared, sizeof(declared)));
|
|
REQUIRE(write_exact(truncated_descriptor.get(), "{\"schema_version\":1", 18U));
|
|
}
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**server, 1, serve_error);
|
|
auto resilience_after_truncation = transact(
|
|
socket_path,
|
|
make_enqueue_request(
|
|
channel_token,
|
|
"intent-resilience-truncated",
|
|
"guest-capability-inventory",
|
|
std::string(64, '7')
|
|
)
|
|
);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(resilience_after_truncation.has_value());
|
|
auto resilience_truncated_success = decode_success(*resilience_after_truncation);
|
|
REQUIRE(resilience_truncated_success.has_value());
|
|
REQUIRE(resilience_truncated_success->status == "queued");
|
|
REQUIRE(fixture->registry->record_count() > count_before_resilience);
|
|
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**server, 1, serve_error);
|
|
{
|
|
auto stalled = connect_to(socket_path);
|
|
REQUIRE(stalled.get() >= 0);
|
|
}
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**server, 1, serve_error);
|
|
auto resilience_after_disconnect = transact(
|
|
socket_path,
|
|
make_enqueue_request(
|
|
channel_token,
|
|
"intent-resilience-disconnect",
|
|
"guest-capability-inventory",
|
|
std::string(64, '6')
|
|
)
|
|
);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(resilience_after_disconnect.has_value());
|
|
auto resilience_disconnect_success = decode_success(*resilience_after_disconnect);
|
|
REQUIRE(resilience_disconnect_success.has_value());
|
|
REQUIRE(resilience_disconnect_success->status == "queued");
|
|
REQUIRE(fixture->registry->record_count() > count_before_resilience + 1U);
|
|
|
|
server->reset();
|
|
REQUIRE(::access(socket_path.c_str(), F_OK) != 0);
|
|
|
|
// A stop token tears down idle accept and partial header/body service
|
|
// without a concurrent listener/channel close. Destruction happens only
|
|
// after each owning worker has joined.
|
|
{
|
|
auto idle_config = server_config_for(
|
|
*fixture, fixture->temp.root() / "idle-stop.sock", start_ms + 120'000U
|
|
);
|
|
auto idle_server = glove::control::observation_intent_unix_server::create(idle_config);
|
|
REQUIRE(idle_server.has_value());
|
|
std::expected<bool, std::string> idle_result =
|
|
std::unexpected(std::string{"worker did not run"});
|
|
const auto started = std::chrono::steady_clock::now();
|
|
std::jthread idle_worker{[&](std::stop_token stop) {
|
|
idle_result = (*idle_server)->serve_one_for(5'000, stop);
|
|
}};
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{20});
|
|
idle_worker.request_stop();
|
|
idle_worker.join();
|
|
REQUIRE(idle_result.has_value());
|
|
REQUIRE(!*idle_result);
|
|
REQUIRE(std::chrono::steady_clock::now() - started < std::chrono::milliseconds{250});
|
|
idle_server->reset();
|
|
|
|
// A competing raw accept consumes readiness without taking public
|
|
// ownership of the observation transport. The listener itself must be
|
|
// nonblocking, and the next service call must remain deadline/stop bounded.
|
|
auto race_config = server_config_for(
|
|
*fixture, fixture->temp.root() / "consumed-readiness.sock", start_ms + 120'000U
|
|
);
|
|
auto race_server = glove::control::observation_intent_unix_server::create(race_config);
|
|
REQUIRE(race_server.has_value());
|
|
auto competing_listener = duplicate_listener_for(race_server->get()->socket_path());
|
|
REQUIRE(competing_listener.get() >= 0);
|
|
const int listener_flags = ::fcntl(competing_listener.get(), F_GETFL);
|
|
REQUIRE(listener_flags >= 0);
|
|
REQUIRE((listener_flags & O_NONBLOCK) != 0);
|
|
|
|
const auto compete_after_wait = [&](std::uint64_t timeout_ms, bool cancel) -> bool {
|
|
std::expected<bool, std::string> result =
|
|
std::unexpected(std::string{"worker did not run"});
|
|
const auto started = std::chrono::steady_clock::now();
|
|
std::jthread worker{[&](std::stop_token stop) {
|
|
result = (*race_server)->serve_one_for(timeout_ms, stop);
|
|
}};
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{10});
|
|
auto pending = connect_to(race_server->get()->socket_path());
|
|
if (pending.get() < 0) {
|
|
worker.request_stop();
|
|
worker.join();
|
|
return false;
|
|
}
|
|
::pollfd event{.fd = competing_listener.get(), .events = POLLIN, .revents = 0};
|
|
if (::poll(&event, 1, 10) != 1 || (event.revents & POLLIN) == 0) {
|
|
worker.request_stop();
|
|
worker.join();
|
|
return false;
|
|
}
|
|
unique_fd consumed{::accept(competing_listener.get(), nullptr, nullptr)};
|
|
if (consumed.get() < 0) {
|
|
worker.request_stop();
|
|
worker.join();
|
|
return false;
|
|
}
|
|
if (cancel) {
|
|
worker.request_stop();
|
|
}
|
|
worker.join();
|
|
return result.has_value() && !*result &&
|
|
std::chrono::steady_clock::now() - started < std::chrono::milliseconds{250};
|
|
};
|
|
|
|
bool deadline_race_exercised = false;
|
|
for (unsigned attempt = 0; attempt < 16U && !deadline_race_exercised; ++attempt) {
|
|
deadline_race_exercised = compete_after_wait(40, false);
|
|
}
|
|
REQUIRE(deadline_race_exercised);
|
|
|
|
bool cancellation_race_exercised = false;
|
|
for (unsigned attempt = 0; attempt < 16U && !cancellation_race_exercised; ++attempt) {
|
|
cancellation_race_exercised = compete_after_wait(5'000, true);
|
|
}
|
|
REQUIRE(cancellation_race_exercised);
|
|
race_server->reset();
|
|
|
|
for (const bool partial_body : {false, true}) {
|
|
const auto name = partial_body ? "partial-body-stop.sock" : "partial-header-stop.sock";
|
|
auto partial_config = server_config_for(
|
|
*fixture, fixture->temp.root() / name, start_ms + 120'000U, 5'000
|
|
);
|
|
auto partial_server =
|
|
glove::control::observation_intent_unix_server::create(partial_config);
|
|
REQUIRE(partial_server.has_value());
|
|
auto partial_client = connect_to(partial_server->get()->socket_path());
|
|
REQUIRE(partial_client.get() >= 0);
|
|
const auto declared = htonl(128U);
|
|
if (partial_body) {
|
|
REQUIRE(write_exact(partial_client.get(), &declared, sizeof(declared)));
|
|
REQUIRE(write_exact(partial_client.get(), "{", 1U));
|
|
} else {
|
|
REQUIRE(write_exact(partial_client.get(), &declared, 1U));
|
|
}
|
|
std::expected<bool, std::string> partial_result =
|
|
std::unexpected(std::string{"worker did not run"});
|
|
const auto partial_started = std::chrono::steady_clock::now();
|
|
std::jthread partial_worker{[&](std::stop_token stop) {
|
|
partial_result = (*partial_server)->serve_one_for(5'000, stop);
|
|
}};
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{20});
|
|
partial_worker.request_stop();
|
|
partial_worker.join();
|
|
REQUIRE(partial_result.has_value());
|
|
REQUIRE(!*partial_result);
|
|
REQUIRE(
|
|
std::chrono::steady_clock::now() - partial_started < std::chrono::milliseconds{250}
|
|
);
|
|
partial_server->reset();
|
|
}
|
|
}
|
|
|
|
REQUIRE(::chmod(fixture->temp.root().c_str(), 0755) == 0);
|
|
REQUIRE(
|
|
!glove::control::observation_intent_unix_server::create(
|
|
server_config_for(*fixture, fixture->temp.root() / "unsafe.sock", start_ms + 120'000U)
|
|
)
|
|
.has_value()
|
|
);
|
|
REQUIRE(::chmod(fixture->temp.root().c_str(), 0700) == 0);
|
|
|
|
REQUIRE(write_owner_only(socket_path, "not-a-socket"));
|
|
REQUIRE(!glove::control::observation_intent_unix_server::create(
|
|
server_config_for(*fixture, socket_path, start_ms + 120'000U)
|
|
)
|
|
.has_value());
|
|
std::filesystem::remove(socket_path);
|
|
|
|
auto mismatched_config =
|
|
server_config_for(*fixture, fixture->temp.root() / "mismatch.sock", start_ms + 120'000U);
|
|
mismatched_config.profile_digest = std::string(64, 'f');
|
|
auto mismatch_server =
|
|
glove::control::observation_intent_unix_server::create(mismatched_config);
|
|
REQUIRE(mismatch_server.has_value());
|
|
const auto count_before_mismatch = fixture->registry->record_count();
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**mismatch_server, 1, serve_error);
|
|
auto mismatch_response = transact(
|
|
mismatch_server->get()->socket_path(),
|
|
make_enqueue_request(
|
|
channel_token, "intent-mismatch", "guest-capability-inventory", std::string(64, '9')
|
|
)
|
|
);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(mismatch_response.has_value());
|
|
auto mismatch_error = decode_error(*mismatch_response);
|
|
REQUIRE(mismatch_error.has_value());
|
|
REQUIRE(mismatch_error->code == "intent_rejected");
|
|
REQUIRE(fixture->registry->record_count() == count_before_mismatch);
|
|
|
|
auto expired_config =
|
|
server_config_for(*fixture, fixture->temp.root() / "expired.sock", start_ms - 1U);
|
|
auto expired_server = glove::control::observation_intent_unix_server::create(expired_config);
|
|
REQUIRE(expired_server.has_value());
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**expired_server, 1, serve_error);
|
|
auto expired_response = transact(
|
|
expired_server->get()->socket_path(),
|
|
make_enqueue_request(
|
|
channel_token, "intent-expired", "guest-capability-inventory", std::string(64, '8')
|
|
)
|
|
);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(expired_response.has_value());
|
|
auto expired_error = decode_error(*expired_response);
|
|
REQUIRE(expired_error.has_value());
|
|
REQUIRE(expired_error->code == "intent_expired");
|
|
|
|
// F3: peer-credential enforcement. A server configured with an expected
|
|
// uid that cannot match the (same-uid) test client rejects connections
|
|
// before any frame is read, and the registry stays untouched.
|
|
{
|
|
auto guarded_config =
|
|
server_config_for(*fixture, fixture->temp.root() / "guarded.sock", start_ms + 120'000U);
|
|
guarded_config.expected_peer_uid = ::geteuid() + 1U;
|
|
auto guarded_server =
|
|
glove::control::observation_intent_unix_server::create(guarded_config);
|
|
REQUIRE(guarded_server.has_value());
|
|
const auto count_before_guard = fixture->registry->record_count();
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**guarded_server, 1, serve_error);
|
|
auto guarded_response = transact(
|
|
guarded_server->get()->socket_path(),
|
|
make_enqueue_request(channel_token, "intent-guarded")
|
|
);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
// The peer is rejected without a response frame.
|
|
REQUIRE(!guarded_response.has_value());
|
|
REQUIRE(fixture->registry->record_count() == count_before_guard);
|
|
}
|
|
|
|
// Durable replay survives server reconstruction; host timestamps are
|
|
// stamped only by the first acceptance and a changed stable body conflicts.
|
|
{
|
|
auto reconstructed_config = server_config_for(
|
|
*fixture, fixture->temp.root() / "reconstructed.sock", start_ms + 120'000U
|
|
);
|
|
auto reconstructed_server =
|
|
glove::control::observation_intent_unix_server::create(reconstructed_config);
|
|
REQUIRE(reconstructed_server.has_value());
|
|
const auto reconstructed_socket = reconstructed_server->get()->socket_path();
|
|
const auto count_before_reconstruction = fixture->registry->record_count();
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**reconstructed_server, 1, serve_error);
|
|
auto first = transact(
|
|
reconstructed_socket, make_enqueue_request(channel_token, "reconstructed-intent")
|
|
);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(first.has_value());
|
|
auto first_success = decode_success(*first);
|
|
REQUIRE(first_success.has_value());
|
|
reconstructed_server->reset();
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{2});
|
|
reconstructed_server =
|
|
glove::control::observation_intent_unix_server::create(reconstructed_config);
|
|
REQUIRE(reconstructed_server.has_value());
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**reconstructed_server, 2, serve_error);
|
|
auto replay = transact(
|
|
reconstructed_server->get()->socket_path(),
|
|
make_enqueue_request(channel_token, "reconstructed-intent")
|
|
);
|
|
REQUIRE(replay.has_value());
|
|
auto reconstructed_replay_success = decode_success(*replay);
|
|
REQUIRE(reconstructed_replay_success.has_value());
|
|
REQUIRE(reconstructed_replay_success->sequence == first_success->sequence);
|
|
REQUIRE(reconstructed_replay_success->intent_digest == first_success->intent_digest);
|
|
auto changed = transact(
|
|
reconstructed_server->get()->socket_path(),
|
|
make_enqueue_request(
|
|
channel_token,
|
|
"reconstructed-intent",
|
|
"guest-capability-inventory",
|
|
std::string(64, '1')
|
|
)
|
|
);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(changed.has_value());
|
|
REQUIRE(decode_error(*changed)->code == "idempotency_conflict");
|
|
REQUIRE(fixture->registry->record_count() == count_before_reconstruction + 1U);
|
|
}
|
|
|
|
// F5: a transient accept failure (real fd exhaustion) is retried with a
|
|
// bounded backoff and must not permanently kill the channel.
|
|
{
|
|
auto retry_config =
|
|
server_config_for(*fixture, fixture->temp.root() / "retry.sock", start_ms + 120'000U);
|
|
auto retry_server = glove::control::observation_intent_unix_server::create(retry_config);
|
|
REQUIRE(retry_server.has_value());
|
|
const auto retry_socket = retry_server->get()->socket_path();
|
|
|
|
auto held = connect_to(retry_socket);
|
|
REQUIRE(held.get() >= 0);
|
|
const auto held_request = make_enqueue_request(channel_token, "intent-retry");
|
|
const auto held_size = htonl(static_cast<std::uint32_t>(held_request.size()));
|
|
REQUIRE(write_exact(held.get(), &held_size, sizeof(held_size)));
|
|
REQUIRE(write_exact(held.get(), held_request.data(), held_request.size()));
|
|
|
|
// Genuinely exhaust descriptors so the next accept() fails with
|
|
// EMFILE where the platform enforces it at allocation time.
|
|
std::vector<int> exhausted;
|
|
bool exhausted_descriptors = false;
|
|
for (;;) {
|
|
const int descriptor = ::dup(0);
|
|
if (descriptor < 0) {
|
|
exhausted_descriptors = errno == EMFILE;
|
|
break;
|
|
}
|
|
exhausted.push_back(descriptor);
|
|
if (exhausted.size() > 4096U) {
|
|
break;
|
|
}
|
|
}
|
|
if (exhausted_descriptors) {
|
|
// Every failed accept and backoff shares the original deadline;
|
|
// retries cannot turn 40 ms into one fresh timeout per attempt.
|
|
const auto deadline_started = std::chrono::steady_clock::now();
|
|
auto deadline_result = (*retry_server)->serve_one_for(40);
|
|
REQUIRE(deadline_result.has_value());
|
|
REQUIRE(!*deadline_result);
|
|
REQUIRE(
|
|
std::chrono::steady_clock::now() - deadline_started < std::chrono::milliseconds{250}
|
|
);
|
|
|
|
// Cancellation also interrupts the transient accept backoff.
|
|
std::expected<bool, std::string> stop_result =
|
|
std::unexpected(std::string{"worker did not run"});
|
|
const auto stop_started = std::chrono::steady_clock::now();
|
|
std::jthread retry_worker{[&](std::stop_token stop) {
|
|
stop_result = (*retry_server)->serve_one_for(2'000, stop);
|
|
}};
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{10});
|
|
retry_worker.request_stop();
|
|
retry_worker.join();
|
|
REQUIRE(stop_result.has_value());
|
|
REQUIRE(!*stop_result);
|
|
REQUIRE(
|
|
std::chrono::steady_clock::now() - stop_started < std::chrono::milliseconds{250}
|
|
);
|
|
|
|
for (const int descriptor : exhausted) {
|
|
::close(descriptor);
|
|
}
|
|
// The channel recovers: both the queued connection and the new
|
|
// recovery request are accepted and processed.
|
|
const auto count_before_retry = fixture->registry->record_count();
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**retry_server, 2, serve_error);
|
|
auto retry_response = transact(
|
|
retry_socket, make_enqueue_request(channel_token, "intent-retry-recovered")
|
|
);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(retry_response.has_value());
|
|
REQUIRE(decode_success(*retry_response).has_value());
|
|
REQUIRE(fixture->registry->record_count() == count_before_retry + 2U);
|
|
} else {
|
|
// macOS keeps new allocations below its high-water mark, so the
|
|
// accept may simply succeed; the channel must still stay healthy
|
|
// and process both the queued and a fresh request.
|
|
for (const int descriptor : exhausted) {
|
|
::close(descriptor);
|
|
}
|
|
const auto count_before_retry = fixture->registry->record_count();
|
|
serve_error.reset();
|
|
server_thread = serve_in_background(**retry_server, 2, serve_error);
|
|
auto retry_response = transact(
|
|
retry_socket, make_enqueue_request(channel_token, "intent-retry-recovered")
|
|
);
|
|
server_thread.join();
|
|
REQUIRE(!serve_error.has_value());
|
|
REQUIRE(retry_response.has_value());
|
|
REQUIRE(decode_success(*retry_response).has_value());
|
|
REQUIRE(fixture->registry->record_count() >= count_before_retry + 1U);
|
|
}
|
|
}
|
|
|
|
auto cleanup_config =
|
|
server_config_for(*fixture, fixture->temp.root() / "cleanup.sock", start_ms + 120'000U);
|
|
auto cleanup_server = glove::control::observation_intent_unix_server::create(cleanup_config);
|
|
REQUIRE(cleanup_server.has_value());
|
|
struct stat created_socket{};
|
|
REQUIRE(::lstat(cleanup_config.socket_path.c_str(), &created_socket) == 0);
|
|
const auto created_inode = created_socket.st_ino;
|
|
REQUIRE(::unlink(cleanup_config.socket_path.c_str()) == 0);
|
|
REQUIRE(write_owner_only(cleanup_config.socket_path, "replacement"));
|
|
struct stat replacement{};
|
|
REQUIRE(::lstat(cleanup_config.socket_path.c_str(), &replacement) == 0);
|
|
REQUIRE(replacement.st_ino != created_inode);
|
|
cleanup_server->reset();
|
|
REQUIRE(std::filesystem::exists(cleanup_config.socket_path));
|
|
REQUIRE(std::filesystem::file_size(cleanup_config.socket_path) > 0U);
|
|
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
auto main() -> int {
|
|
return run();
|
|
}
|