GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 39 / 0 / 39
Functions: 100.0% 4 / 0 / 4
Branches: 93.3% 28 / 0 / 30

src/x86_decode.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_X86_DECODE_HPP
2 #define DETOURMODKIT_X86_DECODE_HPP
3
4 #include "DetourModKit/memory.hpp"
5
6 #include <array>
7 #include <cstdint>
8 #include <cstring>
9 #include <optional>
10
11 namespace DetourModKit::detail
12 {
13 /**
14 * @brief Decodes an E9 rel32 near JMP at @p address and returns its absolute destination.
15 * @details Copies the candidate instruction bytes into a local buffer under a single SEH fault guard
16 * (seh_read_bytes), then inspects the copy. Read-then-test on a local buffer closes the time-of-check to
17 * time-of-use gap that is_readable + a raw dereference leaves open: the target page can change protection
18 * or unmap between the probe and the access, and these decoders run on the nested-hook detection and
19 * prologue-recovery paths where the page state is most likely to be in flux. A faulting read returns
20 * nullopt rather than taking down the host.
21 * @param address Absolute address of the candidate instruction.
22 * @return The absolute jump destination, or std::nullopt when the bytes are unreadable or the opcode is not E9.
23 */
24 12 [[nodiscard]] inline std::optional<std::uintptr_t> decode_e9_rel32(std::uintptr_t address) noexcept
25 {
26 12 std::array<std::uint8_t, 5> code{};
27
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 10 times.
24 if (!Memory::seh_read_bytes(address, code.data(), code.size()))
28 {
29 2 return std::nullopt;
30 }
31
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 9 times.
10 if (code[0] != 0xE9)
32 {
33 1 return std::nullopt;
34 }
35 9 std::int32_t disp = 0;
36 9 std::memcpy(&disp, code.data() + 1, sizeof(disp));
37 9 return static_cast<std::uintptr_t>(static_cast<std::int64_t>(address) + 5 + disp);
38 }
39
40 /**
41 * @brief Decodes an EB rel8 short JMP at @p address and returns its absolute destination.
42 * @details Copies the candidate instruction bytes into a local buffer under a single SEH fault guard
43 * (seh_read_bytes), then inspects the copy. Read-then-test on a local buffer closes the time-of-check to
44 * time-of-use gap that is_readable + a raw dereference leaves open: the target page can change protection
45 * or unmap between the probe and the access, and this decoder runs on hook-detection pre-flight paths
46 * (the VMT slot pre-flight) where the page state is most likely to be in flux. A faulting read returns
47 * nullopt rather than taking down the host.
48 * @param address Absolute address of the candidate instruction.
49 * @return The absolute jump destination (rel8 sign-extended), or std::nullopt when the bytes are unreadable or the
50 * opcode is not EB.
51 */
52 4 [[nodiscard]] inline std::optional<std::uintptr_t> decode_eb_rel8(std::uintptr_t address) noexcept
53 {
54 4 std::array<std::uint8_t, 2> code{};
55
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 3 times.
8 if (!Memory::seh_read_bytes(address, code.data(), code.size()))
56 {
57 1 return std::nullopt;
58 }
59
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 2 times.
3 if (code[0] != 0xEB)
60 {
61 1 return std::nullopt;
62 }
63 2 const auto disp = static_cast<std::int8_t>(code[1]);
64 2 return static_cast<std::uintptr_t>(static_cast<std::int64_t>(address) + 2 + disp);
65 }
66
67 /**
68 * @brief Decodes an FF 25 disp32 indirect JMP at @p address and returns the target stored in its memory slot.
69 * @details FF 25 disp32 on x86-64 is RIP-relative: the 32-bit signed displacement is added to the address of the
70 * next instruction. On x86 (32-bit) the same encoding is absolute, which this decoder does not handle.
71 * Copies the candidate instruction bytes into a local buffer under a single SEH fault guard
72 * (seh_read_bytes), then inspects the copy. Read-then-test on a local buffer closes the time-of-check to
73 * time-of-use gap that is_readable + a raw dereference leaves open: the target page can change protection
74 * or unmap between the probe and the access, and these decoders run on the nested-hook detection and
75 * prologue-recovery paths where the page state is most likely to be in flux. A faulting read returns
76 * nullopt rather than taking down the host.
77 * @param address Absolute address of the candidate instruction.
78 * @return The final indirect target read from the slot, or std::nullopt when the instruction bytes or the slot are
79 * unreadable or the opcode is not FF 25.
80 */
81 9 [[nodiscard]] inline std::optional<std::uintptr_t> decode_ff25_indirect(std::uintptr_t address) noexcept
82 {
83 static_assert(sizeof(void *) == 8, "decode_ff25_indirect assumes x86-64 RIP-relative semantics");
84 9 std::array<std::uint8_t, 6> code{};
85
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 8 times.
18 if (!Memory::seh_read_bytes(address, code.data(), code.size()))
86 {
87 1 return std::nullopt;
88 }
89
6/6
✓ Branch 10 → 11 taken 7 times.
✓ Branch 10 → 13 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 6 times.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 17 taken 6 times.
8 if (code[0] != 0xFF || code[1] != 0x25)
90 {
91 2 return std::nullopt;
92 }
93 6 std::int32_t disp = 0;
94 6 std::memcpy(&disp, code.data() + 2, sizeof(disp));
95 6 const auto slot_addr = static_cast<std::uintptr_t>(static_cast<std::int64_t>(address) + 6 + disp);
96 // The slot stores the final indirect target; read it under the same fault guard rather than is_readable + a raw
97 // dereference.
98 6 const auto indirect_destination = Memory::seh_read<std::uintptr_t>(slot_addr);
99
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 5 times.
6 if (!indirect_destination)
100 {
101 1 return std::nullopt;
102 }
103 5 return *indirect_destination;
104 }
105
106 /**
107 * @brief Decodes a `mov rax, imm64; jmp rax` absolute-jump pair at @p address and returns the imm64 destination.
108 * @details Some inline hooks emit this 12-byte absolute jump -- `48 B8 <imm64>` (REX.W mov rax, imm64) immediately
109 * followed by `FF E0` (jmp rax) -- when the detour trampoline is beyond rel32 reach and the hooking
110 * library does not use the FF 25 RIP-relative form. Unlike FF 25 the absolute target is the imm64 baked
111 * directly into the instruction, so no pointer-slot dereference is needed. Copies the candidate bytes
112 * into a local buffer under a single SEH fault guard (seh_read_bytes), then inspects the copy.
113 * Read-then-test on a local buffer closes the time-of-check to time-of-use gap that is_readable + a raw
114 * dereference leaves open: the target page can change protection or unmap between the probe and the
115 * access, and this decoder
116 * runs on the prologue-recovery path where the page state is most likely to be in flux. A faulting read
117 * returns nullopt rather than taking down the host.
118 * @param address Absolute address of the candidate instruction pair.
119 * @return The absolute jump destination (the imm64), or std::nullopt when the bytes are unreadable or the opcodes
120 * are not `48 B8 ... FF E0`.
121 */
122 6 [[nodiscard]] inline std::optional<std::uintptr_t> decode_mov_rax_imm64_jmp_rax(std::uintptr_t address) noexcept
123 {
124 static_assert(sizeof(void *) == 8, "decode_mov_rax_imm64_jmp_rax assumes a 64-bit absolute target");
125 6 std::array<std::uint8_t, 12> code{};
126
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 4 times.
12 if (!Memory::seh_read_bytes(address, code.data(), code.size()))
127 {
128 2 return std::nullopt;
129 }
130 // 48 B8 = REX.W + mov rax, imm64; bytes [2..9] hold the imm64; FF E0 = jmp rax.
131
8/10
✓ Branch 10 → 11 taken 4 times.
✗ Branch 10 → 17 not taken.
✓ Branch 12 → 13 taken 3 times.
✓ Branch 12 → 17 taken 1 time.
✓ Branch 14 → 15 taken 3 times.
✗ Branch 14 → 17 not taken.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 2 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 21 taken 2 times.
4 if (code[0] != 0x48 || code[1] != 0xB8 || code[10] != 0xFF || code[11] != 0xE0)
132 {
133 2 return std::nullopt;
134 }
135 2 std::uintptr_t destination = 0;
136 2 std::memcpy(&destination, code.data() + 2, sizeof(destination));
137 2 return destination;
138 }
139 } // namespace DetourModKit::detail
140
141 #endif // DETOURMODKIT_X86_DECODE_HPP
142