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.

        * 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              | 122 ++++++++++++++++++++++++++
 tests/run-readelf-core-ioperm-huge.sh |  40 +++++++++
 5 files changed, 175 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;
        }
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
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)
diff --git a/tests/core-ioperm-huge.c b/tests/core-ioperm-huge.c
new file mode 100644
index 00000000..c50ba73e
--- /dev/null
+++ b/tests/core-ioperm-huge.c
@@ -0,0 +1,122 @@
+/* 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.  */
+#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;
+}
diff --git a/tests/run-readelf-core-ioperm-huge.sh 
b/tests/run-readelf-core-ioperm-huge.sh
new file mode 100755
index 00000000..e0a6a8bf
--- /dev/null
+++ b/tests/run-readelf-core-ioperm-huge.sh
@@ -0,0 +1,40 @@
+#! /usr/bin/env bash
+# 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
-- 
2.43.0

Reply via email to