yams/tools/fuzzing/startup_fuzz_support.h
Trevon 6248c87bcc
Some checks are pending
Fuzz Smoke / libFuzzer smoke (push) Waiting to run
test(daemon): replay startup schedules under a fuzzer (#335)
Startup ordering defects were only observable by waiting minutes on a live
daemon, and the one that aborted it took a live restart loop to find. Give the
startup contracts a deterministic harness that runs in milliseconds.

+ startup_fuzz_support.h drives the real ServiceManagerFsm and the real
  DeferralGate with virtual time: legal phase advances, injected failure,
  shutdown and terminal reset, idle/busy, pressure/calm, starvation deadlines
  and cancellation
+ oracles fail closed on shutdown resurrection without reset, failure-to-ready,
  eligibility disagreeing with fsm.isReady(), admission outside the virtual
  deadline, and ineligible work touching the admission gate; fuzzed IEEE-754
  components must never yield non-finite or out-of-range scores
+ catch2_startup_replay runs named regressions plus 1024 seeded traces per run
  and reports the root seed and action offset for replay
+ fuzz_startup builds the same harness for libFuzzer/AFL, with a seed generator
  that refuses to overwrite different existing files
- the fuzzing lane no longer defaults to TSan: harnesses link
  -fsanitize=fuzzer,address,undefined and the two runtimes cannot share a
  process; combining them now fails with an explanation instead of producing a
  binary that cannot start
- documented the lane invocation, target naming and the coverage limits,
  including that governor call ordering inside the poller is not covered

Scope: contract-valid single-owner schedules. Executor interleavings, late
callbacks, concurrent FSM instances and filesystem/model faults are not modeled.

Signed-off-by: trvon <git@trevon.dev>
2026-10-22 16:48:50 -06:00

220 lines
8.7 KiB
C++

// Copyright (c) 2026 YAMS Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <algorithm>
#include <array>
#include <bit>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <span>
#include <yams/daemon/components/HostPressure.h>
#include <yams/daemon/components/ServiceManagerFsm.h>
#include "../../src/daemon/components/semantic_graph_candidate_policy.h"
#include "../../src/daemon/components/startup_work_policy.h"
namespace yams::fuzz {
// Stable byte vocabulary: a saved input is replayable by Catch2 and libFuzzer/AFL.
// Only contract-valid FSM dispatches are generated; this is not a thread scheduler
// or a model of arbitrary late callbacks. One replay owns the process-global tinyfsm.
enum class StartupAction : std::uint8_t {
Advance,
Fail,
Shutdown,
FinishShutdown,
Idle,
Busy,
Pressure,
Calm,
Tick,
Deadline,
Probe,
Cancel,
Reset,
Count
};
struct StartupReplayResult {
const char* error{nullptr};
std::size_t offset{0};
std::uint32_t statesVisited{0};
std::size_t admissions{0};
std::size_t forcedAdmissions{0};
[[nodiscard]] bool ok() const noexcept { return error == nullptr; }
};
inline StartupReplayResult replayStartup(std::span<const std::uint8_t> input) {
using namespace daemon;
ServiceManagerFsm fsm;
DeferralGate gate;
auto now = DeferralGate::Clock::time_point{};
constexpr auto maxDeferral = std::chrono::milliseconds{300000};
bool idle = true;
bool pressure = false;
bool cancelled = false;
bool sawShutdown = false;
bool sawFailure = false;
StartupReplayResult result;
result.statesVisited = 1U << static_cast<unsigned>(ServiceManagerState::Uninitialized);
// Bounded even for a huge fuzz input; no real clocks, IO, queues or sleeps.
for (std::size_t i = 0; i < std::min(input.size(), std::size_t{512}); ++i) {
result.offset = i;
const auto before = fsm.snapshot().state;
const auto action = static_cast<StartupAction>(
(input[i] & 0x7fU) % static_cast<std::uint8_t>(StartupAction::Count));
switch (action) {
case StartupAction::Advance:
switch (before) {
case ServiceManagerState::Uninitialized:
(void)fsm.tryStartOpeningDatabase();
break;
case ServiceManagerState::OpeningDatabase:
fsm.dispatch(DatabaseOpenedEvent{});
break;
case ServiceManagerState::DatabaseReady:
fsm.dispatch(MigrationStartedEvent{});
break;
case ServiceManagerState::MigratingSchema:
fsm.dispatch(MigrationCompletedEvent{});
break;
case ServiceManagerState::SchemaReady:
if ((input[i] & 0x80U) != 0) {
fsm.dispatch(VectorsInitializedEvent{64});
} else {
fsm.dispatch(SearchEngineBuildStartedEvent{});
}
break;
case ServiceManagerState::VectorsReady:
fsm.dispatch(SearchEngineBuildStartedEvent{});
break;
case ServiceManagerState::BuildingSearchEngine:
fsm.dispatch(SearchEngineBuiltEvent{});
break;
default:
break;
}
break;
case StartupAction::Fail:
if (before != ServiceManagerState::Stopped &&
before != ServiceManagerState::Failed) {
fsm.dispatch(InitializationFailedEvent{"injected startup failure"});
}
break;
case StartupAction::Shutdown:
fsm.dispatch(ShutdownEvent{});
sawShutdown = true;
break;
case StartupAction::FinishShutdown:
if (before == ServiceManagerState::ShuttingDown) {
(void)fsm.completeShutdown();
}
break;
case StartupAction::Idle:
idle = true;
break;
case StartupAction::Busy:
idle = false;
break;
case StartupAction::Pressure:
pressure = true;
break;
case StartupAction::Calm:
pressure = false;
break;
case StartupAction::Tick:
now += std::chrono::milliseconds{100};
break;
case StartupAction::Deadline:
now += maxDeferral + std::chrono::milliseconds{1};
break;
case StartupAction::Cancel:
cancelled = true;
break;
case StartupAction::Reset:
if (before == ServiceManagerState::Stopped ||
before == ServiceManagerState::Failed) {
fsm.reset();
gate = DeferralGate{};
cancelled = sawShutdown = sawFailure = false;
}
break;
case StartupAction::Probe:
case StartupAction::Count:
break;
}
const auto phase = fsm.snapshot().state;
result.statesVisited |= 1U << static_cast<unsigned>(phase);
sawFailure = sawFailure || phase == ServiceManagerState::Failed;
if (sawShutdown && phase != ServiceManagerState::ShuttingDown &&
phase != ServiceManagerState::Stopped) {
result.error = "shutdown resurrected without an explicit terminal reset";
return result;
}
if (sawFailure && phase == ServiceManagerState::Ready) {
result.error = "failed startup became ready without reset";
return result;
}
const bool stopping = cancelled || sawShutdown;
const bool eligible = embed::startupMaintenanceEligible(phase, stopping, idle);
// Oracle is independent of the policy implementation, using the real FSM API.
if (eligible != (fsm.isReady() && !stopping && idle)) {
result.error = "maintenance eligibility disagrees with actual startup lifecycle";
return result;
}
const auto deferralsBefore = gate.deferrals();
const auto forcedBefore = gate.forcedRuns();
const auto since = gate.deferredSince();
bool admitted = false;
if (eligible) {
admitted = gate.admit(pressure, now, maxDeferral);
const bool expected = !pressure || (since && now - *since >= maxDeferral);
if (admitted != expected) {
result.error = "pressure/starvation admission violated the virtual deadline";
return result;
}
}
if (!eligible && (admitted || gate.deferrals() != deferralsBefore ||
gate.forcedRuns() != forcedBefore)) {
result.error = "ineligible startup work touched the pressure admission gate";
return result;
}
result.admissions += admitted ? 1 : 0;
result.forcedAdmissions += static_cast<std::size_t>(gate.forcedRuns() - forcedBefore);
}
return result;
}
// Fuzz raw IEEE-754 values as well as lifecycle order. The production helpers reject
// unusable vectors/scores and normalize legal scores BEFORE adaptive thresholding.
inline bool semanticScoresBounded(std::span<const std::uint8_t> input) {
std::array<float, 32> left{};
std::array<float, 32> right{};
const auto count = std::min(input.size() / 8, left.size());
auto decode = [&](std::size_t offset) {
std::uint32_t bits = 0;
for (unsigned byte = 0; byte < 4; ++byte) {
bits |= static_cast<std::uint32_t>(input[offset + byte]) << (byte * 8);
}
return std::bit_cast<float>(bits);
};
for (std::size_t i = 0; i < count; ++i) {
left[i] = decode(i * 8);
right[i] = decode(i * 8 + 4);
}
const auto a = std::span<const float>{left.data(), count};
const auto b = std::span<const float>{right.data(), count};
const auto ia = daemon::embed::semanticInverseNorm(a);
const auto ib = daemon::embed::semanticInverseNorm(b);
const auto score = daemon::embed::semanticCosineSimilarity(a, ia, b, ib);
const auto self = daemon::embed::semanticCosineSimilarity(a, ia, a, ia);
return std::isfinite(ia) && ia >= 0 && std::isfinite(ib) && ib >= 0 && std::isfinite(score) &&
score >= -1 && score <= 1 && std::isfinite(self) && self >= 0 && self <= 1;
}
} // namespace yams::fuzz