GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 92.9% 52 / 0 / 56
Functions: 100.0% 4 / 0 / 4
Branches: 69.6% 32 / 0 / 46

src/internal/image_identity.cpp
Line Branch Exec Source
1 /**
2 * @file internal/image_identity.cpp
3 * @brief Guarded PE-header identity read shared by scan::image_identity and the RTTI generation token.
4 */
5
6 #include "internal/image_identity.hpp"
7
8 #include "internal/memory_guarded.hpp"
9 #include "internal/memory_representation_win32.hpp"
10
11 #include <windows.h>
12
13 #include <cstddef>
14 #include <cstring>
15 #include <optional>
16
17 namespace DetourModKit
18 {
19 namespace detail
20 {
21 namespace
22 {
23 // The Windows loader caps a PE at 96 sections; a larger count is corrupt and would otherwise let the
24 // section walk below run away.
25 constexpr std::uint32_t MAX_SECTIONS = 96;
26
27 // A wild e_lfanew is the signature of a forged or truncated header. The NT headers are re-bounded against
28 // SizeOfImage once that field has been read; this cap only keeps the first read off a wild address.
29 constexpr std::uint32_t MAX_NT_OFFSET = 0x100000;
30
31 41696 [[nodiscard]] constexpr std::uint64_t mix(std::uint64_t seed, std::uint64_t value) noexcept
32 {
33 41696 seed ^= value + 0x9E3779B97F4A7C15ULL + (seed << 6) + (seed >> 2);
34 41696 return seed;
35 }
36
37 // True when [offset, offset + bytes) lies wholly inside an image of image_size bytes, with an explicit
38 // wrap guard so a hostile offset/size cannot alias a low offset.
39 [[nodiscard]] constexpr bool
40 1200 fits_image(std::uint64_t offset, std::uint64_t bytes, std::uint32_t image_size) noexcept
41 {
42
3/4
✓ Branch 2 → 3 taken 1200 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 1199 times.
✓ Branch 3 → 5 taken 1 time.
1200 return bytes <= image_size && offset <= static_cast<std::uint64_t>(image_size) - bytes;
43 }
44 } // namespace
45
46 606 ImageIdentityFields image_identity_at(std::uintptr_t module_base) noexcept
47 {
48
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 606 times.
606 if (!is_plausible_ptr(module_base))
49 {
50 return ImageIdentityFields{};
51 }
52
53 606 const std::optional<IMAGE_DOS_HEADER> dos = guarded_read<IMAGE_DOS_HEADER>(module_base);
54
4/8
✓ Branch 7 → 8 taken 606 times.
✗ Branch 7 → 14 not taken.
✓ Branch 9 → 10 taken 606 times.
✗ Branch 9 → 14 not taken.
✓ Branch 11 → 12 taken 606 times.
✗ Branch 11 → 14 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 606 times.
1212 if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE || dos->e_lfanew <= 0 ||
55
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 606 times.
606 static_cast<std::uint32_t>(dos->e_lfanew) > MAX_NT_OFFSET)
56 {
57 return ImageIdentityFields{};
58 }
59
60 606 const auto nt_offset = static_cast<std::uint32_t>(dos->e_lfanew);
61 606 const std::optional<IMAGE_NT_HEADERS64> nt = guarded_read<IMAGE_NT_HEADERS64>(module_base + nt_offset);
62
1/2
✓ Branch 23 → 24 taken 606 times.
✗ Branch 23 → 30 not taken.
1212 if (!nt || nt->Signature != IMAGE_NT_SIGNATURE ||
63
1/2
✓ Branch 25 → 26 taken 606 times.
✗ Branch 25 → 30 not taken.
606 nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC ||
64
4/6
✓ Branch 21 → 22 taken 606 times.
✗ Branch 21 → 30 not taken.
✓ Branch 27 → 28 taken 606 times.
✗ Branch 27 → 30 not taken.
✓ Branch 32 → 33 taken 3 times.
✓ Branch 32 → 34 taken 603 times.
1818 nt->FileHeader.SizeOfOptionalHeader < sizeof(IMAGE_OPTIONAL_HEADER64) ||
65
2/2
✓ Branch 29 → 30 taken 3 times.
✓ Branch 29 → 31 taken 603 times.
606 nt->OptionalHeader.SizeOfImage == 0)
66 {
67 3 return ImageIdentityFields{};
68 }
69
70 // Every later address is bounded by the image size this header declares. Guarding contains a concurrent
71 // unmap fault, but does not pin the mapping; callers that authorize work across a transition revalidate.
72 603 const std::uint32_t image_size = nt->OptionalHeader.SizeOfImage;
73
1/2
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 38 taken 603 times.
603 if (!fits_image(nt_offset, sizeof(IMAGE_NT_HEADERS64), image_size))
74 {
75 return ImageIdentityFields{};
76 }
77
78 603 const std::uint32_t num_sections = nt->FileHeader.NumberOfSections;
79
4/4
✓ Branch 39 → 40 taken 598 times.
✓ Branch 39 → 41 taken 5 times.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 40 → 42 taken 597 times.
603 if (num_sections == 0 || num_sections > MAX_SECTIONS)
80 {
81 6 return ImageIdentityFields{};
82 }
83
84 // IMAGE_FIRST_SECTION: the section table starts immediately after the optional header, whose length is
85 // SizeOfOptionalHeader. Using sizeof(IMAGE_NT_HEADERS64) would misplace the table whenever that size
86 // differs from the compile-time struct size.
87 597 const std::uint64_t table_offset = static_cast<std::uint64_t>(nt_offset) +
88 597 offsetof(IMAGE_NT_HEADERS64, OptionalHeader) +
89 597 nt->FileHeader.SizeOfOptionalHeader;
90 597 const std::uint64_t table_bytes = static_cast<std::uint64_t>(num_sections) * sizeof(IMAGE_SECTION_HEADER);
91
2/2
✓ Branch 44 → 45 taken 1 time.
✓ Branch 44 → 46 taken 596 times.
597 if (!fits_image(table_offset, table_bytes, image_size))
92 {
93 1 return ImageIdentityFields{};
94 }
95
96 // One guarded read for the whole table rather than one per header keeps the fault-guard entry count bounded
97 // on a path that a warm TypeIdentity revalidates on every call. The buffer is the loader's own section cap,
98 // so it needs no heap.
99 alignas(alignof(IMAGE_SECTION_HEADER)) std::byte section_table[MAX_SECTIONS * sizeof(IMAGE_SECTION_HEADER)];
100
2/2
✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 49 taken 595 times.
596 if (!guarded_read_bytes(
101 module_base + static_cast<std::uintptr_t>(table_offset),
102 section_table,
103 static_cast<std::size_t>(table_bytes)
104 ))
105 {
106 1 return ImageIdentityFields{};
107 }
108
109 595 std::uint64_t digest = mix(0x0DDC0FFEEULL, static_cast<std::uint64_t>(num_sections));
110
2/2
✓ Branch 59 → 51 taken 10237 times.
✓ Branch 59 → 60 taken 595 times.
10832 for (std::uint32_t i = 0; i < num_sections; ++i)
111 {
112 10237 IMAGE_SECTION_HEADER section{};
113 10237 std::memcpy(
114 &section,
115 10237 section_table + static_cast<std::size_t>(i) * sizeof(IMAGE_SECTION_HEADER),
116 sizeof(section)
117 );
118 10237 std::uint64_t name = 0;
119
2/2
✓ Branch 53 → 52 taken 81896 times.
✓ Branch 53 → 54 taken 10237 times.
92133 for (std::size_t byte = 0; byte < IMAGE_SIZEOF_SHORT_NAME; ++byte)
120 {
121 81896 name |= static_cast<std::uint64_t>(section.Name[byte]) << (byte * 8);
122 }
123 10237 digest = mix(digest, name);
124 10237 digest = mix(digest, static_cast<std::uint64_t>(section.VirtualAddress));
125 10237 digest = mix(digest, static_cast<std::uint64_t>(section.Misc.VirtualSize));
126 10237 digest = mix(digest, static_cast<std::uint64_t>(section.Characteristics));
127 }
128
129 return ImageIdentityFields{
130 595 .timestamp = nt->FileHeader.TimeDateStamp,
131 .size_of_image = image_size,
132 .section_digest = digest,
133 .valid = true,
134 595 };
135 }
136
137 51 std::uint64_t image_generation_token(std::uintptr_t module_base) noexcept
138 {
139 51 const ImageIdentityFields fields = image_identity_at(module_base);
140
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 51 times.
51 if (!fields.valid)
141 {
142 return 0;
143 }
144 // The base separates two modules that carry byte-identical headers; the section digest separates
145 // layout-differing images the loader mapped at one base across an unload/reload.
146 51 std::uint64_t token = static_cast<std::uint64_t>(module_base);
147 51 token = mix(token, static_cast<std::uint64_t>(fields.size_of_image));
148 51 token = mix(token, static_cast<std::uint64_t>(fields.timestamp));
149 51 token = mix(token, fields.section_digest);
150
1/2
✓ Branch 8 → 9 taken 51 times.
✗ Branch 8 → 10 not taken.
51 return token == 0 ? 1 : token;
151 }
152 } // namespace detail
153 } // namespace DetourModKit
154