PR #2103: reject invalid symbol table entry size in FindSymbol Imported from GitHub PR https://github.com/abseil/abseil-cpp/pull/2103 **Unchecked symbol-table entry size in FindSymbol** `symtab->sh_entsize` is read verbatim from the object file and used as the divisor for `num_symbols` and as the per-entry read stride. A crafted ELF whose `SHT_SYMTAB`/`SHT_DYNSYM` header carries an entry size of zero divides by zero here; any other value that differs from `sizeof(ElfW(Sym))` slides the fixed-size symbol reads off the real entries. Added the same exact-size check the section-header paths already apply to `e_shentsize`, so a malformed table is skipped instead of trusted. Merge acc0db4c825b09bc06d59b3a2e6f1981e4ce28d1 into 38f8c1535de9ebc12c6340e5c1c8ebd72afde560 Merging this change closes #2103 PiperOrigin-RevId: 978863309 Change-Id: I80cd8c0b69c617ae8f70321d51d582ffeeb9cfc2
diff --git a/absl/debugging/symbolize_elf.inc b/absl/debugging/symbolize_elf.inc index 182d0fd..e5e171c 100644 --- a/absl/debugging/symbolize_elf.inc +++ b/absl/debugging/symbolize_elf.inc
@@ -774,6 +774,25 @@ return SYMBOL_NOT_FOUND; } + // In the ELF specification, sh_entsize is a generic section header field that + // holds the entry size in bytes for sections containing fixed-size entries. + // For symbol tables (SHT_SYMTAB and SHT_DYNSYM), the ELF specification + // mandates that each entry is an ElfW(Sym), so sh_entsize should always equal + // sizeof(ElfW(Sym)). + // + // However, because sh_entsize is read directly from the untrusted object + // file, a crafted or corrupt file can specify an unexpected value: + // 1. Setting sh_entsize to 0 would cause a division-by-zero (SIGFPE) in the + // num_symbols calculation below. + // 2. Setting sh_entsize to a value other than sizeof(ElfW(Sym)) would + // desynchronize our fixed-size ElfW(Sym) buffer reads from the per-entry + // stride in the file. + // Reject such malformed tables, mirroring the e_shentsize check we apply to + // section headers. + if (symtab->sh_entsize != sizeof(ElfW(Sym))) { + return SYMBOL_NOT_FOUND; + } + // Read multiple symbols at once to save read() calls. ElfW(Sym) *buf = reinterpret_cast<ElfW(Sym) *>(tmp_buf); const size_t buf_entries = tmp_buf_size / sizeof(buf[0]);
diff --git a/absl/debugging/symbolize_test.cc b/absl/debugging/symbolize_test.cc index 2cb1638..f3b15e1 100644 --- a/absl/debugging/symbolize_test.cc +++ b/absl/debugging/symbolize_test.cc
@@ -14,6 +14,7 @@ #include "absl/debugging/symbolize.h" +#include <cerrno> #include <cstddef> #include <cstdint> #include <cstring> @@ -27,6 +28,7 @@ #include "absl/base/attributes.h" #include "absl/base/casts.h" #include "absl/base/config.h" +#include "absl/base/internal/direct_mmap.h" #include "absl/base/internal/low_level_alloc.h" #include "absl/base/internal/per_thread_tls.h" #include "absl/base/optimization.h" @@ -46,6 +48,7 @@ #ifndef _WIN32 #include <fcntl.h> #include <sys/mman.h> +#include <unistd.h> #endif #if defined(MAP_ANON) && !defined(MAP_ANONYMOUS) @@ -440,6 +443,120 @@ close(fd); } + +#if defined(ABSL_INTERNAL_HAVE_ELF_SYMBOLIZE) +// Builds a minimal ELF image whose SHT_SYMTAB section header carries the given +// sh_entsize. Everything else is just enough to reach FindSymbol(): a single +// executable PT_LOAD segment (so the object is initialized) and a symbol table +// whose sh_link points at the null section header. +static std::string MakeElfWithSymtabEntSize(uint64_t sym_entsize) { + ElfW(Ehdr) ehdr; + memset(&ehdr, 0, sizeof(ehdr)); + memcpy(ehdr.e_ident, ELFMAG, SELFMAG); + ehdr.e_ident[EI_CLASS] = (sizeof(void*) == 8) ? ELFCLASS64 : ELFCLASS32; + ehdr.e_ident[EI_DATA] = ELFDATA2LSB; + ehdr.e_ident[EI_VERSION] = EV_CURRENT; + ehdr.e_type = ET_DYN; + ehdr.e_version = EV_CURRENT; + ehdr.e_phoff = sizeof(ElfW(Ehdr)); + ehdr.e_phentsize = sizeof(ElfW(Phdr)); + ehdr.e_phnum = 1; + ehdr.e_shoff = sizeof(ElfW(Ehdr)) + sizeof(ElfW(Phdr)); + ehdr.e_shentsize = sizeof(ElfW(Shdr)); + ehdr.e_shnum = 2; + ehdr.e_shstrndx = 0; + + ElfW(Phdr) phdr; + memset(&phdr, 0, sizeof(phdr)); + phdr.p_type = PT_LOAD; + phdr.p_flags = PF_R | PF_X; + phdr.p_filesz = 0x1000; + phdr.p_memsz = 0x1000; + phdr.p_align = 0x1000; + + // Section 0 is the mandatory null header, which also serves as the string + // table referenced by sh_link below (its contents are irrelevant here). + ElfW(Shdr) null_shdr; + memset(&null_shdr, 0, sizeof(null_shdr)); + + ElfW(Shdr) symtab; + memset(&symtab, 0, sizeof(symtab)); + symtab.sh_type = SHT_SYMTAB; + symtab.sh_link = 0; + symtab.sh_size = sizeof(ElfW(Sym)); + symtab.sh_entsize = sym_entsize; + symtab.sh_offset = ehdr.e_shoff + 2 * sizeof(ElfW(Shdr)); + + std::string image; + image.append(reinterpret_cast<const char*>(&ehdr), sizeof(ehdr)); + image.append(reinterpret_cast<const char*>(&phdr), sizeof(phdr)); + image.append(reinterpret_cast<const char*>(&null_shdr), sizeof(null_shdr)); + image.append(reinterpret_cast<const char*>(&symtab), sizeof(symtab)); + // Pad so the symtab sh_offset lands inside the file. + image.resize(image.size() + sizeof(ElfW(Sym)), '\0'); + return image; +} + +// Helper class that creates a temporary file on disk with the given content +// and unlinks it upon destruction. +class TempFile { + public: + explicit TempFile(absl::string_view content) { + std::string dir = testing::TempDir(); + if (dir.empty() || dir.back() != '/') { + dir.push_back('/'); + } + path_ = dir + "absl_bad_symtab_XXXXXX"; + int fd = mkstemp(path_.data()); + CHECK_NE(fd, -1); + CHECK_EQ(write(fd, content.data(), content.size()), + static_cast<ssize_t>(content.size())); + close(fd); + } + ~TempFile() { unlink(path_.c_str()); } + + TempFile(const TempFile&) = delete; + TempFile& operator=(const TempFile&) = delete; + + const char* path() const { return path_.c_str(); } + + private: + std::string path_; +}; + +// A crafted object file can set the symbol table's sh_entsize to zero, which is +// used verbatim as the divisor for the symbol count in FindSymbol(). Before +// the fix this divided by zero (SIGFPE) while symbolizing; now the malformed +// table is skipped and the address simply fails to resolve. +TEST(Symbolize, InvalidSymtabEntSizeDoesNotCrash) { + TempFile file(MakeElfWithSymtabEntSize(/*sym_entsize=*/0)); + + // Map an executable page to provide a memory address to symbolize. + const size_t page_size = static_cast<size_t>(sysconf(_SC_PAGESIZE)); + void* region = + absl::base_internal::DirectMmap(nullptr, page_size, PROT_READ | PROT_EXEC, + MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + if (region == MAP_FAILED) { + GTEST_SKIP() << "Unable to map an executable page: errno=" << errno; + } + absl::Cleanup unmap = [&] { + absl::base_internal::DirectMunmap(region, page_size); + }; + + // absl::Symbolize() needs this file mapping hint to associate addresses + // within the mapped region with the ELF file on disk and attempt + // symbolization. + ASSERT_TRUE(absl::debugging_internal::RegisterFileMappingHint( + region, static_cast<char*>(region) + page_size, 0, file.path())); + + // Output buffer for absl::Symbolize(). 512 bytes is arbitrarily chosen to + // receive any demangled symbol name for the purposes of this test. + char symbol_buf[512]; + // The point of the test is that this returns instead of crashing with SIGFPE. + EXPECT_FALSE(absl::Symbolize(static_cast<char*>(region) + 8, symbol_buf, + sizeof(symbol_buf))); +} +#endif // ABSL_INTERNAL_HAVE_ELF_SYMBOLIZE #endif // !ABSL_INTERNAL_HAVE_DARWIN_SYMBOLIZE && // !ABSL_INTERNAL_HAVE_EMSCRIPTEN_SYMBOLIZE