Fix BZ 23187 -- stack overflow for many Phdr[]s

Message ID CALoOobMmpZyrAsXqKrLuvO+c1Et88Cq6Kb2rY8N76NA5ihmCYA@mail.gmail.com
State New, archived
Headers

Commit Message

Paul Pluzhnikov May 21, 2018, 8:34 p.m. UTC
  Greetings,

Attached patch fixes several instances of unconstrained stack allocation,
which causes ld.so to overflow stack and crash while processing DSO with
1000s of Phdr[]s, and adds a test case.

2018-05-21  Paul Pluzhnikov  <ppluzhnikov@google.com>

         [BZ #23187]
         * elf/Makefile (tst-many-phdrs): New test.
         * elf/tst-many-phdrs.c: New.
         * elf/dl-load.c (_dl_map_object_from_fd): Constrain stack
allocation.
         (open_verify): Likewise.
  

Patch

diff --git a/elf/Makefile b/elf/Makefile
index 2dcd2b88e0..eeaecd4f30 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -186,7 +186,7 @@  tests += restest1 preloadtest loadfail multiload origtest resolvfail \
 	 tst-tlsalign tst-tlsalign-extern tst-nodelete-opened \
 	 tst-nodelete2 tst-audit11 tst-audit12 tst-dlsym-error tst-noload \
 	 tst-latepthread tst-tls-manydynamic tst-nodelete-dlclose \
-	 tst-debug1 tst-main1 tst-absolute-sym tst-big-note
+	 tst-debug1 tst-main1 tst-absolute-sym tst-big-note tst-many-phdrs
 #	 reldep9
 tests-internal += loadtest unload unload2 circleload1 \
 	 neededtest neededtest2 neededtest3 neededtest4 \
diff --git a/elf/dl-load.c b/elf/dl-load.c
index 431236920f..a551d97b43 100644
--- a/elf/dl-load.c
+++ b/elf/dl-load.c
@@ -809,6 +809,8 @@  _dl_map_object_from_fd (const char *name, const char *origname, int fd,
   const ElfW(Ehdr) *header;
   const ElfW(Phdr) *phdr;
   const ElfW(Phdr) *ph;
+  ElfW(Phdr) *phdr_malloced = NULL;
+  struct loadcmd *loadcmds_malloced = NULL;
   size_t maplength;
   int type;
   /* Initialize to keep the compiler happy.  */
@@ -827,6 +829,10 @@  _dl_map_object_from_fd (const char *name, const char *origname, int fd,
     call_lose:
       lose (errval, fd, name, realname, l, errstring,
 	    make_consistent ? r : NULL, nsid);
+      free (phdr_malloced);
+      free (loadcmds_malloced);
+      phdr_malloced = NULL;
+      loadcmds_malloced = NULL;
     }
 
   /* Look again to see if the real name matched another already loaded.  */
@@ -960,7 +966,18 @@  _dl_map_object_from_fd (const char *name, const char *origname, int fd,
     phdr = (void *) (fbp->buf + header->e_phoff);
   else
     {
-      phdr = alloca (maplength);
+      if (maplength < __MAX_ALLOCA_CUTOFF)
+	phdr = alloca (maplength);
+      else
+	{
+	  phdr_malloced = malloc (maplength);
+	  if (phdr_malloced == NULL)
+	    {
+	      errstring = N_("cannot allocate memory for program header");
+	      goto call_lose_errno;
+	    }
+	  phdr = phdr_malloced;
+	}
       __lseek (fd, header->e_phoff, SEEK_SET);
       if ((size_t) __libc_read (fd, (void *) phdr, maplength) != maplength)
 	{
@@ -976,10 +993,24 @@  _dl_map_object_from_fd (const char *name, const char *origname, int fd,
 
   {
     /* Scan the program header table, collecting its load commands.  */
-    struct loadcmd loadcmds[l->l_phnum];
+    struct loadcmd *loadcmds;
     size_t nloadcmds = 0;
+    const size_t loadcmds_size = sizeof (*loadcmds) * l->l_phnum;
     bool has_holes = false;
 
+    if (loadcmds_size < __MAX_ALLOCA_CUTOFF)
+      loadcmds = alloca (loadcmds_size);
+    else
+      {
+	loadcmds_malloced = malloc (loadcmds_size);
+	if (loadcmds_malloced == NULL)
+	  {
+	    errstring = N_("cannot allocate memory for program header");
+	    goto call_lose_errno;
+	  }
+	loadcmds = loadcmds_malloced;
+      }
+
     /* The struct is initialized to zero so this is not necessary:
     l->l_ld = 0;
     l->l_phdr = 0;
@@ -1126,6 +1157,11 @@  _dl_map_object_from_fd (const char *name, const char *origname, int fd,
 				  maplength, has_holes, loader);
     if (__glibc_unlikely (errstring != NULL))
       goto call_lose;
+
+    free (phdr_malloced);
+    free (loadcmds_malloced);
+    phdr_malloced = NULL;
+    loadcmds_malloced = NULL;
   }
 
   if (l->l_ld == 0)
@@ -1460,7 +1496,7 @@  open_verify (const char *name, int fd,
   if (fd != -1)
     {
       ElfW(Ehdr) *ehdr;
-      ElfW(Phdr) *phdr, *ph;
+      ElfW(Phdr) *phdr, *ph, *phdr_malloced = NULL;
       ElfW(Word) *abi_note;
       ElfW(Word) *abi_note_malloced = NULL;
       unsigned int osversion;
@@ -1596,7 +1632,18 @@  open_verify (const char *name, int fd,
 	phdr = (void *) (fbp->buf + ehdr->e_phoff);
       else
 	{
-	  phdr = alloca (maplength);
+	  if (maplength < __MAX_ALLOCA_CUTOFF)
+	    phdr = alloca (maplength);
+	  else
+	    {
+	      phdr_malloced = malloc (maplength);
+	      if (phdr_malloced == NULL)
+		{
+		  errstring = N_("cannot allocate memory for program header");
+		  goto call_lose;
+		}
+	      phdr = phdr_malloced;
+	    }
 	  __lseek (fd, ehdr->e_phoff, SEEK_SET);
 	  if ((size_t) __libc_read (fd, (void *) phdr, maplength) != maplength)
 	    {
@@ -1687,6 +1734,7 @@  open_verify (const char *name, int fd,
 
 	    break;
 	  }
+      free (phdr_malloced);
       free (abi_note_malloced);
     }
 
diff --git a/elf/tst-many-phdrs.c b/elf/tst-many-phdrs.c
new file mode 100644
index 0000000000..ea8b0a4ec5
--- /dev/null
+++ b/elf/tst-many-phdrs.c
@@ -0,0 +1,170 @@ 
+/* Copyright (C) 2018 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <http://www.gnu.org/licenses/>.  */
+
+/* A test that generates DSO with 65535 Phdr[]s, and asks ld.so
+   to list its dependencies (after adjusting stack limits).
+
+   This is used to test for stack overflow in dl-load.c  */
+
+#include <elf.h>
+#include <limits.h>
+#include <link.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/resource.h>
+#include <support/check.h>
+
+static const char *
+get_ld_so (void)
+{
+  struct link_map *lm = _r_debug.r_map;
+  while (lm != NULL)
+    {
+      if (lm->l_name != NULL && strstr (lm->l_name, "elf/ld") != NULL)
+	return lm->l_name;
+      lm = lm->l_next;
+    }
+  FAIL_EXIT1 ("Did not find ld.so");
+}
+
+static const char *
+write_dso (void)
+{
+  ElfW(Ehdr) ehdr;
+  ElfW(Phdr) phdr;
+  ElfW(Dyn) dyn;
+  const char *const ld_so = get_ld_so ();
+
+  FILE *fp = fopen (ld_so, "rb");
+  if (fp == NULL)
+    FAIL_EXIT1 ("fopen %s: %m", ld_so);
+
+  /* Use Ehdr from ld.so so we don't have to depend on specific e_machine
+     and e_ident[EI_DATA] -- we simply use the same values as ld.so.  */
+  if (fread (&ehdr, sizeof (ehdr), 1, fp) != 1)
+    FAIL_EXIT1 ("fread: %m");
+  fclose (fp);
+
+  static char fname[] = "/tmp/tst-many-phdrs-XXXXXX";
+  int fd = mkstemp (fname);
+  if (fd == -1)
+    FAIL_EXIT1 ("mkstemp failed: %m");
+
+  fp = fdopen (fd, "wb");
+  if (fp == NULL)
+    FAIL_EXIT1 ("fdopen failed: %m");
+
+  ehdr.e_phoff = sizeof (ehdr) + sizeof (dyn);
+  ehdr.e_phentsize = sizeof (phdr);
+  ehdr.e_phnum = 0xFFFF;  /* Max possible number of Phdr[]s.  */
+
+  if (1 != fwrite (&ehdr, sizeof (ehdr), 1, fp))
+    FAIL_EXIT1 ("fwrite ehdr: %m");
+
+  /* Write a single Dyn DT_NUL entry.  That isn't enough to make this a
+     valid DSO (dynstr and dynsym and hash are also necessary), but is enough
+     to trigger stack overflow when listing its dependencies.  */
+  memset (&dyn, 0, sizeof (dyn));
+  if (1 != fwrite (&dyn, sizeof (dyn), 1, fp))
+    FAIL_EXIT1 ("fwrite dyn: %m");
+
+  /* Add PT_DYNAMIC pointing to the Dyn entry we just wrote.  */
+  memset (&phdr, 0, sizeof (phdr));
+  phdr.p_offset = sizeof (ehdr);
+  phdr.p_vaddr = sizeof (ehdr);
+  phdr.p_paddr = sizeof (ehdr);
+  phdr.p_filesz = sizeof (dyn);
+  phdr.p_memsz = sizeof (dyn);
+  phdr.p_flags = PF_R;
+  phdr.p_type = PT_DYNAMIC;
+
+  if (1 != fwrite (&phdr, sizeof (phdr), 1, fp))
+    FAIL_EXIT1 ("fwrite phdr: %m");
+
+  /* Now the rest of Phdr[]s.  */
+  memset (&phdr, 0, sizeof (phdr));
+  phdr.p_offset = 0;
+  phdr.p_filesz = sizeof (ehdr);
+  phdr.p_memsz = sizeof (ehdr);
+  phdr.p_flags = PF_R;
+
+  for (int j = 1; j < ehdr.e_phnum; j++)
+    {
+      /* At least one PT_LOAD is required.  */
+      phdr.p_type = j == 1 ? PT_LOAD : PT_NOTE;
+      if (1 != fwrite (&phdr, sizeof (phdr), 1, fp))
+        FAIL_EXIT1 ("fwrite phdr: %m");
+    }
+
+  fclose (fp);
+  return fname;
+}
+
+static int
+do_test (int argc, char *argv[])
+{
+  const char *const dso = write_dso ();
+  const char *const ld_so = get_ld_so ();
+  struct rlimit rl;
+
+  if (getrlimit (RLIMIT_STACK, &rl) == -1)
+    FAIL_EXIT1 ("getrlimit (RLIMIT_STACK): %m");
+
+  char cmd[4 * 4096];
+  int n = snprintf (cmd, sizeof (cmd), "%s --list %s", ld_so, dso);
+  if (n >= sizeof (cmd))
+    FAIL_EXIT1 ("buffer too small: need %d bytes", n);
+
+  printf ("cmd: %s\n", cmd);
+
+  if (rl.rlim_cur == RLIM_INFINITY)
+    {
+      printf ("Capping stack at 8MiB\n");
+      rl.rlim_cur = 8 << 20;
+    }
+
+  while (rl.rlim_cur >= 128 * 1024)
+    {
+      printf ("Stack size: %zu\n", (size_t) rl.rlim_cur);
+      int rc = system (cmd);
+
+      if (rc != 0)
+        {
+          /* Clean up.  Comment this out to debug ld.so.  */
+          unlink (dso);
+
+          if (WIFEXITED (rc))
+            FAIL_EXIT1 ("child exited with %d", WEXITSTATUS (rc));
+          else if (WIFSIGNALED (rc))
+            FAIL_EXIT1 ("child exited with signal %d", WTERMSIG (rc));
+          else
+            FAIL_EXIT1 ("child exited with rc %d", rc);
+        }
+
+      rl.rlim_cur /= 2;
+      if (setrlimit (RLIMIT_STACK, &rl) == -1)
+        FAIL_EXIT1 ("setrlimit: %m");
+    }
+
+  unlink (dso);
+  return 0;
+}
+
+#define TEST_FUNCTION_ARGV do_test
+#include <support/test-driver.c>