]> git.ipfire.org Git - thirdparty/glibc.git/commitdiff
elf: Fix stack overflow in _dl_map_object_from_fd with large e_phnum (BZ 26577)
authorAdhemerval Zanella <adhemerval.zanella@linaro.org>
Thu, 7 May 2026 21:04:05 +0000 (18:04 -0300)
committerAdhemerval Zanella <adhemerval.zanella@linaro.org>
Mon, 11 May 2026 18:49:41 +0000 (15:49 -0300)
The _dl_map_object_from_fd uses a VLA (loadcmds[l->l_phnum]) whose size
is proportional to e_phnum.  A crafted ELF with e_phnum == 0x7FFF
allocates ~1.5 MB (32767 × 48 bytes on 64-bit machine) on the stack,
which adds to the previous ~1.75 MB alloca for the phdr table that
precedes it.

This patch follow Florian's suggestion [1] to use a two-pass approach
(collect-then-map) with a single-pass struct dl_pt_load_iterator that
precomputes the metadata needed by _dl_map_segments (p_align_max,
has_holes, first/last segment bounds, nloadcmds) and then yields one
struct loadcmd at a time through _dl_pt_load_iterator_next, holding at
most one loadcmd on the stack at a time.  The same iterator is
threaded through _dl_map_segments in dl-map-segments.h.

The main complex part is the test, which adds python-generated crafted
ET_DYN that has e_phnum == 0x7FFF: one PT_LOAD covering the ELF header
so the loader exercises the full iterator path, and the remaining
headers PT_NULL.  The test runs two subtests under a reduced stack limit
(phdr alloca + 1 MB headroom ≈ 2.75 MB, well below the 3.25 MB the
unfixed VLA code requires).

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

[1] https://sourceware.org/pipermail/libc-alpha/2026-February/175136.html
Reviewed-by: H.J. Lu <hjl.tools@gmail.com>
elf/Makefile
elf/dl-load.c
elf/dl-load.h
elf/dl-map-segments.h
elf/gen-tst-bz26577-mod.py [new file with mode: 0644]
elf/tst-bz26577.c [new file with mode: 0644]

index c835eb8156d2c3ae182057f032056d06cd27240b..896d098d001e18cb884aa5a9ce0994ba1131e3b1 100644 (file)
@@ -404,6 +404,7 @@ tests += \
   tst-auxobj \
   tst-auxobj-dlopen \
   tst-big-note \
+  tst-bz26577 \
   tst-debug1 \
   tst-deep1 \
   tst-dl-is_dso \
@@ -1223,6 +1224,7 @@ modules-names-nobuild += \
   tst-audit24bmod1 \
   tst-audit24bmod2 \
   tst-big-note-lib \
+  tst-bz26577-mod \
   tst-nodeps1-mod \
   tst-nodeps2-mod \
   tst-ro-dynamic-mod \
@@ -2758,6 +2760,13 @@ $(objpfx)tst-big-note: $(objpfx)tst-big-note-lib.so
 $(objpfx)tst-big-note-lib.so: $(objpfx)tst-big-note-lib.o
        $(LINK.o) -shared -o $@ $(LDFLAGS.so) $(dt-relr-ldflag) $<
 
