GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 1 / 0 / 1
Functions: 100.0% 1 / 0 / 1
Branches: -% 0 / 0 / 0

include/DetourModKit/rtti.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_RTTI_HPP
2 #define DETOURMODKIT_RTTI_HPP
3
4 #include "DetourModKit/region.hpp"
5
6 #include <atomic>
7 #include <cstddef>
8 #include <cstdint>
9 #include <optional>
10 #include <string>
11 #include <string_view>
12
13 namespace DetourModKit
14 {
15 /**
16 * @namespace DetourModKit::rtti
17 * @brief MSVC RTTI introspection primitives.
18 * @details Walks the x64 MSVC COL/TypeDescriptor layout to recover the mangled type-descriptor name for a runtime
19 * object. The implementation operates on raw addresses and never invokes typeid() or dynamic_cast, so it
20 * works across DLL boundaries against third-party MSVC binaries. Every entry point except the allocating
21 * TypeIdentity constructor is noexcept and guarded: an unreadable page, missing COL, or zero RVA produces
22 * a failure return. Names are returned in
23 * the MSVC mangled form (for example ".?AVMyClass@ns@@") for exact byte-equal comparison.
24 *
25 * When the host binary is compiled with RTTI disabled (/GR-), the TypeDescriptor records are not emitted
26 * and every RTTI-based resolver returns its fail-closed sentinel rather than a fault or a wrong answer.
27 * The raw-byte fallbacks are @ref scan::find_string_xref and @ref scan::read_code_constant. Only
28 * @ref RttiPresence::Absent proves a complete records-free sweep. The failure-mode discussion is in
29 * docs/guides/rtti/rtti-walker.md and docs/guides/rtti/rtti-self-heal.md.
30 * @warning `[B-100]` Under the loader lock, call @ref TypeIdentity::matches only after it is warm, or use another
31 * Callback-safe entry point. Cold identity paths and setup routes can query the loader or scan an image.
32 */
33 namespace rtti
34 {
35 /// Default cap on the mangled-name length read into a heap-allocated string.
36 inline constexpr std::size_t DEFAULT_TYPE_NAME_MAX = 256;
37
38 /// Hard upper bound on any single mangled-name read.
39 inline constexpr std::size_t MAX_TYPE_NAME_LEN = 1024;
40
41 /**
42 * @enum Traversal
43 * @brief Completeness of a reverse-RTTI section/page sweep.
44 * @details A reverse resolver answers "is there a unique vtable for this type" or "does this scope hold any
45 * record" by sweeping the module's readable non-executable sections. A verdict that depends on having
46 * seen the WHOLE image, such as a unique vtable or an authoritative absence, is trustworthy only under
47 * @ref Complete. A truncated sweep can hide a second primary (false uniqueness) or the only record
48 * (false absence), so the checked reverse forms surface this rather than reporting a positive prefix
49 * as final.
50 */
51 enum class Traversal : std::uint8_t
52 {
53 /// Every qualifying section was enumerated and every page in it was read.
54 Complete = 0,
55 /** @brief The sweep under-covered the image, so a unique or absent verdict cannot be authorized. */
56 Incomplete = 1,
57 /** @brief The internal fixed buffer filled, so unseen qualifying sections or matches may exist. */
58 Saturated = 2
59 };
60
61 /**
62 * @enum NameStatus
63 * @brief Outcome of a checked mangled-name read (@ref type_name_checked).
64 */
65 enum class NameStatus : std::uint8_t
66 {
67 /// The full NUL-terminated name was copied.
68 Ok = 0,
69 /** @brief The NUL-terminated copy is a proper prefix and must not be compared for identity. */
70 Truncated = 1,
71 /// No name was read (null/low vtable, missing or forged COL, unreadable page).
72 Failed = 2
73 };
74
75 /**
76 * @struct NameRead
77 * @brief Result of @ref type_name_checked: bytes written plus whether the copy is the complete name.
78 */
79 struct NameRead
80 {
81 /// Name bytes written excluding the NUL terminator.
82 std::size_t written = 0;
83 /// Whether the copy is complete, a truncated prefix, or a failure.
84 NameStatus status = NameStatus::Failed;
85 };
86
87 /**
88 * @struct VtablesResult
89 * @brief Result of @ref vtables_for_type_checked: the match count plus the sweep completeness.
90 */
91 struct VtablesResult
92 {
93 /// Distinct matching sub-object vtables found (the same value @ref vtables_for_type returns).
94 std::size_t count = 0;
95 /** @brief Sweep completeness; under Incomplete or Saturated, count is only a floor. */
96 Traversal completeness = Traversal::Complete;
97 };
98
99 /**
100 * @enum RttiPresence
101 * @brief Trit answer of @ref region_rtti_presence, separating an authoritative absence from an incomplete
102 * sweep.
103 */
104 enum class RttiPresence : std::uint8_t
105 {
106 /// At least one resolvable RTTI record was found (sound regardless of completeness: a hit is a hit).
107 Present = 0,
108 /// The sweep completed and found no record: an authoritative absence (an MSVC /GR- scope, a data module).
109 Absent = 1,
110 /** @brief The sweep did not complete, so absence cannot be concluded. */
111 Incomplete = 2
112 };
113
114 /**
115 * @brief Reads the MSVC RTTI mangled type-descriptor name for the object whose runtime vtable is at @p vtable.
116 * @details Walks vtable[-1] to the COL, the TypeDescriptor RVA, and the zero-terminated mangled name. The
117 * @c col.pSelf cross-check rejects a forged or relocated COL, and any signature other than the x64
118 * value is rejected. Reads are page-bounded and guarded. The first NUL terminates the result.
119 * @param vtable Runtime vtable pointer (the first qword of the object).
120 * @param max_len Maximum mangled-name length to copy; clamped to @ref MAX_TYPE_NAME_LEN. Zero is replaced with
121 * @ref DEFAULT_TYPE_NAME_MAX.
122 * @return The mangled name on success, std::nullopt on any failure (null vtable, unmapped page, missing COL,
123 * bad RVA, allocation failure).
124 * @note Performs one heap allocation for the returned std::string. For per-frame identity probes use @ref
125 * vtable_is_type or @ref type_name_into to avoid the allocation.
126 * @note Setup/control-plane only: the read allocates and runs the loader-querying COL prelude.
127 */
128 [[nodiscard]] std::optional<std::string>
129 type_name_of(Address vtable, std::size_t max_len = DEFAULT_TYPE_NAME_MAX) noexcept;
130
131 /**
132 * @brief Zero-allocation form of @ref type_name_of.
133 * @details Writes the mangled name into @p out (always NUL-terminated when @p out_len > 0) and returns the
134 * number of bytes written excluding the terminator. On any failure the output buffer's first byte is
135 * set to '\0' and 0 is returned.
136 * @param vtable Runtime vtable pointer (the first qword of the object).
137 * @param out Destination buffer. Must be non-null when @p out_len > 0.
138 * @param out_len Capacity of @p out including the NUL terminator. The function never writes more than @p
139 * out_len bytes.
140 * @return Number of name bytes written (excluding the NUL terminator), or 0 on failure or empty output.
141 * @note Setup/control-plane only: each call runs the COL prelude, which queries the loader. Cache a
142 * @ref TypeIdentity for a per-frame check.
143 */
144 [[nodiscard]] std::size_t type_name_into(Address vtable, char *out, std::size_t out_len) noexcept;
145
146 /**
147 * @brief Truncation-reporting form of @ref type_name_into.
148 * @details Writes the mangled name into @p out exactly as @ref type_name_into, but reports
149 * @ref NameStatus::Truncated whenever the real name did not fit @p out or the @ref MAX_TYPE_NAME_LEN
150 * hard cap, so an identity comparison can reject a truncated read instead of matching a prefix.
151 * @param vtable Runtime vtable pointer (the first qword of the object).
152 * @param out Destination buffer; always NUL-terminated when @p out_len > 0. Must be non-null when @p
153 * out_len > 0.
154 * @param out_len Capacity of @p out including the NUL terminator.
155 * @return @ref NameRead::written name bytes (excluding the NUL) and a @ref NameRead::status of @ref
156 * NameStatus::Ok (complete), @ref NameStatus::Truncated (a prefix; do not compare for identity), or
157 * @ref NameStatus::Failed (nothing read; @p out is left empty).
158 * @note Setup/control-plane only (see @ref type_name_into).
159 */
160 [[nodiscard]] NameRead type_name_checked(Address vtable, char *out, std::size_t out_len) noexcept;
161
162 /**
163 * @brief A stable, mapping-scoped identity token for the module currently mapped over @p addr.
164 * @details Folds the image base, SizeOfImage, PE TimeDateStamp, and the section table into a 64-bit token,
165 * read through the guarded engine. The token is stable while one image stays mapped and changes when
166 * a same-base replacement changes an identity-bearing PE header field. It carries the same
167 * discrimination as @ref scan::image_identity. @ref TypeIdentity keys its cached resolve on it, and a
168 * @ref HealedOffset consumer compares @ref HealedOffset::generation against it.
169 * @param addr Any address inside the module of interest (typically a module base or a live object pointer).
170 * @return A nonzero identity token for a module-backed address; 0 when @p addr is not inside any loaded module
171 * (an unmapped address or a private @c VirtualAlloc buffer carries no module-backed identity to track).
172 * @note Setup/control-plane only: resolves the owning module through the loader before reading its PE header.
173 * @warning Like @ref scan::image_identity, this is layout identity rather than content identity. A replacement
174 * that preserves every folded header field while changing only section bytes remains invisible.
175 */
176 [[nodiscard]] std::uint64_t image_generation(Address addr) noexcept;
177
178 /**
179 * @brief Tests whether the MSVC RTTI mangled name for @p vtable equals @p expected exactly.
180 * @details Performs a byte-exact comparison of the mangled name plus the terminating NUL, rejecting both proper
181 * prefix and substring matches. The read is bounded by the length of @p expected plus one byte, so no
182 * allocation occurs and the per-call cost is dominated by the SEH-guarded read of @p expected.size() +
183 * 1 bytes from the name buffer.
184 * @param vtable Runtime vtable pointer.
185 * @param expected Mangled name to compare against. Must be non-empty and shorter than @ref MAX_TYPE_NAME_LEN.
186 * @return true on exact match; false on mismatch, on any read failure, or when @p expected is empty or
187 * oversized.
188 * @note Setup/control-plane only: each call runs the COL prelude, which queries the loader.
189 * @ref TypeIdentity::matches is the per-frame route.
190 */
191 [[nodiscard]] bool vtable_is_type(Address vtable, std::string_view expected) noexcept;
192
193 /**
194 * @class PointerTableCache
195 * @brief Generation-bearing cache for repeated @ref find_in_pointer_table calls with one expected type.
196 * @details Stores the resolved vtable together with its image base and generation. Concurrent reads are
197 * supported; publication is non-blocking, and a competing publisher leaves the existing snapshot for
198 * the next call to validate.
199 */
200 class PointerTableCache
201 {
202 public:
203 /// Constructs an empty cache.
204 PointerTableCache() noexcept = default;
205 PointerTableCache(const PointerTableCache &) = delete;
206 PointerTableCache &operator=(const PointerTableCache &) = delete;
207 PointerTableCache(PointerTableCache &&) = delete;
208 PointerTableCache &operator=(PointerTableCache &&) = delete;
209 ~PointerTableCache() noexcept = default;
210
211 /**
212 * @brief Clears the cached identity so the next lookup starts cold.
213 * @note Setup/control-plane only: waits for an in-progress cache publication to finish.
214 */
215 void reset() noexcept;
216
217 private:
218 friend std::optional<Address> find_in_pointer_table(
219 Address table,
220 std::size_t slot_count,
221 std::string_view expected,
222 PointerTableCache &cache,
223 std::size_t stride
224 ) noexcept;
225
226 // Single-writer sequence protects a coherent {vtable, image base, generation} snapshot.
227 std::atomic_flag m_writer{};
228 std::atomic<std::uint32_t> m_seq{0};
229 std::atomic<Address> m_vtable{Address{}};
230 std::atomic<Address> m_image_base{Address{}};
231 std::atomic<std::uint64_t> m_generation{0};
232 // Advanced by reset() so a lookup that started earlier cannot publish across the reset boundary.
233 std::atomic<std::uint64_t> m_epoch{0};
234 };
235
236 /**
237 * @brief Scans a pointer-table for the first slot whose object has the given RTTI type-descriptor name.
238 * @details Treats @p table as an array of @p slot_count entries each @p stride bytes wide. A cold cache (or a
239 * nullptr @p vtable_cache) walks RTTI per slot via @ref vtable_is_type. A warm cache compares each
240 * slot against the cached vtable. If no slot carries it, the stale value is cleared and one cold pass
241 * runs. A cold-path match refreshes @p vtable_cache. The cache shape is one std::atomic<Address> per
242 * expected name. A null Address encodes "cold".
243 * @param table Base address of the pointer table.
244 * @param slot_count Number of slots to scan.
245 * @param expected Mangled name to match.
246 * @param vtable_cache Optional caller-owned cache slot. Pass nullptr to skip caching (every call walks RTTI).
247 * @param stride Byte distance between adjacent slot addresses. Defaults to sizeof(std::uintptr_t) for a packed
248 * pointer array; pass a larger stride for tables that interleave per-slot metadata between
249 * pointers.
250 * @return The object pointer (the value stored in the slot) on first match, or std::nullopt if no slot matched.
251 * @note The cold path walks RTTI for each slot. A warm cache costs two guarded reads and one compare per slot.
252 * @warning The warm-cache path assumes one canonical vtable address per expected name. If multiple derived
253 * concrete classes share the same base-mangled name and the table holds a mix of them, only slots
254 * whose vtable equals the first-resolved instance are returned on the warm path. Other matches are
255 * skipped. For MSVC RTTI this is correct: mangled names encode the most-derived class, not the base.
256 * @warning This compatibility overload's raw atomic carries no image generation. Clear it at module-lifecycle
257 * boundaries, or use the @ref PointerTableCache overload for generation-checked caching.
258 * @note Callback-safe on the warm-cache path (guarded reads and compares). A cold or stale cache walks RTTI
259 * through the loader-querying prelude, which is setup/control-plane work.
260 */
261 [[nodiscard]] std::optional<Address> find_in_pointer_table(
262 Address table,
263 std::size_t slot_count,
264 std::string_view expected,
265 std::atomic<Address> *vtable_cache = nullptr,
266 std::size_t stride = sizeof(std::uintptr_t)
267 ) noexcept;
268
269 /**
270 * @brief Generation-checked overload of @ref find_in_pointer_table.
271 * @details A warm snapshot is accepted only while its image generation remains current. A warm call reads the
272 * image-generation token twice, once before the slot sweep and once before it returns. A stale
273 * snapshot is cleared and cold-resolved. Publication revalidates the type and generation before it
274 * caches them.
275 * @param table Base address of the pointer table.
276 * @param slot_count Number of slots to scan.
277 * @param expected Mangled name to match; one cache instance is dedicated to one expected name.
278 * @param cache Caller-owned generation-bearing cache.
279 * @param stride Byte distance between adjacent slot addresses.
280 * @return The first matching object pointer, or std::nullopt.
281 * @note Prefer this overload when the cache survives module unload/reload boundaries.
282 * @note Callback-safe on the warm-cache path; a cold or stale cache is setup/control-plane work (see the
283 * compatibility overload).
284 */
285 [[nodiscard]] std::optional<Address> find_in_pointer_table(
286 Address table,
287 std::size_t slot_count,
288 std::string_view expected,
289 PointerTableCache &cache,
290 std::size_t stride = sizeof(std::uintptr_t)
291 ) noexcept;
292
293 /**
294 * @brief Resolves the primary (most-derived) vtable for a class by its
295 * MSVC mangled name, scoped to one module image.
296 * @details Sweeps the module's readable, non-executable sections for a COL whose TypeDescriptor name equals
297 * @p mangled and whose COL.offset is 0, and returns the vtable that points back to that COL. Every
298 * candidate passes the same COL prelude the forward walker uses, so a forged or coincidental match is
299 * rejected. COL.offset == 0 selects the most-derived instance's vtable. For a class used only as a
300 * secondary or virtual base, use @ref vtables_for_type.
301 * @param mangled Exact MSVC mangled name (e.g. ".?AVMyClass@ns@@").
302 * @param range Module image to search. Defaults to the host EXE. The scope is required because the same mangled
303 * name can appear in several loaded modules and COL RVAs are image-base-relative.
304 * @return The primary vtable on a unique match; std::nullopt on absence, invalid scope, ambiguous primaries, or
305 * incomplete traversal. A partial sweep cannot authorize uniqueness because a second primary may be in
306 * the un-swept region. Use @ref vtables_for_type_checked to distinguish absence from an incomplete
307 * traversal.
308 * @note Setup/control-plane only: it sweeps the module's readable sections, so run it once at init (or behind a
309 * cached @ref TypeIdentity), never per-frame.
310 */
311 [[nodiscard]] std::optional<Address>
312 vtable_for_type(std::string_view mangled, Region range = Region::host()) noexcept;
313
314 /**
315 * @brief Collects every sub-object vtable sharing a class's mangled name.
316 * @details Multiple or virtual inheritance gives one class several COLs, one per base sub-object, each
317 * referenced by its own vtable. This returns all of them. Each match is validated through the COL
318 * prelude exactly as @ref vtable_for_type.
319 * @param mangled Exact MSVC mangled name.
320 * @param out Destination buffer for the matching vtable addresses, written in ascending COL.offset order (the
321 * primary, offset 0, first). May be nullptr only when @p out_cap is 0 (count-only query).
322 * @param out_cap Capacity of @p out; at most @p out_cap addresses are written even when more matches exist.
323 * @param range Module image to search. Defaults to the host EXE.
324 * @return Number of distinct matching vtables found (capped at an internal upper bound that far exceeds any
325 * real inheritance graph). A return value greater than @p out_cap signals the output was truncated.
326 * An incomplete or saturated sweep makes the count a lower bound; a caller that needs an authoritative
327 * total uses @ref vtables_for_type_checked.
328 * @note Setup/control-plane only (see @ref vtable_for_type).
329 */
330 [[nodiscard]] std::size_t vtables_for_type(
331 std::string_view mangled,
332 Address *out,
333 std::size_t out_cap,
334 Region range = Region::host()
335 ) noexcept;
336
337 /**
338 * @brief Completeness-reporting form of @ref vtables_for_type.
339 * @param mangled Exact MSVC mangled name.
340 * @param out Destination buffer for the matching vtable addresses, ascending COL.offset order (primary first).
341 * May be nullptr only when @p out_cap is 0 (count-only query).
342 * @param out_cap Capacity of @p out; at most @p out_cap addresses are written even when more matches exist.
343 * @param range Module image to search. Defaults to the host EXE.
344 * @return The distinct-match @ref VtablesResult::count (a @ref VtablesResult::completeness other than @ref
345 * Traversal::Complete means the count is a floor, not the authoritative total).
346 * @note Setup/control-plane only (see @ref vtable_for_type).
347 */
348 [[nodiscard]] VtablesResult vtables_for_type_checked(
349 std::string_view mangled,
350 Address *out,
351 std::size_t out_cap,
352 Region range = Region::host()
353 ) noexcept;
354
355 /**
356 * @brief Reports whether a module region currently contains any resolvable MSVC RTTI record.
357 * @details Sweeps @p range for any COL that passes the reverse resolver's validation checks. The two answers
358 * are asymmetric:
359 * - true is sound but only proves SOME record exists, not that the caller's type resolves (a /GR-
360 * executable that links a /GR CRT returns true off those library COLs);
361 * - false means "no record was found in what was swept" and is not by itself proof of absence. Use
362 * @ref region_rtti_presence when absence versus an incomplete sweep matters.
363 * @param range Module image to inspect. Defaults to the host EXE.
364 * @return true if @p range holds at least one resolvable RTTI record; false if none was found in the swept
365 * portion or @p range is not a valid mapped image.
366 * @note Setup/control-plane only (see @ref vtable_for_type). It carries no re-sweep throttle, so a
367 * records-free scope pays a full sweep on every call.
368 * @note An absent verdict on a still-packed image is a transient truth about the CURRENT mapping, not proof the
369 * binary was built /GR-; re-inspect after the image unpacks rather than caching the result as permanent.
370 */
371 [[nodiscard]] bool region_has_rtti(Region range = Region::host()) noexcept;
372
373 /**
374 * @brief Completeness-reporting form of @ref region_has_rtti.
375 * @param range Module image to inspect. Defaults to the host EXE.
376 * @return @ref RttiPresence::Present, @ref RttiPresence::Absent, or @ref RttiPresence::Incomplete. An invalid
377 * @p range reports Incomplete.
378 * @note Setup/control-plane only (see @ref vtable_for_type).
379 */
380 [[nodiscard]] RttiPresence region_rtti_presence(Region range = Region::host()) noexcept;
381
382 /**
383 * @brief Cached, self-healing, generation-aware identity handle for a class vtable.
384 * @details Resolves the primary vtable for a mangled name lazily via @ref vtable_for_type and caches it. A
385 * module-backed resolve is published only when the image generation is stable across the sweep. The
386 * warm path re-validates that stamp on every call and refreshes the full module extent after a remap.
387 * @ref invalidate forces an immediate cold resolve. A private-buffer scope has no module generation
388 * and must be reset explicitly.
389 * @note Take identity from the cached vtable ADDRESS (the vtable[-1]
390 * COL-anchored value), never from the vtable's slot contents: under the MSVC linker's identical-COMDAT
391 * folding (/OPT:ICF) two distinct classes can share folded function-pointer slots, so a slot-content
392 * comparison is not class-unique.
393 * @note Owns its mangled name (a private std::string copy), so no lifetime coupling to the caller's buffer.
394 * Non-copyable and non-movable. Hold it as a static or a long-lived member.
395 */
396 class TypeIdentity
397 {
398 public:
399 /**
400 * @brief Constructs a cached identity for @p mangled, scoped to @p range.
401 * @details Construction allocates the owned name copy and can throw std::bad_alloc.
402 * @param mangled Exact MSVC mangled name. Copied into owned storage.
403 * @param range Module image to resolve in. Defaults to the host EXE.
404 * @note Setup/control-plane only: cache construction allocates.
405 */
406 explicit TypeIdentity(std::string_view mangled, Region range = Region::host());
407
408 TypeIdentity(const TypeIdentity &) = delete;
409 TypeIdentity &operator=(const TypeIdentity &) = delete;
410 TypeIdentity(TypeIdentity &&) = delete;
411 TypeIdentity &operator=(TypeIdentity &&) = delete;
412 14 ~TypeIdentity() noexcept = default;
413
414 /**
415 * @brief Tests whether @p vtable is this type's primary vtable.
416 * @details Resolves on first call, then compares. Returns false when the type cannot be resolved, so a
417 * missing type never matches.
418 * @param vtable Candidate vtable (an object's first qword).
419 * @return true when @p vtable equals the resolved primary vtable.
420 * @note Callback-safe once warm: the generation check performs bounded guarded PE-header reads; a changed
421 * image triggers a setup-cost resolve.
422 */
423 [[nodiscard]] bool matches(Address vtable) const noexcept;
424
425 /**
426 * @brief Returns the resolved primary vtable, resolving on first use.
427 * @return The vtable address, or std::nullopt if it cannot be resolved in the configured module range.
428 * @note Callback-safe once warm: the first call resolves (a setup-cost module sweep), and a successful
429 * result is cached. An unresolved result is not cached, but the re-sweep is throttled to at most
430 * once per internal cooldown, so per-frame polling for an absent type does not re-scan the module
431 * each frame.
432 */
433 [[nodiscard]] std::optional<Address> vtable() const noexcept;
434
435 /**
436 * @brief Drops the cached resolve so the next @ref vtable / @ref matches re-resolves from scratch.
437 * @details Idempotent and safe to call at any time. Use it when a consumer knows the resolving module was
438 * unloaded or reloaded. Does not change the mangled name or range the handle was constructed with.
439 * @note Setup/control-plane only: waits for an in-progress cache publication to finish; never throws.
440 */
441 void invalidate() noexcept;
442
443 private:
444 std::string m_mangled;
445 Region m_range;
446 bool m_tracks_module_range{false};
447
448 // m_cached holds the resolved primary vtable and is written only on a SUCCESSFUL (non-null) resolve.
449 // m_resolved latches that success and is published with release after m_cached is stored, so an
450 // acquire-load that observes m_resolved == true also observes the cached value. A failed resolve latches
451 // neither flag, so a later call retries once the type becomes resolvable instead of caching the miss as
452 // permanent.
453 mutable std::atomic<Address> m_cached{Address{}};
454 mutable std::atomic<bool> m_resolved{false};
455
456 // The resolving module's image_generation at the last successful resolve (0 = none, or a non-module range).
457 // The warm path re-reads the current generation and drops the cache when it differs, so an unload or a
458 // detectable same-base remap invalidates the cached vtable instead of matching against a module that is
459 // no longer mapped.
460 mutable std::atomic<std::uint64_t> m_image_stamp{0};
461 mutable std::atomic<Address> m_image_base{Address{}};
462
463 // Serializes the short publish/clear transaction; the RTTI sweep itself runs without holding it.
464 mutable std::atomic_flag m_cache_writer{};
465 // Incremented whenever the cache is cleared so a resolve already in flight cannot republish afterward.
466 mutable std::atomic<std::uint64_t> m_cache_epoch{0};
467
468 // Millisecond timestamp of the last resolve attempt that controls a later retry (0 = never). It bounds
469 // whole-module retries. Successful warm calls do not modify it.
470 mutable std::atomic<std::uint64_t> m_last_attempt_ms{0};
471 };
472 } // namespace rtti
473 } // namespace DetourModKit
474
475 #endif // DETOURMODKIT_RTTI_HPP
476