[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