]> git.ipfire.org Git - thirdparty/glibc.git/commitdiff
elf: Eliminate alloca for program-header table in the ELF loader
authorAdhemerval Zanella <adhemerval.zanella@linaro.org>
Fri, 8 May 2026 18:59:22 +0000 (15:59 -0300)
committerAdhemerval Zanella <adhemerval.zanella@linaro.org>
Mon, 11 May 2026 18:49:41 +0000 (15:49 -0300)
The ELF loader allocates the program-header table on the stack with
alloca(e_phnum * sizeof(ElfW(Phdr))) in two places: once in
open_verify to call elf_machine_reject_phdr_p, and again in
_dl_map_object_from_fd to scan segment types.  Both fall back to
alloca only when the table does not fit in the initial fbp->buf read;
for a crafted ELF with e_phnum == 0x7FFF this means up to ~1.8 MB
(32767 × 56 bytes on a 64-bit host) on the stack in each call, with
no guard against the combination exhausting the available stack space.

A latent variant of this problem exists even for ordinary shared
libraries when dlopen is called from a thread running with
PTHREAD_STACK_MIN stack (16 KB on Linux).  The nptl/tst-minstack-exit
test demonstrates that glibc code paths must operate correctly under
minimum-stack conditions; loading a shared library with even a modest
number of program headers can overflow the remaining stack through the
alloca-based phdr table.

This patch eliminates both allocas by replacing them with a single
_dl_map_object_scan_phdrs function that reads program headers in
fixed-size chunks into the existing fbp->buf scratch buffer (512 B on
32-bit, 832 B on 64-bit) using pread.  When all headers fit within
the bytes already captured by open_verify's initial read() call (the
common case), no extra syscall is needed.  This should be the case for
most of the ELF objects and should not required additional syscalls.

The slow path issues as many pread calls as necessary without any stack
growth proportional to e_phnum.  The elf_machine_reject_phdr_p interface
is redesigned around a new struct dl_machine_phdr_info and on MIPS this
captures the PT_MIPS_ABIFLAGS entry in-flight, so the compatibility check
in elf_machine_reject_phdr_p no longer needs to re-scan the program-header
table.

Checked on aarch64-linux-gnu, x86_64-linux-gnu, and i686-linux-gnu.

NB: this patch depends on https://sourceware.org/pipermail/libc-alpha/2026-May/177239.html
Reviewed-by: H.J. Lu <hjl.tools@gmail.com>
elf/Makefile
elf/dl-load.c
elf/dl-load.h
elf/dl-machine-reject-phdr.h
elf/tst-bz26577-minstack.c [new file with mode: 0644]
sysdeps/mips/dl-machine-reject-phdr.h

index 896d098d001e18cb884aa5a9ce0994ba1131e3b1..f4d22c159914f6a5cbeb8615e09919febc47b449 100644 (file)
@@ -405,6 +405,7 @@ tests += \
   tst-auxobj-dlopen \
   tst-big-note \
   tst-bz26577 \
+  tst-bz26577-minstack \
   tst-debug1 \
   tst-deep1 \
   tst-dl-is_dso \
@@ -2767,6 +2768,9 @@ $(objpfx)tst-bz26577-mod.so: gen-tst-bz26577-mod.py $(..)/scripts/glibcelf.py \
        PYTHONPATH=$(..)scripts $(PYTHON) $< $@ $(objpfx)ld.so
 generated += tst-bz26577-mod.so
 
+$(objpfx)tst-bz26577-minstack: $(shared-thread-library)
+$(objpfx)tst-bz26577-minstack.out: $(objpfx)tst-bz26577-mod.so
+
 $(objpfx)tst-unwind-ctor: $(objpfx)tst-unwind-ctor-lib.so
 LDLIBS-tst-unwind-ctor += $(libunwind)
 LDFLAGS-tst-unwind-ctor-lib.so = -Wl,--unresolved-symbols=ignore-all
index 498d6be8a20b76ecb06027be73358d06cfbe0672..f6e391a468978c83301f9c99b2aa2a5bc8e4a97c 100644 (file)
@@ -935,65 +935,184 @@ _dl_notify_new_object (int mode, Lmid_t nsid, struct link_map *l)
 #endif
 }
 
