[PATCH] elf: Fix stack overflow in _dl_map_object_from_fd with large e_phnum (BZ 26577)

H.J. Lu hjl.tools@gmail.com
Fri May 8 23:36:57 GMT 2026


On Fri, May 8, 2026 at 5:05 AM Adhemerval Zanella
<adhemerval.zanella@linaro.org> wrote:
>
> 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
> ---
>  elf/Makefile               |   9 ++
>  elf/dl-load.c              | 150 ++++++++++++++++++--------------
>  elf/dl-load.h              |  48 ++++++++++-
>  elf/dl-map-segments.h      |  57 +++++++------
>  elf/gen-tst-bz26577-mod.py | 153 +++++++++++++++++++++++++++++++++
>  elf/tst-bz26577.c          | 169 +++++++++++++++++++++++++++++++++++++
>  6 files changed, 491 insertions(+), 95 deletions(-)
>  create mode 100644 elf/gen-tst-bz26577-mod.py
>  create mode 100644 elf/tst-bz26577.c
>
> diff --git a/elf/Makefile b/elf/Makefile
> index c835eb8156d..896d098d001 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -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
> diff --git a/elf/dl-load.c b/elf/dl-load.c
> index 48575fff06a..498d6be8a20 100644
> --- a/elf/dl-load.c
> +++ b/elf/dl-load.c
> @@ -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 don’t 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))
>        {
> diff --git a/elf/dl-load.h b/elf/dl-load.h
> index 897c4034c5e..d4e52dd8561 100644
> --- a/elf/dl-load.h
> +++ b/elf/dl-load.h
> @@ -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); */
> diff --git a/elf/dl-map-segments.h b/elf/dl-map-segments.h
> index 139004ae2f4..1854fab7dc3 100644
> --- a/elf/dl-map-segments.h
> +++ b/elf/dl-map-segments.h
> @@ -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
> index 00000000000..a2fd2232b7e
> --- /dev/null
> +++ b/elf/gen-tst-bz26577-mod.py
> @@ -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
> index 00000000000..e40821546d4
> --- /dev/null
> +++ b/elf/tst-bz26577.c
> @@ -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>
> --
> 2.43.0
>

LGTM.

Reviewed-by: H.J. Lu <hjl.tools@gmail.com>

Thanks.

-- 
H.J.


More information about the Libc-alpha mailing list