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