-/* Initialize the PT_LOAD iterator IT by scanning the program header table
-   PHDR of PHNUM entries using PAGESIZE for alignment.  Precomputes
-   p_align_max, has_holes, and first/last segment metadata needed by
-   _dl_map_segments.
-   Returns NULL on success or an error message string on failure.  */
-static const char *
+/* Initialize the PT_LOAD iterator IT for reading program headers from FD
+   at file offset PHOFF with PHNUM entries.  Zeros all precomputed fields
+   so the caller's scan loop can fill them in.  */
+static void
 _dl_pt_load_iterator_init (struct dl_pt_load_iterator *it,
-                          const ElfW(Phdr) *phdr, uint16_t phnum,
-                          bool *has_holes)
+                          int fd, ElfW(Off) phoff, uint16_t phnum)
 {
-  const size_t pagesize = GLRO(dl_pagesize);
-  it->phdr = phdr;
-  it->phdr_end = phdr + phnum;
-  it->pagesize= pagesize;
-  it->p_align_max   = 0;
+  it->fd = fd;
+  it->phoff = phoff;
+  it->phnum = phnum;
+  it->idx = 0;
+  it->pagesize = GLRO (dl_pagesize);
+  it->p_align_max = 0;
   it->nloadcmds = 0;
   it->first_mapstart = 0;
-  it->last_mapstart  = 0;
-  it->last_allocend  = 0;
-  *has_holes = false;
+  it->last_mapstart = 0;
+  it->last_allocend = 0;
+}
 