+tst-bz26577-ARGS = -- $(host-test-program-cmd)
+$(objpfx)tst-bz26577.out: $(objpfx)tst-bz26577-mod.so
+$(objpfx)tst-bz26577-mod.so: gen-tst-bz26577-mod.py $(..)/scripts/glibcelf.py \
+                             $(objpfx)ld.so
+       PYTHONPATH=$(..)scripts $(PYTHON) $< $@ $(objpfx)ld.so
+generated += 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 48575fff06a9c127c5825d4a3dbad6532867677a..498d6be8a20b76ecb06027be73358d06cfbe0672 100644 (file)
@@ -935,6 +935,70 @@ _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 *
+_dl_pt_load_iterator_init (struct dl_pt_load_iterator *it,
+                          const ElfW(Phdr) *phdr, uint16_t phnum,
+                          bool *has_holes)
+{
+  const size_t pagesize = GLRO(dl_pagesize);
+  it->phdr = phdr;
+  it->phdr_end = phdr + phnum;
+  it->pagesize= pagesize;
+  it->p_align_max   = 0;
+  it->nloadcmds = 0;
+  it->first_mapstart = 0;
+  it->last_mapstart  = 0;
+  it->last_allocend  = 0;
+  *has_holes = false;
+
+  ElfW(Addr) prev_mapend = 0;
+
+  for (const ElfW(Phdr) *ph = phdr; ph < phdr + phnum; ++ph)
+    {
+      if (ph->p_type != PT_LOAD)
+       continue;
+
+      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++;
+    }
+
+  return NULL;
+}
+
 /* Map in the shared object NAME, actually located in REALNAME, and already
    opened on FD.  */
 
@@ -1099,17 +1163,25 @@ _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.  */
-    struct loadcmd loadcmds[l->l_phnum];
-    size_t nloadcmds = 0;
-    bool has_holes = false;
+    /* 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.  */
+    struct dl_pt_load_iterator it;
+    bool has_holes;
     bool empty_dynamic = false;
-    ElfW(Addr) p_align_max = 0;
 
-    /* The struct is initialized to zero so this is not necessary:
-    l->l_ld = 0;
-    l->l_phdr = 0;
-    l->l_addr = 0; */
+    errstring = _dl_pt_load_iterator_init (&it, phdr, l->l_phnum, &has_holes);
+    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)
        {
@@ -1135,46 +1207,7 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
          break;
 
        case PT_LOAD:
-         /* A load command tells us to map in part of the file.
-            We record the load commands and process them all later.  */
-         if (__glibc_unlikely (((ph->p_vaddr - ph->p_offset)
-                                & (GLRO(dl_pagesize) - 1)) != 0))
-           {
-             errstring
-               = N_("ELF load command address/offset not page-aligned");
-             goto lose;
-           }
-
-         struct loadcmd *c = &loadcmds[nloadcmds++];
-         c->mapstart = ALIGN_DOWN (ph->p_vaddr, GLRO(dl_pagesize));
-         c->mapend = ALIGN_UP (ph->p_vaddr + ph->p_filesz, GLRO(dl_pagesize));
-         c->dataend = ph->p_vaddr + ph->p_filesz;
-         c->allocend = ph->p_vaddr + ph->p_memsz;
-         /* Remember the maximum p_align.  */
-         if (powerof2 (ph->p_align) && ph->p_align > p_align_max)
-           p_align_max = ph->p_align;
-         c->mapoff = ALIGN_DOWN (ph->p_offset, GLRO(dl_pagesize));
-
-         DIAG_PUSH_NEEDS_COMMENT;
-
-#if __GNUC_PREREQ (11, 0)
-         /* Suppress invalid GCC warning:
-            ‘(((char *)loadcmds.113_68 + _933 + 16))[329406144173384849].mapend’ may be used uninitialized [-Wmaybe-uninitialized]
-            See: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=106008
-          */
-         DIAG_IGNORE_NEEDS_COMMENT_GCC (11, "-Wmaybe-uninitialized");
-#endif
-         /* Determine whether there is a gap between the last segment
-            and this one.  */
-         if (nloadcmds > 1 && c[-1].mapend != c->mapstart)
-           has_holes = true;
-         DIAG_POP_NEEDS_COMMENT;
-
-         /* Optimize a common case.  */
-         c->prot = pf_to_prot (ph->p_flags);
-
-         /* Architecture-specific adjustment of segment alignment. */
-         p_align_max = _dl_map_segment_align (c, p_align_max);
+         /* PT_LOAD segments are handled by the iterator.  */
          break;
 
        case PT_TLS:
@@ -1189,7 +1222,7 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
          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
+         /* Since we dont know the load address yet only store the
             offset.  We will adjust it later.  */
          l->l_tls_initimage = (void *) ph->p_vaddr;
 
@@ -1220,19 +1253,6 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
          break;
        }
 
