read_number_entries () reads the number of entries of an archive symbol
index. On the mmap path, taken for images created with elf_memory ()
(and for mmap-backed files), it copied sizeof (union u) == 8 bytes
instead of the width-appropriate 4 bytes for a 32-bit ("symdef") index.
For a minimal in-memory archive whose index member holds only the 4-byte
count field, this read 4 bytes past the caller's buffer. Use w, matching
the pread path and the 64-bit case, so both paths read exactly the bytes
they need.
* libelf/elf_getarsym.c (read_number_entries): Use w, not sizeof u,
for the mmap memcpy.
* tests/ar-getarsym-mem.c: New file.
* tests/Makefile.am (check_PROGRAMS): Add ar-getarsym-mem.
(TESTS): Likewise.
(ar_getarsym_mem_LDADD): New variable.
* tests/.gitignore: Add ar-getarsym-mem.
Signed-off-by: Kane Wang <[email protected]>
---
libelf/elf_getarsym.c | 2 +-
tests/.gitignore | 1 +
tests/Makefile.am | 5 +-
tests/ar-getarsym-mem.c | 143 ++++++++++++++++++++++++++++++++++++++++
4 files changed, 148 insertions(+), 3 deletions(-)
create mode 100644 tests/ar-getarsym-mem.c
diff --git a/libelf/elf_getarsym.c b/libelf/elf_getarsym.c
index 281f0c1c..88d184f9 100644
--- a/libelf/elf_getarsym.c
+++ b/libelf/elf_getarsym.c
@@ -55,7 +55,7 @@ read_number_entries (uint64_t *nump, Elf *elf, size_t *offp, bool index64_p)
if (elf->map_address != NULL)
/* Use memcpy instead of pointer dereference so as not to assume the
field is naturally aligned within the file. */
- memcpy (&u, elf->map_address + *offp, sizeof u);
+ memcpy (&u, elf->map_address + *offp, w);
else if ((size_t) pread_retry (elf->fildes, &u, w, *offp) != w)
return -1;
diff --git a/tests/.gitignore b/tests/.gitignore
index 4681024f..72b02a5f 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -12,6 +12,7 @@
/arls
/arsymtest
/ar-extract-ar
+/ar-getarsym-mem
/asm-tst1
/asm-tst2
/asm-tst3
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 30a7b6ca..51fea2f6 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -68,7 +68,7 @@ check_PROGRAMS = arextract arsymtest newfile saridx scnnames sectiondump \
cu-dwp-section-info declfiles test-manyfuncs \
eu_search_cfi eu_search_macros \
eu_search_lines eu_search_die \
- ar-extract-ar elf-from-memory \
+ ar-extract-ar ar-getarsym-mem elf-from-memory \
$(asm_TESTS)
asm_TESTS = asm-tst1 asm-tst2 asm-tst3 asm-tst4 asm-tst5 \
@@ -239,7 +239,7 @@ TESTS = run-arextract.sh run-arsymtest.sh run-ar.sh newfile test-nlist \
run-test-manyfuncs.sh \
run-eu-search-cfi.sh run-eu-search-macros.sh \
run-eu-search-lines.sh run-eu-search-die.sh \
- elf-from-memory
+ elf-from-memory ar-getarsym-mem
if !BIARCH
export ELFUTILS_DISABLE_BIARCH = 1
@@ -782,6 +782,7 @@ libeu = ../lib/libeu.a
arextract_LDADD = $(libelf)
arsymtest_LDADD = $(libelf)
ar_extract_ar_LDADD = $(libelf)
+ar_getarsym_mem_LDADD = $(libelf)
newfile_LDADD = $(libelf)
saridx_LDADD = $(libeu) $(libelf)
scnnames_LDADD = $(libelf)
diff --git a/tests/ar-getarsym-mem.c b/tests/ar-getarsym-mem.c
new file mode 100644
index 00000000..7b4892e1
--- /dev/null
+++ b/tests/ar-getarsym-mem.c
@@ -0,0 +1,143 @@
+/* Test elf_getarsym on a truncated in-memory 32-bit archive index.
+ 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/>. */
+
+/* elf_getarsym reads the number of entries of an archive symbol index
+ through read_number_entries (). On the mmap path, taken for images
+ created with elf_memory (), it used to copy sizeof (union { uint64_t;
+ uint32_t; }) == 8 bytes instead of the width-appropriate 4 bytes for
+ a 32-bit ("symdef") index. For a minimal archive whose index member
+ holds only the 4-byte count field, that copied 4 bytes past the end
+ of the caller's buffer.
+
+ This reproduces by building such a minimal 32-bit archive in an
+ elf_memory () image placed at the very end of a page, with an
+ inaccessible guard page immediately following it. An unfixed build
+ reads into the guard page and crashes; a fixed build reads exactly 4
+ bytes and returns the (zero-entry) index normally. */
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include <ar.h>
+#include <errno.h>
+#include <libelf.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+/* SARMAG + sizeof (struct ar_hdr) + a 4-byte 32-bit index count. */
+#define ARSZ (8 + 60 + 4)
+
+/* Build a minimal 32-bit ("symdef") archive whose index member consists
+ of only the count field, encoding zero entries. */
+static void
+build_archive (unsigned char *p)
+{
+ memset (p, 0, ARSZ);
+ memcpy (p, "!<arch>\n", 8);
+
+ struct ar_hdr *hdr = (struct ar_hdr *) (p + 8);
+ memset (hdr, ' ', sizeof *hdr);
+ memcpy (hdr->ar_name, "/ ", 16);
+ memcpy (hdr->ar_date, "0 ", 12);
+ memcpy (hdr->ar_uid, "0 ", 6);
+ memcpy (hdr->ar_gid, "0 ", 6);
+ memcpy (hdr->ar_mode, "100644 ", 8);
+ memcpy (hdr->ar_size, "4 ", 10);
+ memcpy (hdr->ar_fmag, "`\n", 2);
+
+ /* 4-byte count, stored big-endian as on disk; zero entries. */
+ p[68] = 0;
+ p[69] = 0;
+ p[70] = 0;
+ p[71] = 0;
+}
+
+int
+main (void)
+{
+ int result = 1;
+
+ long pgsz = sysconf (_SC_PAGESIZE);
+ if (pgsz <= 0 || (size_t) pgsz < ARSZ)
+ {
+ printf ("SKIP: unusable page size (%ld)\n", pgsz);
+ return 77;
+ }
+
+ /* Map two pages and make the second one inaccessible. The archive is
+ placed at the end of the first page so that an 8-byte read of the
+ 4-byte count field at offset 68 crosses into the guard page. */
+ unsigned char *map = mmap (NULL, (size_t) pgsz * 2,
+ PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (map == MAP_FAILED)
+ {
+ printf ("SKIP: mmap failed: %m\n");
+ return 77;
+ }
+ if (mprotect (map + pgsz, (size_t) pgsz, PROT_NONE) != 0)
+ {
+ printf ("SKIP: mprotect failed: %m\n");
+ munmap (map, (size_t) pgsz * 2);
+ return 77;
+ }
+
+ unsigned char *buf = map + pgsz - ARSZ;
+ build_archive (buf);
+
+ if (elf_version (EV_CURRENT) == EV_NONE)
+ {
+ printf ("FAIL: elf_version: %s\n", elf_errmsg (-1));
+ goto out;
+ }
+
+ Elf *elf = elf_memory ((char *) buf, ARSZ);
+ if (elf == NULL)
+ {
+ printf ("FAIL: elf_memory: %s\n", elf_errmsg (-1));
+ goto out;
+ }
+
+ size_t narsym = (size_t) -1;
+ Elf_Arsym *arsym = elf_getarsym (elf, &narsym);
+ /* A zero-entry index still gets the terminating sentinel entry, so
+ the table is non-NULL and the reported count is one. */
+ if (arsym == NULL || narsym != 1
+ || arsym[0].as_name != NULL || arsym[0].as_off != 0
+ || arsym[0].as_hash != ~0UL)
+ {
+ printf ("FAIL: unexpected arsym (narsym=%zu, arsym=%p)\n",
+ narsym, (void *) arsym);
+ result = 1;
+ }
+ else
+ {
+ printf ("PASS: truncated 32-bit index read within bounds\n");
+ result = 0;
+ }
+
+ elf_end (elf);
+
+out:
+ munmap (map, (size_t) pgsz * 2);
+ return result;
+}
--
2.43.0
