11#include < gtest/gtest.h>
2+ #include < cstdio> // stderr is a MACRO — `import std;` cannot export it
23
34import std;
45import 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+
188223struct 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.
237272TEST (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
250283TEST (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
261292TEST (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