+/* Scan all program headers from IT->fd in chunks, using FBP->buf as a
+   scratch buffer.  Fills in IT's precomputed PT_LOAD metadata and collects
+   segment attributes into L.  Returns NULL on success, or an error message
+   string on failure; sets *ERRVALP to errno for I/O errors, 0 otherwise.  */
+static const char *
+_dl_map_object_scan_phdrs (struct dl_pt_load_iterator *it,
+                          struct filebuf *fbp, struct link_map *l, int mode,
+                          unsigned int *stack_flagsp, bool *has_holesp,
+                          bool *empty_dynamicp, int *errvalp)
+{
   ElfW(Addr) prev_mapend = 0;
-
-  for (const ElfW(Phdr) *ph = phdr; ph < phdr + phnum; ++ph)
+  const ElfW(Half) phdrs_per_buf = sizeof (fbp->buf) / sizeof (ElfW(Phdr));
+  ElfW(Phdr) *chunk = (ElfW(Phdr) *) fbp->buf;
+  struct dl_machine_phdr_info minfo;
+  elf_machine_phdr_info_init (&minfo);
+
+  /* Fast path: if all program headers fit within the bytes already read
+     into fbp->buf by open_verify, iterate them directly without any
+     additional pread syscalls.  The slow path falls through to pread
+     in chunks (which overwrites fbp->buf, but the caller has already
+     saved the ELF header to a local copy).  */
+  const bool cached
+    = (it->phoff + (ElfW(Off)) it->phnum * sizeof (ElfW(Phdr))
+       <= (ElfW(Off)) fbp->len);
+
+  for (ElfW(Half) base = 0; base < it->phnum; )
     {
-      if (ph->p_type != PT_LOAD)
-       continue;
+      ElfW(Half) batch;
+      const ElfW(Phdr) *batch_ptr;
+
+      if (__glibc_likely (cached))
+       {
+         batch = it->phnum;
+         batch_ptr = (const ElfW(Phdr) *) (fbp->buf + it->phoff);
+       }
+      else
+       {
+         batch = it->phnum - base;
+         if (batch > phdrs_per_buf)
+           batch = phdrs_per_buf;
+         size_t bytes = (size_t) batch * sizeof (ElfW(Phdr));
+         ElfW(Off) off = it->phoff + (ElfW(Off)) base * sizeof (ElfW(Phdr));
+         if (__pread64_nocancel (it->fd, chunk, bytes, off) != bytes)
+           {
+             *errvalp = errno;
+             return N_("cannot read file data");
+           }
+         batch_ptr = chunk;
+       }
+
+      for (ElfW(Half) i = 0; i < batch; i++)
+       {
+         const ElfW(Phdr) *ph = &batch_ptr[i];
+         elf_machine_phdr_collect (&minfo, ph);
+         switch (ph->p_type)
+           {
+           case PT_LOAD:
+             {
+               if (__glibc_unlikely (((ph->p_vaddr - ph->p_offset)
+                                      & (it->pagesize - 1)) != 0))
+                 {
+                   *errvalp = 0;
+                   return N_("ELF load command address/offset not page-aligned");
+                 }
+               ElfW(Addr) mapstart = ALIGN_DOWN (ph->p_vaddr, it->pagesize);
+               ElfW(Addr) mapend = ALIGN_UP (ph->p_vaddr + ph->p_filesz,
+                                             it->pagesize);
+               ElfW(Off)  mapoff = ALIGN_DOWN (ph->p_offset, it->pagesize);
+               int prot = pf_to_prot (ph->p_flags);
+               if (powerof2 (ph->p_align) && ph->p_align > it->p_align_max)
+                 it->p_align_max = ph->p_align;
+               it->p_align_max = _dl_map_segment_align (&(struct loadcmd) {
+                                                          .mapstart = mapstart,
+                                                          .mapend   = mapend,
+                                                          .mapoff   = mapoff,
+                                                          .prot     = prot },
+                                                        it->p_align_max);
+               if (it->nloadcmds > 0 && prev_mapend != mapstart)
+                 *has_holesp = true;
+               prev_mapend = mapend;
+               if (it->nloadcmds == 0)
+                 it->first_mapstart = mapstart;
+               it->last_mapstart = mapstart;
+               it->last_allocend = ph->p_vaddr + ph->p_memsz;
+               it->nloadcmds++;
+             }
+             break;
+
+           /* These entries tell us where to find things once the file's
+              segments are mapped in.  We record the addresses it says
+              verbatim, and later correct for the run-time load address.  */
+           case PT_DYNAMIC:
+             if (ph->p_filesz == 0)
+               *empty_dynamicp = true; /* Usually separate debuginfo.  */
+             else
+               {
+                 /* Debuginfo only files from "objcopy --only-keep-debug"
+                    contain a PT_DYNAMIC segment with p_filesz == 0.  Skip
+                    such a segment to avoid a crash later.  */
+                 l->l_ld = (void *) ph->p_vaddr;
+                 l->l_ldnum = ph->p_memsz / sizeof (ElfW(Dyn));
+                 l->l_ld_readonly = (ph->p_flags & PF_W) == 0;
+               }
+             break;
 
-      if (__glibc_unlikely (((ph->p_vaddr - ph->p_offset)
-                            & (pagesize - 1)) != 0))
-       return N_("ELF load command address/offset not page-aligned");
-
-      ElfW(Addr) mapstart = ALIGN_DOWN (ph->p_vaddr, pagesize);
-      ElfW(Addr) mapend = ALIGN_UP (ph->p_vaddr + ph->p_filesz, pagesize);
-      ElfW(Off) mapoff = ALIGN_DOWN (ph->p_offset, pagesize);
-      int prot = pf_to_prot (ph->p_flags);
-      /* Remember the maximum p_align.  */
-      if (powerof2 (ph->p_align) && ph->p_align > it->p_align_max)
-       it->p_align_max = ph->p_align;
-
-      /* Use architecture-specific logic to potentially adjust p_align_max
-        (e.g., for Transparent Huge Page eligibility on Linux).  */
-      it->p_align_max = _dl_map_segment_align (&(struct loadcmd) {
-                                                .mapstart = mapstart,
-                                                .mapend = mapend,
-                                                .mapoff = mapoff,
-                                                .prot = prot },
-                                              it->p_align_max);
-
-      if (it->nloadcmds > 0 && prev_mapend != mapstart)
-       *has_holes = true;
-      prev_mapend = mapend;
-
-      if (it->nloadcmds == 0)
-       it->first_mapstart = mapstart;
-
-      it->last_mapstart = mapstart;
-      it->last_allocend = ph->p_vaddr + ph->p_memsz;
-      it->nloadcmds++;
+           case PT_PHDR:
+             l->l_phdr = (void *) ph->p_vaddr;
+             break;
+
+           case PT_TLS:
+             if (ph->p_memsz == 0)
+               /* Nothing to do for an empty segment.  */
+               break;
+
+             l->l_tls_blocksize = ph->p_memsz;
+             l->l_tls_align = ph->p_align;
+             if (ph->p_align == 0)
+               l->l_tls_firstbyte_offset = 0;
+             else
+               l->l_tls_firstbyte_offset = ph->p_vaddr & (ph->p_align - 1);
+             l->l_tls_initimage_size = ph->p_filesz;
+             /* Since we don't know the load address yet only store the
+                offset.  We will adjust it later.  */
+             l->l_tls_initimage = (void *) ph->p_vaddr;
+
+             /* l->l_tls_modid is assigned below, once there is no
+                possibility for failure.  */
+
+             if (l->l_type != lt_library
+                 && GL(dl_tls_dtv_slotinfo_list) == NULL)
+               {
+#ifdef SHARED
+                 /* We are loading the executable itself when the dynamic
+                    linker was executed directly.  The setup will happen
+                    later.  */
+                 assert (l->l_prev == NULL || (mode & __RTLD_AUDIT) != 0);
+#else
+                 assert (false && "TLS not initialized in static application");
+#endif
+               }
+             break;
+
+           case PT_GNU_STACK:
+             *stack_flagsp = pf_to_prot (ph->p_flags);
+             break;
+
+           case PT_GNU_RELRO:
+             l->l_relro_addr = ph->p_vaddr;
+             l->l_relro_size = ph->p_memsz;
+             break;
+           }
+       }
+      base += batch;
+    }
+
+  if (__glibc_unlikely (elf_machine_reject_phdr_p (&minfo, l, it->fd)))
+    {
+      *errvalp = 0;
+      return N_("ELF file incompatible with this system");
     }
 
   return NULL;
@@ -1012,8 +1131,7 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
                        const void *stack_endp, Lmid_t nsid)
 {
   struct link_map *l = NULL;
-  const ElfW(Ehdr) *header;
-  const ElfW(Phdr) *phdr;
+  ElfW(Ehdr) header;
   const ElfW(Phdr) *ph;
   size_t maplength;
   int type;
@@ -1123,8 +1241,10 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
   if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_FILES))
     _dl_debug_printf ("file=%s [%lu];  generating link map\n", name, nsid);
 
