diff --git a/.gitignore b/.gitignore index 26938bb954..ba3cd4cecb 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ datadog/maven/resources **/launcher* /gradle.properties **/hs_err* +*.core # cursor AI history .history diff --git a/ddprof-lib/src/main/cpp/codeCache.h b/ddprof-lib/src/main/cpp/codeCache.h index 6ced816af8..d9a78d0e31 100644 --- a/ddprof-lib/src/main/cpp/codeCache.h +++ b/ddprof-lib/src/main/cpp/codeCache.h @@ -481,6 +481,26 @@ class CodeCacheArray { } return total; } + +#ifdef UNIT_TEST + // Test teardown only: deletes every CodeCache and empties the array. + // Must not race add(): a publisher would spin on _count after the reset + // or store its pointer past the memset. + void freeForTest() { + for (int i = 0; i < count(); i++) { + CodeCache *lib = at(i); + if (lib != nullptr) { + NativeMem::record(NM_NATIVE_SYMBOLS, -lib->memoryUsage()); + NativeMem::recordOverhead(NM_NATIVE_SYMBOLS, -lib->nameOverhead()); + delete lib; + } + } + memset(_libs, 0, sizeof(_libs)); + _reserved = 0; + _count = 0; + _overflow_reported = false; + } +#endif }; #endif // _CODECACHE_H diff --git a/ddprof-lib/src/main/cpp/libraries.h b/ddprof-lib/src/main/cpp/libraries.h index a7045a934c..78511d9e47 100644 --- a/ddprof-lib/src/main/cpp/libraries.h +++ b/ddprof-lib/src/main/cpp/libraries.h @@ -1,3 +1,8 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + #ifndef _LIBRARIES_H #define _LIBRARIES_H @@ -89,6 +94,11 @@ class Libraries { // outlive every later lookup. The real population path is updateSymbols() // reading the loaded libraries; compiled only into gtest binaries. bool addLibraryForTest(CodeCache *lib) { return _native_libs.add(lib); } + + // Test teardown only; also deletes caches passed to addLibraryForTest(). + // LibraryPatcher's tables and updateBuildIds()' set match caches by address, + // so call it only in a binary that never patches nor updates build ids. + void freeNativeLibsForTest() { _native_libs.freeForTest(); } #endif // Delete copy constructor and assignment operator to prevent copies diff --git a/ddprof-lib/src/main/cpp/symbols_linux.cpp b/ddprof-lib/src/main/cpp/symbols_linux.cpp index 8eb542fee7..d3f0345c36 100644 --- a/ddprof-lib/src/main/cpp/symbols_linux.cpp +++ b/ddprof-lib/src/main/cpp/symbols_linux.cpp @@ -7,6 +7,7 @@ #ifdef __linux__ #include +#include #include #include #include @@ -322,6 +323,24 @@ typedef Elf64_Dyn ElfDyn; static char _debuginfod_cache_buf[PATH_MAX] = {0}; +// Nothing checks the alignment of sh_offset or DT_SYMTAB, so a crafted image can +// misalign the table; copying avoids UB from a misaligned ElfSymbol* access. +static_assert(offsetof(ElfSymbol, st_name) == 0, "symbolNameAt() reads st_name at offset 0"); +static uint32_t symbolNameAt(const void* sym) { + uint32_t name; + memcpy(&name, sym, sizeof(name)); + return name; +} + +// Same for relocation tables (sh_offset, DT_JMPREL, DT_RELA/DT_REL). Copies the +// Elf64_Rel prefix, so it also serves Elf64_Rela; the caller must ensure +// sizeof(ElfRelocation) bytes remain in the table. +static ElfRelocation relocationAt(const void* rel) { + ElfRelocation r; + memcpy(&r, rel, sizeof(r)); + return r; +} + class ElfParser { private: CodeCache* _cc; @@ -675,37 +694,43 @@ void ElfParser::parseDynamicSection() { loadSymbolTable(symtab, syment * nsyms, syment, strtab, strsz); } - // base() is NULL for ET_EXEC (non-PIE) images - adding r->r_offset to it - // via pointer arithmetic is UB (base + r->r_offset on a null base), even + // base() is NULL for ET_EXEC (non-PIE) images - adding r.r_offset to it + // via pointer arithmetic is UB (base + r.r_offset on a null base), even // though the intent is just "sym addresses are already absolute". Do the // addition in integer space, same fix as the .plt case above. uintptr_t base_addr = (uintptr_t)this->base(); - if (jmprel != NULL && pltrelsz != 0) { + // Smaller entries cannot hold r_offset and r_info (hashImportLayout() + // skips them too). Walking only while a whole entry fits keeps offs + // within the table, so offs += relent cannot wrap. + if (jmprel != NULL && pltrelsz != 0 && relent >= sizeof(ElfRelocation)) { // Parse .rela.plt table - for (size_t offs = 0; offs < pltrelsz; offs += relent) { - ElfRelocation* r = (ElfRelocation*)(jmprel + offs); - ElfSymbol* sym = (ElfSymbol*)(symtab + ELF_R_SYM(r->r_info) * syment); - if (sym->st_name != 0) { - const char* sym_name = strAt(strtab, strsz, sym->st_name); + for (size_t offs = 0; relent <= pltrelsz - offs; offs += relent) { + ElfRelocation r = relocationAt(jmprel + offs); + uint32_t st_name = symbolNameAt(symtab + ELF_R_SYM(r.r_info) * syment); + if (st_name != 0) { + const char* sym_name = strAt(strtab, strsz, st_name); if (sym_name != NULL) { - _cc->addImport((void**)(base_addr + r->r_offset), sym_name); + _cc->addImport((void**)(base_addr + r.r_offset), sym_name); } } } } - if (rel != NULL && relsz != 0) { + if (rel != NULL && relsz != 0 && relent >= sizeof(ElfRelocation)) { // Relocation entries for imports can be found in .rela.dyn, for example // if a shared library is built without PLT (-fno-plt). However, if both // entries exist, addImport saves them both. - for (size_t offs = relcount * relent; offs < relsz; offs += relent) { - ElfRelocation* r = (ElfRelocation*)(rel + offs); - if (ELF_R_TYPE(r->r_info) == R_GLOB_DAT || ELF_R_TYPE(r->r_info) == R_ABS64) { - ElfSymbol* sym = (ElfSymbol*)(symtab + ELF_R_SYM(r->r_info) * syment); - if (sym->st_name != 0) { - const char* sym_name = strAt(strtab, strsz, sym->st_name); + // The leading DT_RELACOUNT entries are relative relocations, without + // a symbol; clamp the start as hashImportLayout() does. + size_t start = relcount * relent < relsz ? relcount * relent : relsz; + for (size_t offs = start; relent <= relsz - offs; offs += relent) { + ElfRelocation r = relocationAt(rel + offs); + if (ELF_R_TYPE(r.r_info) == R_GLOB_DAT || ELF_R_TYPE(r.r_info) == R_ABS64) { + uint32_t st_name = symbolNameAt(symtab + ELF_R_SYM(r.r_info) * syment); + if (st_name != 0) { + const char* sym_name = strAt(strtab, strsz, st_name); if (sym_name != NULL) { - _cc->addImport((void**)(base_addr + r->r_offset), sym_name); + _cc->addImport((void**)(base_addr + r.r_offset), sym_name); } } } @@ -1043,17 +1068,17 @@ void ElfParser::addRelocationSymbols(ElfSection* reltab, const char* plt) { // Offset-based iteration (see loadSymbolTable) so a huge rel_ent cannot // overflow the relocation pointer past the section end. for (size_t off = 0; rel_ent <= reltab_size - off; off += rel_ent, plt += PLT_ENTRY_SIZE) { - ElfRelocation* r = (ElfRelocation*)(relocations + off); - if (ELF_R_SYM(r->r_info) > max_sym_index) { + ElfRelocation r = relocationAt(relocations + off); + if (ELF_R_SYM(r.r_info) > max_sym_index) { continue; } - ElfSymbol* sym = (ElfSymbol*)(symbols + (size_t)ELF_R_SYM(r->r_info) * sym_ent); + uint32_t st_name = symbolNameAt(symbols + (size_t)ELF_R_SYM(r.r_info) * sym_ent); char name[256]; - if (sym->st_name == 0) { + if (st_name == 0) { strcpy(name, "@plt"); } else { - const char* sym_name = strAt(strings, strings_size, sym->st_name); + const char* sym_name = strAt(strings, strings_size, st_name); if (sym_name == NULL) { continue; // plt advances via the for-increment } @@ -1342,21 +1367,23 @@ class LoadedImage { // Hashes every relocation in rel (size bytes, entries relent apart) that names // a symbol: its offset, type and symbol index, and the symbol's name. Returns -// false if a symbol or its name lies outside the readable image. +// false if a symbol or its name lies outside the readable image. relent must be +// at least sizeof(ElfRelocation) (hashImportLayout() checks it), so every entry +// the walk visits holds a whole r_offset and r_info and the walk advances. bool hashSymbolRelocations(const LoadedImage& image, uintptr_t rel, size_t size, size_t relent, uintptr_t symtab, size_t syment, uintptr_t strtab, size_t strsz, u64& hash) { - for (size_t offs = 0; offs + sizeof(ElfRelocation) <= size; offs += relent) { - const ElfRelocation* r = (const ElfRelocation*)(rel + offs); - size_t index = ELF_R_SYM(r->r_info); + for (size_t offs = 0; relent <= size - offs; offs += relent) { + ElfRelocation r = relocationAt(reinterpret_cast(rel + offs)); + size_t index = ELF_R_SYM(r.r_info); if (index == 0) continue; // e.g. R_*_RELATIVE: no import uintptr_t sym = symtab + index * syment; if (!image.readable(sym, sizeof(ElfSymbol))) return false; - size_t name = ((const ElfSymbol*)sym)->st_name; + size_t name = symbolNameAt(reinterpret_cast(sym)); if (name >= strsz) return false; const char* str = (const char*)(strtab + name); - hash = fnv1a(&r->r_offset, sizeof(r->r_offset), hash); - hash = fnv1a(&r->r_info, sizeof(r->r_info), hash); + hash = fnv1a(&r.r_offset, sizeof(r.r_offset), hash); + hash = fnv1a(&r.r_info, sizeof(r.r_info), hash); // Including the terminating NUL keeps adjacent names apart. hash = fnv1a(str, strnlen(str, strsz - name - 1) + 1, hash); } diff --git a/ddprof-lib/src/test/cpp/codeCache_ut.cpp b/ddprof-lib/src/test/cpp/codeCache_ut.cpp index dd6806be53..07862037c6 100644 --- a/ddprof-lib/src/test/cpp/codeCache_ut.cpp +++ b/ddprof-lib/src/test/cpp/codeCache_ut.cpp @@ -6,6 +6,7 @@ #include #include "codeCache.h" +#include #include // memoryUsage() must reflect the actual per-symbol name length, not a fixed @@ -81,25 +82,76 @@ TEST(CodeCacheTest, CopyIsDeepAndDoesNotDoubleFree) { EXPECT_EQ(expected, src.memoryUsage()); // src's names still intact } // src destroyed — no double free +class CodeCacheArrayTest : public ::testing::Test { +protected: + CodeCacheArray arr; + + void TearDown() override { + arr.freeForTest(); + } +}; + // CodeCacheArray::memoryUsage() is the aggregation production actually consumes // (Profiler::dump()). Verify it sums the per-library values — guards the loop // bound and the accumulation against regressions the per-instance tests miss. -TEST(CodeCacheTest, ArrayMemoryUsageSumsLibraries) { - CodeCacheArray arr; - CodeCache *a = new CodeCache("liba"); - CodeCache *b = new CodeCache("libb"); +TEST_F(CodeCacheArrayTest, ArrayMemoryUsageSumsLibraries) { + auto a = std::make_unique("liba"); + auto b = std::make_unique("libb"); // Populate before registering: add() must run pre-publication. a->add(reinterpret_cast(0x1000), 16, "alpha_symbol"); b->add(reinterpret_cast(0x2000), 16, "beta_symbol_longer_name"); long long expected = (long long)a->memoryUsage() + (long long)b->memoryUsage(); - ASSERT_TRUE(arr.add(a)); - ASSERT_TRUE(arr.add(b)); + ASSERT_TRUE(arr.add(a.get())); + a.release(); + ASSERT_TRUE(arr.add(b.get())); + b.release(); EXPECT_EQ(expected, (long long)arr.memoryUsage()); +} - // CodeCacheArray does not own the entries. - delete a; - delete b; +// freeForTest() must restore the NM gauges to their pre-add baseline and +// leave the array reusable for a later cycle. +TEST_F(CodeCacheArrayTest, FreeForTestRestoresAccountingAndAllowsReuse) { + const long long live_before = NativeMem::live(NM_NATIVE_SYMBOLS); + const long long overhead_before = NativeMem::overhead(NM_NATIVE_SYMBOLS); + arr.freeForTest(); + EXPECT_EQ(live_before, NativeMem::live(NM_NATIVE_SYMBOLS)); + EXPECT_EQ(overhead_before, NativeMem::overhead(NM_NATIVE_SYMBOLS)); + + for (int cycle = 0; cycle < 2; cycle++) { + auto a = std::make_unique("cleanup_liba"); + auto b = std::make_unique("cleanup_libb_with_longer_name"); + a->add(reinterpret_cast(0x1000), 16, "alpha_symbol"); + b->add(reinterpret_cast(0x2000), 32, "beta_symbol_longer_name"); + b->add(reinterpret_cast(0x3000), 8, "gamma"); + CodeCache *first = a.get(); + CodeCache *second = b.get(); + const long long live = a->memoryUsage() + b->memoryUsage(); + const long long overhead = a->nameOverhead() + b->nameOverhead(); + ASSERT_GT(live, 0); + + ASSERT_TRUE(arr.add(a.get())); + a.release(); + ASSERT_TRUE(arr.add(b.get())); + b.release(); + ASSERT_EQ(2, arr.count()); + EXPECT_EQ(first, arr[0]); + EXPECT_EQ(second, arr[1]); + EXPECT_EQ(live, static_cast(arr.memoryUsage())); + EXPECT_EQ(live_before + live, NativeMem::live(NM_NATIVE_SYMBOLS)); + EXPECT_EQ(overhead_before + overhead, NativeMem::overhead(NM_NATIVE_SYMBOLS)); + + for (int cleanup = 0; cleanup < 2; cleanup++) { + arr.freeForTest(); + EXPECT_EQ(live_before, NativeMem::live(NM_NATIVE_SYMBOLS)); + EXPECT_EQ(overhead_before, NativeMem::overhead(NM_NATIVE_SYMBOLS)); + EXPECT_EQ(0, arr.count()); + EXPECT_EQ(0u, arr.memoryUsage()); + for (int slot = 0; slot < MAX_NATIVE_LIBS; slot++) { + EXPECT_EQ(nullptr, arr.at(slot)) << slot; + } + } + } } // The two-phase add used under JitCodeCache's stubs lock: prepareAdd() hands diff --git a/ddprof-lib/src/test/cpp/elfparser_ut.cpp b/ddprof-lib/src/test/cpp/elfparser_ut.cpp index 3b396826a2..a02dbedbc1 100644 --- a/ddprof-lib/src/test/cpp/elfparser_ut.cpp +++ b/ddprof-lib/src/test/cpp/elfparser_ut.cpp @@ -4,6 +4,7 @@ #include #include +#include "arch.h" #include "codeCache.h" #include "libraries.h" #include "symbols.h" @@ -19,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -30,9 +32,9 @@ #include // Forward declaration for ElfParser functionality from symbols_linux.cpp -// The actual implementation will be available through the patched upstream file class ElfParser { public: + static void parseProgramHeaders(CodeCache* cc, const char* base, const char* end, bool relocate_dyn); static bool parseFile(CodeCache* cc, const char* base, const char* file_name, bool use_debug); }; @@ -53,15 +55,6 @@ class ElfParserGlobalSetup { // Install global crash handler for all tests in this file static ElfParserGlobalSetup global_setup; -// CodeCacheArray is process-lifetime storage with no destructor (the global -// production array is never freed); a test-local array must delete its caches -// explicitly or LeakSanitizer flags every parsed library. -static void drainCodeCacheArray(CodeCacheArray& array) { - for (int i = 0; i < array.count(); ++i) { - delete array.at(i); - } -} - // Resolve a path under ddprof-lib/build/test/resources. The Gradle test task // runs from the build directory, but the GitLab runner invokes binaries from // the repo root, so a cwd-based lookup misses there; derive it from the @@ -108,6 +101,8 @@ class ElfReladyn : public ::testing::Test { ASSERT_THAT(handle, ::testing::NotNull()); _libs = Libraries::instance(); + // Local-array parses also fill the global inode cache; rebuild the singleton independently. + Symbols::clearParsingCaches(); _libs->updateSymbols(false); _libreladyn = _libs->findLibraryByName("libreladyn"); ASSERT_THAT(_libreladyn, ::testing::NotNull()); @@ -115,7 +110,14 @@ class ElfReladyn : public ::testing::Test { // This method is called after each test. void TearDown() override { - // Clean up resources. + if (_libs == nullptr) { + return; // SetUp skipped (missing resource): nothing was parsed + } + // parseLibraries skips inodes already in _parsed_inodes; clearing here + // lets the next parse (a later SetUp or a local array) reparse them. + Symbols::clearParsingCaches(); + _libs->freeNativeLibsForTest(); + _libreladyn = nullptr; // freed above; a mid-test TearDown() must not leave it dangling } CodeCache* libreladyn() { @@ -128,6 +130,33 @@ TEST_F(ElfReladyn, resolveFromRela_plt) { ASSERT_THAT(sym, ::testing::NotNull()); } +// A local-array parse between fixture tests must not break the next SetUp +// reparse of the global library array. +TEST_F(ElfReladyn, reinitializeAfterLocalLibraryParse) { + const u64 reladyn_file_id = libreladyn()->fileId(); + TearDown(); + + { + CodeCacheArray local_libraries; + auto free_libraries = [](CodeCacheArray* libs) { libs->freeForTest(); }; + std::unique_ptr cleanup(&local_libraries, free_libraries); + Symbols::parseLibraries(&local_libraries, false); + + bool parsed_reladyn = false; + for (int i = 0; i < local_libraries.count(); i++) { + CodeCache* lib = local_libraries[i]; + if (lib != nullptr && lib->fileId() == reladyn_file_id) { + parsed_reladyn = true; + break; + } + } + ASSERT_TRUE(parsed_reladyn); + } + + ASSERT_NO_FATAL_FAILURE(SetUp()); + ASSERT_THAT(libreladyn()->findImport(im_pthread_create), ::testing::NotNull()); +} + TEST_F(ElfReladyn, resolveFromRela_dyn_R_GLOB_DAT) { void* sym = libreladyn()->findImport(im_pthread_setspecific); ASSERT_THAT(sym, ::testing::NotNull()); @@ -144,10 +173,6 @@ class ElfTest : public ::testing::Test { // Reset global or static state Symbols::clearParsingCaches(); } - - void TearDown() override { - // probably some free of the array cache to be done - } }; @@ -189,7 +214,11 @@ TEST_F(ElfTest, invalidElf) { // Call the parsing function with the invalid ELF mapping Symbols::parseLibraries(&cc_array, false); - drainCodeCacheArray(cc_array); + + // The binary's own mapping is always collected and parsed. + const int parsed = cc_array.count(); + cc_array.freeForTest(); + EXPECT_GT(parsed, 0); munmap(addr, 4096); unlink("/tmp/invalid_elf"); @@ -218,7 +247,10 @@ TEST_F(ElfTest, nonElfFile) { // Call the parsing function with the non-ELF mapping Symbols::parseLibraries(&cc_array, false); - drainCodeCacheArray(cc_array); + + const int parsed = cc_array.count(); + cc_array.freeForTest(); + EXPECT_GT(parsed, 0); // we could add checks here, though I am mainly relying on asan to crash // if something is wrong @@ -247,7 +279,10 @@ TEST_F(ElfTest, invalidElfSmallMapping) { // Call the parsing function with the small invalid ELF mapping Symbols::parseLibraries(&cc_array, false); - drainCodeCacheArray(cc_array); + + const int parsed = cc_array.count(); + cc_array.freeForTest(); + EXPECT_GT(parsed, 0); munmap(addr, 16); unlink("/tmp/invalid_elf_small"); @@ -275,7 +310,9 @@ TEST_F(ElfTest, nonElfFileSmallMapping) { // Call the parsing function with the small non-ELF mapping Symbols::parseLibraries(&cc_array, false); - drainCodeCacheArray(cc_array); + const int parsed = cc_array.count(); + cc_array.freeForTest(); + EXPECT_GT(parsed, 0); munmap(addr, contentSize); unlink("/tmp/non_elf_small"); } @@ -286,10 +323,6 @@ class ElfTestParam : public ::testing::TestWithParam { // Reset global or static state Symbols::clearParsingCaches(); } - - void TearDown() override { - // probably some free of the array cache to be done - } }; @@ -330,10 +363,13 @@ TEST_P(ElfTestParam, invalidElfSmallMappingAfterUnmap) { // Call the parsing function in the main thread Symbols::parseLibraries(&cc_array, false); - drainCodeCacheArray(cc_array); // Join the unmapper thread to ensure it has finished unmapper.join(); + + const int parsed = cc_array.count(); + cc_array.freeForTest(); + EXPECT_GT(parsed, 0); } INSTANTIATE_TEST_SUITE_P( @@ -371,8 +407,11 @@ TEST_P(ElfTestParam, invalidElfSmallMappingAfterUnmap) { // Call the parsing function in the main thread, this is where we can crash Symbols::parseLibraries(&cc_array, false); - drainCodeCacheArray(cc_array); unmapper.join(); + + const int parsed = cc_array.count(); + cc_array.freeForTest(); + EXPECT_GT(parsed, 0); } INSTANTIATE_TEST_SUITE_P( @@ -415,8 +454,9 @@ Elf64_Ehdr validEhdr() { } // Write the bytes to a unique temp file and run ElfParser::parseFile over it, -// mirroring how Symbols::parseLibraries() parses an on-disk library. -void parseElfBytes(const std::vector& bytes) { +// mirroring an on-disk library parse. The caller-visible CodeCache lets tests +// assert what the parser resolved. +void parseElfBytes(const std::vector& bytes, CodeCache* cc, bool use_debug) { char path[] = "/tmp/elf_regress_XXXXXX"; int fd = mkstemp(path); ASSERT_NE(fd, -1); @@ -427,11 +467,15 @@ void parseElfBytes(const std::vector& bytes) { FAIL() << "short write to " << path; return; } - CodeCache cc("regress"); - ElfParser::parseFile(&cc, nullptr, path, /*use_debug=*/false); + ElfParser::parseFile(cc, nullptr, path, use_debug); unlink(path); } +void parseElfBytes(const std::vector& bytes) { + CodeCache cc("regress"); + parseElfBytes(bytes, &cc, /*use_debug=*/false); +} + } // namespace // Regression test for the build-id parser hardening (found by fuzz_elf). @@ -592,4 +636,339 @@ TEST_F(ElfTest, symbolNameOffsetOutOfBounds) { parseElfBytes(b); // must not crash: strAt() rejects st_name == strtab_size } +// Misaligned .symtab and .rela.plt must still resolve symbols and the PLT name. +// Only -fsanitize=alignment (asan config) catches a regression: arm64 and +// x86-64 perform the unaligned loads in hardware. +TEST_F(ElfTest, symbolTableMisalignedEntries) { + const uint16_t NSEC = 6; + const uint64_t shoff = sizeof(Elf64_Ehdr); + const uint64_t shstr_off = shoff + NSEC * sizeof(Elf64_Shdr); + const char shstrtab[] = "\0.symtab\0.strtab\0.shstrtab\0.plt\0.rela.plt"; + const uint64_t sym_off = shstr_off + sizeof(shstrtab); + ASSERT_NE(sym_off % alignof(Elf64_Sym), 0u); // layout must stay misaligned + + Elf64_Sym sym[2]; + memset(sym, 0, sizeof(sym)); + sym[0].st_name = 1; // "mis_main" + sym[0].st_value = 0x20000; + sym[0].st_size = 4; + sym[1].st_name = 10; // "mis_helper" + sym[1].st_value = 0x21000; + sym[1].st_size = 4; + const uint64_t str_off = sym_off + sizeof(sym); + const char strtab[] = "\0mis_main\0mis_helper\0"; // 22 bytes: names at 1 and 10 + // 4 pad bytes push .rela.plt off its natural alignment too, so the + // relocation entry read in addRelocationSymbols() is misaligned as well. + const char rela_pad[4] = {0, 0, 0, 0}; + const uint64_t rela_off = str_off + sizeof(strtab) + sizeof(rela_pad); + ASSERT_NE(rela_off % alignof(Elf64_Rela), 0u); + Elf64_Rela rela; + memset(&rela, 0, sizeof(rela)); + rela.r_offset = 0x4000; + rela.r_info = ELF64_R_INFO(1, 1026); // JUMP_SLOT-like; type is not interpreted + + Elf64_Ehdr e = validEhdr(); + e.e_shoff = shoff; + e.e_shentsize = sizeof(Elf64_Shdr); + e.e_shnum = NSEC; + e.e_shstrndx = 3; + + Elf64_Shdr sh[6]; + memset(sh, 0, sizeof(sh)); + sh[1].sh_name = 1; // ".symtab" + sh[1].sh_type = SHT_SYMTAB; + sh[1].sh_offset = sym_off; + sh[1].sh_size = sizeof(sym); + sh[1].sh_link = 2; + sh[1].sh_entsize = sizeof(Elf64_Sym); + sh[2].sh_name = 9; // ".strtab" + sh[2].sh_type = SHT_STRTAB; + sh[2].sh_offset = str_off; + sh[2].sh_size = sizeof(strtab); + sh[3].sh_name = 17; // ".shstrtab" + sh[3].sh_type = SHT_STRTAB; + sh[3].sh_offset = shstr_off; + sh[3].sh_size = sizeof(shstrtab); + sh[4].sh_name = 27; // ".plt" + sh[4].sh_type = SHT_PROGBITS; + sh[4].sh_addr = 0x3000; + sh[4].sh_size = 32; + sh[5].sh_name = 32; // ".rela.plt" + sh[5].sh_type = SHT_RELA; + sh[5].sh_offset = rela_off; + sh[5].sh_size = sizeof(rela); + sh[5].sh_link = 1; + sh[5].sh_entsize = sizeof(Elf64_Rela); + + std::vector b; + auto app = [&](const void* p, size_t n) { + const char* c = static_cast(p); + b.insert(b.end(), c, c + n); + }; + app(&e, sizeof(e)); + app(sh, sizeof(sh)); + app(shstrtab, sizeof(shstrtab)); + app(sym, sizeof(sym)); + app(strtab, sizeof(strtab)); + app(rela_pad, sizeof(rela_pad)); + app(&rela, sizeof(rela)); + + CodeCache cc("regress"); + ASSERT_NO_FATAL_FAILURE(parseElfBytes(b, &cc, /*use_debug=*/true)); + // Both misaligned .symtab entries resolved at their st_value addresses. + EXPECT_EQ(cc.findSymbol("mis_main"), (const void*)0x20000); + EXPECT_EQ(cc.findSymbol("mis_helper"), (const void*)0x21000); + // The PLT name came from the misaligned symbol-index lookup in + // addRelocationSymbols(): symbol 1 resolves to "mis_helper@plt", placed at + // the first stub after the PLT header. + EXPECT_EQ(cc.findSymbol("mis_helper@plt"), (const void*)(0x3000 + PLT_HEADER_SIZE)); +} + +// Loaded image with misaligned .dynsym, .rela.plt and .rela.dyn: both imports +// must resolve and both relocation tables must be hashed into the fingerprint. +// As above, only the asan config catches an alignment regression. +TEST_F(ElfTest, dynamicSectionMisalignedEntries) { +#if defined(__x86_64__) + const uint32_t glob_dat = R_X86_64_GLOB_DAT; +#elif defined(__aarch64__) + const uint32_t glob_dat = R_AARCH64_GLOB_DAT; +#else + GTEST_SKIP() << "R_GLOB_DAT imports are only recorded on x86-64 and aarch64"; + const uint32_t glob_dat = 0; +#endif + const size_t IMAGE_SIZE = 4096; + const uint64_t GOT_PLT_SLOT = 0x800; + const uint64_t GOT_DAT_SLOT = 0x808; + const int NPH = 2; + const int NDYN = 10; + + const uint64_t ph_off = sizeof(Elf64_Ehdr); + const uint64_t dyn_off = ph_off + NPH * sizeof(Elf64_Phdr); // ElfDyn stays aligned + const char strtab[] = "\0pthread_create\0pthread_setspecific"; // names at 1 and 16 + const uint64_t str_off = dyn_off + NDYN * sizeof(Elf64_Dyn); + const uint64_t sym_off = str_off + sizeof(strtab); + const uint64_t jmprel_off = sym_off + 3 * sizeof(Elf64_Sym); + const uint64_t rela_off = jmprel_off + sizeof(Elf64_Rela); + ASSERT_LE(rela_off + sizeof(Elf64_Rela), GOT_PLT_SLOT); + // Layout must stay misaligned for every table under test. + ASSERT_NE(sym_off % alignof(Elf64_Sym), 0u); + ASSERT_NE(jmprel_off % alignof(Elf64_Rela), 0u); + ASSERT_NE(rela_off % alignof(Elf64_Rela), 0u); + + // The image itself must be aligned so only the table offsets misalign. + std::unique_ptr mem(aligned_alloc(IMAGE_SIZE, IMAGE_SIZE), &free); + ASSERT_THAT(mem.get(), ::testing::NotNull()); + char* image = static_cast(mem.get()); + memset(image, 0, IMAGE_SIZE); + + Elf64_Ehdr e = validEhdr(); + e.e_phoff = ph_off; + e.e_phentsize = sizeof(Elf64_Phdr); + e.e_phnum = NPH; + memcpy(image, &e, sizeof(e)); + + Elf64_Phdr ph[NPH]; + memset(ph, 0, sizeof(ph)); + ph[0].p_type = PT_LOAD; + ph[0].p_filesz = IMAGE_SIZE; + ph[0].p_memsz = IMAGE_SIZE; + ph[1].p_type = PT_DYNAMIC; + ph[1].p_vaddr = dyn_off; + ph[1].p_offset = dyn_off; + ph[1].p_filesz = NDYN * sizeof(Elf64_Dyn); + ph[1].p_memsz = NDYN * sizeof(Elf64_Dyn); + memcpy(image + ph_off, ph, sizeof(ph)); + + // Unrelocated (vaddr) d_ptr values, as musl leaves them; both parsers + // rebase pointers that fall below the image. + const Elf64_Dyn dyn[NDYN] = { + {DT_SYMTAB, {sym_off}}, + {DT_STRTAB, {str_off}}, + {DT_SYMENT, {sizeof(Elf64_Sym)}}, + {DT_STRSZ, {sizeof(strtab)}}, + {DT_JMPREL, {jmprel_off}}, + {DT_PLTRELSZ, {sizeof(Elf64_Rela)}}, + {DT_RELA, {rela_off}}, + {DT_RELASZ, {sizeof(Elf64_Rela)}}, + {DT_RELAENT, {sizeof(Elf64_Rela)}}, + {DT_NULL, {0}}, + }; + memcpy(image + dyn_off, dyn, sizeof(dyn)); + memcpy(image + str_off, strtab, sizeof(strtab)); + + Elf64_Sym sym[3]; + memset(sym, 0, sizeof(sym)); + sym[1].st_name = 1; // "pthread_create" + sym[2].st_name = 16; // "pthread_setspecific" + memcpy(image + sym_off, sym, sizeof(sym)); + + Elf64_Rela jmprel; + memset(&jmprel, 0, sizeof(jmprel)); + jmprel.r_offset = GOT_PLT_SLOT; + jmprel.r_info = ELF64_R_INFO(1, 0); // .rela.plt entries are not type-checked + memcpy(image + jmprel_off, &jmprel, sizeof(jmprel)); + + Elf64_Rela rela; + memset(&rela, 0, sizeof(rela)); + rela.r_offset = GOT_DAT_SLOT; + rela.r_info = ELF64_R_INFO(2, glob_dat); + memcpy(image + rela_off, &rela, sizeof(rela)); + + CodeCache cc("inmem"); + ElfParser::parseProgramHeaders(&cc, image, image + IMAGE_SIZE, /*relocate_dyn=*/false); + + // parseDynamicSection(): both misaligned relocations resolved their + // misaligned symbols and recorded the GOT slot they name. + EXPECT_EQ(cc.peekImport(im_pthread_create), (void**)(image + GOT_PLT_SLOT)); + EXPECT_EQ(cc.peekImport(im_pthread_setspecific), (void**)(image + GOT_DAT_SLOT)); + + // hashSymbolRelocations(): a fingerprint of 0 means a table was rejected. + const Elf64_Phdr* phdrs = reinterpret_cast(image + ph_off); + const u64 fingerprint = Symbols::imageFingerprint(phdrs, NPH, (uintptr_t)image); + EXPECT_NE(fingerprint, 0u); + EXPECT_EQ(cc.imageFingerprint(), fingerprint); + + // Both misaligned relocation tables are part of the hash: moving either + // entry's GOT slot must change the fingerprint. + jmprel.r_offset = GOT_PLT_SLOT + 2 * sizeof(void*); + memcpy(image + jmprel_off, &jmprel, sizeof(jmprel)); + const u64 plt_moved = Symbols::imageFingerprint(phdrs, NPH, (uintptr_t)image); + EXPECT_NE(plt_moved, 0u); + EXPECT_NE(plt_moved, fingerprint); + + rela.r_offset = GOT_DAT_SLOT + 2 * sizeof(void*); + memcpy(image + rela_off, &rela, sizeof(rela)); + const u64 both_moved = Symbols::imageFingerprint(phdrs, NPH, (uintptr_t)image); + EXPECT_NE(both_moved, 0u); + EXPECT_NE(both_moved, plt_moved); + + // DT_RELASZ ends 8 bytes into a second entry whose GLOB_DAT r_info lies past + // the table; the in-table entry is not a GLOB_DAT, so any recorded import + // means the walk read past DT_RELASZ. + const int RELASZ_INDEX = 7; + ASSERT_EQ(dyn[RELASZ_INDEX].d_tag, DT_RELASZ); + ASSERT_LE(rela_off + sizeof(Elf64_Rela) + sizeof(Elf64_Rel), GOT_PLT_SLOT); + Elf64_Dyn truncated = {DT_RELASZ, {sizeof(Elf64_Rela) + sizeof(uint64_t)}}; + memcpy(image + dyn_off + RELASZ_INDEX * sizeof(Elf64_Dyn), &truncated, sizeof(truncated)); + rela.r_info = 0; + memcpy(image + rela_off, &rela, sizeof(rela)); + Elf64_Rel stray; + stray.r_offset = GOT_DAT_SLOT; + stray.r_info = ELF64_R_INFO(2, glob_dat); + memcpy(image + rela_off + sizeof(Elf64_Rela), &stray, sizeof(stray)); + CodeCache truncated_cc("truncated"); + ElfParser::parseProgramHeaders(&truncated_cc, image, image + IMAGE_SIZE, /*relocate_dyn=*/false); + EXPECT_EQ(truncated_cc.peekImport(im_pthread_setspecific), nullptr); +} + +// An undersized, a near-SIZE_MAX, and a table-truncating DT_RELAENT/PLTRELSZ: +// no walk may read an entry that does not fit in the table, record imports the +// fingerprint skips, or wrap its offset back before the table. +TEST_F(ElfTest, dynamicSectionInvalidRelocationEntrySize) { + const size_t IMAGE_SIZE = 4096; + const uint64_t GOT_PLT_SLOT = 0x800; + const int NPH = 2; + const int NDYN = 8; + + const uint64_t ph_off = sizeof(Elf64_Ehdr); + const uint64_t dyn_off = ph_off + NPH * sizeof(Elf64_Phdr); + const char strtab[] = "\0pthread_create"; + const uint64_t str_off = dyn_off + NDYN * sizeof(Elf64_Dyn); + const uint64_t sym_off = (str_off + sizeof(strtab) + 7) & ~(uint64_t)7; + const uint64_t jmprel_off = sym_off + 2 * sizeof(Elf64_Sym); + // Room for a full entry plus the 16 bytes the truncated-table case reads. + ASSERT_LE(jmprel_off + sizeof(Elf64_Rela) + sizeof(Elf64_Rel), GOT_PLT_SLOT); + + std::unique_ptr mem(aligned_alloc(IMAGE_SIZE, IMAGE_SIZE), &free); + ASSERT_THAT(mem.get(), ::testing::NotNull()); + char* image = static_cast(mem.get()); + memset(image, 0, IMAGE_SIZE); + + Elf64_Ehdr e = validEhdr(); + e.e_phoff = ph_off; + e.e_phentsize = sizeof(Elf64_Phdr); + e.e_phnum = NPH; + memcpy(image, &e, sizeof(e)); + + Elf64_Phdr ph[NPH]; + memset(ph, 0, sizeof(ph)); + ph[0].p_type = PT_LOAD; + ph[0].p_filesz = IMAGE_SIZE; + ph[0].p_memsz = IMAGE_SIZE; + ph[1].p_type = PT_DYNAMIC; + ph[1].p_vaddr = dyn_off; + ph[1].p_offset = dyn_off; + ph[1].p_filesz = NDYN * sizeof(Elf64_Dyn); + ph[1].p_memsz = NDYN * sizeof(Elf64_Dyn); + memcpy(image + ph_off, ph, sizeof(ph)); + + const int PLTRELSZ_INDEX = 5; + const int RELAENT_INDEX = 6; + const Elf64_Dyn dyn[NDYN] = { + {DT_SYMTAB, {sym_off}}, + {DT_STRTAB, {str_off}}, + {DT_SYMENT, {sizeof(Elf64_Sym)}}, + {DT_STRSZ, {sizeof(strtab)}}, + {DT_JMPREL, {jmprel_off}}, + {DT_PLTRELSZ, {sizeof(Elf64_Rela)}}, // PLTRELSZ_INDEX + {DT_RELAENT, {8}}, // RELAENT_INDEX: half an Elf64_Rel + {DT_NULL, {0}}, + }; + memcpy(image + dyn_off, dyn, sizeof(dyn)); + memcpy(image + str_off, strtab, sizeof(strtab)); + + Elf64_Sym sym[2]; + memset(sym, 0, sizeof(sym)); + sym[1].st_name = 1; // "pthread_create" + // The last 8 bytes before .rela.plt. Read as an r_info by a relocation walk + // that wraps back before the table, they name symbol 0xffffffff, which lies + // outside the image (see the huge-stride case below). + sym[1].st_size = 0xffffffff00000000ULL; + memcpy(image + sym_off, sym, sizeof(sym)); + + // A well-formed entry: with the 8-byte stride the first iteration still + // reads it whole, so only the entry-size check keeps it out. + Elf64_Rela jmprel; + memset(&jmprel, 0, sizeof(jmprel)); + jmprel.r_offset = GOT_PLT_SLOT; + jmprel.r_info = ELF64_R_INFO(1, 0); + memcpy(image + jmprel_off, &jmprel, sizeof(jmprel)); + + { + CodeCache cc("undersized"); + ElfParser::parseProgramHeaders(&cc, image, image + IMAGE_SIZE, /*relocate_dyn=*/false); + EXPECT_EQ(cc.peekImport(im_pthread_create), nullptr); + } + + // No entry fits, so nothing is recorded. A walk that wraps back before the + // table meets the index planted in sym[1].st_size and yields fingerprint 0. + Elf64_Dyn huge_relent = {DT_RELAENT, {SIZE_MAX - 7}}; + memcpy(image + dyn_off + RELAENT_INDEX * sizeof(Elf64_Dyn), &huge_relent, sizeof(huge_relent)); + { + CodeCache cc("huge"); + ElfParser::parseProgramHeaders(&cc, image, image + IMAGE_SIZE, /*relocate_dyn=*/false); + EXPECT_EQ(cc.peekImport(im_pthread_create), nullptr); + EXPECT_NE(cc.imageFingerprint(), 0u); + } + + // DT_PLTRELSZ ends 8 bytes into a second entry whose r_info lies past the + // table; the in-table entry names no symbol, so any recorded import means + // the walk read past DT_PLTRELSZ. + Elf64_Dyn relaent = {DT_RELAENT, {sizeof(Elf64_Rela)}}; + Elf64_Dyn truncated = {DT_PLTRELSZ, {sizeof(Elf64_Rela) + sizeof(uint64_t)}}; + memcpy(image + dyn_off + RELAENT_INDEX * sizeof(Elf64_Dyn), &relaent, sizeof(relaent)); + memcpy(image + dyn_off + PLTRELSZ_INDEX * sizeof(Elf64_Dyn), &truncated, sizeof(truncated)); + jmprel.r_info = 0; + memcpy(image + jmprel_off, &jmprel, sizeof(jmprel)); + Elf64_Rel stray; + stray.r_offset = GOT_PLT_SLOT; + stray.r_info = ELF64_R_INFO(1, 0); + memcpy(image + jmprel_off + sizeof(Elf64_Rela), &stray, sizeof(stray)); + { + CodeCache cc("truncated"); + ElfParser::parseProgramHeaders(&cc, image, image + IMAGE_SIZE, /*relocate_dyn=*/false); + EXPECT_EQ(cc.peekImport(im_pthread_create), nullptr); + } +} + #endif //__linux__