[PATCH] elf: Eliminate alloca for program-header table in the ELF loader

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


On Sat, May 9, 2026 at 3:00 AM Adhemerval Zanella
<adhemerval.zanella@linaro.org> wrote:
>
> 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
> ---
>  elf/Makefile                          |   4 +
>  elf/dl-load.c                         | 376 +++++++++++++-------------
>  elf/dl-load.h                         |  37 ++-
>  elf/dl-machine-reject-phdr.h          |  34 ++-
>  elf/tst-bz26577-minstack.c            |  66 +++++
>  sysdeps/mips/dl-machine-reject-phdr.h |  48 +++-
>  6 files changed, 355 insertions(+), 210 deletions(-)
>  create mode 100644 elf/tst-bz26577-minstack.c
>
> diff --git a/elf/Makefile b/elf/Makefile
> index 896d098d001..f4d22c15991 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -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
> diff --git a/elf/dl-load.c b/elf/dl-load.c
> index 498d6be8a20..f6e391a4689 100644
> --- a/elf/dl-load.c
> +++ b/elf/dl-load.c
> @@ -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;
> +  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);
>
> -  for (const ElfW(Phdr) *ph = phdr; ph < phdr + phnum; ++ph)
> +  /* 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_unlikely (((ph->p_vaddr - ph->p_offset)
> -                            & (pagesize - 1)) != 0))
> -       return N_("ELF load command address/offset not page-aligned");
> +      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;
> +       }
>
> -      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;
> +      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;
>
> -      /* 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);
> +           /* 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 (it->nloadcmds > 0 && prev_mapend != mapstart)
> -       *has_holes = true;
> -      prev_mapend = mapend;
> +           case PT_PHDR:
> +             l->l_phdr = (void *) ph->p_vaddr;
> +             break;
>
> -      if (it->nloadcmds == 0)
> -       it->first_mapstart = mapstart;
> +           case PT_TLS:
> +             if (ph->p_memsz == 0)
> +               /* Nothing to do for an empty segment.  */
> +               break;
>
> -      it->last_mapstart = mapstart;
> -      it->last_allocend = ph->p_vaddr + ph->p_memsz;
> -      it->nloadcmds++;
> +             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
>      {
> diff --git a/elf/dl-load.h b/elf/dl-load.h
> index d4e52dd8561..e58028038c9 100644
> --- a/elf/dl-load.h
> +++ b/elf/dl-load.h
> @@ -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;
>      }
> diff --git a/elf/dl-machine-reject-phdr.h b/elf/dl-machine-reject-phdr.h
> index 954c22cec64..9a3e1fb8110 100644
> --- a/elf/dl-machine-reject-phdr.h
> +++ b/elf/dl-machine-reject-phdr.h
> @@ -21,12 +21,36 @@
>
>  #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
> index 00000000000..6b8fa5e9bf4
> --- /dev/null
> +++ b/elf/tst-bz26577-minstack.c
> @@ -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>
> diff --git a/sysdeps/mips/dl-machine-reject-phdr.h b/sysdeps/mips/dl-machine-reject-phdr.h
> index 240e4e7fc92..e64494c1982 100644
> --- a/sysdeps/mips/dl-machine-reject-phdr.h
> +++ b/sysdeps/mips/dl-machine-reject-phdr.h
> @@ -36,8 +36,37 @@
>      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;
> --
> 2.43.0
>

LGTM.

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

Thanks.

-- 
H.J.


More information about the Libc-alpha mailing list