-  /* This is the ELF header.  We read it in `open_verify'.  */
-  header = (void *) fbp->buf;
+  /* The ELF header is already validate in `open_verify', make a local copy
+     because _dl_map_object_scan_phdrs may overwrite fbp->buf when reading
+     phdrs via pread in the slow path.  */
+  memcpy (&header, fbp->buf, sizeof header);
 
   /* Enter the new object in the list of loaded objects.  */
   l = _dl_new_object (realname, name, l_type, loader, mode, nsid);
@@ -1136,26 +1256,9 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
       errstring = N_("cannot create shared object descriptor");
       goto lose_errno;
     }
-
-  /* Extract the remaining details we need from the ELF header
-     and then read in the program header table.  */
-  l->l_entry = header->e_entry;
-  type = header->e_type;
-  l->l_phnum = header->e_phnum;
-
-  maplength = header->e_phnum * sizeof (ElfW(Phdr));
-  if (header->e_phoff + maplength <= (size_t) fbp->len)
-    phdr = (void *) (fbp->buf + header->e_phoff);
-  else
-    {
-      phdr = alloca (maplength);
-      if ((size_t) __pread64_nocancel (fd, (void *) phdr, maplength,
-                                      header->e_phoff) != maplength)
-       {
-         errstring = N_("cannot read file data");
-         goto lose_errno;
-       }
-    }
+  l->l_entry = header.e_entry;
+  type = header.e_type;
+  l->l_phnum = header.e_phnum;
 
    /* On most platforms presume that PT_GNU_STACK is absent and the stack is
     * executable.  Other platforms default to a nonexecutable stack and don't
@@ -1163,96 +1266,29 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
    unsigned int stack_flags = DEFAULT_STACK_PROT_PERMS;
 
   {
-    /* Scan the program header table, collecting its load commands.  The init
-       pass precomputes p_align_max, has_holes, and first/last segment
-       metadata; subsequent calls to _dl_pt_load_iterator_next yield one
-       loadcmd at a time.  */
+    /* Single pass over the program header table: initialize the PT_LOAD
+       iterator (precomputing p_align_max, has_holes, and first/last segment
+       metadata) and collect all other segment attributes simultaneously.
+       Program headers are read in chunks into fbp->buf via pread so that
+       no large stack buffer is needed regardless of e_phnum.  */
     struct dl_pt_load_iterator it;
     bool has_holes;
     bool empty_dynamic = false;
 
