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 |