glove/fuzz/wallet_status_json_fuzzer.cpp
0xkite c473a0e102
fix(control): bind Pi wallet status content to connected state and harden wallet fuzz clock
GLM complementary review remediation on the wallet stack:
- The Pi status adapter content string now reflects the structured
  connected flag ("Wallet is not connected.") instead of an
  unconditional "Wallet status is available." that contradicted
  connected:false; bound by a new REQUIRE on the disconnected path.
- The wallet_status_json fuzzer clock now advances its monotonic
  component from fuzz input, so fuzzed frames traverse the deeper
  revalidation ladder and deadline paths instead of a single fixed-
  authority success path.
- docs/architecture.md: documented the one-strike authority teardown
  contract (no retry/resync by the bridge) and the ABA generation-
  reuse defense living in authority contracts, gated on adversarial
  authority tests before any live composition.

Validation (umask 0022): dev CTest 79/79, ASan/UBSan 79/79,
TSan 79/79; wallet_status_json fuzzer 10,000 units fuzz_exit=0;
clang-format clean; git diff --check clean.
2026-08-29 10:49:09 -06:00

168 lines
6 KiB
C++

#include "src/control/wallet_status_bridge.hpp"
#include "src/control/wallet_status_json.hpp"
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
namespace {
using steady_time = std::chrono::steady_clock::time_point;
[[nodiscard]] auto session() -> glove::control::session_record {
return {
.schema_version = 1,
.session_id = "fuzz-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 = "fuzz-status-policy",
.wallet_server_alias = "fuzz-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 advancing_clock final : public glove::control::wallet_status_clock {
public:
explicit advancing_clock(const std::uint8_t* data, std::size_t size) noexcept
: data_{data}, size_{size}, offset_{0U} {}
[[nodiscard]] auto now() noexcept -> glove::control::wallet_status_time override {
monotonic_ms_ += consume_u64() % 2'000U;
return {
.unix_time_ms = 10'000U + monotonic_ms_ / 4U,
.monotonic_time = steady_time{std::chrono::milliseconds{monotonic_ms_}},
};
}
private:
[[nodiscard]] auto consume_u64() noexcept -> std::uint64_t {
std::uint64_t value = 0U;
const std::size_t available = size_ > offset_ ? size_ - offset_ : 0U;
const std::size_t take = available < 8U ? available : 8U;
for (std::size_t index = 0U; index < take; ++index) {
value = (value << 8U) | static_cast<std::uint8_t>(data_[offset_ + index]);
}
offset_ = take < available ? offset_ + take : size_;
return value;
}
const std::uint8_t* data_;
std::size_t size_;
std::size_t offset_;
std::uint64_t monotonic_ms_{0U};
};
class fixed_plan_authority final : public glove::control::wallet_status_plan_authority {
public:
[[nodiscard]] auto snapshot(steady_time /*deadline*/) const
-> glove::control::wallet_status_authority_result<
glove::control::wallet_status_plan_snapshot> override {
return glove::control::wallet_status_plan_snapshot{
.binding = binding(),
.session = session(),
.policy = policy(),
.adapter_command_digest = std::string(64U, 'd'),
};
}
};
class fixed_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 fixed_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 fixed_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 = "fuzz-wallet-host",
.allowed_chain_ids = {8453U},
},
};
}
};
} // namespace
extern "C" auto LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) -> int {
if (size > glove::control::max_wallet_status_frame_bytes) {
return 0;
}
// libFuzzer owns this byte span for the duration of the callback. Neither
// parser retains the non-owning view.
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
const std::string_view frame{reinterpret_cast<const char*>(data), size};
static_cast<void>(glove::control::valid_wallet_status_json(frame));
static_cast<void>(glove::control::decode_wallet_status_response(frame));
glove::control::wallet_status_bridge bridge{
std::make_unique<fixed_plan_authority>(),
std::make_unique<fixed_readiness_authority>(),
std::make_unique<fixed_adapter_authority>(),
std::make_unique<fixed_server_authority>(),
std::make_unique<advancing_clock>(data, size),
};
static_cast<void>(bridge.handle_request(frame, steady_time{std::chrono::seconds{1}}));
return 0;
}