-    if (__glibc_unlikely (nloadcmds == 0))
-      {
-       /* This only happens for a bogus object that will be caught with
-          another error below.  But we don't want to go through the
-          calculations below using NLOADCMDS - 1.  */
-       errstring = N_("object file has no loadable segments");
-       goto lose;
-      }
-
-    /* Align all PT_LOAD segments to the maximum p_align.  */
-    for (size_t i = 0; i < nloadcmds; i++)
-      loadcmds[i].mapalign = p_align_max;
-
     /* 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
@@ -1254,13 +1274,13 @@ _dl_map_object_from_fd (const char *name, const char *origname, int fd,
       }
 
     /* Length of the sections to be loaded.  */
-    maplength = loadcmds[nloadcmds - 1].allocend - loadcmds[0].mapstart;
+    maplength = it.last_allocend - it.first_mapstart;
 
     /* Now process the load commands and map segments into memory.
        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, loadcmds, nloadcmds,
+    errstring = _dl_map_segments (l, fd, header, type, &it,
                                  maplength, has_holes, loader);
     if (__glibc_unlikely (errstring != NULL))
       {
index 897c4034c5eb14fe8027955ab84c733892cf0a60..d4e52dd856152a95e2792f7836cf4e5f0f071e00 100644 (file)
@@ -22,6 +22,8 @@
 
 #include <link.h>
 #include <sys/mman.h>
+#include <libc-pointer-arith.h>
+#include <stackinfo.h>
 
 
 /* On some systems, no flag bits are given to specify file mapping.  */
@@ -81,6 +83,49 @@ 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.  */
+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.  */
+  ElfW(Addr) p_align_max;       /* Maximum p_align over all PT_LOAD segments.  */
+  ElfW(Addr) pagesize;          /* System page size (GLRO(dl_pagesize)).  */
+
+  /* Fields below are precomputed by _dl_pt_load_iterator_init and
+     are intended for use by _dl_map_segments.  */
+  ElfW(Addr) first_mapstart;    /* mapstart of the first PT_LOAD segment.  */
+  ElfW(Addr) last_mapstart;     /* mapstart of the last PT_LOAD segment.  */
+  ElfW(Addr) last_allocend;     /* allocend of the last PT_LOAD segment.  */
+  size_t nloadcmds;             /* Number of PT_LOAD segments found.  */
+};
+
+/* 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.  */
+static __always_inline bool
+_dl_pt_load_iterator_next (struct dl_pt_load_iterator *it, struct loadcmd *c)
+{
+  while (it->phdr < it->phdr_end)
+    {
+      const ElfW(Phdr) *ph = it->phdr++;
+      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->mapalign = it->p_align_max;
+      return true;
+    }
+  return false;
+}
+
+
 /* This is a subroutine of _dl_map_segments.  It should be called for each
    load command, some time after L->l_addr has been set correctly.  It is
    responsible for setting the l_phdr fields  */
