Hi Kane,

On Mon, 2026-07-13 at 17:04 +0800, Kane Wang wrote:
> Changes since v1:
>   - tests/core-ioperm-huge.c: Add the `#include <config.h>` guard
>   - run-readelf-core-ioperm-huge.sh: shebang `#!/usr/bin/env bash` ->
>     `#! /bin/sh`

Those updates look correct.

> handle_core_item () alloca ()s n_descsz bytes when a core note is a
> single bit-array ("b"/"B") item, such as NT_386_IOPERM.  n_descsz is
> taken straight from the untrusted ELF/core file with no upper bound, so
> a malformed core claiming a multi-megabyte descriptor exhausts the
> process stack and crashes readelf (DoS).
> 
> Cap the allocation at 64 KiB and derive count/convsize from the capped
> size, so convert () cannot write past data either.  Real ioperm
> bitmaps (one bit per I/O port, at most 8 KiB) are unaffected; only
> pathological inputs are truncated.

Thanks, we currently build the eu-readelf program with
(src/Makefile.am):
readelf_no_Wstack_usage = yes
Removing that reveals a couple more places where we might blow up the
stack.

>       * src/readelf.c (handle_core_item): Cap the bit-array alloca at
>       64 KiB; derive count and convsize from the capped size.
>       * tests/core-ioperm-huge.c: New generator for a core with a
>       16 MiB NT_386_IOPERM note (sparse descriptor).
>       * tests/run-readelf-core-ioperm-huge.sh: New test.
>       * tests/Makefile.am (check_PROGRAMS, TESTS, EXTRA_DIST): Add
>       core-ioperm-huge and run-readelf-core-ioperm-huge.sh.
>       (core_ioperm_huge_LDADD): New.
>       * tests/.gitignore: Add core-ioperm-huge.
> 
> Signed-off-by: Kane Wang <[email protected]>
> ---
>  src/readelf.c                         |  10 +-
>  tests/.gitignore                      |   1 +
>  tests/Makefile.am                     |   4 +
>  tests/core-ioperm-huge.c              | 126 ++++++++++++++++++++++++++
>  tests/run-readelf-core-ioperm-huge.sh |  40 ++++++++
>  5 files changed, 179 insertions(+), 2 deletions(-)
>  create mode 100644 tests/core-ioperm-huge.c
>  create mode 100755 tests/run-readelf-core-ioperm-huge.sh
> 
> diff --git a/src/readelf.c b/src/readelf.c
> index 9d3bbbf6..631c2112 100644
> --- a/src/readelf.c
> +++ b/src/readelf.c
> @@ -13018,8 +13018,14 @@ handle_core_item (Elf *core, const GElf_Ehdr *ehdr,
>      {
>        if (*repeated_size > size && (item->format == 'b' || item->format == 
> 'B'))
>       {
> -       data = alloca (*repeated_size);
> -       count *= *repeated_size / size;
> +       /* The descriptor size of a core note comes from an untrusted
> +          ELF file.  Cap the stack allocation so a malformed note that
> +          claims a very large n_descsz cannot exhaust the process
> +          stack and crash readelf.  count and convsize are derived
> +          from the capped size so convert () cannot write past data.  */
> +       size_t alloc_size = MIN (*repeated_size, 64 * 1024);
> +       data = alloca (alloc_size);
> +       count *= alloc_size / size;
>         convsize = count * size;
>         *repeated_size -= convsize;
>       }

Right, this limits the data alloced on the stack and the count used by
the following convert call (which calls xlatetom which would then try
to put all the converted data on the stack).

> diff --git a/tests/.gitignore b/tests/.gitignore
> index 72b02a5f..37ffe9ce 100644
> --- a/tests/.gitignore
> +++ b/tests/.gitignore
> @@ -13,6 +13,7 @@
>  /arsymtest
>  /ar-extract-ar
>  /ar-getarsym-mem
> +/core-ioperm-huge
>  /asm-tst1
>  /asm-tst2
>  /asm-tst3

Ack. New test.

> diff --git a/tests/Makefile.am b/tests/Makefile.am
> index e5e03737..08c0a23d 100644
> --- a/tests/Makefile.am
> +++ b/tests/Makefile.am
> @@ -69,6 +69,7 @@ check_PROGRAMS = arextract arsymtest newfile saridx 
> scnnames sectiondump \
>                 eu_search_cfi eu_search_macros \
>                 eu_search_lines eu_search_die \
>                 ar-extract-ar elf-from-memory ar-getarsym-mem \
> +               core-ioperm-huge \
>                 $(asm_TESTS)
>  
>  asm_TESTS = asm-tst1 asm-tst2 asm-tst3 asm-tst4 asm-tst5 \
> @@ -150,6 +151,7 @@ TESTS = run-arextract.sh run-arsymtest.sh run-ar.sh 
> newfile test-nlist \
>       run-readelf-compressed.sh \
>       run-readelf-const-values.sh \
>       run-readelf-cu-index.sh \
> +     run-readelf-core-ioperm-huge.sh \
>       run-varlocs-self.sh run-exprlocs-self.sh \
>       run-readelf-test1.sh run-readelf-test2.sh run-readelf-test3.sh \
>       run-readelf-test4.sh run-readelf-twofiles.sh \
> @@ -372,6 +374,7 @@ EXTRA_DIST = run-arextract.sh run-arsymtest.sh run-ar.sh \
>            run-readelf-compressed-zstd.sh \
>            run-readelf-const-values.sh testfile-const-values.debug.bz2 \
>            run-readelf-cu-index.sh \
> +          run-readelf-core-ioperm-huge.sh \
>            run-addrcfi.sh run-dwarfcfi.sh \
>            testfile11-debugframe.bz2 testfile12-debugframe.bz2 \
>            testfileaarch64-debugframe.bz2 testfilearm-debugframe.bz2 \
> @@ -787,6 +790,7 @@ arextract_LDADD = $(libelf)
>  arsymtest_LDADD = $(libelf)
>  ar_extract_ar_LDADD = $(libelf)
>  ar_getarsym_mem_LDADD = $(libelf)
> +core_ioperm_huge_LDADD =
>  newfile_LDADD = $(libelf)
>  saridx_LDADD = $(libeu) $(libelf)
>  scnnames_LDADD = $(libelf)

OK. core_ioperm_huge uses various Elf data structures, but does no real
calls to any libelf function.

> diff --git a/tests/core-ioperm-huge.c b/tests/core-ioperm-huge.c
> new file mode 100644
> index 00000000..10d533b6
> --- /dev/null
> +++ b/tests/core-ioperm-huge.c
> @@ -0,0 +1,126 @@
> +/* Build a minimal ET_CORE ELF with a single NT_386_IOPERM note whose
> +   n_descsz is far larger than a typical process stack, to exercise the
> +   bit-array ("b"/"B") path in handle_core_item.
> +   Copyright (C) 2026 KylinSoft Co., Ltd.
> +   This file is part of elfutils.
> +
> +   This file is free software; you can redistribute it and/or modify
> +   it under the terms of the GNU General Public License as published by
> +   the Free Software Foundation; either version 3 of the License, or
> +   (at your option) any later version.
> +
> +   elfutils 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 General Public License for more details.
> +
> +   You should have received a copy of the GNU General Public License
> +   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
> +
> +/* NT_386_IOPERM lives in elfutils' own elf.h but not necessarily in the
> +   system <elf.h>, so define it when missing.  */
> +#ifdef HAVE_CONFIG_H
> +# include <config.h>
> +#endif
> +
> +#include <elf.h>
> +#ifndef NT_386_IOPERM
> +# define NT_386_IOPERM 0x201
> +#endif
> +
> +#include <fcntl.h>
> +#include <stdint.h>
> +#include <stdio.h>
> +#include <string.h>
> +#include <unistd.h>
> +
> +/* Larger than any plausible process stack so an unfixed readelf, which
> +   alloca()s n_descsz bytes, crashes; a fixed one caps the allocation.  */
> +#define DESCSZ (16 * 1024 * 1024)
> +
> +static void
> +put (int fd, const void *buf, size_t n)
> +{
> +  if (write (fd, buf, n) != (ssize_t) n)
> +    {
> +      perror ("write");
> +      _exit (2);
> +    }
> +}
> +
> +int
> +main (int argc, char **argv)
> +{
> +  if (argc != 2)
> +    {
> +      fprintf (stderr, "usage: %s output.core\n", argv[0]);
> +      return 2;
> +    }
> +
> +  int fd = open (argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0600);
> +  if (fd < 0)
> +    {
> +      perror ("open");
> +      return 2;
> +    }
> +
> +  /* Layout:  Elf64_Ehdr (64) | Elf64_Phdr (56) | Elf64_Nhdr (12)
> +   *          | "CORE\0" padded to 8 | desc (DESCSZ, sparse).  */
> +  const size_t phoff = sizeof (Elf64_Ehdr);
> +  const size_t note_off = phoff + sizeof (Elf64_Phdr);
> +  const char name[] = "CORE";
> +  const size_t name_padded = (sizeof (name) + 3) & ~(size_t) 3; /* 8 */
> +  const size_t filesz = sizeof (Elf64_Nhdr) + name_padded + DESCSZ;
> +
> +  Elf64_Ehdr ehdr;
> +  memset (&ehdr, 0, sizeof ehdr);
> +  memcpy (ehdr.e_ident, ELFMAG, SELFMAG);
> +  ehdr.e_ident[EI_CLASS] = ELFCLASS64;
> +  ehdr.e_ident[EI_DATA] = ELFDATA2LSB;
> +  ehdr.e_ident[EI_VERSION] = EV_CURRENT;
> +  ehdr.e_type = ET_CORE;
> +  ehdr.e_machine = EM_X86_64;
> +  ehdr.e_version = EV_CURRENT;
> +  ehdr.e_phoff = phoff;
> +  ehdr.e_ehsize = sizeof (Elf64_Ehdr);
> +  ehdr.e_phentsize = sizeof (Elf64_Phdr);
> +  ehdr.e_phnum = 1;
> +  put (fd, &ehdr, sizeof ehdr);
> +
> +  Elf64_Phdr phdr;
> +  memset (&phdr, 0, sizeof phdr);
> +  phdr.p_type = PT_NOTE;
> +  phdr.p_flags = PF_R;
> +  phdr.p_offset = note_off;
> +  phdr.p_filesz = filesz;
> +  phdr.p_memsz = filesz;
> +  phdr.p_align = 4;
> +  put (fd, &phdr, sizeof phdr);
> +
> +  Elf64_Nhdr nhdr;
> +  memset (&nhdr, 0, sizeof nhdr);
> +  nhdr.n_namesz = sizeof (name); /* 5, includes the NUL */
> +  nhdr.n_descsz = DESCSZ;
> +  nhdr.n_type = NT_386_IOPERM;
> +  put (fd, &nhdr, sizeof nhdr);
> +
> +  char namebuf[8];
> +  memset (namebuf, 0, sizeof namebuf);
> +  memcpy (namebuf, name, sizeof (name));
> +  put (fd, namebuf, name_padded);
> +
> +  /* Make the descriptor a sparse hole instead of writing 16 MiB.  */
> +  if (lseek (fd, (off_t) (note_off + filesz - 1), SEEK_SET) == (off_t) -1
> +      || write (fd, "", 1) != 1)
> +    {
> +      perror ("lseek/write");
> +      _exit (2);
> +    }
> +
> +  if (close (fd) != 0)
> +    {
> +      perror ("close");
> +      return 2;
> +    }
> +  return 0;
> +}

OK, creates an Elf core file with just some mem/file operations setting
up the Elf header, Phdrs and Note.

> diff --git a/tests/run-readelf-core-ioperm-huge.sh 
> b/tests/run-readelf-core-ioperm-huge.sh
> new file mode 100755
> index 00000000..1bf852b6
> --- /dev/null
> +++ b/tests/run-readelf-core-ioperm-huge.sh
> @@ -0,0 +1,40 @@
> +#! /bin/sh
> +# Copyright (C) 2026 KylinSoft Co., Ltd.
> +# This file is part of elfutils.
> +#
> +# This file is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU General Public License as published by
> +# the Free Software Foundation; either version 3 of the License, or
> +# (at your option) any later version.
> +#
> +# elfutils 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 General Public License for more details.
> +#
> +# You should have received a copy of the GNU General Public License
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
> +
> +. $srcdir/test-subr.sh
> +
> +# A core note's n_descsz is taken straight from the untrusted ELF file.
> +# For a bit-array ("b"/"B") core item such as NT_386_IOPERM, handle_core_item
> +# in readelf used to alloca() n_descsz bytes.  A malformed core claiming a
> +# 16 MiB descriptor would exhaust the process stack and crash readelf.
> +# The descriptor is kept as a sparse hole, so the input is tiny on disk.
> +
> +tempfiles core-ioperm-huge.core
> +
> +testrun ${abs_builddir}/core-ioperm-huge core-ioperm-huge.core
> +
> +# readelf must not crash; it caps the stack allocation and prints the
> +# (here empty) ioperm bitmap normally.
> +testrun_compare ${abs_top_builddir}/src/readelf -n core-ioperm-huge.core 
> <<EOF
> +
> +Note segment of 16777236 bytes at offset 0x78:
> +  Owner          Data size  Type
> +  CORE            16777216  386_IOPERM
> +    ioperm: <>
> +EOF
> +
> +exit 0

Nice test, indeed crashes before the patch, produces the empty bitmap
with the patch.

Pushed as https://sourceware.org/cgit/elfutils/commit/?id=22437a8e96db

Thanks,

Mark

Reply via email to