Skip to content

Commit ea8b1be

Browse files
committed
test(pack): rewrite the PE fixture plainly, and make it say where it dies
Splitting the test localised the macOS ARM64 SIGSEGV to ThePeFixtureItselfIsWellFormed — fixture code that calls no module at all. So it is not mcpp.pack.binfmt, and the three probes that came back clean (ASan + UBSan under clang 22 + libc++, the same module compiled by clang on x86_64 Linux, gcc everywhere) were looking in the right place for the wrong thing. The fixture used two `std::span` parameters and two lambdas that mutated a captured string through a captured cursor. It now takes vectors, indexes explicitly, and captures nothing — and traces each phase to stderr, so if it moves again the log names the step instead of costing another CI round. `#include <cstdio>` is not redundant next to `import std;`: `stderr` is a macro, and a module cannot export one.
1 parent a3b05a3 commit ea8b1be

1 file changed

Lines changed: 75 additions & 47 deletions

File tree

tests/unit/test_pack_binfmt.cpp

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <gtest/gtest.h>
2+
#include <cstdio> // stderr is a MACRO — `import std;` cannot export it
23

34
import std;
45
import mcpp.pack.binfmt;
@@ -104,87 +105,121 @@ std::string elf_with_needed(std::span<const std::string_view> needed) {
104105

105106
// ─── a minimal PE32+ image ──────────────────────────────────────────────
106107
//
107-
// One section mapping RVA 0x1000 → file 0x200, an import directory and a
108+
// One section mapping RVA 0x1000 → file 0x400, an import directory and a
108109
// delay-import directory inside it.
109-
std::string pe_with_imports(std::span<const std::string_view> imports,
110-
std::span<const std::string_view> delayImports) {
111-
constexpr std::size_t kNt = 0x40;
112-
constexpr std::size_t kOptSize = 0xF0; // 112 + 16 directories * 8
113-
constexpr std::size_t kSecAt = kNt + 24 + kOptSize;
114-
constexpr std::size_t kRawAt = 0x400;
115-
constexpr std::uint32_t kSecVa = 0x1000;
110+
//
111+
// WRITTEN AS PLAINLY AS POSSIBLE, and the breadcrumbs are not decoration.
112+
// The first version used two `std::span` parameters and two lambdas that
113+
// mutated a captured string through a captured cursor; it segfaulted on the
114+
// macOS ARM64 runner and NOWHERE else — not under ASan+UBSan with clang 22 +
115+
// libc++, not as a clang module on x86_64 Linux, not under gcc. Splitting the
116+
// test proved the crash is HERE, in fixture code that touches no module at
117+
// all, so the shapes went and the trace stayed: if it moves again, the log
118+
// says which step.
119+
void trace(const char* step, const std::string& b) {
120+
std::fprintf(stderr, "[pe-fixture] %-14s size=%zu\n", step, b.size());
121+
std::fflush(stderr);
122+
}
116123

117-
std::string b;
118-
b.resize(kRawAt, '\0');
124+
std::string pe_with_imports(const std::vector<std::string>& imports,
125+
const std::vector<std::string>& delayImports) {
126+
const std::size_t kNt = 0x40;
127+
const std::size_t kOptSize = 0xF0; // 112 + 16 directories * 8
128+
const std::size_t kSecAt = kNt + 24 + kOptSize;
129+
const std::size_t kRawAt = 0x400;
130+
const std::uint32_t kSecVa = 0x1000;
131+
const std::size_t dirsAt = kNt + 24 + 112;
132+
133+
std::string b(kRawAt, '\0');
134+
trace("start", b);
119135
b[0] = 'M'; b[1] = 'Z';
120136
put(b, 0x3C, kNt, 4);
121137
b[kNt] = 'P'; b[kNt + 1] = 'E'; // "PE\0\0"
122138
put(b, kNt + 4, 0x8664, 2); // Machine = AMD64
123139
put(b, kNt + 6, 1, 2); // NumberOfSections
124140
put(b, kNt + 20, kOptSize, 2); // SizeOfOptionalHeader
125141
put(b, kNt + 24, 0x20b, 2); // PE32+
126-
const std::size_t dirsAt = kNt + 24 + 112;
127142
put(b, dirsAt - 4, 16, 4); // NumberOfRvaAndSizes
143+
trace("headers", b);
128144

129145
// Section header: name, VirtualSize, VirtualAddress, SizeOfRawData,
130146
// PointerToRawData.
131-
const std::string_view secName = ".rdata";
147+
const std::string secName = ".rdata";
132148
for (std::size_t i = 0; i < secName.size(); ++i) b[kSecAt + i] = secName[i];
133149
put(b, kSecAt + 8, 0x1000, 4);
134150
put(b, kSecAt + 12, kSecVa, 4);
135151
put(b, kSecAt + 16, 0x1000, 4);
136152
put(b, kSecAt + 20, kRawAt, 4);
137-
138-
auto rva_of = [&](std::size_t fileOff) {
139-
return static_cast<std::uint32_t>(kSecVa + (fileOff - kRawAt));
140-
};
153+
trace("section", b);
141154

142155
// Names first, so the descriptors can point at them.
143-
std::vector<std::uint32_t> importNameRvas, delayNameRvas;
156+
std::vector<std::uint32_t> importNameRvas;
157+
std::vector<std::uint32_t> delayNameRvas;
144158
std::size_t cursor = kRawAt;
145-
auto emit_name = [&](std::string_view n) {
146-
auto rva = rva_of(cursor);
147-
for (char c : n) put(b, cursor++, static_cast<unsigned char>(c), 1);
148-
put(b, cursor++, 0, 1);
149-
return rva;
150-
};
151-
for (auto n : imports) importNameRvas.push_back(emit_name(n));
152-
for (auto n : delayImports) delayNameRvas.push_back(emit_name(n));
159+
for (std::size_t which = 0; which < 2; ++which) {
160+
const std::vector<std::string>& names = which == 0 ? imports : delayImports;
161+
for (std::size_t k = 0; k < names.size(); ++k) {
162+
const std::uint32_t rva =
163+
static_cast<std::uint32_t>(kSecVa + (cursor - kRawAt));
164+
const std::string& n = names[k];
165+
for (std::size_t i = 0; i < n.size(); ++i) {
166+
put(b, cursor, static_cast<unsigned char>(n[i]), 1);
167+
++cursor;
168+
}
169+
put(b, cursor, 0, 1);
170+
++cursor;
171+
if (which == 0) importNameRvas.push_back(rva);
172+
else delayNameRvas.push_back(rva);
173+
}
174+
}
175+
trace("names", b);
153176

154177
// Import descriptors (20 bytes each) + an all-zero terminator.
155-
cursor = (cursor + 15) & ~std::size_t{15};
178+
cursor = (cursor + 15) & ~static_cast<std::size_t>(15);
156179
const std::size_t importAt = cursor;
157-
for (auto rva : importNameRvas) {
180+
for (std::size_t k = 0; k < importNameRvas.size(); ++k) {
158181
put(b, cursor + 0, 0x9000, 4); // OriginalFirstThunk (nonzero)
159-
put(b, cursor + 12, rva, 4); // Name
182+
put(b, cursor + 12, importNameRvas[k], 4); // Name
160183
put(b, cursor + 16, 0x9100, 4); // FirstThunk (nonzero)
161184
cursor += 20;
162185
}
163-
for (int i = 0; i < 20; ++i) put(b, cursor++, 0, 1);
186+
for (std::size_t i = 0; i < 20; ++i) { put(b, cursor, 0, 1); ++cursor; }
187+
trace("imports", b);
164188

165189
// Delay-import descriptors (32 bytes each). grAttrs bit 0 = the fields
166190
// are RVAs; without it a descriptor is the pre-VC7 address form and must
167191
// be skipped rather than misread.
168-
cursor = (cursor + 15) & ~std::size_t{15};
192+
cursor = (cursor + 15) & ~static_cast<std::size_t>(15);
169193
const std::size_t delayAt = cursor;
170-
for (auto rva : delayNameRvas) {
171-
put(b, cursor + 0, 1, 4); // grAttrs = dlattrRva
172-
put(b, cursor + 4, rva, 4); // rvaDLLName
194+
for (std::size_t k = 0; k < delayNameRvas.size(); ++k) {
195+
put(b, cursor + 0, 1, 4); // grAttrs = dlattrRva
196+
put(b, cursor + 4, delayNameRvas[k], 4); // rvaDLLName
173197
cursor += 32;
174198
}
175-
for (int i = 0; i < 32; ++i) put(b, cursor++, 0, 1);
199+
for (std::size_t i = 0; i < 32; ++i) { put(b, cursor, 0, 1); ++cursor; }
200+
trace("delay", b);
176201

177202
if (!imports.empty()) {
178-
put(b, dirsAt + 1 * 8, rva_of(importAt), 4);
203+
put(b, dirsAt + 1 * 8,
204+
static_cast<std::uint32_t>(kSecVa + (importAt - kRawAt)), 4);
179205
put(b, dirsAt + 1 * 8 + 4, 20 * (imports.size() + 1), 4);
180206
}
181207
if (!delayImports.empty()) {
182-
put(b, dirsAt + 13 * 8, rva_of(delayAt), 4);
208+
put(b, dirsAt + 13 * 8,
209+
static_cast<std::uint32_t>(kSecVa + (delayAt - kRawAt)), 4);
183210
put(b, dirsAt + 13 * 8 + 4, 32 * (delayImports.size() + 1), 4);
184211
}
212+
trace("directories", b);
185213
return b;
186214
}
187215

216+
// The three names every PE test below builds an image around. A function, not
217+
// a namespace-scope constant: a `std::vector<std::string>` at namespace scope
218+
// in a test TU is a static initializer, and this file is already investigating
219+
// one platform-specific crash.
220+
std::vector<std::string> pe_imports() { return {"KERNEL32.dll", "vcruntime140.dll"}; }
221+
std::vector<std::string> pe_delayed() { return {"dbghelp.dll"}; }
222+
188223
struct TempFile {
189224
std::filesystem::path path;
190225
TempFile(std::string_view tag, std::string_view bytes)
@@ -235,9 +270,7 @@ TEST(PackBinfmt, AnElfWithNoDynamicSectionHasZeroDepsAndIsNotAnError) {
235270
// A test that cannot localise its own failure is a test that costs a CI round
236271
// per hypothesis.
237272
TEST(PackBinfmt, ThePeFixtureItselfIsWellFormed) {
238-
std::array<std::string_view, 2> imports{"KERNEL32.dll", "vcruntime140.dll"};
239-
std::array<std::string_view, 1> delayed{"dbghelp.dll"};
240-
auto bytes = pe_with_imports(imports, delayed);
273+
auto bytes = pe_with_imports(pe_imports(), pe_delayed());
241274
ASSERT_GT(bytes.size(), 0x400u);
242275
EXPECT_EQ(bytes.substr(0, 2), "MZ");
243276
EXPECT_EQ(bytes.substr(0x40, 4), std::string("PE\0\0", 4));
@@ -248,9 +281,7 @@ TEST(PackBinfmt, ThePeFixtureItselfIsWellFormed) {
248281
}
249282

250283
TEST(PackBinfmt, IdentifiesPe) {
251-
std::array<std::string_view, 2> imports{"KERNEL32.dll", "vcruntime140.dll"};
252-
std::array<std::string_view, 1> delayed{"dbghelp.dll"};
253-
TempFile f{"peid", pe_with_imports(imports, delayed)};
284+
TempFile f{"peid", pe_with_imports(pe_imports(), pe_delayed())};
254285

255286
auto id = bf::identify(f.path);
256287
EXPECT_EQ(id.format, bf::Format::Pe);
@@ -259,9 +290,7 @@ TEST(PackBinfmt, IdentifiesPe) {
259290
}
260291

261292
TEST(PackBinfmt, ReadsBothPeImportDirectories) {
262-
std::array<std::string_view, 2> imports{"KERNEL32.dll", "vcruntime140.dll"};
263-
std::array<std::string_view, 1> delayed{"dbghelp.dll"};
264-
TempFile f{"peimp", pe_with_imports(imports, delayed)};
293+
TempFile f{"peimp", pe_with_imports(pe_imports(), pe_delayed())};
265294

266295
auto names = bf::needed_names(f.path);
267296
ASSERT_TRUE(names.has_value()) << names.error();
@@ -292,8 +321,7 @@ TEST(PackBinfmt, TruncatedInputIsRejectedRatherThanRead) {
292321
// Malformed input is ordinary: a half-downloaded file, a text file named
293322
// `.exe`. Every read is bounds-checked, so the parser is total over it.
294323
for (std::size_t keep : {0u, 4u, 0x40u, 0x80u}) {
295-
std::array<std::string_view, 1> imports{"KERNEL32.dll"};
296-
auto bytes = pe_with_imports(imports, {});
324+
auto bytes = pe_with_imports({"KERNEL32.dll"}, {});
297325
bytes.resize(keep);
298326
TempFile f{"trunc", bytes};
299327
auto names = bf::needed_names(f.path);

0 commit comments

Comments
 (0)