include/DetourModKit/manifest.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_MANIFEST_HPP | ||
| 2 | #define DETOURMODKIT_MANIFEST_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file manifest.hpp | ||
| 6 | * @brief Signature manifest: patch-fragile resolve contracts as editable data in its own INI, not the settings INI. | ||
| 7 | * @warning `[B-100]` Never parse, compile, resolve, gate, or derive a scope under the loader lock. Pure value accessors | ||
| 8 | * on a compiled @ref Signature do not allocate or query the loader. | ||
| 9 | */ | ||
| 10 | |||
| 11 | #include "DetourModKit/anchor.hpp" | ||
| 12 | #include "DetourModKit/error.hpp" | ||
| 13 | #include "DetourModKit/region.hpp" | ||
| 14 | #include "DetourModKit/scan.hpp" | ||
| 15 | |||
| 16 | #include <cstddef> | ||
| 17 | #include <cstdint> | ||
| 18 | #include <filesystem> | ||
| 19 | #include <limits> | ||
| 20 | #include <span> | ||
| 21 | #include <string> | ||
| 22 | #include <string_view> | ||
| 23 | #include <vector> | ||
| 24 | |||
| 25 | namespace DetourModKit | ||
| 26 | { | ||
| 27 | // Binding::read_register needs only this declaration. Include hook.hpp to name a hook::Gpr value. | ||
| 28 | namespace hook | ||
| 29 | { | ||
| 30 | enum class Gpr : std::uint8_t; | ||
| 31 | } | ||
| 32 | |||
| 33 | namespace manifest | ||
| 34 | { | ||
| 35 | namespace detail | ||
| 36 | { | ||
| 37 | class GateAccess; | ||
| 38 | } // namespace detail | ||
| 39 | |||
| 40 | /** @brief How a consumer interprets a resolved value. The consumer, not this module, acts on it. */ | ||
| 41 | enum class BindingKind : std::uint8_t | ||
| 42 | { | ||
| 43 | /// The resolved value is the target address, for example an inline-hook target or a global. | ||
| 44 | Address, | ||
| 45 | /// The resolved value is a chain base that the consumer walks through @ref Binding::offsets. | ||
| 46 | PointerChain, | ||
| 47 | /// The resolved value is a mid-hook site, where @ref hook::gpr reads @ref Binding::read_register. | ||
| 48 | MidHookRegister, | ||
| 49 | /** @brief The resolved value is a vtable base. Hook the slot at @ref Binding::vmt_index. */ | ||
| 50 | VmtMethod | ||
| 51 | }; | ||
| 52 | |||
| 53 | /// The @ref Binding::xmm_index sentinel: the site reads a GPR, not an XMM lane. | ||
| 54 | inline constexpr std::uint8_t XMM_INDEX_UNUSED = 0xFF; | ||
| 55 | |||
| 56 | /** @brief How to read a resolved value. A field that @ref kind does not read is inert and must stay default. */ | ||
| 57 | struct Binding | ||
| 58 | { | ||
| 59 | /// How to interpret the resolved value. | ||
| 60 | BindingKind kind = BindingKind::Address; | ||
| 61 | /// PointerChain: one or more byte offsets walked from the resolved base with @ref memory::walk semantics. | ||
| 62 | std::vector<std::ptrdiff_t> offsets; | ||
| 63 | /// PointerChain: byte width of the final read (1, 2, 4, or 8), for example 4 for a float. | ||
| 64 | std::uint8_t value_width = 8; | ||
| 65 | /// MidHookRegister: the register that the mid-hook callback reads. | ||
| 66 | hook::Gpr read_register{}; | ||
| 67 | /// MidHookRegister: XMM lane 0 through 15 for a float site, or @ref XMM_INDEX_UNUSED for a GPR value. | ||
| 68 | std::uint8_t xmm_index = XMM_INDEX_UNUSED; | ||
| 69 | /// VmtMethod: the zero-based vtable slot to hook, from 0 through 4095. | ||
| 70 | std::size_t vmt_index = 0; | ||
| 71 | }; | ||
| 72 | |||
| 73 | /** @brief One editable ladder rung for a @ref scan::Candidate. Resolution ignores the fields of other modes. */ | ||
| 74 | struct CandidateSpec | ||
| 75 | { | ||
| 76 | /// Human-readable rung name, carried into the winning @ref scan::Hit for diagnostics. | ||
| 77 | std::string name; | ||
| 78 | /// Which resolution strategy this rung uses. | ||
| 79 | scan::Mode mode = scan::Mode::Direct; | ||
| 80 | /// Direct / RipRelative: the AOB DSL string, for example "48 8B 05 ?? ?? ?? ??". | ||
| 81 | std::string pattern; | ||
| 82 | /// Direct: signed byte delta added to the match, where a negative value walks backward. | ||
| 83 | std::ptrdiff_t walk_back = 0; | ||
| 84 | /// RipRelative: byte offset from the match to the signed 4-byte displacement field. | ||
| 85 | std::ptrdiff_t displacement_at = 0; | ||
| 86 | /// RipRelative: total length of the referencing instruction (the next-IP base for the displacement). | ||
| 87 | std::size_t instruction_length = 0; | ||
| 88 | /// RttiVtable: the MSVC mangled type name, for example ".?AVCameraManager@@". | ||
| 89 | std::string mangled; | ||
| 90 | /// StringXref: same as @ref SignatureRecord::xref_text. | ||
| 91 | std::string string_text; | ||
| 92 | /// StringXref: same as @ref SignatureRecord::xref_encoding. | ||
| 93 | scan::StringEncoding string_encoding = scan::StringEncoding::Utf8; | ||
| 94 | /// StringXref: same as @ref SignatureRecord::xref_return. | ||
| 95 | scan::XrefReturn string_return = scan::XrefReturn::ReferencingInstruction; | ||
| 96 | /// StringXref: same as @ref SignatureRecord::xref_require_terminator. | ||
| 97 | bool string_require_terminator = true; | ||
| 98 | /// StringXref: same as @ref SignatureRecord::xref_broad_match. | ||
| 99 | bool string_broad_match = false; | ||
| 100 | }; | ||
| 101 | |||
| 102 | /** | ||
| 103 | * @brief An owning, serializable @ref anchor::Anchor plus @ref Binding. Resolution ignores the fields of other | ||
| 104 | * kinds. | ||
| 105 | */ | ||
| 106 | struct SignatureRecord | ||
| 107 | { | ||
| 108 | /// Stable merge and lookup key, for example "player.health", echoed into the drift report and gate result. | ||
| 109 | std::string label; | ||
| 110 | /// Which anchor backend resolves this signature (one of the six serializable kinds). | ||
| 111 | anchor::AnchorKind kind = anchor::AnchorKind::RipGlobal; | ||
| 112 | /** @brief The module basename for @ref Region::module_named. Empty resolves within the fallback scope. */ | ||
| 113 | std::string module; | ||
| 114 | |||
| 115 | /// RipGlobal / CodeOperand: the candidate ladder that resolves to the address or the instruction site. | ||
| 116 | std::vector<CandidateSpec> ladder; | ||
| 117 | |||
| 118 | /// VtableIdentity: the MSVC mangled type name to resolve through the reverse-RTTI walk. | ||
| 119 | std::string mangled; | ||
| 120 | |||
| 121 | /// CodeOperand: whether to read an immediate or a memory-operand displacement. | ||
| 122 | scan::OperandKind operand_kind = scan::OperandKind::Immediate; | ||
| 123 | /// CodeOperand: index into the instruction's visible operands. | ||
| 124 | std::uint8_t operand_index = 0; | ||
| 125 | /// CodeOperand: 0 keeps the decoded value, and 1 through 8 narrows non-RIP low bytes and sign-extends. | ||
| 126 | std::uint8_t byte_width = 0; | ||
| 127 | |||
| 128 | /// StringXref: the exact literal content to anchor on (no quotes). | ||
| 129 | std::string xref_text; | ||
| 130 | /** | ||
| 131 | * @brief StringXref: the literal's byte encoding. Utf16le, for a wchar_t literal, needs well-formed UTF-8 | ||
| 132 | * that resolution converts to UTF-16LE. @ref parse, @ref Signature::compile, @ref Signature::adopt, | ||
| 133 | * and @ref serialize_checked reject malformed text. Utf8 evidence stays byte-transparent. | ||
| 134 | */ | ||
| 135 | scan::StringEncoding xref_encoding = scan::StringEncoding::Utf8; | ||
| 136 | /// StringXref: whether to return the referencing instruction, its enclosing function, or the pointer slot. | ||
| 137 | scan::XrefReturn xref_return = scan::XrefReturn::ReferencingInstruction; | ||
| 138 | /// StringXref: match a trailing NUL so a prefix of a longer literal is not matched. | ||
| 139 | bool xref_require_terminator = true; | ||
| 140 | /// StringXref: keep the lea/mov shape scan and add the broad Zydis sweep for rarer reference shapes. | ||
| 141 | bool xref_broad_match = false; | ||
| 142 | |||
| 143 | /// Manual: the pinned literal value, taken as-is. | ||
| 144 | std::int64_t manual_value = 0; | ||
| 145 | |||
| 146 | /** @brief In-memory only, never in a file: a post-resolve validator, as @ref anchor::Anchor::validator. */ | ||
| 147 | anchor::AnchorValidator validator = nullptr; | ||
| 148 | /** @brief In-memory only: the @ref validator context. The borrowed pointee must outlive each resolve. */ | ||
| 149 | const void *validator_context = nullptr; | ||
| 150 | /** @brief In-memory only: run @ref validator on a Manual anchor too, so the pinned literal is checked. */ | ||
| 151 | bool validate_manual = false; | ||
| 152 | /// In-memory only: fail closed when a backend-resolvable anchor carries no @ref validator. | ||
| 153 | bool require_validator = false; | ||
| 154 | |||
| 155 | /// How the consumer interprets the resolved value. | ||
| 156 | Binding binding{}; | ||
| 157 | |||
| 158 | /** @brief The @ref Signature::current_fingerprint baseline captured when authored, or 0 if none exists. */ | ||
| 159 | std::uint64_t expected_fingerprint = 0; | ||
| 160 | |||
| 161 | /** @brief RipGlobal: page class of byte-tier rungs. If all rungs anchor on instructions, use Executable. */ | ||
| 162 | scan::Pages pages = scan::Pages::Readable; | ||
| 163 | |||
| 164 | /** @brief ExportName: the exact, case-sensitive, undecorated export in @ref module, such as "Sleep". */ | ||
| 165 | std::string export_name; | ||
| 166 | |||
| 167 | /** @brief The optional live-image baseline (file key `image_identity`) that the identity gates compare. */ | ||
| 168 | scan::ImageIdentity expected_image_identity{}; | ||
| 169 | |||
| 170 | /** @brief The optional winning-span content baseline, file key `winning_bytes` in lowercase hex. */ | ||
| 171 | scan::WinningEvidence expected_winning_bytes{}; | ||
| 172 | }; | ||
| 173 | |||
| 174 | /** @brief The drift verdict of one signature. */ | ||
| 175 | enum class FingerprintState : std::uint8_t | ||
| 176 | { | ||
| 177 | /// No baseline exists (@ref SignatureRecord::expected_fingerprint is 0), so drift is unknown. | ||
| 178 | Unset, | ||
| 179 | /// The live fingerprint equals the baseline. | ||
| 180 | Match, | ||
| 181 | /// The live fingerprint differs from the baseline. | ||
| 182 | Drifted | ||
| 183 | }; | ||
| 184 | |||
| 185 | /** @brief A compiled signature that owns its evidence. Build one with @ref compile or @ref adopt. */ | ||
| 186 | class Signature | ||
| 187 | { | ||
| 188 | public: | ||
| 189 | /** | ||
| 190 | * @brief Compiles a file record into a resolvable signature. | ||
| 191 | * @return The Signature, or one of these errors: | ||
| 192 | * - BadPattern: a rung AOB that does not compile. | ||
| 193 | * - EmptyCandidates: a RipGlobal or CodeOperand record with no ladder. | ||
| 194 | * - InvalidArg: a Quorum, CallArgHome, or Unset kind, or empty required evidence of the record or | ||
| 195 | * of an RttiVtable or StringXref rung. It also means a ladder on a kind other than RipGlobal or | ||
| 196 | * CodeOperand, or an out-of-range persisted policy field such as CodeOperand byte_width. It also | ||
| 197 | * means a label or string field that cannot round-trip, or a nonzero image baseline with a zero | ||
| 198 | * size_of_image. It also means a truncated or over-long content baseline, Utf16le evidence that | ||
| 199 | * breaks the @ref SignatureRecord::xref_encoding rule, or a @ref Binding that breaks a field | ||
| 200 | * rule. It also means a RipRelative rung that @ref scan::Candidate::rip_relative rejects. | ||
| 201 | * @details A record that meets several conditions returns the code of the first failed check. | ||
| 202 | * @note Setup/control-plane only. | ||
| 203 | */ | ||
| 204 | [[nodiscard]] static Result<Signature> compile(SignatureRecord record); | ||
| 205 | |||
| 206 | /** | ||
| 207 | * @brief Adopts an in-code @ref anchor::Anchor and copies its borrowed views, so it can outlive @p source. | ||
| 208 | * @return The Signature, or InvalidArg for a @ref compile InvalidArg condition outside the ladder, or a | ||
| 209 | * RipGlobal or CodeOperand anchor without candidates. It checks each candidate only against the | ||
| 210 | * @ref SignatureRecord::xref_encoding rule. | ||
| 211 | * @details For a RipGlobal or CodeOperand kind, the record holds each candidate as a rung. A byte rung | ||
| 212 | * holds its pattern in canonical AOB text, and a text rung holds its mangled name or string. A | ||
| 213 | * @ref compile of that record rebuilds the same candidates and fingerprint, which | ||
| 214 | * `ManifestAdoptTest.RenderedLadderCompilesToTheSameCandidates` proves. A candidate that | ||
| 215 | * @ref compile rejects makes @ref serialize_checked reject the record. | ||
| 216 | * @note Setup/control-plane only. | ||
| 217 | */ | ||
| 218 | [[nodiscard]] static Result<Signature> adopt(const anchor::Anchor &source); | ||
| 219 | |||
| 220 | /** | ||
| 221 | * @brief Resolves this signature through its anchor backend, fail-closed. See @ref SignatureRecord::module. | ||
| 222 | * @note Setup/control-plane only (see @ref anchor::resolve). | ||
| 223 | */ | ||
| 224 | [[nodiscard]] anchor::ResolvedAnchor resolve(Region fallback_scope = Region::host()) const; | ||
| 225 | |||
| 226 | /** | ||
| 227 | * @brief The scope: @ref Region::module_named for the record's module, or @ref Region::host for none. | ||
| 228 | * @note Setup/control-plane only: queries the loader. | ||
| 229 | */ | ||
| 230 | [[nodiscard]] Region scope() const noexcept; | ||
| 231 | |||
| 232 | /** | ||
| 233 | * @brief The live fingerprint: a hash of @ref anchor::anchor_fingerprint, @ref Binding, label, and module. | ||
| 234 | * It reads no game memory, so it is stable across runs and rebuilds on one platform. | ||
| 235 | */ | ||
| 236 | [[nodiscard]] std::uint64_t current_fingerprint() const noexcept; | ||
| 237 | |||
| 238 | /** @brief Compares @ref current_fingerprint to the record baseline. See @ref FingerprintState. */ | ||
| 239 | [[nodiscard]] FingerprintState fingerprint_state() const noexcept; | ||
| 240 | |||
| 241 | /** | ||
| 242 | * @brief Makes the live fingerprint the baseline after a verified repair. Persist @ref record to keep it. | ||
| 243 | * @note Setup/control-plane only. | ||
| 244 | */ | ||
| 245 | void recapture_fingerprint() noexcept; | ||
| 246 | |||
| 247 | /** | ||
| 248 | * @brief Re-resolves and recaptures the fingerprint, image identity, and winning-span content baselines. | ||
| 249 | * @param fallback_scope The scope for a record that names no module. It must match the later gate scope. | ||
| 250 | * @return Success, NoMatch if the signature does not resolve, or UnexpectedShape if the resolved rung | ||
| 251 | * witnesses no image or content span. Only a RipGlobal hit from a Direct or RipRelative rung can | ||
| 252 | * witness both, with at most @ref scan::MAX_MUTATION_WITNESS_BYTES of content. | ||
| 253 | * @details Only this call captures live image and content baselines for @ref GatePolicy::mutation_strict. A | ||
| 254 | * failure keeps all three previous baselines. Persist @ref record to keep the new ones. | ||
| 255 | * @note Setup/control-plane only. | ||
| 256 | */ | ||
| 257 | [[nodiscard]] Result<void> recapture(Region fallback_scope = Region::host()); | ||
| 258 | |||
| 259 | /// The signature's stable key. | ||
| 260 | [[nodiscard]] std::string_view label() const noexcept; | ||
| 261 | /// Which anchor backend resolves this signature. | ||
| 262 | [[nodiscard]] anchor::AnchorKind kind() const noexcept; | ||
| 263 | /// The consumer-facing binding (register / offsets / vtable slot). | ||
| 264 | [[nodiscard]] const Binding &binding() const noexcept; | ||
| 265 | /// The owning record, for @ref serialize_checked after a recapture. | ||
| 266 | [[nodiscard]] const SignatureRecord &record() const noexcept; | ||
| 267 | |||
| 268 | private: | ||
| 269 | friend class detail::GateAccess; | ||
| 270 | |||
| 271 | // Only compile() and adopt() construct. | ||
| 272 | Signature(SignatureRecord record, std::vector<scan::Candidate> ladder) noexcept; | ||
| 273 | |||
| 274 | // A borrowed view of this object's storage. It is rebuilt per call and valid only for that call. | ||
| 275 | [[nodiscard]] anchor::Anchor make_anchor() const noexcept; | ||
| 276 | |||
| 277 | // Also returns the winning match span for the mutation gate. | ||
| 278 | [[nodiscard]] anchor::ResolvedAnchor resolve_for_gate(Region fallback_scope, Region &winning_span) const; | ||
| 279 | |||
| 280 | SignatureRecord m_record; | ||
| 281 | std::vector<scan::Candidate> m_ladder; | ||
| 282 | }; | ||
| 283 | |||
| 284 | /// The INI format version that this build reads and writes, bumped only for an incompatible format change. | ||
| 285 | inline constexpr std::uint32_t SCHEMA_VERSION = 1; | ||
| 286 | |||
| 287 | /** @brief The `[manifest]` metadata. Only an in-code change that breaks older manifests bumps @ref revision. */ | ||
| 288 | struct ManifestHeader | ||
| 289 | { | ||
| 290 | /// The format version that the file declares. | ||
| 291 | std::uint32_t schema = SCHEMA_VERSION; | ||
| 292 | /// The author's signature-contract epoch, or 0 if unversioned, compared only by @ref revision_compatible. | ||
| 293 | std::uint32_t revision = 0; | ||
| 294 | }; | ||
| 295 | |||
| 296 | /** @brief A parsed manifest: its @ref ManifestHeader plus the signature records in file order. */ | ||
| 297 | struct Manifest | ||
| 298 | { | ||
| 299 | /// The `[manifest]` metadata (schema and contract revision). | ||
| 300 | ManifestHeader header{}; | ||
| 301 | /// The signatures, one per `[sig.<label>]` section, in file order. | ||
| 302 | std::vector<SignatureRecord> records{}; | ||
| 303 | }; | ||
| 304 | |||
| 305 | /** @brief Parse and persistence caps. A violation returns SizeTooLarge and publishes no partial result. */ | ||
| 306 | struct ManifestLimits | ||
| 307 | { | ||
| 308 | /// Largest accepted encoded text size in bytes. | ||
| 309 | std::size_t max_file_bytes{1u << 20}; | ||
| 310 | /// Largest accepted INI section count, by default sized to the default record and rung caps: 1 + 512 * 33. | ||
| 311 | std::size_t max_sections{16897}; | ||
| 312 | /// Largest accepted number of keys within any one section. | ||
| 313 | std::size_t max_keys_per_section{64}; | ||
| 314 | /// Largest accepted number of `[sig.<label>]` records. | ||
| 315 | std::size_t max_records{512}; | ||
| 316 | /// Largest accepted number of candidate-ladder rungs on any one record. | ||
| 317 | std::size_t max_rungs_per_record{32}; | ||
| 318 | /// Largest accepted size in bytes of any single string field or heredoc value. | ||
| 319 | std::size_t max_field_bytes{64u << 10}; | ||
| 320 | /// Largest accepted sum of all decoded value bytes across the manifest. | ||
| 321 | std::size_t max_total_decoded_bytes{4u << 20}; | ||
| 322 | |||
| 323 | /// Returns limits equal to a default-constructed @ref ManifestLimits. | ||
| 324 | 512 | [[nodiscard]] static constexpr ManifestLimits conservative() noexcept { return ManifestLimits{}; } | |
| 325 | |||
| 326 | /** @brief Uncapped limits for a trusted authoring tool only. Grammar and semantic checks stay on. */ | ||
| 327 | [[nodiscard]] static constexpr ManifestLimits advanced() noexcept | ||
| 328 | { | ||
| 329 | // Parenthesized because public headers must compile with <windows.h>'s function-like max macro active. | ||
| 330 | constexpr std::size_t MAX_VALUE = (std::numeric_limits<std::size_t>::max)(); | ||
| 331 | return ManifestLimits{ | ||
| 332 | .max_file_bytes = MAX_VALUE, | ||
| 333 | .max_sections = MAX_VALUE, | ||
| 334 | .max_keys_per_section = MAX_VALUE, | ||
| 335 | .max_records = MAX_VALUE, | ||
| 336 | .max_rungs_per_record = MAX_VALUE, | ||
| 337 | .max_field_bytes = MAX_VALUE, | ||
| 338 | .max_total_decoded_bytes = MAX_VALUE, | ||
| 339 | }; | ||
| 340 | } | ||
| 341 | }; | ||
| 342 | |||
| 343 | /** | ||
| 344 | * @brief Returns true if @p build_revision is 0, which opts out, or equals @ref ManifestHeader::revision. If it | ||
| 345 | * returns false, pass no file records to @ref overlay. | ||
| 346 | */ | ||
| 347 | [[nodiscard]] bool revision_compatible(const ManifestHeader &header, std::uint32_t build_revision) noexcept; | ||
| 348 | |||
| 349 | /** | ||
| 350 | * @brief Parses manifest INI text. A failure rejects the whole manifest. | ||
| 351 | * @return The @ref Manifest, OutOfMemory, or one of these errors: | ||
| 352 | * - MissingHeader: no `[manifest]` section, no `schema` key, or an unsupported schema. | ||
| 353 | * - MalformedLine: an unparsable line, field, or enum token, a noncomment key line without `=`, or an | ||
| 354 | * empty key. It also covers a non-canonical section or key spelling, a section that is neither | ||
| 355 | * `[manifest]` nor `sig.`-prefixed, and a key line before the first header. It also covers a key that | ||
| 356 | * its record's kind, binding kind, or rung mode does not read. It also covers a rung under a kind | ||
| 357 | * other than RipGlobal or CodeOperand, and Utf16le evidence that breaks the | ||
| 358 | * @ref SignatureRecord::xref_encoding rule. | ||
| 359 | * - ManifestIdentityCollision: a case-, whitespace-, or exactly-duplicated section, or a | ||
| 360 | * whitespace-variant or exactly-duplicated key. A miscased key returns MalformedLine instead. | ||
| 361 | * - ManifestFramingUnsafe: an unterminated `<<<` heredoc value, an opener with an empty tag, or a | ||
| 362 | * heredoc whose first body line is its terminator. | ||
| 363 | * - SizeTooLarge: encoded text, a section, key, field, record, rung, or aggregate over @p limits. | ||
| 364 | * @details A missing optional key takes its default, so an absent `revision` is 0. A present key must parse, so | ||
| 365 | * a blank enum, numeric, boolean, or baseline value is MalformedLine. A blank `schema` is | ||
| 366 | * MissingHeader, and a blank `offsets` is an empty list. A blank string value reads as empty. | ||
| 367 | * @note Setup/control-plane only. | ||
| 368 | */ | ||
| 369 | [[nodiscard]] Result<Manifest> | ||
| 370 | parse(std::string_view text, const ManifestLimits &limits = ManifestLimits::conservative()); | ||
| 371 | |||
| 372 | /** | ||
| 373 | * @brief Serializes a manifest to INI text that @ref parse reads back and @ref Signature::compile accepts. | ||
| 374 | * @return The text, OutOfMemory, or one of these errors: | ||
| 375 | * - InvalidArg, EmptyCandidates, or BadPattern: a record that @ref Signature::compile rejects, with the | ||
| 376 | * code that compile returns. | ||
| 377 | * - ManifestIdentityCollision: two record labels that fold to one section, or a label that folds into | ||
| 378 | * another record's rung section. | ||
| 379 | * - SizeTooLarge: encoded text, a record, rung, field, or aggregate that exceeds @p limits. | ||
| 380 | * @details A manifest that fails several checks returns the code of the first failed check. It writes | ||
| 381 | * `revision` only if non-zero, and `schema` is always @ref SCHEMA_VERSION. | ||
| 382 | * @note Setup/control-plane only. | ||
| 383 | */ | ||
| 384 | [[nodiscard]] Result<std::string> | ||
| 385 | serialize_checked(const Manifest &manifest, const ManifestLimits &limits = ManifestLimits::conservative()); | ||
| 386 | |||
| 387 | /** | ||
| 388 | * @brief Reads and parses a manifest file, whole or not at all. | ||
| 389 | * @return The @ref Manifest, a @ref parse error for corrupt contents, OutOfMemory, or one of these errors: | ||
| 390 | * - SizeTooLarge: the file exceeds @ref ManifestLimits::max_file_bytes at the size query, or the bytes | ||
| 391 | * already read overrun that cap. | ||
| 392 | * - FileOpenFailed: the file is missing, locked, denied, or not a regular disk file. Any other length | ||
| 393 | * change after the size query is also FileOpenFailed, even growth past the cap in a later read chunk. | ||
| 394 | * @details A failed load is retryable. A caller can treat a missing file as no overrides. | ||
| 395 | * @note Setup/control-plane only. | ||
| 396 | */ | ||
| 397 | [[nodiscard]] Result<Manifest> | ||
| 398 | load(const std::filesystem::path &path, const ManifestLimits &limits = ManifestLimits::conservative()); | ||
| 399 | |||
| 400 | /** | ||
| 401 | * @brief Writes @ref serialize_checked output to a file. | ||
| 402 | * @return Success, a @ref serialize_checked error, SizeTooLarge above the platform single-write bound, | ||
| 403 | * FileOpenFailed, FileWriteFailed if the write or flush fails, or OutOfMemory in the write phase. | ||
| 404 | * @details A @ref serialize_checked error or SizeTooLarge returns before the file opens and leaves it | ||
| 405 | * unchanged. The write then truncates @p path in place and is not atomic across a crash. A tear inside | ||
| 406 | * a heredoc, a section header, or a key line before its `=` fails the next @ref load closed. Any other | ||
| 407 | * tear can parse as a valid manifest with fewer keys, rungs, or records, or with a truncated value. | ||
| 408 | * For crash durability, atomically replace the target with a flushed temporary file. | ||
| 409 | * @note Setup/control-plane only. | ||
| 410 | */ | ||
| 411 | [[nodiscard]] Result<void> save( | ||
| 412 | const std::filesystem::path &path, | ||
| 413 | const Manifest &manifest, | ||
| 414 | const ManifestLimits &limits = ManifestLimits::conservative() | ||
| 415 | ); | ||
| 416 | |||
| 417 | /** | ||
| 418 | * @brief Merges in-code anchor defaults with optional file overrides by label. It copies each borrowed view. | ||
| 419 | * @return The merged signatures in @p defaults order. A per-signature problem never fails the call. | ||
| 420 | * @details The merge follows these rules: | ||
| 421 | * - Without a same-label override, @ref Signature::adopt adopts the default. | ||
| 422 | * - With one, @ref Signature::compile replaces it with the complete file record. The record inherits | ||
| 423 | * every in-memory-only field of the default. | ||
| 424 | * - An override that fails to compile, changes the declared @ref anchor::ResultDomain, or crosses | ||
| 425 | * between Manual and a backend kind falls back to the default. | ||
| 426 | * - An override whose label matches no default, or whose default is non-serializable, is ignored. | ||
| 427 | * - Without an accepted override, a default that @ref Signature::adopt rejects is left out. Gate a | ||
| 428 | * Quorum or CallArgHome default through @ref anchor::evaluate_gate. | ||
| 429 | * @note Setup/control-plane only. Only allocation failure throws. | ||
| 430 | */ | ||
| 431 | [[nodiscard]] Result<std::vector<Signature>> | ||
| 432 | overlay(std::span<const anchor::Anchor> defaults, std::span<const SignatureRecord> overrides); | ||
| 433 | |||
| 434 | /** @brief Thresholds for @ref resolve_and_gate. */ | ||
| 435 | struct GatePolicy | ||
| 436 | { | ||
| 437 | /// If true, safe-disable an entry whose fingerprint is @ref FingerprintState::Drifted. | ||
| 438 | bool reject_on_fingerprint_drift = true; | ||
| 439 | /// If true, also safe-disable an entry with no baseline (@ref FingerprintState::Unset). | ||
| 440 | bool reject_unset_fingerprint = false; | ||
| 441 | /** @brief Health floor in [0, 1]. If the trusted fraction falls below it, every entry is safe-disabled. */ | ||
| 442 | double min_resolved_fraction = 0.0; | ||
| 443 | /** | ||
| 444 | * @brief If true, safe-disable an entry that is not mutation-capable: a Manual pin or a domain mismatch. | ||
| 445 | * @details MidHookRegister needs a code site, and VmtMethod needs a vtable. Address and PointerChain need a | ||
| 446 | * CodeSite or DataAddress, never a vtable or Scalar. | ||
| 447 | */ | ||
| 448 | bool require_mutation_safe_binding = false; | ||
| 449 | /** @brief If true, a captured image baseline of a mutation-capable entry must match the live image. */ | ||
| 450 | bool require_live_image_identity = false; | ||
| 451 | /** @brief If true, safe-disable a mutation-capable entry that has no captured image baseline. */ | ||
| 452 | bool require_captured_image_identity = false; | ||
| 453 | /** | ||
| 454 | * @brief If true, a mutation-capable entry needs a winning-span content baseline that still matches. | ||
| 455 | * @details The baseline must equal the scan witness and a guarded reread of the match span directly before | ||
| 456 | * publication. Unlike the image identity gates, this check sees an in-place patch of the match | ||
| 457 | * span under equal PE headers. For write-time certainty, use a checked mutation or install. | ||
| 458 | */ | ||
| 459 | bool require_winning_evidence_baseline = false; | ||
| 460 | /** @brief If true, safe-disable a mutation-capable entry unless the gate checked a contract revision. */ | ||
| 461 | bool require_contract_revision = false; | ||
| 462 | |||
| 463 | /** @brief Rejects drift and an unset baseline. One untrusted entry safe-disables the whole manifest. */ | ||
| 464 | 10 | [[nodiscard]] static constexpr GatePolicy strict() noexcept | |
| 465 | { | ||
| 466 | return GatePolicy{ | ||
| 467 | .reject_on_fingerprint_drift = true, | ||
| 468 | .reject_unset_fingerprint = true, | ||
| 469 | .min_resolved_fraction = 1.0, | ||
| 470 | 10 | }; | |
| 471 | } | ||
| 472 | |||
| 473 | /** | ||
| 474 | * @brief @ref strict plus every mutation requirement, for a manifest that authorizes a write. It passes no | ||
| 475 | * mutation-capable entry unless the @ref ManifestHeader overload gets a non-zero build revision. | ||
| 476 | */ | ||
| 477 | 10 | [[nodiscard]] static constexpr GatePolicy mutation_strict() noexcept | |
| 478 | { | ||
| 479 | 10 | GatePolicy policy = strict(); | |
| 480 | 10 | policy.require_mutation_safe_binding = true; | |
| 481 | 10 | policy.require_live_image_identity = true; | |
| 482 | 10 | policy.require_captured_image_identity = true; | |
| 483 | 10 | policy.require_winning_evidence_baseline = true; | |
| 484 | 10 | policy.require_contract_revision = true; | |
| 485 | 10 | return policy; | |
| 486 | } | ||
| 487 | }; | ||
| 488 | |||
| 489 | /** @brief A trusted signature. Its label and binding borrow from a @ref Signature that must outlive it. */ | ||
| 490 | struct GatedSignature | ||
| 491 | { | ||
| 492 | /// The signature's key (a view into the source Signature). | ||
| 493 | std::string_view label; | ||
| 494 | /// Which anchor backend resolved it. | ||
| 495 | anchor::AnchorKind kind = anchor::AnchorKind::Manual; | ||
| 496 | /** @brief The resolved value as an address. Interpret it through @ref binding. */ | ||
| 497 | Address address; | ||
| 498 | /// The consumer-facing binding (a pointer into the source Signature). | ||
| 499 | const Binding *binding = nullptr; | ||
| 500 | }; | ||
| 501 | |||
| 502 | /** @brief The gate that safe-disabled a signature, so a log can tell a locate failure from a refused write. */ | ||
| 503 | enum class GateReason : std::uint8_t | ||
| 504 | { | ||
| 505 | /// Not rejected. | ||
| 506 | None, | ||
| 507 | /// @ref Signature::resolve did not return a unique @ref anchor::AnchorStatus::Resolved. | ||
| 508 | Unresolved, | ||
| 509 | /// The declared definition changed after its baseline capture. | ||
| 510 | FingerprintDrifted, | ||
| 511 | /// No fingerprint baseline was captured and the policy requires one. | ||
| 512 | FingerprintUnset, | ||
| 513 | /// The binding cannot safely mutate the resolved typed domain, or it is a Manual pin. | ||
| 514 | BindingCannotMutate, | ||
| 515 | /// No contract revision was checked, or the checked revision is incompatible. | ||
| 516 | ContractRevision, | ||
| 517 | /// The captured image baseline is absent, or it no longer matches the live image. | ||
| 518 | ImageIdentity, | ||
| 519 | /// The winning-span content baseline is absent, unwitnessed, over-long, unreadable, or no longer matches. | ||
| 520 | WinningEvidence, | ||
| 521 | /// The whole-manifest trusted fraction fell below @ref GatePolicy::min_resolved_fraction. | ||
| 522 | HealthFloor, | ||
| 523 | }; | ||
| 524 | |||
| 525 | /** @brief One safe-disabled signature and why it was not trusted. */ | ||
| 526 | struct RejectedSignature | ||
| 527 | { | ||
| 528 | /// The signature's key (a view into the source Signature). | ||
| 529 | std::string_view label; | ||
| 530 | /** @brief The resolve outcome. A status other than Resolved explains a locate failure. */ | ||
| 531 | anchor::AnchorStatus status = anchor::AnchorStatus::Unresolved; | ||
| 532 | /// The drift verdict (see @ref FingerprintState). | ||
| 533 | FingerprintState fingerprint = FingerprintState::Unset; | ||
| 534 | /// The specific gate that rejected this entry. | ||
| 535 | GateReason reason = GateReason::None; | ||
| 536 | }; | ||
| 537 | |||
| 538 | /** @brief A gated manifest: its trusted and safe-disabled signatures, plus the quality summary. */ | ||
| 539 | struct GateResult | ||
| 540 | { | ||
| 541 | /// Signatures that no gate rejected. | ||
| 542 | std::vector<GatedSignature> trusted; | ||
| 543 | /// Safe-disabled signatures, each with the @ref GateReason that rejected it. | ||
| 544 | std::vector<RejectedSignature> rejected; | ||
| 545 | /// The quality summary of the whole manifest, from @ref anchor::assess_quality. | ||
| 546 | anchor::AnchorQuality quality; | ||
| 547 | |||
| 548 | /** @brief Returns the trusted entry for @p label, or nullptr if that label was rejected or is absent. */ | ||
| 549 | [[nodiscard]] const GatedSignature *find(std::string_view label) const noexcept; | ||
| 550 | }; | ||
| 551 | |||
| 552 | /** | ||
| 553 | * @brief Resolves each signature and partitions the set into trusted and safe-disabled entries. | ||
| 554 | * @param signatures The result borrows the labels and bindings of these signatures. | ||
| 555 | * @param scope The scope for a signature that names no module. | ||
| 556 | * @note Setup/control-plane only. | ||
| 557 | */ | ||
| 558 | [[nodiscard]] GateResult resolve_and_gate( | ||
| 559 | std::span<const Signature> signatures, | ||
| 560 | const GatePolicy &policy = {}, | ||
| 561 | Region scope = Region::host() | ||
| 562 | ); | ||
| 563 | |||
| 564 | /** | ||
| 565 | * @brief Like @ref resolve_and_gate, and rejects a mutation-capable entry that fails @ref revision_compatible. | ||
| 566 | * @param build_revision The build's contract revision. 0 skips the check, as the plain overload does. | ||
| 567 | * @note Setup/control-plane only. | ||
| 568 | */ | ||
| 569 | [[nodiscard]] GateResult resolve_and_gate( | ||
| 570 | std::span<const Signature> signatures, | ||
| 571 | const ManifestHeader &header, | ||
| 572 | std::uint32_t build_revision, | ||
| 573 | const GatePolicy &policy = {}, | ||
| 574 | Region scope = Region::host() | ||
| 575 | ); | ||
| 576 | |||
| 577 | /** @brief Returns the static file token that names @p kind. */ | ||
| 578 | [[nodiscard]] std::string_view binding_kind_to_string(BindingKind kind) noexcept; | ||
| 579 | |||
| 580 | /** @brief Returns a static name for @p state. */ | ||
| 581 | [[nodiscard]] std::string_view fingerprint_state_to_string(FingerprintState state) noexcept; | ||
| 582 | |||
| 583 | /** @brief Returns a static name for @p reason. */ | ||
| 584 | [[nodiscard]] std::string_view gate_reason_to_string(GateReason reason) noexcept; | ||
| 585 | } // namespace manifest | ||
| 586 | } // namespace DetourModKit | ||
| 587 | |||
| 588 | #endif // DETOURMODKIT_MANIFEST_HPP | ||
| 589 |