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: the resolved patch-fragile contract as editable data, so a broken mod is a text edit. | ||
| 7 | * @details SignatureRecord owns serializable anchor evidence and its consumer binding. Signature compiles candidate | ||
| 8 | * ladders into owned storage. @ref resolve_and_gate resolves those contracts, checks their fingerprints and | ||
| 9 | * quality, and safe-disables unresolved or drifted entries before a wrong register or offset can be consumed. | ||
| 10 | * @note The file format is a separate INI parsed by the already-linked simpleini, never the settings INI. The parser | ||
| 11 | * and emitter live entirely in the implementation; this header names no INI type. | ||
| 12 | * @warning `[B-100]` Never parse, compile, resolve, gate, or derive a scope under the loader lock. Pure value | ||
| 13 | * accessors on a compiled @ref Signature do not allocate or query the loader. | ||
| 14 | */ | ||
| 15 | |||
| 16 | #include "DetourModKit/anchor.hpp" | ||
| 17 | #include "DetourModKit/error.hpp" | ||
| 18 | #include "DetourModKit/region.hpp" | ||
| 19 | #include "DetourModKit/scan.hpp" | ||
| 20 | |||
| 21 | #include <cstddef> | ||
| 22 | #include <cstdint> | ||
| 23 | #include <filesystem> | ||
| 24 | #include <limits> | ||
| 25 | #include <span> | ||
| 26 | #include <string> | ||
| 27 | #include <string_view> | ||
| 28 | #include <vector> | ||
| 29 | |||
| 30 | namespace DetourModKit | ||
| 31 | { | ||
| 32 | // Forward-declare the one hook:: type a Binding names: a scoped enum with a fixed underlying type is complete | ||
| 33 | // enough to declare a member, so a consumer reading only Address / PointerChain bindings need not pull the whole | ||
| 34 | // hooking surface into every manifest TU. The full definition is reached only where the register is used. | ||
| 35 | namespace hook | ||
| 36 | { | ||
| 37 | enum class Gpr : std::uint8_t; | ||
| 38 | } | ||
| 39 | |||
| 40 | namespace manifest | ||
| 41 | { | ||
| 42 | namespace detail | ||
| 43 | { | ||
| 44 | class GateAccess; | ||
| 45 | } // namespace detail | ||
| 46 | |||
| 47 | /** | ||
| 48 | * @enum BindingKind | ||
| 49 | * @brief How a consumer interprets what a signature located. This is the register / offset / vtable repair | ||
| 50 | * surface. | ||
| 51 | * @details The @ref anchor backends locate the address and the binding says what to read there. The binding | ||
| 52 | * itself is inert. This module resolves the address and returns the binding, and the consumer | ||
| 53 | * performs the register read (@ref hook::gpr), the pointer-chain walk (@ref memory::walk with | ||
| 54 | * @ref memory::read), or the virtual-method hook (@ref hook::VmtHook::hook_method). | ||
| 55 | */ | ||
| 56 | enum class BindingKind : std::uint8_t | ||
| 57 | { | ||
| 58 | /// The resolved value IS the address the mod wants (an inline-hook target or a resolved global). | ||
| 59 | Address, | ||
| 60 | /// The resolved value is a chain base: walk @ref Binding::offsets, then read value_width bytes. | ||
| 61 | PointerChain, | ||
| 62 | /// The resolved value is a mid-hook site; the callback reads @ref Binding::read_register via hook::gpr. | ||
| 63 | MidHookRegister, | ||
| 64 | /// The resolved value is a vtable base; hook the virtual slot at @ref Binding::vmt_index. | ||
| 65 | VmtMethod | ||
| 66 | }; | ||
| 67 | |||
| 68 | /// Sentinel for @ref Binding::xmm_index: no XMM register bound (the site reads a GPR, not a float slot). | ||
| 69 | inline constexpr std::uint8_t XMM_INDEX_UNUSED = 0xFF; | ||
| 70 | |||
| 71 | /** | ||
| 72 | * @struct Binding | ||
| 73 | * @brief The consumer-facing interpretation of a resolved signature: which register, which offset chain, which | ||
| 74 | * slot. | ||
| 75 | * @details Only the fields the active @ref kind uses are meaningful. The rest keep their defaults, which is | ||
| 76 | * the designated-initializer discipline @ref anchor::Anchor follows. | ||
| 77 | */ | ||
| 78 | struct Binding | ||
| 79 | { | ||
| 80 | /// How to interpret the resolved value. | ||
| 81 | BindingKind kind = BindingKind::Address; | ||
| 82 | /// PointerChain: byte offsets walked left to right from the resolved base (@ref memory::walk semantics). | ||
| 83 | std::vector<std::ptrdiff_t> offsets; | ||
| 84 | /// PointerChain: byte width of the leaf read at the end of the walk (e.g. 4 for a float, 8 for a pointer). | ||
| 85 | std::uint8_t value_width = 8; | ||
| 86 | /// MidHookRegister: the register the mid-hook callback reads (edit after a rcx -> rax drift). | ||
| 87 | hook::Gpr read_register{}; | ||
| 88 | /// MidHookRegister: an XMM lane for a float site, or @ref XMM_INDEX_UNUSED when the value lives in a GPR. | ||
| 89 | std::uint8_t xmm_index = XMM_INDEX_UNUSED; | ||
| 90 | /// VmtMethod: the zero-based virtual-table slot to hook; valid values are 0 through 4095. | ||
| 91 | std::size_t vmt_index = 0; | ||
| 92 | }; | ||
| 93 | |||
| 94 | /** | ||
| 95 | * @struct CandidateSpec | ||
| 96 | * @brief One candidate-ladder rung in owning, text-editable form; compiled into a @ref scan::Candidate at load. | ||
| 97 | * @details The serializable twin of a @ref scan::Candidate, which owns compiled Pattern bytes that no author | ||
| 98 | * can edit by hand. For a byte-tier rung the file carries the source AOB string and decode | ||
| 99 | * parameters, and @ref Signature::compile turns them back into a @ref scan::Candidate. Only the | ||
| 100 | * fields the active @ref mode uses are read. | ||
| 101 | */ | ||
| 102 | struct CandidateSpec | ||
| 103 | { | ||
| 104 | /// Human-readable rung name, carried into the winning @ref scan::Hit for diagnostics. | ||
| 105 | std::string name; | ||
| 106 | /// Which resolution strategy this rung uses. | ||
| 107 | scan::Mode mode = scan::Mode::Direct; | ||
| 108 | /// Direct / RipRelative: the AOB DSL string, e.g. "48 8B 05 ?? ?? ?? ??". | ||
| 109 | std::string pattern; | ||
| 110 | /// Direct: signed byte delta added to the match (negative walks backward); 0 returns the match itself. | ||
| 111 | std::ptrdiff_t walk_back = 0; | ||
| 112 | /// RipRelative: byte offset from the match to the signed 4-byte displacement field. | ||
| 113 | std::ptrdiff_t displacement_at = 0; | ||
| 114 | /// RipRelative: total length of the referencing instruction (the next-IP base for the displacement). | ||
| 115 | std::size_t instruction_length = 0; | ||
| 116 | /// RttiVtable: the MSVC mangled type name, e.g. ".?AVCameraManager@@". | ||
| 117 | std::string mangled; | ||
| 118 | /// StringXref: the exact literal content to anchor on (no quotes). | ||
| 119 | std::string string_text; | ||
| 120 | /// StringXref: how the literal is stored in the image. Utf16le follows the @ref SignatureRecord rule. | ||
| 121 | scan::StringEncoding string_encoding = scan::StringEncoding::Utf8; | ||
| 122 | /// StringXref: whether to return the referencing instruction, its enclosing function, or the pointer slot. | ||
| 123 | scan::XrefReturn string_return = scan::XrefReturn::ReferencingInstruction; | ||
| 124 | /// StringXref: match a trailing NUL so a prefix of a longer literal is not matched. | ||
| 125 | bool string_require_terminator = true; | ||
| 126 | /// StringXref: keep the lea/mov shape scan and add the broad Zydis sweep for rarer reference shapes. | ||
| 127 | bool string_broad_match = false; | ||
| 128 | }; | ||
| 129 | |||
| 130 | /** | ||
| 131 | * @struct SignatureRecord | ||
| 132 | * @brief An owning, serializable superset of @ref anchor::Anchor plus its @ref Binding: the unit an INI file | ||
| 133 | * round-trips. | ||
| 134 | * @details Where @ref anchor::Anchor is a static aggregate of non-owning views authored in code, a | ||
| 135 | * SignatureRecord owns every string and ladder rung so it survives being read from a file and stored. | ||
| 136 | * Only the fields the active @ref kind uses are meaningful (the RipGlobal / CodeOperand ladder, the | ||
| 137 | * VtableIdentity mangled name, the StringXref facets, the ExportName module + export name, or the | ||
| 138 | * Manual literal); the rest keep their defaults. The two composite anchor kinds | ||
| 139 | * @ref anchor::AnchorKind::Quorum and | ||
| 140 | * @ref anchor::AnchorKind::CallArgHome are deliberately not serializable here: a Quorum composes | ||
| 141 | * voting members by pointer and CallArgHome has no resolver, so both stay in-code constructs gated | ||
| 142 | * through @ref anchor::evaluate_gate rather than the file. | ||
| 143 | */ | ||
| 144 | struct SignatureRecord | ||
| 145 | { | ||
| 146 | /// Stable merge / lookup key, e.g. "player.health"; echoed into the drift report and the gate result. | ||
| 147 | std::string label; | ||
| 148 | /// Which anchor backend resolves this signature (one of the six serializable kinds). | ||
| 149 | anchor::AnchorKind kind = anchor::AnchorKind::RipGlobal; | ||
| 150 | /** | ||
| 151 | * @brief Empty resolves within the host image (or the fallback scope); otherwise a module basename scoped | ||
| 152 | * through @ref Region::module_named. For ExportName this names the module whose export table holds | ||
| 153 | * @ref export_name. | ||
| 154 | */ | ||
| 155 | std::string module; | ||
| 156 | |||
| 157 | /// RipGlobal / CodeOperand: the candidate ladder resolving to the address or the instruction site. | ||
| 158 | std::vector<CandidateSpec> ladder; | ||
| 159 | |||
| 160 | /// VtableIdentity: the MSVC mangled type name to resolve through the reverse-RTTI walk. | ||
| 161 | std::string mangled; | ||
| 162 | |||
| 163 | /// CodeOperand: whether to read an immediate or a memory-operand displacement. | ||
| 164 | scan::OperandKind operand_kind = scan::OperandKind::Immediate; | ||
| 165 | /// CodeOperand: index into the instruction's visible operands. | ||
| 166 | std::uint8_t operand_index = 0; | ||
| 167 | /// CodeOperand: 0 preserves the decoded value; 1 through 8 narrows non-RIP low bytes and sign-extends. | ||
| 168 | std::uint8_t byte_width = 0; | ||
| 169 | |||
| 170 | /// StringXref: the exact literal content to anchor on (no quotes). | ||
| 171 | std::string xref_text; | ||
| 172 | /** | ||
| 173 | * @brief StringXref: byte encoding of the literal in the image (Utf16le for wchar_t literals). | ||
| 174 | * @details Utf16le evidence must contain well-formed UTF-8 because resolution converts it to UTF-16LE. | ||
| 175 | * @ref parse, @ref Signature::compile, @ref Signature::adopt, and @ref serialize_checked reject | ||
| 176 | * malformed text. Utf8 evidence remains byte-transparent. | ||
| 177 | */ | ||
| 178 | scan::StringEncoding xref_encoding = scan::StringEncoding::Utf8; | ||
| 179 | /// StringXref: whether to return the referencing instruction, its enclosing function, or the pointer slot. | ||
| 180 | scan::XrefReturn xref_return = scan::XrefReturn::ReferencingInstruction; | ||
| 181 | /// StringXref: match a trailing NUL so a prefix of a longer literal is not matched. | ||
| 182 | bool xref_require_terminator = true; | ||
| 183 | /// StringXref: keep the lea/mov shape scan and add the broad Zydis sweep for rarer reference shapes. | ||
| 184 | bool xref_broad_match = false; | ||
| 185 | |||
| 186 | /// Manual: the pinned literal value, taken as-is. | ||
| 187 | std::int64_t manual_value = 0; | ||
| 188 | |||
| 189 | /** | ||
| 190 | * @brief Optional post-resolve validator threaded onto the compiled @ref anchor::Anchor, mirroring @ref | ||
| 191 | * anchor::Anchor::validator. In-memory only: a function pointer cannot round-trip through an INI | ||
| 192 | * file, so @ref parse never populates it and @ref serialize_checked never writes it. A consumer | ||
| 193 | * attaches it programmatically (after loading a manifest, or on a hand-built record) so a | ||
| 194 | * file-loaded or adopted signature can still assert a domain invariant instead of trusting the raw | ||
| 195 | * resolved address. | ||
| 196 | */ | ||
| 197 | anchor::AnchorValidator validator = nullptr; | ||
| 198 | /** | ||
| 199 | * @brief Opaque pointer forwarded verbatim to @ref validator. | ||
| 200 | * @details This field is in-memory only. | ||
| 201 | * The pointer is copied, but its pointee remains borrowed. | ||
| 202 | * The consumer must keep that pointee valid during each signature resolve. | ||
| 203 | */ | ||
| 204 | const void *validator_context = nullptr; | ||
| 205 | /** | ||
| 206 | * @brief Run @ref validator on a Manual anchor too, instead of taking the pinned literal unchecked. | ||
| 207 | * @details In-memory only. | ||
| 208 | */ | ||
| 209 | bool validate_manual = false; | ||
| 210 | /// Reject a backend-resolvable anchor that carries no @ref validator (fails closed). In-memory only. | ||
| 211 | bool require_validator = false; | ||
| 212 | |||
| 213 | /// How the consumer interprets the resolved value. | ||
| 214 | Binding binding{}; | ||
| 215 | |||
| 216 | /** | ||
| 217 | * @brief The @ref anchor::anchor_fingerprint captured when authored. A zero value means "not captured yet". | ||
| 218 | * @details The fingerprint is a content hash of the signature's own declarative definition: its locate | ||
| 219 | * evidence (pattern bytes / mangled name / xref literal), its @ref Binding contract, and its | ||
| 220 | * label and module scope. It never reads the game's code. Persist it so the gate can distinguish | ||
| 221 | * a relocated target from a signature edit. A relocation retains the same fingerprint. A | ||
| 222 | * signature edit without a new baseline changes it. A value of 0 reports as "unknown", never as | ||
| 223 | * "drifted", so an author without a baseline is not falsely rejected. | ||
| 224 | */ | ||
| 225 | std::uint64_t expected_fingerprint = 0; | ||
| 226 | |||
| 227 | /** | ||
| 228 | * @brief RipGlobal: page-protection class for byte-tier candidates. Defaults to @ref scan::Pages::Readable | ||
| 229 | * for backward-compatible data-global resolution; set @ref scan::Pages::Executable when every rung | ||
| 230 | * anchors on an instruction. Serialized as the optional `pages` key for RipGlobal records only. | ||
| 231 | * @details Ignored by other kinds. | ||
| 232 | */ | ||
| 233 | scan::Pages pages = scan::Pages::Readable; | ||
| 234 | |||
| 235 | /** | ||
| 236 | * @brief ExportName: the exact, case-sensitive export symbol name (no decoration), e.g. "Sleep". The owning | ||
| 237 | * module is the shared @ref module field (empty resolves the export within the fallback scope). | ||
| 238 | * @details Serialized as the `export_name` key for ExportName records only; ignored by other kinds. | ||
| 239 | */ | ||
| 240 | std::string export_name; | ||
| 241 | |||
| 242 | /** | ||
| 243 | * @brief The optional live-image baseline captured for this signature. | ||
| 244 | * @details Serialized as `image_identity` when present. A configured identity gate rejects a captured | ||
| 245 | * baseline that does not match the resolved image. | ||
| 246 | */ | ||
| 247 | scan::ImageIdentity expected_image_identity{}; | ||
| 248 | |||
| 249 | /** | ||
| 250 | * @brief The optional winning-span content baseline captured for this signature. | ||
| 251 | * @details Serialized as `winning_bytes`, a lowercase hex string of the captured span. This is the only | ||
| 252 | * baseline that sees target CONTENT: @ref expected_fingerprint hashes the record's own | ||
| 253 | * declaration and @ref expected_image_identity folds PE header fields, so an in-place code patch | ||
| 254 | * that preserves the section table moves this and neither of the others. Only a byte-signature | ||
| 255 | * rung can produce one. | ||
| 256 | */ | ||
| 257 | scan::WinningEvidence expected_winning_bytes{}; | ||
| 258 | }; | ||
| 259 | |||
| 260 | /** | ||
| 261 | * @enum FingerprintState | ||
| 262 | * @brief The drift verdict for one signature: no baseline, the declared definition is unchanged, or it changed. | ||
| 263 | */ | ||
| 264 | enum class FingerprintState : std::uint8_t | ||
| 265 | { | ||
| 266 | /// No baseline was captured (@ref SignatureRecord::expected_fingerprint is 0); drift cannot be judged. | ||
| 267 | Unset, | ||
| 268 | /// The live fingerprint equals the captured baseline: the signature's declared definition is unchanged. | ||
| 269 | Match, | ||
| 270 | /// The live fingerprint differs from the baseline (see @ref SignatureRecord::expected_fingerprint). | ||
| 271 | Drifted | ||
| 272 | }; | ||
| 273 | |||
| 274 | /** | ||
| 275 | * @class Signature | ||
| 276 | * @brief A compiled, resolvable signature: owns its candidate storage and presents an @ref anchor::Anchor view. | ||
| 277 | * @details The bridge from the owning, serializable @ref SignatureRecord to the borrowed @ref anchor::Anchor | ||
| 278 | * the engine resolves. It owns the compiled ladder (a std::vector<scan::Candidate>) and the record's | ||
| 279 | * owned strings, and it rebuilds a borrowed @ref anchor::Anchor on demand rather than | ||
| 280 | * caching one, so moving a Signature can never leave a stored view dangling - the same discipline | ||
| 281 | * @ref scan::OwnedScanRequest::view uses. Construct one from a file record with @ref compile, or adopt | ||
| 282 | * an in-code anchor with @ref adopt. | ||
| 283 | */ | ||
| 284 | class Signature | ||
| 285 | { | ||
| 286 | public: | ||
| 287 | /** | ||
| 288 | * @brief Compiles a file record into a resolvable signature, failing closed on an uncompilable rung. | ||
| 289 | * @param record The owning record (moved in; its strings back the resolved anchor view). | ||
| 290 | * @return The compiled Signature, or an Error: BadPattern (a ladder rung's AOB failed to compile), | ||
| 291 | * EmptyCandidates (a RipGlobal / CodeOperand record with no ladder), or InvalidArg (a record whose | ||
| 292 | * kind is the non-serializable Quorum / CallArgHome / Unset, whose kind's required evidence is | ||
| 293 | * empty, whose persisted policy fields (including CodeOperand byte_width) are out of range, whose | ||
| 294 | * label or string fields that cannot round-trip through the file grammar, whose Utf16le string | ||
| 295 | * evidence breaks the @ref SignatureRecord::xref_encoding rule, or whose binding carries a | ||
| 296 | * non-default value in a field its @ref BindingKind never reads). | ||
| 297 | * @note Setup/control-plane only: compiling a ladder parses each rung's Pattern. | ||
| 298 | */ | ||
| 299 | [[nodiscard]] static Result<Signature> compile(SignatureRecord record); | ||
| 300 | |||
| 301 | /** | ||
| 302 | * @brief Adopts an in-code @ref anchor::Anchor and owns its evidence. | ||
| 303 | * @param source The in-code anchor. | ||
| 304 | * The function copies its borrowed views. | ||
| 305 | * @return The owning Signature, or an Error: InvalidArg (a Quorum, CallArgHome, or Unset anchor, a | ||
| 306 | * serializable anchor whose required evidence is empty, an out-of-range persisted policy field | ||
| 307 | * (including CodeOperand byte_width), a label or string field that cannot round-trip through | ||
| 308 | * the file grammar, or Utf16le string evidence that breaks the | ||
| 309 | * @ref SignatureRecord::xref_encoding rule). | ||
| 310 | * @details The counterpart to @ref compile for a signature that originates in code rather than a file. It | ||
| 311 | * copies the anchor's borrowed site candidates and strings into this object so the adopted | ||
| 312 | * signature outlives the caller's anchor table. The resulting record carries no ladder text (a | ||
| 313 | * compiled Pattern cannot be turned back into its source AOB), so @ref serialize_checked of an | ||
| 314 | * adopted signature's record omits its ladder; capture a fresh record from the file side to | ||
| 315 | * serialize it. | ||
| 316 | * @note Setup/control-plane only: the adoption copies the anchor's evidence into owned storage. | ||
| 317 | */ | ||
| 318 | [[nodiscard]] static Result<Signature> adopt(const anchor::Anchor &source); | ||
| 319 | |||
| 320 | /** | ||
| 321 | * @brief Resolves this signature to a value through its anchor backend, fail-closed. | ||
| 322 | * @param fallback_scope The module image to resolve within when the record names no module; defaults to the | ||
| 323 | * host executable. A record that names a module always resolves within that module, | ||
| 324 | * ignoring this argument. | ||
| 325 | * @return A @ref anchor::ResolvedAnchor carrying the outcome and (on success) the value. | ||
| 326 | * @note Setup/control-plane only (see @ref anchor::resolve). | ||
| 327 | */ | ||
| 328 | [[nodiscard]] anchor::ResolvedAnchor resolve(Region fallback_scope = Region::host()) const; | ||
| 329 | |||
| 330 | /** | ||
| 331 | * @brief The effective scope this signature resolves within. | ||
| 332 | * @return @ref Region::module_named for the record's module, or @ref Region::host when it names none. | ||
| 333 | * @note Setup/control-plane only: queries the loader. | ||
| 334 | */ | ||
| 335 | [[nodiscard]] Region scope() const noexcept; | ||
| 336 | |||
| 337 | /** | ||
| 338 | * @brief The live fingerprint of this signature, recomputed from its current declarative inputs. | ||
| 339 | * @return A content hash over the signature's declared definition: the @ref anchor::anchor_fingerprint of | ||
| 340 | * the locate evidence (compiled ladder, mangled name, xref literal) combined with the @ref Binding | ||
| 341 | * contract (register / offset chain / value width / vtable slot), the record label, and the module | ||
| 342 | * scope @ref resolve walks. | ||
| 343 | * @details Content-derived and address-independent: it reads no game memory, so it is stable across runs | ||
| 344 | * and rebuilds on one platform and changes exactly when the signature's declared definition | ||
| 345 | * changes - a re-authored pattern, a renamed type, a different literal, or an edited binding. | ||
| 346 | */ | ||
| 347 | [[nodiscard]] std::uint64_t current_fingerprint() const noexcept; | ||
| 348 | |||
| 349 | /** | ||
| 350 | * @brief Compares the live fingerprint to the captured baseline. | ||
| 351 | * @return @ref FingerprintState::Unset when no baseline was captured, @ref FingerprintState::Match when the | ||
| 352 | * declared definition is unchanged, else @ref FingerprintState::Drifted. | ||
| 353 | */ | ||
| 354 | [[nodiscard]] FingerprintState fingerprint_state() const noexcept; | ||
| 355 | |||
| 356 | /** | ||
| 357 | * @brief Adopts the live fingerprint as the new baseline, after a verified repair. | ||
| 358 | * @details Call this once a hand edit (new pattern, moved register, shifted offset) has been confirmed | ||
| 359 | * correct, so the gate trusts the repaired signature again on the next run. Persist the updated | ||
| 360 | * @ref record afterward to make the recapture durable. | ||
| 361 | * @note Setup/control-plane only: the recapture mutates the trust baseline. | ||
| 362 | */ | ||
| 363 | void recapture_fingerprint() noexcept; | ||
| 364 | |||
| 365 | /** | ||
| 366 | * @brief Re-resolves this signature and adopts the live fingerprint, image identity, and winning-span | ||
| 367 | * content as the new baselines. | ||
| 368 | * @param fallback_scope The default module image for a signature that names no module; must be the same | ||
| 369 | * scope the consumer will gate under, since a baseline captured in one scope does not | ||
| 370 | * describe another. | ||
| 371 | * @return Nothing on success, or an Error explaining why no baseline was adopted. | ||
| 372 | * @details The recapture @ref GatePolicy::mutation_strict needs: it is the only operation that fills | ||
| 373 | * @ref SignatureRecord::expected_image_identity and | ||
| 374 | * @ref SignatureRecord::expected_winning_bytes from live evidence. | ||
| 375 | * | ||
| 376 | * Atomic: every baseline is computed before any is stored, so a failure leaves all three at their | ||
| 377 | * previous values rather than a half-updated mixture that would gate on one game version's | ||
| 378 | * content and another's identity. Fails with @ref ErrorCode::NoMatch when the signature does not | ||
| 379 | * resolve, and with @ref ErrorCode::UnexpectedShape when the resolved rung witnesses no owning | ||
| 380 | * image or no usable content span - an RTTI, export, string-xref, or Manual kind, or evidence | ||
| 381 | * longer than @ref scan::MAX_MUTATION_WITNESS_BYTES. Persist @ref record afterward to make it | ||
| 382 | * durable. | ||
| 383 | * @note Setup/control-plane only: re-resolving walks the signature's scope. | ||
| 384 | */ | ||
| 385 | [[nodiscard]] Result<void> recapture(Region fallback_scope = Region::host()); | ||
| 386 | |||
| 387 | /// The signature's stable key. | ||
| 388 | [[nodiscard]] std::string_view label() const noexcept; | ||
| 389 | /// Which anchor backend resolves this signature. | ||
| 390 | [[nodiscard]] anchor::AnchorKind kind() const noexcept; | ||
| 391 | /// The consumer-facing binding (register / offsets / vtable slot). | ||
| 392 | [[nodiscard]] const Binding &binding() const noexcept; | ||
| 393 | /// The owning record backing this signature (for @ref serialize_checked after @ref recapture_fingerprint). | ||
| 394 | [[nodiscard]] const SignatureRecord &record() const noexcept; | ||
| 395 | |||
| 396 | private: | ||
| 397 | friend class detail::GateAccess; | ||
| 398 | |||
| 399 | // The two factories are the only construction path: compile() parses a record's ladder text into m_ladder, | ||
| 400 | // adopt() copies an anchor's site into m_ladder, and both keep the owning record so make_anchor() can view | ||
| 401 | // its strings. The compiled ladder is stored separately from the record's text ladder because the resolver | ||
| 402 | // needs scan::Candidate objects, which are not what the file round-trips. | ||
| 403 | Signature(SignatureRecord record, std::vector<scan::Candidate> ladder) noexcept; | ||
| 404 | |||
| 405 | // Builds a borrowed anchor::Anchor viewing this object's owned storage. Rebuilt on demand (never cached) so | ||
| 406 | // no view outlives a move of *this; the returned Anchor is valid only for the duration of the call it | ||
| 407 | // feeds. | ||
| 408 | [[nodiscard]] anchor::Anchor make_anchor() const noexcept; | ||
| 409 | |||
| 410 | // Resolves through the private provenance path and returns the selected match span for the mutation gate. | ||
| 411 | [[nodiscard]] anchor::ResolvedAnchor resolve_for_gate(Region fallback_scope, Region &winning_span) const; | ||
| 412 | |||
| 413 | SignatureRecord m_record; | ||
| 414 | std::vector<scan::Candidate> m_ladder; | ||
| 415 | }; | ||
| 416 | |||
| 417 | /// The manifest INI format version this build reads and writes. Bumped only on an incompatible format change. | ||
| 418 | inline constexpr std::uint32_t SCHEMA_VERSION = 1; | ||
| 419 | |||
| 420 | /** | ||
| 421 | * @struct ManifestHeader | ||
| 422 | * @brief The `[manifest]` metadata: the DetourModKit parse-format schema and the author's contract revision. | ||
| 423 | * @details Two independent version axes. @ref schema is the file-format version, which states whether this | ||
| 424 | * build can parse the file at all. @ref parse rejects a schema it does not understand. @ref revision | ||
| 425 | * is the mod author's own signature-contract epoch, bumped only when an in-code change makes older | ||
| 426 | * manifests incompatible (a renamed label, a re-meaning of a binding, a dropped signature). | ||
| 427 | * DetourModKit never interprets @ref revision; a consumer compares it to its build's expected value | ||
| 428 | * through @ref revision_compatible and safe-ignores a stale file. This catches staleness the | ||
| 429 | * per-signature fingerprint gate cannot, such as a renamed label or a changed meaning for an existing | ||
| 430 | * binding. | ||
| 431 | */ | ||
| 432 | struct ManifestHeader | ||
| 433 | { | ||
| 434 | /// The format version the file declares; @ref parse rejects a value this build cannot read. | ||
| 435 | std::uint32_t schema = SCHEMA_VERSION; | ||
| 436 | /// The author's signature-contract epoch (0 = unversioned); compared to a build revision, never by DMK. | ||
| 437 | std::uint32_t revision = 0; | ||
| 438 | }; | ||
| 439 | |||
| 440 | /** | ||
| 441 | * @struct Manifest | ||
| 442 | * @brief A parsed manifest: its @ref ManifestHeader plus the signature records in file order. | ||
| 443 | */ | ||
| 444 | struct Manifest | ||
| 445 | { | ||
| 446 | /// The `[manifest]` metadata (schema and contract revision). | ||
| 447 | ManifestHeader header{}; | ||
| 448 | /// The signatures, one per `[sig.<label>]` section, in file order. | ||
| 449 | std::vector<SignatureRecord> records{}; | ||
| 450 | }; | ||
| 451 | |||
| 452 | /** | ||
| 453 | * @struct ManifestLimits | ||
| 454 | * @brief The resource caps the manifest parser and checked persistence functions enforce. | ||
| 455 | * @details A default-constructed value is @ref conservative(). Trusted authoring tools may opt into | ||
| 456 | * @ref advanced(); untrusted files must use bounded limits. A violation returns | ||
| 457 | * @ref ErrorCode::SizeTooLarge without publishing a partial result. | ||
| 458 | */ | ||
| 459 | struct ManifestLimits | ||
| 460 | { | ||
| 461 | /// Largest accepted encoded text size in bytes. | ||
| 462 | std::size_t max_file_bytes{1u << 20}; | ||
| 463 | /// Largest accepted number of INI sections (header, records, and rung sub-sections combined). | ||
| 464 | std::size_t max_sections{1u << 15}; | ||
| 465 | /// Largest accepted number of keys within any one section. | ||
| 466 | std::size_t max_keys_per_section{64}; | ||
| 467 | /// Largest accepted number of `[sig.<label>]` records. | ||
| 468 | std::size_t max_records{512}; | ||
| 469 | /// Largest accepted number of candidate-ladder rungs on any one record. | ||
| 470 | std::size_t max_rungs_per_record{32}; | ||
| 471 | /// Largest accepted size in bytes of any single string field or heredoc value. | ||
| 472 | std::size_t max_field_bytes{64u << 10}; | ||
| 473 | /// Largest accepted sum of all decoded value bytes across the manifest. | ||
| 474 | std::size_t max_total_decoded_bytes{4u << 20}; | ||
| 475 | |||
| 476 | /// Returns limits equal to a default-constructed @ref ManifestLimits. | ||
| 477 | 376 | [[nodiscard]] static constexpr ManifestLimits conservative() noexcept { return ManifestLimits{}; } | |
| 478 | |||
| 479 | /** | ||
| 480 | * @brief Raises every numeric cap to its maximum while retaining grammar and semantic validation. | ||
| 481 | * @return Limits intended only for a trusted authoring tool, never for an untrusted file. | ||
| 482 | */ | ||
| 483 | [[nodiscard]] static constexpr ManifestLimits advanced() noexcept | ||
| 484 | { | ||
| 485 | // Parenthesized because public headers must compile with <windows.h>'s function-like max macro active. | ||
| 486 | constexpr std::size_t MAX_VALUE = (std::numeric_limits<std::size_t>::max)(); | ||
| 487 | return ManifestLimits{ | ||
| 488 | .max_file_bytes = MAX_VALUE, | ||
| 489 | .max_sections = MAX_VALUE, | ||
| 490 | .max_keys_per_section = MAX_VALUE, | ||
| 491 | .max_records = MAX_VALUE, | ||
| 492 | .max_rungs_per_record = MAX_VALUE, | ||
| 493 | .max_field_bytes = MAX_VALUE, | ||
| 494 | .max_total_decoded_bytes = MAX_VALUE, | ||
| 495 | }; | ||
| 496 | } | ||
| 497 | }; | ||
| 498 | |||
| 499 | /** | ||
| 500 | * @brief Reports whether a manifest may be applied under a build's signature-contract revision. | ||
| 501 | * @param header The parsed manifest header. | ||
| 502 | * @param build_revision The revision this build authored its in-code signatures against; 0 disables the check. | ||
| 503 | * @return true when @p build_revision is 0 (the consumer opts out of revision gating) or the manifest's | ||
| 504 | * @ref ManifestHeader::revision equals it; false when the file targets a different contract epoch. | ||
| 505 | * @details Bump @p build_revision (and the file's `revision`) only on an incompatible contract change (see | ||
| 506 | * @ref ManifestHeader). On a false result a consumer logs and falls back to its in-code defaults. | ||
| 507 | */ | ||
| 508 | [[nodiscard]] bool revision_compatible(const ManifestHeader &header, std::uint32_t build_revision) noexcept; | ||
| 509 | |||
| 510 | /** | ||
| 511 | * @brief Parses a manifest's INI text. | ||
| 512 | * @param text The manifest text (a `[manifest]` header plus one `[sig.<label>]` section per contract). | ||
| 513 | * @param limits The resource caps to enforce; the default is @ref ManifestLimits::conservative(). | ||
| 514 | * @return The parsed @ref Manifest (header plus records in file order), or an Error: MissingHeader (no | ||
| 515 | * `[manifest]` section or an unsupported schema), MalformedLine (a line, field, or enum token that | ||
| 516 | * does not parse, a noncomment key line without `=`, an empty key, a non-canonical section or key | ||
| 517 | * spelling, a key that is inert for its record's declared binding kind or its rung's mode, or Utf16le | ||
| 518 | * string evidence that breaks the @ref SignatureRecord::xref_encoding rule), ManifestIdentityCollision | ||
| 519 | * (a case-, whitespace-, or | ||
| 520 | * exactly-duplicated section, or a whitespace-variant or exactly-duplicated key, but a miscased key is | ||
| 521 | * MalformedLine before collision detection), ManifestFramingUnsafe (an unterminated `<<<` heredoc | ||
| 522 | * value, an opener with an empty tag, or a heredoc whose first body line is its terminator), | ||
| 523 | * SizeTooLarge (encoded text, a section, key, field, record, rung, or aggregate exceeding @p limits), | ||
| 524 | * or OutOfMemory (an allocation failed). | ||
| 525 | * @details Fails closed: a manifest that cannot be trusted to describe the signatures faithfully is rejected | ||
| 526 | * whole, never partially applied. A raw prepass rejects every identity collision before the | ||
| 527 | * case-sensitive backend reads the text, so no merged or swallowed record can masquerade as another. | ||
| 528 | * A missing optional key falls back to its default, so an absent `revision` is 0. A key that is | ||
| 529 | * present must parse, so a blank enum, numeric, or boolean value is MalformedLine instead of a | ||
| 530 | * default. A blank string-valued key reads as empty. | ||
| 531 | * @note Setup/control-plane only: parses and allocates bounded manifest state. | ||
| 532 | */ | ||
| 533 | [[nodiscard]] Result<Manifest> | ||
| 534 | parse(std::string_view text, const ManifestLimits &limits = ManifestLimits::conservative()); | ||
| 535 | |||
| 536 | /** | ||
| 537 | * @brief Serializes a manifest to INI text, rejecting anything that could not round-trip. | ||
| 538 | * @param manifest The header (its @ref ManifestHeader::revision is emitted when non-zero) and records to emit. | ||
| 539 | * @param limits The resource caps to enforce; the default is @ref ManifestLimits::conservative(). | ||
| 540 | * @return The manifest text, round-trippable through @ref parse, or an Error: InvalidArg (a record whose label | ||
| 541 | * or a string field cannot be framed, an out-of-range persisted policy field (including CodeOperand | ||
| 542 | * byte_width), a binding carrying a non-default inert field, or Utf16le string evidence that breaks | ||
| 543 | * the @ref SignatureRecord::xref_encoding rule), ManifestIdentityCollision (two records | ||
| 544 | * whose labels fold to one section, or a record whose label folds into another record's rung section), | ||
| 545 | * SizeTooLarge (encoded text, a record, rung, field, or aggregate exceeding @p limits), or OutOfMemory. | ||
| 546 | * The `schema` line always reflects this build's @ref SCHEMA_VERSION. | ||
| 547 | * @details The single encoder: @ref save routes through it, so a value that a later @ref parse could not read | ||
| 548 | * back is refused at write time rather than persisted. A rejection is a typed error, never an empty or | ||
| 549 | * truncated string. | ||
| 550 | * @note Setup/control-plane only: validates and allocates bounded manifest text. | ||
| 551 | */ | ||
| 552 | [[nodiscard]] Result<std::string> | ||
| 553 | serialize_checked(const Manifest &manifest, const ManifestLimits &limits = ManifestLimits::conservative()); | ||
| 554 | |||
| 555 | /** | ||
| 556 | * @brief Reads and parses a manifest file. | ||
| 557 | * @param path Source file path. | ||
| 558 | * @param limits The resource caps to enforce; the default is @ref ManifestLimits::conservative(). | ||
| 559 | * @return The parsed @ref Manifest, or FileOpenFailed (missing, locked, denied, or not a regular disk file), a | ||
| 560 | * parse error (MissingHeader / MalformedLine / ManifestIdentityCollision / ManifestFramingUnsafe) when | ||
| 561 | * the file is present but its contents are corrupt, SizeTooLarge (the file exceeds | ||
| 562 | * @ref ManifestLimits::max_file_bytes at the size query, or the bytes already read overrun the cap), or | ||
| 563 | * OutOfMemory. Any other length change detected after the size query fails as FileOpenFailed, including | ||
| 564 | * growth whose cap overrun would only land in a later read chunk. | ||
| 565 | * @details The read is materialized whole into a bounded buffer or not at all: a non-disk special file, an | ||
| 566 | * oversize file, a file a writer extends after the size query, and an allocation failure each return a | ||
| 567 | * typed error and touch no previously loaded manifest, so the caller's trusted generation survives a | ||
| 568 | * failed reload and the same input is retryable. | ||
| 569 | * @note A missing file is a distinct, recoverable FileOpenFailed, so an overlay can treat "no file" as "no | ||
| 570 | * overrides" (the defaults pass through) rather than a hard failure. | ||
| 571 | * @note Setup/control-plane only: performs bounded file I/O and parsing. | ||
| 572 | */ | ||
| 573 | [[nodiscard]] Result<Manifest> | ||
| 574 | load(const std::filesystem::path &path, const ManifestLimits &limits = ManifestLimits::conservative()); | ||
| 575 | |||
| 576 | /** | ||
| 577 | * @brief Writes a manifest to a file via @ref serialize_checked. | ||
| 578 | * @param path Destination file path. | ||
| 579 | * @param manifest The manifest to serialize. | ||
| 580 | * @param limits The resource caps to enforce; the default is @ref ManifestLimits::conservative(). | ||
| 581 | * @return Empty on success, or an Error: any @ref serialize_checked rejection (the manifest could not be | ||
| 582 | * encoded to a round-trippable form), SizeTooLarge when the encoded text exceeds the platform's | ||
| 583 | * single-write bound, FileOpenFailed when the file could not be opened for writing, FileWriteFailed | ||
| 584 | * when the stream failed during the write or flush, or OutOfMemory when the write phase itself fails | ||
| 585 | * to allocate. | ||
| 586 | * @details The encode is validated before the file is opened, so a manifest that cannot round-trip never | ||
| 587 | * reaches disk. The write truncates @p path in place and is not atomic across a crash. A tear | ||
| 588 | * inside a line or heredoc fails the next @ref load closed, so the in-code defaults stay in effect. | ||
| 589 | * A tear at a record boundary parses as a valid shorter manifest. For a crash-durable replacement, | ||
| 590 | * stage @ref serialize_checked output through a temporary file, flush it to disk, and replace the | ||
| 591 | * target with the platform's atomic replace. | ||
| 592 | * @note Setup/control-plane only: performs bounded serialization and file I/O. | ||
| 593 | */ | ||
| 594 | [[nodiscard]] Result<void> save( | ||
| 595 | const std::filesystem::path &path, | ||
| 596 | const Manifest &manifest, | ||
| 597 | const ManifestLimits &limits = ManifestLimits::conservative() | ||
| 598 | ); | ||
| 599 | |||
| 600 | /** | ||
| 601 | * @brief Merges in-code anchor defaults with optional file overrides. | ||
| 602 | * @param defaults The in-code anchors. | ||
| 603 | * The function copies each borrowed view. | ||
| 604 | * @param overrides The file records from @ref load. | ||
| 605 | * An empty span passes the defaults through untouched. | ||
| 606 | * @return The merged, compiled signatures in @p defaults order. A per-signature problem never fails the whole | ||
| 607 | * overlay (fail-soft); the Result carries a failure only if a future merge-wide error mode is added. | ||
| 608 | * @note Setup/control-plane only, and not noexcept: like the resolvers it drives, its sole throwing path is | ||
| 609 | * allocation failure. A bad file entry does not throw or fail; it falls back to the in-code default. | ||
| 610 | * @details The adoption model in one call, fail-soft like @ref config::bind. | ||
| 611 | * - A default with no same-label override is adopted as-is (@ref Signature::adopt). | ||
| 612 | * - A default with a same-label override is replaced by the file (@ref Signature::compile), so a | ||
| 613 | * game update that broke two of twenty signatures needs only those two file entries. | ||
| 614 | * - A malformed override falls back to the in-code default, so an override never makes the result | ||
| 615 | * worse than a missing file. | ||
| 616 | * - An override whose label matches no default is inert and is not included. | ||
| 617 | * An accepted override supplies the complete serializable record. | ||
| 618 | * The effective override inherits these code-owned fields: | ||
| 619 | * - @ref SignatureRecord::validator | ||
| 620 | * - @ref SignatureRecord::validator_context | ||
| 621 | * - @ref SignatureRecord::validate_manual | ||
| 622 | * - @ref SignatureRecord::require_validator | ||
| 623 | * These contract changes fall back to the default: | ||
| 624 | * - An override that changes the default's declared @ref anchor::ResultDomain falls back to the | ||
| 625 | * default. | ||
| 626 | * - An override that crosses between Manual and a backend kind falls back to the default. | ||
| 627 | * An override for a non-serializable default is ignored. | ||
| 628 | * A flat file rung cannot preserve a quorum's corroboration. | ||
| 629 | * A default with a non-serializable kind or empty required evidence cannot be adopted. | ||
| 630 | * Callers use @ref anchor::evaluate_gate for non-serializable anchors. | ||
| 631 | */ | ||
| 632 | [[nodiscard]] Result<std::vector<Signature>> | ||
| 633 | overlay(std::span<const anchor::Anchor> defaults, std::span<const SignatureRecord> overrides); | ||
| 634 | |||
| 635 | /** | ||
| 636 | * @struct GatePolicy | ||
| 637 | * @brief The trust thresholds @ref resolve_and_gate applies. Defaults reject drift but tolerate an unset | ||
| 638 | * baseline. | ||
| 639 | */ | ||
| 640 | struct GatePolicy | ||
| 641 | { | ||
| 642 | /** | ||
| 643 | * @brief When true (the default), a signature whose fingerprint no longer matches its captured baseline | ||
| 644 | * (see @ref SignatureRecord::expected_fingerprint) is safe-disabled. | ||
| 645 | */ | ||
| 646 | bool reject_on_fingerprint_drift = true; | ||
| 647 | /** | ||
| 648 | * @brief When true, a signature with no captured baseline (@ref FingerprintState::Unset) is also | ||
| 649 | * safe-disabled. The default false treats "unknown" as trusted, so an author who has not captured | ||
| 650 | * fingerprints yet is not blocked. | ||
| 651 | */ | ||
| 652 | bool reject_unset_fingerprint = false; | ||
| 653 | /** | ||
| 654 | * @brief Optional whole-manifest health floor in [0, 1]: if the fraction of trusted signatures falls below | ||
| 655 | * it, every signature is rejected. The default 0 imposes no floor (each signature stands alone). | ||
| 656 | */ | ||
| 657 | double min_resolved_fraction = 0.0; | ||
| 658 | /** | ||
| 659 | * @brief When true, a resolved signature is trusted to AUTHORIZE A WRITE only when its binding can safely | ||
| 660 | * mutate the resolved typed domain: a Manual pin (no live evidence, cannot self-heal) authorizes no | ||
| 661 | * mutation, and the binding kind must match the resolved domain - a MidHook needs a code site, a | ||
| 662 | * VmtMethod a vtable, and an Address / pointer chain a CodeSite or DataAddress, never a vtable or | ||
| 663 | * Scalar. The default false leaves a read-only manifest free to carry a Manual or value-only | ||
| 664 | * binding. | ||
| 665 | */ | ||
| 666 | bool require_mutation_safe_binding = false; | ||
| 667 | /** | ||
| 668 | * @brief When true, a captured image baseline must match the live image for a mutation-capable entry. | ||
| 669 | * @details An absent baseline leaves the entry image-agnostic. Manual values are unaffected. Pair with | ||
| 670 | * @ref require_captured_image_identity to make the baseline mandatory rather than optional. | ||
| 671 | */ | ||
| 672 | bool require_live_image_identity = false; | ||
| 673 | /** | ||
| 674 | * @brief When true, a mutation-capable entry with no captured image baseline is safe-disabled. | ||
| 675 | * @details Closes the read-only default's tolerance of an absent baseline. | ||
| 676 | */ | ||
| 677 | bool require_captured_image_identity = false; | ||
| 678 | /** | ||
| 679 | * @brief When true, a mutation-capable entry must carry a winning-span content baseline that still matches. | ||
| 680 | * @details This gate compares target content. It first compares the baseline with the scan witness. | ||
| 681 | * Directly before trust publication, it reads the selected match span through the guarded memory | ||
| 682 | * primitive. It compares every byte again. An absent baseline, an absent span, an over-long span, | ||
| 683 | * a read fault, or any byte difference rejects the entry. This check catches an equal-layout | ||
| 684 | * in-place code patch, which @ref require_live_image_identity cannot see. It checks freshness | ||
| 685 | * directly before gate publication, not at a later consumer write. A consumer that requires | ||
| 686 | * write-time certainty must use a checked mutation or install operation. | ||
| 687 | */ | ||
| 688 | bool require_winning_evidence_baseline = false; | ||
| 689 | /** | ||
| 690 | * @brief When true, a mutation-capable entry is safe-disabled unless a contract revision was actually | ||
| 691 | * checked. | ||
| 692 | * @details The plain @ref resolve_and_gate overload runs no revision check at all, and the header-threaded | ||
| 693 | * overload skips it when @c build_revision is 0. Either path would otherwise authorize a write | ||
| 694 | * against a manifest whose author contract was never compared. | ||
| 695 | */ | ||
| 696 | bool require_contract_revision = false; | ||
| 697 | |||
| 698 | /** | ||
| 699 | * @brief The strictest gate. Reject drift and an unset baseline, and require every signature to resolve. | ||
| 700 | * @details Inverts the lenient default: an unset baseline is treated as untrusted. The manifest passes | ||
| 701 | * only when the ENTIRE set is trusted (min_resolved_fraction 1.0). A single drifted or unresolved | ||
| 702 | * feature therefore safe-disables the whole manifest. | ||
| 703 | * @return A GatePolicy with reject_on_fingerprint_drift and reject_unset_fingerprint both true and | ||
| 704 | * min_resolved_fraction 1.0. | ||
| 705 | */ | ||
| 706 | 9 | [[nodiscard]] static constexpr GatePolicy strict() noexcept | |
| 707 | { | ||
| 708 | return GatePolicy{ | ||
| 709 | .reject_on_fingerprint_drift = true, | ||
| 710 | .reject_unset_fingerprint = true, | ||
| 711 | .min_resolved_fraction = 1.0, | ||
| 712 | 9 | }; | |
| 713 | } | ||
| 714 | |||
| 715 | /** | ||
| 716 | * @brief The strict gate PLUS every mutation-authorization requirement, for a manifest that drives a patch. | ||
| 717 | * @details A mutation-capable entry needs a captured fingerprint and a captured live image identity that | ||
| 718 | * matches. It also needs resolve evidence that matches its baseline and a fresh guarded read. The | ||
| 719 | * entry needs a mutation-safe typed binding that is not a Manual and a checked contract revision. | ||
| 720 | * The revision check requires the @ref ManifestHeader overload with a nonzero build revision. | ||
| 721 | * Read-only lookup is unaffected. The plain overload, a zero build revision, and an uncaptured | ||
| 722 | * baseline all remain usable for resolution. They cannot authorize a write. | ||
| 723 | * @return A strict policy with every mutation requirement armed. | ||
| 724 | */ | ||
| 725 | 9 | [[nodiscard]] static constexpr GatePolicy mutation_strict() noexcept | |
| 726 | { | ||
| 727 | 9 | GatePolicy policy = strict(); | |
| 728 | 9 | policy.require_mutation_safe_binding = true; | |
| 729 | 9 | policy.require_live_image_identity = true; | |
| 730 | 9 | policy.require_captured_image_identity = true; | |
| 731 | 9 | policy.require_winning_evidence_baseline = true; | |
| 732 | 9 | policy.require_contract_revision = true; | |
| 733 | 9 | return policy; | |
| 734 | } | ||
| 735 | }; | ||
| 736 | |||
| 737 | /** | ||
| 738 | * @struct GatedSignature | ||
| 739 | * @brief One trusted signature: its resolved address paired with the binding that says how to read it. | ||
| 740 | * @details @ref label and @ref binding are views into the source @ref Signature, so a GateResult is valid only | ||
| 741 | * while the signatures it gated stay alive. | ||
| 742 | */ | ||
| 743 | struct GatedSignature | ||
| 744 | { | ||
| 745 | /// The signature's key (a view into the source Signature). | ||
| 746 | std::string_view label; | ||
| 747 | /// Which anchor backend resolved it. | ||
| 748 | anchor::AnchorKind kind = anchor::AnchorKind::Manual; | ||
| 749 | /// The resolved value as an address; interpret it per @ref binding. | ||
| 750 | Address address; | ||
| 751 | /// The consumer-facing binding (a pointer into the source Signature). | ||
| 752 | const Binding *binding = nullptr; | ||
| 753 | }; | ||
| 754 | |||
| 755 | /** | ||
| 756 | * @enum GateReason | ||
| 757 | * @brief Which gate safe-disabled a signature, so a log can tell a locate failure from a refused write | ||
| 758 | * authorization. | ||
| 759 | */ | ||
| 760 | enum class GateReason : std::uint8_t | ||
| 761 | { | ||
| 762 | /// Not rejected. | ||
| 763 | None, | ||
| 764 | /// @ref Signature::resolve did not return a unique @ref anchor::AnchorStatus::Resolved. | ||
| 765 | Unresolved, | ||
| 766 | /// The declared definition was edited without re-capturing its baseline. | ||
| 767 | FingerprintDrifted, | ||
| 768 | /// No fingerprint baseline was captured and the policy requires one. | ||
| 769 | FingerprintUnset, | ||
| 770 | /// The binding cannot safely mutate the resolved typed domain, or it is a Manual pin. | ||
| 771 | BindingCannotMutate, | ||
| 772 | /// No contract revision was checked, or the checked revision is incompatible. | ||
| 773 | ContractRevision, | ||
| 774 | /// The captured image baseline is absent, or it no longer matches the live image. | ||
| 775 | ImageIdentity, | ||
| 776 | /// The winning-span content baseline is absent, unwitnessed, over-long, or no longer matches. | ||
| 777 | WinningEvidence, | ||
| 778 | /// The whole-manifest trusted fraction fell below @ref GatePolicy::min_resolved_fraction. | ||
| 779 | HealthFloor, | ||
| 780 | }; | ||
| 781 | |||
| 782 | /** | ||
| 783 | * @struct RejectedSignature | ||
| 784 | * @brief One safe-disabled signature and why it was not trusted. | ||
| 785 | */ | ||
| 786 | struct RejectedSignature | ||
| 787 | { | ||
| 788 | /// The signature's key (a view into the source Signature). | ||
| 789 | std::string_view label; | ||
| 790 | /// The resolve outcome; a non-Resolved status is why locate failed, if it did. | ||
| 791 | anchor::AnchorStatus status = anchor::AnchorStatus::Unresolved; | ||
| 792 | /// The drift verdict (see @ref FingerprintState). | ||
| 793 | FingerprintState fingerprint = FingerprintState::Unset; | ||
| 794 | /// The specific gate that rejected this entry. | ||
| 795 | GateReason reason = GateReason::None; | ||
| 796 | }; | ||
| 797 | |||
| 798 | /** | ||
| 799 | * @struct GateResult | ||
| 800 | * @brief The partition of a gated manifest into trusted and safe-disabled, plus the health summary. | ||
| 801 | */ | ||
| 802 | struct GateResult | ||
| 803 | { | ||
| 804 | /// Signatures healthy enough to act on. | ||
| 805 | std::vector<GatedSignature> trusted; | ||
| 806 | /// Signatures safe-disabled because they failed to resolve, drifted, or fell under the health floor. | ||
| 807 | std::vector<RejectedSignature> rejected; | ||
| 808 | /// The robustness summary of the whole manifest, from @ref anchor::assess_quality. | ||
| 809 | anchor::AnchorQuality quality; | ||
| 810 | |||
| 811 | /** | ||
| 812 | * @brief Looks up a trusted signature by label. | ||
| 813 | * @param label The signature key. | ||
| 814 | * @return The trusted entry, or nullptr when no trusted signature carries that label (it was rejected or | ||
| 815 | * never present). A consumer that safe-disables a feature then finds nothing and does not act. | ||
| 816 | */ | ||
| 817 | [[nodiscard]] const GatedSignature *find(std::string_view label) const noexcept; | ||
| 818 | }; | ||
| 819 | |||
| 820 | /** | ||
| 821 | * @brief Resolves a manifest and partitions it into trusted vs safe-disabled signatures. | ||
| 822 | * @param signatures The compiled signatures (from @ref overlay or @ref Signature::compile). Kept alive by the | ||
| 823 | * caller; the result borrows their labels and bindings. | ||
| 824 | * @param policy The trust thresholds. | ||
| 825 | * @param scope The default module image for signatures that name no module; defaults to the host executable. | ||
| 826 | * @return The partition plus the manifest health summary. | ||
| 827 | * @details A signature is rejected when its @ref Signature::resolve does not return a unique | ||
| 828 | * @ref anchor::AnchorStatus::Resolved, when its fingerprint drifted under | ||
| 829 | * @ref GatePolicy::reject_on_fingerprint_drift or is unset under | ||
| 830 | * @ref GatePolicy::reject_unset_fingerprint, when a configured mutation-authorization gate under | ||
| 831 | * @ref GatePolicy rejects its cleanly resolved entry, or when the whole-manifest trusted fraction | ||
| 832 | * falls below @ref GatePolicy::min_resolved_fraction. The entry's @ref GateReason names the gate | ||
| 833 | * that rejected it. A rejected feature installs no hook and reads no pointer. It stays off. | ||
| 834 | * @note Setup/control-plane only: resolving a manifest walks each signature's scope. | ||
| 835 | */ | ||
| 836 | [[nodiscard]] GateResult resolve_and_gate( | ||
| 837 | std::span<const Signature> signatures, | ||
| 838 | const GatePolicy &policy = {}, | ||
| 839 | Region scope = Region::host() | ||
| 840 | ); | ||
| 841 | |||
| 842 | /** | ||
| 843 | * @brief Resolves and gates a manifest under a mandatory build-revision check for mutation-capable entries. | ||
| 844 | * @param signatures The compiled signatures (from @ref overlay or @ref Signature::compile), kept alive by the | ||
| 845 | * caller. | ||
| 846 | * @param header The parsed @ref ManifestHeader carrying the file's author-contract | ||
| 847 | * @ref ManifestHeader::revision. | ||
| 848 | * @param build_revision The revision this build authored its in-code signatures against; 0 opts out of | ||
| 849 | * @ref revision_compatible, leaving this overload equivalent to the plain | ||
| 850 | * @ref resolve_and_gate. | ||
| 851 | * @param policy The trust thresholds; compose @ref GatePolicy::mutation_strict for a manifest that drives a | ||
| 852 | * write. | ||
| 853 | * @param scope The default module image for signatures that name no module. | ||
| 854 | * @return The partition plus the manifest health summary. | ||
| 855 | * @details A non-zero incompatible revision rejects mutation-capable entries even when they resolve. Manual | ||
| 856 | * values remain available. | ||
| 857 | * @note Setup/control-plane only: resolving a manifest walks each signature's scope. | ||
| 858 | */ | ||
| 859 | [[nodiscard]] GateResult resolve_and_gate( | ||
| 860 | std::span<const Signature> signatures, | ||
| 861 | const ManifestHeader &header, | ||
| 862 | std::uint32_t build_revision, | ||
| 863 | const GatePolicy &policy = {}, | ||
| 864 | Region scope = Region::host() | ||
| 865 | ); | ||
| 866 | |||
| 867 | /** | ||
| 868 | * @brief Maps a @ref BindingKind to a short human-readable label (its file token). | ||
| 869 | * @param kind The binding kind. | ||
| 870 | * @return A static string view naming the kind. | ||
| 871 | */ | ||
| 872 | [[nodiscard]] std::string_view binding_kind_to_string(BindingKind kind) noexcept; | ||
| 873 | |||
| 874 | /** | ||
| 875 | * @brief Maps a @ref FingerprintState to a short human-readable label. | ||
| 876 | * @param state The fingerprint state. | ||
| 877 | * @return A static string view naming the state. | ||
| 878 | */ | ||
| 879 | [[nodiscard]] std::string_view fingerprint_state_to_string(FingerprintState state) noexcept; | ||
| 880 | |||
| 881 | /** | ||
| 882 | * @brief Maps a @ref GateReason to a short human-readable label. | ||
| 883 | * @param reason The rejection reason. | ||
| 884 | * @return A static string view naming the reason. | ||
| 885 | */ | ||
| 886 | [[nodiscard]] std::string_view gate_reason_to_string(GateReason reason) noexcept; | ||
| 887 | } // namespace manifest | ||
| 888 | } // namespace DetourModKit | ||
| 889 | |||
| 890 | #endif // DETOURMODKIT_MANIFEST_HPP | ||
| 891 |