@@ -113,8 +158,7 @@ _dl_postprocess_loadcmd (struct link_map *l, const ElfW(Ehdr) *header,
 
 static const char *_dl_map_segments (struct link_map *l, int fd,
                                      const ElfW(Ehdr) *header, int type,
-                                     const struct loadcmd loadcmds[],
-                                     size_t nloadcmds,
+                                     struct dl_pt_load_iterator *it,
                                      const size_t maplength,
                                      bool has_holes,
                                      struct link_map *loader); */
index 139004ae2f4664943c941876432a00025412a40d..1854fab7dc389a0ab6ce1976f9cb094857dc68d9 100644 (file)
@@ -75,11 +75,14 @@ _dl_map_segment (const struct loadcmd *c, ElfW(Addr) mappref,
 static __always_inline const char *
 _dl_map_segments (struct link_map *l, int fd,
                   const ElfW(Ehdr) *header, int type,
-                  const struct loadcmd loadcmds[], size_t nloadcmds,
+                  struct dl_pt_load_iterator *it,
                   const size_t maplength, bool has_holes,
                   struct link_map *loader)
 {
-  const struct loadcmd *c = loadcmds;
+  /* Fetch the first PT_LOAD segment.  _dl_pt_load_iterator_init already
+     verified nloadcmds > 0, so this call always succeeds.  */
+  struct loadcmd c = { 0 };
+  _dl_pt_load_iterator_next (it, &c);
 
   if (__glibc_likely (type == ET_DYN))
     {
@@ -95,16 +98,16 @@ _dl_map_segments (struct link_map *l, int fd,
          prefer to map such objects at; but this is only a preference,
          the OS can do whatever it likes. */
       ElfW(Addr) mappref
-        = (ELF_PREFERRED_ADDRESS (loader, maplength, c->mapstart)
+        = (ELF_PREFERRED_ADDRESS (loader, maplength, c.mapstart)
            - MAP_BASE_ADDR (l));
 
       /* Remember which part of the address space this object uses.  */
-      l->l_map_start = _dl_map_segment (c, mappref, maplength, fd);
+      l->l_map_start = _dl_map_segment (&c, mappref, maplength, fd);
       if (__glibc_unlikely ((void *) l->l_map_start == MAP_FAILED))
         return DL_MAP_SEGMENTS_ERROR_MAP_SEGMENT;
 
       l->l_map_end = l->l_map_start + maplength;
-      l->l_addr = l->l_map_start - c->mapstart;
+      l->l_addr = l->l_map_start - c.mapstart;
 
       if (has_holes)
         {
@@ -113,12 +116,11 @@ _dl_map_segments (struct link_map *l, int fd,
              unallocated.  Then jump into the normal segment-mapping loop to
              handle the portion of the segment past the end of the file
              mapping.  */
-         if (__glibc_unlikely (loadcmds[nloadcmds - 1].mapstart <
-                               c->mapend))
-           return N_("ELF load command address/offset not page-aligned");
+          if (__glibc_unlikely (it->last_mapstart < c.mapend))
+            return N_("ELF load command address/offset not page-aligned");
           if (__glibc_unlikely
-              (__mprotect ((caddr_t) (l->l_addr + c->mapend),
-                           loadcmds[nloadcmds - 1].mapstart - c->mapend,
+              (__mprotect ((caddr_t) (l->l_addr + c.mapend),
+                           it->last_mapstart - c.mapend,
                            PROT_NONE) < 0))
             return DL_MAP_SEGMENTS_ERROR_MPROTECT;
         }
@@ -129,32 +131,32 @@ _dl_map_segments (struct link_map *l, int fd,
     }
 
   /* Remember which part of the address space this object uses.  */
-  l->l_map_start = c->mapstart + l->l_addr;
+  l->l_map_start = c.mapstart + l->l_addr;
   l->l_map_end = l->l_map_start + maplength;
   l->l_contiguous = !has_holes;
 
-  while (c < &loadcmds[nloadcmds])
+  do
     {
-      if (c->mapend > c->mapstart
+      if (c.mapend > c.mapstart
           /* Map the segment contents from the file.  */
-          && (__mmap ((void *) (l->l_addr + c->mapstart),
-                      c->mapend - c->mapstart, c->prot,
+          && (__mmap ((void *) (l->l_addr + c.mapstart),
+                      c.mapend - c.mapstart, c.prot,
                       MAP_FIXED|MAP_COPY|MAP_FILE,
-                      fd, c->mapoff)
+                      fd, c.mapoff)
               == MAP_FAILED))
         return DL_MAP_SEGMENTS_ERROR_MAP_SEGMENT;
 
     postmap:
-      _dl_postprocess_loadcmd (l, header, c);
+      _dl_postprocess_loadcmd (l, header, &c);
 
-      if (c->allocend > c->dataend)
+      if (c.allocend > c.dataend)
         {
           /* Extra zero pages should appear at the end of this segment,
              after the data mapped from the file.   */
           ElfW(Addr) zero, zeroend, zeropage;
 
-          zero = l->l_addr + c->dataend;
-          zeroend = l->l_addr + c->allocend;
+          zero = l->l_addr + c.dataend;
+          zeroend = l->l_addr + c.allocend;
           zeropage = ((zero + GLRO(dl_pagesize) - 1)
                       & ~(GLRO(dl_pagesize) - 1));
 
@@ -166,18 +168,18 @@ _dl_map_segments (struct link_map *l, int fd,
           if (zeropage > zero)
             {
               /* Zero the final part of the last page of the segment.  */
-              if (__glibc_unlikely ((c->prot & PROT_WRITE) == 0))
+              if (__glibc_unlikely ((c.prot & PROT_WRITE) == 0))
                 {
                   /* Dag nab it.  */
                   if (__mprotect ((caddr_t) (zero
                                              & ~(GLRO(dl_pagesize) - 1)),
-                                  GLRO(dl_pagesize), c->prot|PROT_WRITE) < 0)
+                                  GLRO(dl_pagesize), c.prot|PROT_WRITE) < 0)
                     return DL_MAP_SEGMENTS_ERROR_MPROTECT;
                 }
               memset ((void *) zero, '\0', zeropage - zero);
-              if (__glibc_unlikely ((c->prot & PROT_WRITE) == 0))
+              if (__glibc_unlikely ((c.prot & PROT_WRITE) == 0))
                 __mprotect ((caddr_t) (zero & ~(GLRO(dl_pagesize) - 1)),
-                            GLRO(dl_pagesize), c->prot);
+                            GLRO(dl_pagesize), c.prot);
             }
 
           if (zeroend > zeropage)
@@ -187,7 +189,7 @@ _dl_map_segments (struct link_map *l, int fd,
 
               caddr_t mapat;
               mapat = __mmap ((caddr_t) zeropage, zeroend - zeropage,
-                              c->prot, MAP_ANON|MAP_PRIVATE|MAP_FIXED,
+                              c.prot, MAP_ANON|MAP_PRIVATE|MAP_FIXED,
                               -1, 0);
               if (__glibc_unlikely (mapat == MAP_FAILED))
                 return DL_MAP_SEGMENTS_ERROR_MAP_ZERO_FILL;
@@ -220,13 +222,12 @@ _dl_map_segments (struct link_map *l, int fd,
                 }
             }
         }
-
-      ++c;
     }
+  while (_dl_pt_load_iterator_next (it, &c));
 
   /* Notify ELF_PREFERRED_ADDRESS that we have to load this one
      fixed.  */
-  ELF_FIXED_ADDRESS (loader, c->mapstart);
+  ELF_FIXED_ADDRESS (loader, it->last_mapstart);
 
   return NULL;
 }
diff --git a/elf/gen-tst-bz26577-mod.py b/elf/gen-tst-bz26577-mod.py
new file mode 100644 (file)
index 0000000..a2fd223
--- /dev/null
@@ -0,0 +1,153 @@
+#!/usr/bin/python3
+# Generate a crafted ELF with a large number of PT_NULL program headers
+# for tst-bz26577.
+# 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/>.
+
+# The generated file is an ET_DYN ELF with EVIL_PHNUM (0x7FFF) program
+# headers.  The first header is a PT_LOAD covering the ELF header itself so
+# the dynamic linker actually attempts to map the object and exercises the
+# iterator path that replaced the old loadcmd VLA.  The remaining headers
+# are PT_NULL.  The object has no PT_DYNAMIC, so dlopen and LD_PRELOAD will
+# both fail gracefully after loading.
+
+import os
+import struct
+import sys
+
+import glibcelf
+
+# Must match the definition in tst-bz26577.c.
+EVIL_PHNUM = 0x7FFF
+
+def main():
+    if len(sys.argv) != 3:
+        print('usage: gen-tst-bz26577-mod.py OUTPUT REF-ELF', file=sys.stderr)
+        sys.exit(1)
+
+    output_path = sys.argv[1]
+    ref_elf_path = sys.argv[2]
+
+    # Read EI_CLASS, EI_DATA and e_machine from a target ELF (ld.so) so
+    # the generated file matches the target ABI, not the host Python.
+    ref = glibcelf.Image.readfile(ref_elf_path)
+    ei_class = ref.ehdr.e_ident.ei_class  # ElfClass.ELFCLASS32 or ELFCLASS64
+    ei_data = ref.ehdr.e_ident.ei_data    # ElfData.ELFDATA2LSB or ELFDATA2MSB
+    e_machine = ref.ehdr.e_machine        # Machine.*
+
+    endian = '<' if ei_data == glibcelf.ElfData.ELFDATA2LSB else '>'
+    is64 = (ei_class == glibcelf.ElfClass.ELFCLASS64)
+
+    ehdr_size = glibcelf.Ehdr.layouts[(ei_class, ei_data)].size
+    phdr_size = glibcelf.Phdr.layouts[(ei_class, ei_data)].size
+
+    # File must hold the ELF header plus the full program header table so
+    # that pread() in open_verify and _dl_map_object_from_fd can read all
+    # EVIL_PHNUM entries without a short read.
+    total = ehdr_size + EVIL_PHNUM * phdr_size
+    # Assume workable value, the binary should be reject by the loader anyway.
+    pagesize = 4096
+    total = (total + pagesize - 1) & ~(pagesize - 1)
+
+    buf = bytearray(total)
+
+    # ELF Header:
+    buf[0:4]  = b'\x7fELF'
+    buf[4]    = ei_class.value
+    buf[5]    = ei_data.value
+    buf[6]    = 1   # EV_CURRENT
+    buf[7]    = 0   # ELFOSABI_SYSV
+    # bytes 8..15 remain zero (padding)
+
+    # Pack the ELF header fields that follow e_ident.
+    # ELF64 Ehdr layout (after e_ident[16]):
+    #   e_type(H) e_machine(H) e_version(I)
+    #   e_entry(Q) e_phoff(Q) e_shoff(Q)
+    #   e_flags(I) e_ehsize(H) e_phentsize(H)
+    #   e_phnum(H) e_shentsize(H) e_shnum(H) e_shstrndx(H)
+    # ELF32 Ehdr layout (after e_ident[16]):
+    #   e_type(H) e_machine(H) e_version(I)
+    #   e_entry(I) e_phoff(I) e_shoff(I)
+    #   e_flags(I) e_ehsize(H) e_phentsize(H)
+    #   e_phnum(H) e_shentsize(H) e_shnum(H) e_shstrndx(H)
+    if is64:
+        fmt = endian + '2HI3QI6H'
+    else:
+        fmt = endian + '2H5I6H'
+
+    phoff = ehdr_size   # program header table immediately follows Ehdr
+    fields = (
+        glibcelf.Et.ET_DYN.value,   # e_type
+        e_machine.value,            # e_machine
+        1,                          # e_version (EV_CURRENT)
+        0,                          # e_entry
+        phoff,                      # e_phoff
+        0,                          # e_shoff
+        0,                          # e_flags
+        ehdr_size,                  # e_ehsize
+        phdr_size,                  # e_phentsize
+        EVIL_PHNUM,                 # e_phnum
+        0,                          # e_shentsize
+        0,                          # e_shnum
+        0,                          # e_shstrndx
+    )
+    struct.pack_into(fmt, buf, 16, *fields)
+
+    # Write the first program header as PT_LOAD covering the ELF header
+    # (p_offset=0, p_filesz=ehdr_size, p_memsz=ehdr_size, PF_R). This
+    # ensures the dynamic linker actually maps the segment and exercises
+    # the PT_LOAD iterator path rather than aborting early with
+    # "no loadable segments".  The remaining EVIL_PHNUM-1 headers stay
+    # zero (PT_NULL).
+    #
+    # ELF64 Phdr field order (layout '2I6Q'):
+    #   p_type(I) p_flags(I) p_offset(Q) p_vaddr(Q) p_paddr(Q)
+    #   p_filesz(Q) p_memsz(Q) p_align(Q)
+    # ELF32 Phdr field order (layout '8I'):
+    #   p_type(I) p_offset(I) p_vaddr(I) p_paddr(I)
+    #   p_filesz(I) p_memsz(I) p_flags(I) p_align(I)
+    if is64:
+        phdr_fmt = endian + '2I6Q'
+        phdr_fields = (
+            glibcelf.Pt.PT_LOAD.value,   # p_type
+            glibcelf.Pf.PF_R.value,      # p_flags
+            0,                           # p_offset
+            0,                           # p_vaddr
+            0,                           # p_paddr
+            ehdr_size,                   # p_filesz
+            ehdr_size,                   # p_memsz
+            pagesize,                    # p_align
+        )
+    else:
+        phdr_fmt = endian + '8I'
+        phdr_fields = (
+            glibcelf.Pt.PT_LOAD.value,   # p_type
+            0,                           # p_offset
+            0,                           # p_vaddr
+            0,                           # p_paddr
+            ehdr_size,                   # p_filesz
+            ehdr_size,                   # p_memsz
+            glibcelf.Pf.PF_R.value,      # p_flags
+            pagesize,                    # p_align
+        )
+    struct.pack_into(phdr_fmt, buf, ehdr_size, *phdr_fields)
+
+    with open(output_path, 'wb') as f:
+        f.write(buf)
+
+if __name__ == '__main__':
+    main()
diff --git a/elf/tst-bz26577.c b/elf/tst-bz26577.c
new file mode 100644 (file)
index 0000000..e408215
--- /dev/null
@@ -0,0 +1,169 @@
+/* Tests for BZ #26577: stack overflow when loading a crafted ELF with
+   large e_phnum in _dl_map_object_from_fd.
+   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/>.  */
+
+/* The crafted ELF is generated at build time by gen-tst-bz26577-mod.py
+   (elf/tst-bz26577-mod.so) with an ET_DYN with e_phnum = 0x7FFF, one PT_LOAD
+   segment covering the ELF header and the remaining headers PT_NULL.
+
+   This test exercises the loader against that crafted module via two
+   subtests, each run as a fresh exec with a reduced stack limit:
+
+     1. dlopen() subtest (--restart dlopen <path>): call dlopen on the
+        crafted .so.
+
+     2. LD_PRELOAD startup subtest (--restart, with LD_PRELOAD set): the
+        dynamic linker attempts to load the crafted .so at startup.
+
+   In both cases loader should fail to load the DSO, but without triggering
+   errors like SEGFAULT.  */
+
+
+#include <dlfcn.h>
+#include <elf.h>
+#include <getopt.h>
+#include <link.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <support/check.h>
+#include <support/subprocess.h>
+#include <support/support.h>
+
+/* Number of program headers in the crafted ELF.  Must match EVIL_PHNUM
+   in gen-tst-bz26577-mod.py.  */
+#define EVIL_PHNUM UINT16_C (0x7FFF)
+
+static int restart;
+#define CMDLINE_OPTIONS \
+  { "restart", no_argument, &restart, 1 },
+
+static const char *test_binary;
+
+/* Reduced stack size used in subprocess tests.  The 1 MB headroom should
+   cover the loader required call chain.  */
+static size_t
+evil_stack_size (void)
+{
+  return (size_t) EVIL_PHNUM * sizeof (ElfW(Phdr)) + 1024 * 1024;
+}
+
+struct subtest_args
+{
+  const char *mod_path;
+  const char *subtest;
+};
+
+static void
+run_with_limited_stack (void *closure)
+{
+  const struct subtest_args *args = closure;
+
+  struct rlimit rl;
+  TEST_VERIFY_EXIT (getrlimit (RLIMIT_STACK, &rl) == 0);
+  rl.rlim_cur = evil_stack_size ();
+  TEST_VERIFY_EXIT (setrlimit (RLIMIT_STACK, &rl) == 0);
+
+  if (args->subtest == NULL)
+    setenv ("LD_PRELOAD", args->mod_path, 1);
+
+  char *spawn_argv[6];
+  int i = 0;
+  spawn_argv[i++] = (char *) test_binary;
+  spawn_argv[i++] = (char *) "--direct";
+  spawn_argv[i++] = (char *) "--restart";
+  if (args->subtest != NULL)
+    {
+      spawn_argv[i++] = (char *) args->subtest;
+      spawn_argv[i++] = (char *) args->mod_path;
+    }
+  spawn_argv[i] = NULL;
+
+  struct support_spawn_wrapped *w
+    = support_spawn_wrap (test_binary, spawn_argv, NULL, 0);
+  execve (w->path, (char *const *) w->argv, (char *const *) w->envp);
+  _exit (127);
+}
+
+/* Fork a child with a reduced stack limit and exec this binary to call
+   dlopen on MOD_PATH.  */
+static void
+test_dlopen_large_phnum (const char *mod_path)
+{
+  struct subtest_args args = { mod_path, "dlopen" };
+  struct support_subprocess proc
+    = support_subprocess (run_with_limited_stack, &args);
+  int status = support_process_wait (&proc);
+  if (WIFSIGNALED (status) && WTERMSIG (status) == SIGSEGV)
+    FAIL_EXIT1 ("dlopen test: child killed by SIGSEGV"
+                " (stack overflow from unfixed loadcmd VLA)");
+}
+
+/* Fork a child with a reduced stack limit and exec this binary with
+   LD_PRELOAD set to MOD_PATH.  */
+static void
+test_startup_large_phnum (const char *mod_path)
+{
+  struct subtest_args args = { mod_path, NULL };
+  struct support_subprocess proc
+    = support_subprocess (run_with_limited_stack, &args);
+  int status = support_process_wait (&proc);
+  if (WIFSIGNALED (status) && WTERMSIG (status) == SIGSEGV)
+    FAIL_EXIT1 ("startup test: child killed by SIGSEGV"
+                " (stack overflow from unfixed loadcmd VLA)");
+}
+
+static int
+do_test (int argc, char *argv[])
+{
+  if (restart)
+    {
+      /* dlopen subtest: argv[1] == "dlopen", argv[2] == module path.  */
+      if (argc > 1 && strcmp (argv[1], "dlopen") == 0)
+        {
+          TEST_VERIFY (argc == 3);
+          void *h = dlopen (argv[2], RTLD_LAZY);
+          TEST_VERIFY (h == NULL);
+        }
+      /* LD_PRELOAD subtest: no extra args; loader already exercised the
+         code during startup before main() was reached.  */
+      return 0;
+    }
+
+  /* We must have either one argument (hardcoded paths) or four arguments
+     (ld.so, --library-path, lib-path, binary) after the program name.  */
+  TEST_VERIFY_EXIT (argc == 2 || argc == 5);
+  test_binary = argv[argc - 1];
+
+  char *mod_path = xasprintf ("%s/elf/tst-bz26577-mod.so",
+                             support_objdir_root);
+
+  test_dlopen_large_phnum (mod_path);
+  test_startup_large_phnum (mod_path);
+
+  free (mod_path);
+
+  return 0;
+}
+
+#define TEST_FUNCTION_ARGV do_test
+#include <support/test-driver.c>