485 lines
20 KiB
C++
485 lines
20 KiB
C++
#include "glove/container/digest.hpp"
|
|
#include "glove/container/receipt_journal.hpp"
|
|
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <system_error>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
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_plan_digest =
|
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
constexpr std::string_view profile_digest =
|
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
|
|
|
|
class temporary_directory {
|
|
public:
|
|
temporary_directory() {
|
|
std::string pattern = "/tmp/glove-receipt-journal-test-XXXXXX";
|
|
char* created = ::mkdtemp(pattern.data());
|
|
if (created != nullptr) {
|
|
root_ = created;
|
|
}
|
|
}
|
|
|
|
temporary_directory(const temporary_directory&) = delete;
|
|
auto operator=(const temporary_directory&) -> temporary_directory& = delete;
|
|
|
|
~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_;
|
|
};
|
|
|
|
auto receipt(std::uint64_t cpu_time_ms = 500) -> glove::container::resource_enforcement_receipt {
|
|
return {
|
|
.schema_version = 1,
|
|
.profile_digest = std::string{profile_digest},
|
|
.backend = glove::container::sandbox_backend::linux_production,
|
|
.backend_id = "linux-production:cgroup-v2-v1",
|
|
.configured_limits =
|
|
{
|
|
.cpu_time_ms = 60'000,
|
|
.memory_bytes = std::uint64_t{512} * 1024U * 1024U,
|
|
.pids = 128,
|
|
.wall_time_ms = 120'000,
|
|
.disk_bytes = std::uint64_t{1024} * 1024U * 1024U,
|
|
.terminal_output_bytes = std::uint64_t{16} * 1024U * 1024U,
|
|
},
|
|
.mechanisms =
|
|
{
|
|
.cpu_time = glove::container::enforcement_mechanism::cgroup_v2,
|
|
.memory = glove::container::enforcement_mechanism::cgroup_v2,
|
|
.pids = glove::container::enforcement_mechanism::cgroup_v2,
|
|
.wall_time = glove::container::enforcement_mechanism::watchdog,
|
|
.disk = glove::container::enforcement_mechanism::filesystem_quota,
|
|
.terminal_output = glove::container::enforcement_mechanism::byte_counter,
|
|
.receipt_schema_version = 1,
|
|
},
|
|
.observed =
|
|
{
|
|
.cpu_time_ms = cpu_time_ms,
|
|
.peak_memory_bytes = std::uint64_t{16} * 1024U * 1024U,
|
|
.peak_pids = 2,
|
|
.wall_time_ms = 750,
|
|
.disk_bytes = 4096,
|
|
.terminal_output_bytes = 1024,
|
|
},
|
|
.termination_cause = glove::container::resource_termination_cause::exited,
|
|
.exit_code = 0,
|
|
.started_at_ms = 1'000,
|
|
.finished_at_ms = 1'750,
|
|
.library_projections = {},
|
|
.retained_changes = {},
|
|
};
|
|
}
|
|
|
|
auto refinement_receipt(std::uint64_t cpu_time_ms = 500)
|
|
-> glove::container::refinement_evaluation_receipt {
|
|
using namespace glove::container;
|
|
refinement_outcome outcome{
|
|
.schema = std::string{refinement_outcome_schema},
|
|
.encoding = std::string{refinement_outcome_encoding},
|
|
.metrics = {{"failed_assertions", 0}, {"latency_us", 750'000}, {"passed", 1}},
|
|
};
|
|
const auto outcome_bytes = canonical_refinement_outcome_bytes(outcome).value();
|
|
const auto outcome_digest = sha256_hex(
|
|
std::span<const unsigned char>{
|
|
reinterpret_cast<const unsigned char*>(outcome_bytes.data()), outcome_bytes.size()
|
|
}
|
|
);
|
|
return {
|
|
.schema_version = refinement_evaluation_receipt_schema_version,
|
|
.runtime_template_id = std::string{refinement_runtime_template_id},
|
|
.resource_receipt = receipt(cpu_time_ms),
|
|
.evidence_status = refinement_evidence_status::valid_outcome,
|
|
.variant = refinement_variant::candidate,
|
|
.fixture = {"fixture", std::string(64, 'f'), "fixtures"},
|
|
.base = {"base", std::string(64, 'b'), "skills"},
|
|
.candidate = {"candidate", std::string(64, 'c'), "skills"},
|
|
.matched_context_digest = std::string(64, 'd'),
|
|
.outcome =
|
|
{
|
|
.schema = outcome.schema,
|
|
.encoding = outcome.encoding,
|
|
.digest = *outcome_digest,
|
|
.byte_length = outcome_bytes.size(),
|
|
},
|
|
.evaluated_outcome = outcome,
|
|
.transcript =
|
|
{
|
|
.schema = std::string{raw_pty_transcript_schema},
|
|
.digest = std::string(64, 'e'),
|
|
.byte_count = 1'024,
|
|
.complete = true,
|
|
},
|
|
.evaluator = {
|
|
.schema = std::string{refinement_evaluator_schema},
|
|
.fixture_complete = true,
|
|
.transcript_utf8 = true,
|
|
.required_literals = 1,
|
|
.forbidden_literals = 1,
|
|
},
|
|
};
|
|
}
|
|
|
|
auto read_bytes(const std::filesystem::path& path) -> std::vector<char> {
|
|
std::ifstream input{path, std::ios::binary};
|
|
return {std::istreambuf_iterator<char>{input}, std::istreambuf_iterator<char>{}};
|
|
}
|
|
|
|
auto write_bytes(const std::filesystem::path& path, const std::vector<char>& bytes) -> bool {
|
|
std::ofstream output{path, std::ios::binary | std::ios::trunc};
|
|
output.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
|
|
output.flush();
|
|
return output.good();
|
|
}
|
|
|
|
auto append_bytes(const std::filesystem::path& path, std::string_view bytes) -> bool {
|
|
std::ofstream output{path, std::ios::binary | std::ios::app};
|
|
output.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
|
|
output.flush();
|
|
return output.good();
|
|
}
|
|
|
|
auto persistence_and_reconciliation() -> int {
|
|
temporary_directory temp;
|
|
REQUIRE(!temp.root().empty());
|
|
const auto path = temp.root() / "receipts.journal";
|
|
auto journal = glove::container::receipt_audit_journal::create_new(path, audit_key);
|
|
REQUIRE(journal.has_value());
|
|
auto first = (*journal)->append("session-1", controller_plan_digest, receipt());
|
|
REQUIRE(first.has_value());
|
|
const auto first_anchor = (*journal)->anchor();
|
|
auto second = (*journal)->append("session-2", controller_plan_digest, receipt(501));
|
|
REQUIRE(second.has_value());
|
|
REQUIRE((*journal)->record_count() == 2);
|
|
REQUIRE((*journal)->anchor().sequence == 2);
|
|
REQUIRE((*journal)->durable_bytes() == std::filesystem::file_size(path));
|
|
REQUIRE(!glove::container::receipt_audit_journal::open_existing(path, audit_key).has_value());
|
|
journal->reset();
|
|
|
|
auto reopened =
|
|
glove::container::receipt_audit_journal::open_existing(path, audit_key, first_anchor);
|
|
REQUIRE(reopened.has_value());
|
|
REQUIRE((*reopened)->record_count() == 2);
|
|
REQUIRE((*reopened)->anchor().head_hmac == second->this_hmac);
|
|
REQUIRE(!(*reopened)->repaired_torn_tail());
|
|
auto page = (*reopened)->page_after(first_anchor, 10);
|
|
REQUIRE(page.has_value());
|
|
REQUIRE(page->envelopes.size() == 1);
|
|
REQUIRE(page->envelopes[0] == *second);
|
|
REQUIRE(!page->has_more);
|
|
auto first_lookup =
|
|
(*reopened)->terminal_for_execution("session-1", controller_plan_digest, profile_digest);
|
|
REQUIRE(first_lookup.has_value());
|
|
REQUIRE(first_lookup->has_value());
|
|
REQUIRE(**first_lookup == *first);
|
|
auto missing_lookup = (*reopened)->terminal_for_execution(
|
|
"session-missing", controller_plan_digest, profile_digest
|
|
);
|
|
REQUIRE(missing_lookup.has_value());
|
|
REQUIRE(!missing_lookup->has_value());
|
|
REQUIRE(!(*reopened)
|
|
->terminal_for_execution("../session", controller_plan_digest, profile_digest)
|
|
.has_value());
|
|
REQUIRE((*reopened)->append("session-1", controller_plan_digest, receipt(502)).has_value());
|
|
REQUIRE(!(*reopened)
|
|
->terminal_for_execution("session-1", controller_plan_digest, profile_digest)
|
|
.has_value());
|
|
|
|
auto forged = first_anchor;
|
|
forged.head_hmac[0] = forged.head_hmac[0] == 'a' ? 'b' : 'a';
|
|
REQUIRE(!(*reopened)->page_after(forged, 10).has_value());
|
|
REQUIRE(!(*reopened)->page_after(first_anchor, 0).has_value());
|
|
REQUIRE(!(*reopened)
|
|
->page_after(first_anchor, glove::container::max_receipt_reconciliation_page + 1U)
|
|
.has_value());
|
|
|
|
const auto bytes = read_bytes(path);
|
|
REQUIRE(
|
|
std::search(bytes.begin(), bytes.end(), audit_key.begin(), audit_key.end()) == bytes.end()
|
|
);
|
|
return 0;
|
|
}
|
|
|
|
auto concurrent_appends_are_serialized_and_recoverable() -> int {
|
|
temporary_directory temp;
|
|
REQUIRE(!temp.root().empty());
|
|
const auto path = temp.root() / "receipts.journal";
|
|
auto journal = glove::container::receipt_audit_journal::create_new(path, audit_key);
|
|
REQUIRE(journal.has_value());
|
|
auto* writer = journal->get();
|
|
std::atomic<std::uint64_t> succeeded = 0;
|
|
{
|
|
std::vector<std::thread> workers;
|
|
workers.reserve(8);
|
|
for (std::uint64_t index = 0; index < 8; ++index) {
|
|
workers.emplace_back([&, index] {
|
|
const auto session = "session-" + std::to_string(index);
|
|
const bool appended =
|
|
index % 2U == 0
|
|
? writer->append(session, controller_plan_digest, receipt(500U + index))
|
|
.has_value()
|
|
: writer
|
|
->append_refinement(
|
|
session, controller_plan_digest, refinement_receipt(500U + index)
|
|
)
|
|
.has_value();
|
|
if (appended) {
|
|
succeeded.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
});
|
|
}
|
|
for (auto& worker : workers) {
|
|
worker.join();
|
|
}
|
|
}
|
|
REQUIRE(succeeded.load(std::memory_order_relaxed) == 8);
|
|
REQUIRE(writer->record_count() == 8);
|
|
REQUIRE(writer->anchor().sequence == 8);
|
|
journal->reset();
|
|
|
|
auto reopened = glove::container::receipt_audit_journal::open_existing(path, audit_key);
|
|
REQUIRE(reopened.has_value());
|
|
REQUIRE((*reopened)->record_count() == 8);
|
|
REQUIRE((*reopened)->anchor().sequence == 8);
|
|
return 0;
|
|
}
|
|
|
|
auto mixed_receipts_page_and_recover_without_substitution() -> int {
|
|
temporary_directory temp;
|
|
REQUIRE(!temp.root().empty());
|
|
const auto path = temp.root() / "receipts.journal";
|
|
auto genesis = glove::container::receipt_audit_anchor::create(audit_key);
|
|
REQUIRE(genesis.has_value());
|
|
auto journal = glove::container::receipt_audit_journal::create_new(path, audit_key);
|
|
REQUIRE(journal.has_value());
|
|
auto resource = (*journal)->append("session-resource", controller_plan_digest, receipt());
|
|
REQUIRE(resource.has_value());
|
|
auto refinement = (*journal)->append_refinement(
|
|
"session-refinement", controller_plan_digest, refinement_receipt()
|
|
);
|
|
REQUIRE(refinement.has_value());
|
|
REQUIRE(resource->sequence == 1);
|
|
REQUIRE(refinement->sequence == 2);
|
|
REQUIRE(refinement->previous_hmac == resource->this_hmac);
|
|
auto page = (*journal)->page_after(**genesis, 10);
|
|
REQUIRE(page.has_value());
|
|
REQUIRE(page->envelopes == std::vector{*resource});
|
|
REQUIRE(page->refinement_envelopes == std::vector{*refinement});
|
|
REQUIRE((*journal)->contains_exact(*resource).value_or(false));
|
|
REQUIRE((*journal)->contains_exact(*refinement).value_or(false));
|
|
journal->reset();
|
|
|
|
auto reopened =
|
|
glove::container::receipt_audit_journal::open_existing(path, audit_key, **genesis);
|
|
REQUIRE(reopened.has_value());
|
|
auto found = (*reopened)->refinement_terminal_for_execution(
|
|
"session-refinement", controller_plan_digest, profile_digest
|
|
);
|
|
REQUIRE(found.has_value());
|
|
REQUIRE(found->has_value());
|
|
REQUIRE(**found == *refinement);
|
|
auto wrong_type = (*reopened)->terminal_for_execution(
|
|
"session-refinement", controller_plan_digest, profile_digest
|
|
);
|
|
REQUIRE(wrong_type.has_value());
|
|
REQUIRE(!wrong_type->has_value());
|
|
return 0;
|
|
}
|
|
|
|
auto external_size_changes_poison_reconciliation() -> int {
|
|
temporary_directory temp;
|
|
REQUIRE(!temp.root().empty());
|
|
const auto path = temp.root() / "receipts.journal";
|
|
auto genesis = glove::container::receipt_audit_anchor::create(audit_key);
|
|
REQUIRE(genesis.has_value());
|
|
auto journal = glove::container::receipt_audit_journal::create_new(path, audit_key);
|
|
REQUIRE(journal.has_value());
|
|
REQUIRE((*journal)->append("session-1", controller_plan_digest, receipt()).has_value());
|
|
REQUIRE(append_bytes(path, std::string_view{"x", 1}));
|
|
REQUIRE(!(*journal)->page_after(**genesis, 10).has_value());
|
|
REQUIRE(!(*journal)->append("session-2", controller_plan_digest, receipt()).has_value());
|
|
return 0;
|
|
}
|
|
|
|
auto torn_tail_requires_a_trusted_prefix() -> int {
|
|
temporary_directory temp;
|
|
REQUIRE(!temp.root().empty());
|
|
const auto path = temp.root() / "receipts.journal";
|
|
auto genesis = glove::container::receipt_audit_anchor::create(audit_key);
|
|
REQUIRE(genesis.has_value());
|
|
auto journal = glove::container::receipt_audit_journal::create_new(path, audit_key);
|
|
REQUIRE(journal.has_value());
|
|
auto first = (*journal)->append("session-1", controller_plan_digest, receipt());
|
|
REQUIRE(first.has_value());
|
|
journal->reset();
|
|
REQUIRE(append_bytes(path, std::string_view{"\x00\x00\x01", 3}));
|
|
const auto torn_size = std::filesystem::file_size(path);
|
|
|
|
REQUIRE(!glove::container::receipt_audit_journal::open_existing(path, audit_key).has_value());
|
|
REQUIRE(std::filesystem::file_size(path) == torn_size);
|
|
auto repaired =
|
|
glove::container::receipt_audit_journal::open_existing(path, audit_key, **genesis);
|
|
REQUIRE(repaired.has_value());
|
|
REQUIRE((*repaired)->repaired_torn_tail());
|
|
REQUIRE((*repaired)->record_count() == 1);
|
|
REQUIRE((*repaired)->anchor().head_hmac == first->this_hmac);
|
|
REQUIRE(std::filesystem::file_size(path) < torn_size);
|
|
auto pending = (*repaired)->page_after(**genesis, 10);
|
|
REQUIRE(pending.has_value());
|
|
REQUIRE(pending->envelopes.size() == 1);
|
|
REQUIRE(pending->envelopes[0] == *first);
|
|
return 0;
|
|
}
|
|
|
|
auto accepted_tail_truncation_fails_reconciliation() -> int {
|
|
temporary_directory temp;
|
|
REQUIRE(!temp.root().empty());
|
|
const auto path = temp.root() / "receipts.journal";
|
|
auto journal = glove::container::receipt_audit_journal::create_new(path, audit_key);
|
|
REQUIRE(journal.has_value());
|
|
REQUIRE((*journal)->append("session-1", controller_plan_digest, receipt()).has_value());
|
|
auto second = (*journal)->append("session-2", controller_plan_digest, receipt(501));
|
|
REQUIRE(second.has_value());
|
|
const auto accepted_anchor = (*journal)->anchor();
|
|
journal->reset();
|
|
const auto size = std::filesystem::file_size(path);
|
|
REQUIRE(size > 5);
|
|
std::filesystem::resize_file(path, size - 5U);
|
|
const auto truncated_size = std::filesystem::file_size(path);
|
|
|
|
REQUIRE(
|
|
!glove::container::receipt_audit_journal::open_existing(path, audit_key, accepted_anchor)
|
|
.has_value()
|
|
);
|
|
REQUIRE(std::filesystem::file_size(path) == truncated_size);
|
|
return 0;
|
|
}
|
|
|
|
auto complete_record_tampering_fails_closed() -> int {
|
|
temporary_directory temp;
|
|
REQUIRE(!temp.root().empty());
|
|
const auto path = temp.root() / "receipts.journal";
|
|
auto genesis = glove::container::receipt_audit_anchor::create(audit_key);
|
|
REQUIRE(genesis.has_value());
|
|
auto journal = glove::container::receipt_audit_journal::create_new(path, audit_key);
|
|
REQUIRE(journal.has_value());
|
|
REQUIRE((*journal)->append("session-1", controller_plan_digest, receipt()).has_value());
|
|
journal->reset();
|
|
|
|
auto bytes = read_bytes(path);
|
|
constexpr std::string_view needle = "session-1";
|
|
auto position = std::search(bytes.begin(), bytes.end(), needle.begin(), needle.end());
|
|
REQUIRE(position != bytes.end());
|
|
*position = *position == 's' ? 't' : 's';
|
|
REQUIRE(write_bytes(path, bytes));
|
|
const auto tampered_size = std::filesystem::file_size(path);
|
|
REQUIRE(!glove::container::receipt_audit_journal::open_existing(path, audit_key, **genesis)
|
|
.has_value());
|
|
REQUIRE(std::filesystem::file_size(path) == tampered_size);
|
|
return 0;
|
|
}
|
|
|
|
auto capacity_failure_does_not_advance_the_chain() -> int {
|
|
temporary_directory temp;
|
|
REQUIRE(!temp.root().empty());
|
|
const auto path = temp.root() / "receipts.journal";
|
|
constexpr std::uint64_t journal_limit = 4096;
|
|
auto journal =
|
|
glove::container::receipt_audit_journal::create_new(path, audit_key, journal_limit);
|
|
REQUIRE(journal.has_value());
|
|
std::uint64_t succeeded = 0;
|
|
for (;;) {
|
|
auto appended =
|
|
(*journal)->append("session-1", controller_plan_digest, receipt(500U + succeeded));
|
|
if (!appended) {
|
|
break;
|
|
}
|
|
++succeeded;
|
|
REQUIRE(succeeded < 10);
|
|
}
|
|
REQUIRE(succeeded > 0);
|
|
REQUIRE((*journal)->record_count() == succeeded);
|
|
REQUIRE((*journal)->anchor().sequence == succeeded);
|
|
REQUIRE((*journal)->durable_bytes() <= journal_limit);
|
|
const auto stable_anchor = (*journal)->anchor();
|
|
REQUIRE(!(*journal)->append("session-1", controller_plan_digest, receipt()).has_value());
|
|
REQUIRE((*journal)->anchor() == stable_anchor);
|
|
return 0;
|
|
}
|
|
|
|
auto journal_file_identity_and_key_are_fail_closed() -> int {
|
|
temporary_directory temp;
|
|
REQUIRE(!temp.root().empty());
|
|
const auto path = temp.root() / "receipts.journal";
|
|
auto journal = glove::container::receipt_audit_journal::create_new(path, audit_key);
|
|
REQUIRE(journal.has_value());
|
|
REQUIRE(!glove::container::receipt_audit_journal::create_new(path, audit_key).has_value());
|
|
REQUIRE((*journal)->append("session-1", controller_plan_digest, receipt()).has_value());
|
|
journal->reset();
|
|
|
|
REQUIRE(!glove::container::receipt_audit_journal::open_existing(
|
|
path, "1111111111111111111111111111111111111111111111111111111111111111"
|
|
)
|
|
.has_value());
|
|
REQUIRE(::chmod(path.c_str(), 0644) == 0);
|
|
REQUIRE(!glove::container::receipt_audit_journal::open_existing(path, audit_key).has_value());
|
|
REQUIRE(::chmod(path.c_str(), 0600) == 0);
|
|
|
|
const auto symlink_path = temp.root() / "receipt-link.journal";
|
|
std::filesystem::create_symlink(path, symlink_path);
|
|
REQUIRE(
|
|
!glove::container::receipt_audit_journal::open_existing(symlink_path, audit_key).has_value()
|
|
);
|
|
const auto hardlink_path = temp.root() / "receipt-hardlink.journal";
|
|
std::filesystem::create_hard_link(path, hardlink_path);
|
|
REQUIRE(!glove::container::receipt_audit_journal::open_existing(path, audit_key).has_value());
|
|
return 0;
|
|
}
|
|
|
|
auto run() -> int {
|
|
REQUIRE(persistence_and_reconciliation() == 0);
|
|
REQUIRE(concurrent_appends_are_serialized_and_recoverable() == 0);
|
|
REQUIRE(mixed_receipts_page_and_recover_without_substitution() == 0);
|
|
REQUIRE(external_size_changes_poison_reconciliation() == 0);
|
|
REQUIRE(torn_tail_requires_a_trusted_prefix() == 0);
|
|
REQUIRE(accepted_tail_truncation_fails_reconciliation() == 0);
|
|
REQUIRE(complete_record_tampering_fails_closed() == 0);
|
|
REQUIRE(capacity_failure_does_not_advance_the_chain() == 0);
|
|
REQUIRE(journal_file_identity_and_key_are_fail_closed() == 0);
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
auto main() -> int {
|
|
return run();
|
|
}
|