-    errstring = _dl_pt_load_iterator_init (&it, phdr, l->l_phnum, &has_holes);
+    _dl_pt_load_iterator_init (&it, fd, header.e_phoff, l->l_phnum);
+    has_holes = false;
+
+    errstring = _dl_map_object_scan_phdrs (&it, fbp, l, mode, &stack_flags,
+                                          &has_holes, &empty_dynamic, &errval);
     if (__glibc_unlikely (errstring != NULL))
       goto lose;
 
     if (__glibc_unlikely (it.nloadcmds == 0))
       {
-       /* Avoid the below calculation for bogus objects.  */
        errstring = N_("object file has no loadable segments");
        goto lose;
       }
 
-    for (ph = phdr; ph < &phdr[l->l_phnum]; ++ph)
-      switch (ph->p_type)
-       {
-         /* These entries tell us where to find things once the file's
-            segments are mapped in.  We record the addresses it says
-            verbatim, and later correct for the run-time load address.  */
-       case PT_DYNAMIC:
-         if (ph->p_filesz == 0)
-           empty_dynamic = true; /* Usually separate debuginfo.  */
-         else
-           {
-             /* Debuginfo only files from "objcopy --only-keep-debug"
-                contain a PT_DYNAMIC segment with p_filesz == 0.  Skip
-                such a segment to avoid a crash later.  */
-             l->l_ld = (void *) ph->p_vaddr;
-             l->l_ldnum = ph->p_memsz / sizeof (ElfW(Dyn));
-             l->l_ld_readonly = (ph->p_flags & PF_W) == 0;
-           }
-         break;
-
-       case PT_PHDR:
-         l->l_phdr = (void *) ph->p_vaddr;
-         break;
-
-       case PT_LOAD:
-         /* PT_LOAD segments are handled by the iterator.  */
-         break;
-
-       case PT_TLS:
-         if (ph->p_memsz == 0)
-           /* Nothing to do for an empty segment.  */
-           break;
-
-         l->l_tls_blocksize = ph->p_memsz;
-         l->l_tls_align = ph->p_align;
-         if (ph->p_align == 0)
-           l->l_tls_firstbyte_offset = 0;
-         else
-           l->l_tls_firstbyte_offset = ph->p_vaddr & (ph->p_align - 1);
-         l->l_tls_initimage_size = ph->p_filesz;
-         /* Since we don’t know the load address yet only store the
-            offset.  We will adjust it later.  */
-         l->l_tls_initimage = (void *) ph->p_vaddr;
-
-         /* l->l_tls_modid is assigned below, once there is no
-            possibility for failure.  */
-
-         if (l->l_type != lt_library
-             && GL(dl_tls_dtv_slotinfo_list) == NULL)
-           {
-#ifdef SHARED
-             /* We are loading the executable itself when the dynamic
-                linker was executed directly.  The setup will happen
-                later.  */
-             assert (l->l_prev == NULL || (mode & __RTLD_AUDIT) != 0);
-#else
-             assert (false && "TLS not initialized in static application");
-#endif
-           }
-         break;
-
-       case PT_GNU_STACK:
-         stack_flags = pf_to_prot (ph->p_flags);
-         break;
-
-       case PT_GNU_RELRO:
-         l->l_relro_addr = ph->p_vaddr;
-         l->l_relro_size = ph->p_memsz;
-         break;
-       }
-
     /* dlopen of an executable is not valid because it is not possible
        to perform proper relocations, handle static TLS, or run the
        ELF constructors.  For PIE, the check needs the dynamic
@@ -1280,7 +1316,7 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
        This is responsible for filling in:
        l_map_start, l_map_end, l_addr, l_contiguous, l_phdr
      */
