[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