GCC Code Coverage Report


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

src/internal/memory_fault.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_INTERNAL_MEMORY_FAULT_HPP
2 #define DETOURMODKIT_INTERNAL_MEMORY_FAULT_HPP
3
4 /**
5 * @file memory_fault.hpp
6 * @brief Shared fault-handling primitives for SEH-guarded foreign-memory operations and scanner sweeps.
7 *
8 * Foreign reads, writes, compare-exchange operations, and protection-gated scanner sweeps all run inside a Structured
9 * Exception Handling frame on MSVC. Their filters must agree on which codes belong to an access within the guarded
10 * span. A single predicate keeps that rule consistent and unit-testable. The codes use numeric literals matching
11 * <winnt.h> so this header remains free of <windows.h>. These declarations are internal to the build and are never
12 * installed.
13 */
14
15 #include <cstdint>
16
17 #if defined(_MSC_VER)
18 // Forward-declared at global scope so this header stays free of <windows.h>. ::_EXCEPTION_POINTERS is the Win32 SEH
19 // structure GetExceptionInformation() yields; every translation unit that calls the filter includes <windows.h>,
20 // which supplies the full definition. Declaring the tag at global scope (not inside a namespace) keeps the parameter
21 // type identical to the pointer the __except call sites pass.
22 struct _EXCEPTION_POINTERS;
23 #endif
24
25 namespace DetourModKit
26 {
27 namespace detail
28 {
29 /**
30 * @brief True when @p exception_code is a fault a guarded foreign read may legitimately raise and must swallow
31 * (reporting read failure) rather than let escape and terminate the host.
32 * @details The accepted set, spelled as literals so this header needs no <windows.h>:
33 * - 0xC0000005 EXCEPTION_ACCESS_VIOLATION: the page is unmapped / PAGE_NOACCESS, or the access
34 * collided with a concurrent decommit / reprotect after the probe's readability gate passed.
35 * - 0x80000001 STATUS_GUARD_PAGE_VIOLATION: first touch of a PAGE_GUARD page.
36 * - 0xC0000006 EXCEPTION_IN_PAGE_ERROR: a file-backed or image-mapped page failed to page in
37 * (for example an RTTI / section walk of a module whose backing view was invalidated). Omitting this
38 * code would let the fault continue the handler search and terminate the host, violating the "false
39 * on any fault" contract every probe documents.
40 * Any other code (illegal instruction, breakpoint, stack overflow, ...) did not originate from the
41 * probe's own read, so the filter must continue the search and let the host's real handler see it.
42 * @param exception_code The value returned by GetExceptionCode() inside an __except filter.
43 * @return true to execute the handler (swallow and fail closed); false to continue the search.
44 */
45 214873 [[nodiscard]] constexpr bool is_guarded_read_fault(unsigned long exception_code) noexcept
46 {
47 return exception_code == 0xC0000005ul // EXCEPTION_ACCESS_VIOLATION
48
2/2
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 5 taken 13 times.
19 || exception_code == 0x80000001ul // STATUS_GUARD_PAGE_VIOLATION
49
4/4
✓ Branch 2 → 3 taken 19 times.
✓ Branch 2 → 5 taken 214854 times.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 5 times.
214892 || exception_code == 0xC0000006ul; // EXCEPTION_IN_PAGE_ERROR
50 }
51
52 #if defined(_MSC_VER)
53 /**
54 * @brief The __except filter every MSVC frame-based guarded foreign access uses, over a known [@p lo, @p hi)
55 * span.
56 * @param info The EXCEPTION_POINTERS from GetExceptionInformation() (valid only inside a filter expression).
57 * @param lo First byte of the declared foreign range the operation is permitted to fault inside.
58 * @param hi One past the last byte of that range.
59 * @param fault_address Optional output assigned only when the fault is claimed.
60 * @return EXCEPTION_EXECUTE_HANDLER only when the fault is a guarded-read fault whose faulting address lies in
61 * [@p lo, @p hi) and a consumed PAGE_GUARD was re-armed; EXCEPTION_CONTINUE_SEARCH otherwise.
62 * @details Screening the faulting address is what keeps a fault OUTSIDE the declared span reaching the host's
63 * handlers instead of being silently swallowed. An outside fault is an unrelated DMK defect that
64 * happens to occur inside the __try, or a fault on the caller-owned source/destination buffer rather
65 * than the foreign target. This matches the MinGW vectored handler, which arms only [lo, hi) and
66 * passes through a fault outside it. Re-arming a PAGE_GUARD the OS cleared on dispatch, before the
67 * access fails closed, is what stops a swallowed foreign guard-page fault from leaving the host's
68 * fence disarmed. Routing every MSVC probe (the memory engine's read / write / chain walk and the
69 * scanner's region / window sweeps) through this one entry keeps that behavior identical across them.
70 * A record carrying no faulting address, or a guard-page fault whose fence cannot be restored, is
71 * never claimed. Declared MSVC-only because MinGW has no frame-based SEH.
72 */
73 long guarded_range_fault_filter(
74 ::_EXCEPTION_POINTERS *info,
75 std::uintptr_t lo,
76 std::uintptr_t hi,
77 volatile std::uintptr_t *fault_address = nullptr
78 ) noexcept;
79 #endif
80
81 #if !defined(_MSC_VER) && defined(_WIN64)
82 /**
83 * @brief Runs @p fn(@p ctx) with the process-wide vectored read guard armed over [@p lo, @p hi).
84 * @details MinGW x64 has no frame-based __try / __except, so a bulk in-place foreign read (the scanner's
85 * memchr / SIMD region sweep) cannot wrap itself in SEH the way the MSVC path does. This routes such
86 * a read through the same vectored exception handler, thread-local guard slot, and drain epoch the
87 * guarded byte-copy path uses: the guard is armed for [lo, hi), @p fn performs the read, and a guarded
88 * read fault (is_guarded_read_fault) inside that range is turned into a clean failure (the handler
89 * longjmps back) instead of terminating the host. @p fn must be a self-contained read with no
90 * resources that need unwinding, because a guarded fault abandons its frame via __builtin_longjmp
91 * without running destructors. That is exactly the contract the copy-based guard relies on. When the
92 * handler cannot be installed @p fn is not run. Callers treat false as a skipped/faulted range and
93 * fail uniqueness-sensitive work closed.
94 * @param lo First byte of the foreign range @p fn will read.
95 * @param hi One past the last byte of that range. An empty or wrapping range (hi <= lo) runs @p fn directly
96 * because there is no foreign byte span to guard.
97 * @param fn The read to perform; must be noexcept and must not throw.
98 * @param ctx Opaque pointer forwarded to @p fn.
99 * @return true if @p fn completed without a guarded read fault; false if a fault inside [lo, hi) was swallowed,
100 * in which case @p fn did not run to completion.
101 */
102 [[nodiscard]] bool
103 run_guarded_region(std::uintptr_t lo, std::uintptr_t hi, void (*fn)(void *) noexcept, void *ctx) noexcept;
104 #endif
105 } // namespace detail
106 } // namespace DetourModKit
107
108 #endif // DETOURMODKIT_INTERNAL_MEMORY_FAULT_HPP
109