Hi Kane,
[Sending an HTML only patch is a bit difficult to apply, please try
sending in plain text next time.]
On Fri, 2026-07-03 at 17:20 +0800, 王强 wrote:
> 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.
Well spotted.
> 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.
Yes, that seems to correct fix.
> * 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.
Thanks for adding a testcase.
> 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;
OK. The rest of the function also uses w (not sizeof u) for size/offset
calculations.
> 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
Ack. New testcase.
> 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)
OK. new testcase correctly added.
> 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. */
Thanks. Nice description.
> +#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;
> +}
Nice minimal (empty) ar.
> +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);
Nice trick to get the ar exactly at the edge of a page.
> + 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;
> + }
Good check. This indeed fails without your fix. valgrind also flags it.
And it also shows another bug when building with --enable-sanitize-
undefined:
elfutils/libelf/elf_getarsym.c:264:15: runtime error: variable length
array bound evaluates to non-positive value 0
Which is:
/* Now we can build the data structure. */
Elf_Arsym *arsym = elf->state.ar.ar_sym;
uint64_t (*u64)[n] = file_data;
uint32_t (*u32)[n] = file_data;
for (size_t cnt = 0; cnt < n; ++cnt)
That [n] should be [n+1] because even if we won't enter that loop we
are lying to the compiler about the size of the array (there is indeed
space for n+1 entries, which includes the sentinel). We do add that
sentinel at the end to arsym after the loop.
I'll add:
diff --git a/libelf/elf_getarsym.c b/libelf/elf_getarsym.c
index 343c00ef352d..346596d79506 100644
--- a/libelf/elf_getarsym.c
+++ b/libelf/elf_getarsym.c
@@ -261,8 +261,8 @@ elf_getarsym (Elf *elf, size_t *ptr)
/* Now we can build the data structure. */
Elf_Arsym *arsym = elf->state.ar.ar_sym;
- uint64_t (*u64)[n] = file_data;
- uint32_t (*u32)[n] = file_data;
+ uint64_t (*u64)[n+1] = file_data;
+ uint32_t (*u32)[n+1] = file_data;
for (size_t cnt = 0; cnt < n; ++cnt)
{
arsym[cnt].as_name = str_data;
https://sourceware.org/cgit/elfutils/patch/?id=60cb1486c6fb
> + 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
>
Looks good. Pushed as
https://sourceware.org/cgit/elfutils/patch/?id=8b1921607772
Thanks,
Mark