-    errstring = _dl_map_segments (l, fd, header, type, &it,
+    errstring = _dl_map_segments (l, fd, &header, type, &it,
                                  maplength, has_holes, loader);
     if (__glibc_unlikely (errstring != NULL))
       {
@@ -1313,18 +1349,22 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
   if (l->l_phdr == NULL)
     {
       /* The program header is not contained in any of the segments.
-        We have to allocate memory ourself and copy it over from out
-        temporary place.  */
-      ElfW(Phdr) *newp = (ElfW(Phdr) *) malloc (header->e_phnum
-                                               * sizeof (ElfW(Phdr)));
+        Allocate memory and read the program header table from the file.  */
+      size_t phdr_size = (size_t) header.e_phnum * sizeof (ElfW(Phdr));
+      ElfW(Phdr) *newp = (ElfW(Phdr) *) malloc (phdr_size);
       if (newp == NULL)
        {
          errstring = N_("cannot allocate memory for program header");
          goto lose_errno;
        }
-
-      l->l_phdr = memcpy (newp, phdr,
-                         (header->e_phnum * sizeof (ElfW(Phdr))));
+      if ((size_t) __pread64_nocancel (fd, newp, phdr_size,
+                                      header.e_phoff) != phdr_size)
+       {
+         free (newp);
+         errstring = N_("cannot read file data");
+         goto lose_errno;
+       }
+      l->l_phdr = newp;
       l->l_phdr_allocated = 1;
     }
   else
@@ -1584,8 +1624,6 @@ open_verify (const char *name, int fd,
   if (fd != -1)
     {
       ElfW(Ehdr) *ehdr;
-      ElfW(Phdr) *phdr;
-      size_t maplength;
 
       /* We successfully opened the file.  Now verify it is a file
         we can use.  */
@@ -1711,32 +1749,6 @@ open_verify (const char *name, int fd,
          goto lose;
        }
 
-      maplength = ehdr->e_phnum * sizeof (ElfW(Phdr));
-      if (ehdr->e_phoff + maplength <= (size_t) fbp->len)
-       phdr = (void *) (fbp->buf + ehdr->e_phoff);
-      else
-       {
-         phdr = alloca (maplength);
-         if ((size_t) __pread64_nocancel (fd, (void *) phdr, maplength,
-                                          ehdr->e_phoff) != maplength)
-           {
-             errval = errno;
-             errstring = N_("cannot read file data");
-             goto lose;
-           }
-       }
-
-      if (__glibc_unlikely (elf_machine_reject_phdr_p
-                           (phdr, ehdr->e_phnum, fbp->buf, fbp->len,
-                            loader, fd)))
-       {
-         if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_LIBS))
-           _dl_debug_printf ("    (incompatible ELF headers with the host)\n");
-         __close_nocancel (fd);
-         __set_errno (ENOENT);
-         return -1;
-       }
-
     }
   else
     {
index d4e52dd856152a95e2792f7836cf4e5f0f071e00..e58028038c9382adb6b282597b788a3f70c40eb1 100644 (file)
@@ -24,6 +24,7 @@
 #include <sys/mman.h>
 #include <libc-pointer-arith.h>
 #include <stackinfo.h>
+#include <not-cancel.h>
 
 
 /* On some systems, no flag bits are given to specify file mapping.  */
@@ -85,11 +86,15 @@ struct loadcmd
 
 /* Iterator for PT_LOAD program header segments.  It should be initialized
    by _dl_pt_load_iterator_init once, then _dl_pt_load_iterator_next
-   repeatedly to walk each PT_LOAD segment without storing them all.  */
+   repeatedly to walk each PT_LOAD segment without storing them all.
+   Segments are re-read one at a time via pread so that no large stack
+   buffer is needed for the program header table.  */
 struct dl_pt_load_iterator
 {
-  const ElfW(Phdr) *phdr;       /* Current position in program header table.  */
-  const ElfW(Phdr) *phdr_end;   /* End of program header table.  */
+  int fd;                       /* File descriptor for pread.  */
+  ElfW(Off) phoff;              /* Program header table file offset.  */
+  ElfW(Half) phnum;             /* Total number of program headers.  */
+  ElfW(Half) idx;               /* Index of next header to read.  */
   ElfW(Addr) p_align_max;       /* Maximum p_align over all PT_LOAD segments.  */
   ElfW(Addr) pagesize;          /* System page size (GLRO(dl_pagesize)).  */
 
@@ -103,22 +108,28 @@ struct dl_pt_load_iterator
 
 /* Advance iterator IT to the next PT_LOAD segment and fill C with its
    decoded load command.  Returns true when a segment was found, false
-   when the end of the program header table has been reached.  */
+   when the end of the program header table has been reached or a read
+   error occurs.  */
 static __always_inline bool
 _dl_pt_load_iterator_next (struct dl_pt_load_iterator *it, struct loadcmd *c)
 {
-  while (it->phdr < it->phdr_end)
+  while (it->idx < it->phnum)
     {
-      const ElfW(Phdr) *ph = it->phdr++;
-      if (ph->p_type != PT_LOAD)
+      ElfW(Phdr) ph;
+      ElfW(Off) off = it->phoff + (ElfW(Off)) it->idx * sizeof ph;
+      it->idx++;
+      if (__pread64_nocancel (it->fd, &ph, sizeof ph, off)
+         != (ssize_t) sizeof ph)
+        return false;
+      if (ph.p_type != PT_LOAD)
         continue;
 
-      c->mapstart = ALIGN_DOWN (ph->p_vaddr, it->pagesize);
-      c->mapend = ALIGN_UP (ph->p_vaddr + ph->p_filesz, it->pagesize);
-      c->dataend = ph->p_vaddr + ph->p_filesz;
-      c->allocend = ph->p_vaddr + ph->p_memsz;
-      c->mapoff = ALIGN_DOWN (ph->p_offset, it->pagesize);
-      c->prot = pf_to_prot (ph->p_flags);
+      c->mapstart = ALIGN_DOWN (ph.p_vaddr, it->pagesize);
+      c->mapend   = ALIGN_UP (ph.p_vaddr + ph.p_filesz, it->pagesize);
+      c->dataend  = ph.p_vaddr + ph.p_filesz;
+      c->allocend = ph.p_vaddr + ph.p_memsz;
+      c->mapoff   = ALIGN_DOWN (ph.p_offset, it->pagesize);
+      c->prot     = pf_to_prot (ph.p_flags);
       c->mapalign = it->p_align_max;
       return true;
     }
index 954c22cec6439323a31ebead1581bcdbf1d20f7c..9a3e1fb811013db5f99c131965ff3fbe8af1384c 100644 (file)
 
 #include <stdbool.h>
 
-/* Return true iff ELF program headers are incompatible with the running
-   host.  */
+/* Machine-specific data collected during the program-header scan for use
+   by elf_machine_reject_phdr_p.  Ports that override elf_machine_reject_phdr_p
+   must define their own layout; this generic version carries no data.  */
+struct dl_machine_phdr_info
+{
+};
+
+/* Initialize INFO before the program-header scan begins.  */
+static inline void
+elf_machine_phdr_info_init (struct dl_machine_phdr_info *info
+                           __attribute__ ((__unused__)))
+{
+}
+
+/* Called once per ELF program header PH during the scan.  Records any
+   machine-specific data from PH that elf_machine_reject_phdr_p needs.  */
+static inline void
+elf_machine_phdr_collect (struct dl_machine_phdr_info *info
+                         __attribute__ ((__unused__)),
+                         const ElfW(Phdr) *ph __attribute__ ((__unused__)))
+{
+}
+
+/* Return true iff the program-header data collected in INFO is incompatible
+   with the running host.  */
 static inline bool
-elf_machine_reject_phdr_p (const ElfW(Phdr) *phdr, unsigned int phnum,
-                          const char *buf, size_t len, struct link_map *map,
-                          int fd)
+elf_machine_reject_phdr_p (const struct dl_machine_phdr_info *info
+                          __attribute__ ((__unused__)),
+                          struct link_map *map __attribute__ ((__unused__)),
+                          int fd __attribute__ ((__unused__)))
 {
   return false;
 }
diff --git a/elf/tst-bz26577-minstack.c b/elf/tst-bz26577-minstack.c
new file mode 100644 (file)
index 0000000..6b8fa5e
--- /dev/null
@@ -0,0 +1,66 @@
+/* Test that dlopen on a large-e_phnum DSO does not overflow the stack when
+   called from a thread with PTHREAD_STACK_MIN stack size (BZ #26577).
+   Copyright (C) 2026 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
+   <https://www.gnu.org/licenses/>.  */
+
+/* Before the fix for BZ #26577, _dl_map_object_from_fd and open_verify both
+   used alloca(e_phnum * sizeof(ElfW(Phdr))) when the program-header table did
+   not fit in the initial filebuf read.  Calling dlopen from a thread created
+   with PTHREAD_STACK_MIN stack size (typically 16 KB on Linux) would overflow
+   the stack even for moderate e_phnum values, and catastrophically for the
+   0x7FFF-header DSO used here.
+
+   The crafted DSO (tst-bz26577-mod.so, generated by gen-tst-bz26577-mod.py)
+   has e_phnum == 0x7FFF: one PT_LOAD covering the ELF header and the rest
+   PT_NULL.  dlopen on it must return NULL (no PT_DYNAMIC) without triggering
+   a stack overflow.  */
+
+#include <dlfcn.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <support/check.h>
+#include <support/support.h>
+#include <support/xthread.h>
+
+static void *
+dlopen_thread (void *arg)
+{
+  /* Attempt to load the large-phnum module from a PTHREAD_STACK_MIN thread.
+     With the old alloca-based code this overflows the stack; with the fix the
+     load fails gracefully because the module has no PT_DYNAMIC.  */
+  void *h = dlopen ((const char *) arg, RTLD_LAZY);
+  TEST_VERIFY (h == NULL);
+  return NULL;
+}
+
+static int
+do_test (void)
+{
+  char *path = xasprintf ("%s/elf/tst-bz26577-mod.so", support_objdir_root);
+
+  pthread_attr_t attr;
+  xpthread_attr_init (&attr);
+  xpthread_attr_setstacksize (&attr, PTHREAD_STACK_MIN);
+  pthread_t thr = xpthread_create (&attr, dlopen_thread, path);
+  xpthread_join (thr);
+  xpthread_attr_destroy (&attr);
+
+  free (path);
+  return 0;
+}
+
+#include <support/test-driver.c>
index 240e4e7fc92f0da7e14d79808b4ca3dc1f950f19..e64494c1982081fb2a17f6e6a9e30d1cea1a8ea9 100644 (file)
     return true;                                                             \
   }
 
-/* Search the program headers for the ABI Flags.  */
+/* Machine-specific data collected during the program-header scan.
+   Captures the PT_MIPS_ABIFLAGS entry if present, so
+   elf_machine_reject_phdr_p does not need to re-scan the headers.  */
+struct dl_machine_phdr_info
+{
+  bool has_mips_abiflags;      /* True if mips_abiflags is valid.  */
+  ElfW(Phdr) mips_abiflags;    /* Copy of the PT_MIPS_ABIFLAGS phdr.  */
+};
+
+/* Initialize INFO before the program-header scan begins.  */
+static inline void
+elf_machine_phdr_info_init (struct dl_machine_phdr_info *info)
+{
+  info->has_mips_abiflags = false;
+}
+
+/* Record the PT_MIPS_ABIFLAGS phdr, if present, so it is available
+   without re-reading the program header table.  */
+static inline void
+elf_machine_phdr_collect (struct dl_machine_phdr_info *info,
+                         const ElfW(Phdr) *ph)
+{
+  if (!info->has_mips_abiflags && ph->p_type == PT_MIPS_ABIFLAGS)
+    {
+      info->mips_abiflags = *ph;
+      info->has_mips_abiflags = true;
+    }
+}
 
+/* Search the program headers of an already-loaded object for its
+   PT_MIPS_ABIFLAGS entry (used by cached_fpabi_reject_phdr_p).  */
 static inline const ElfW(Phdr) *
 find_mips_abiflags (const ElfW(Phdr) *phdr, ElfW(Half) phnum)
 {
@@ -145,18 +174,17 @@ static const struct abi_req reqs[Val_GNU_MIPS_ABI_FP_MAX + 1] =
 
 static const struct abi_req none_req = { true, true, true, false, true };
 
-/* Return true iff ELF program headers are incompatible with the running
-   host.  This verifies that floating-point ABIs are compatible and
-   re-configures the hardware mode if necessary.  This code handles both the
-   DT_NEEDED libraries and the dlopen'ed libraries.  It also accounts for the
-   impact of dlclose.  */
+/* Return true iff the program headers collected in INFO are incompatible
+   with the running host.  This verifies that floating-point ABIs are
+   compatible and re-configures the hardware mode if necessary.  This code
+   handles both the DT_NEEDED libraries and the dlopen'ed libraries.  It
+   also accounts for the impact of dlclose.  */
 
 static bool __attribute_used__
-elf_machine_reject_phdr_p (const ElfW(Phdr) *phdr, unsigned int phnum,
-                          const char *buf, size_t len, struct link_map *map,
-                          int fd)
+elf_machine_reject_phdr_p (const struct dl_machine_phdr_info *info,
+                          struct link_map *map, int fd)
 {
-  const ElfW(Phdr) *ph = find_mips_abiflags (phdr, phnum);
+  const ElfW(Phdr) *ph = info->has_mips_abiflags ? &info->mips_abiflags : NULL;
   struct link_map *l;
   Lmid_t nsid;
   int in_abi = -1;