From: 王强 Date: Fri, 3 Jul 2026 13:20:39 +0000 (+0200) Subject: libelf: fix OOB read in elf_getarsym 32-bit archive index count X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=8b1921607772792f200ffe048698847e4895c2b3;p=thirdparty%2Felfutils.git libelf: fix OOB read in elf_getarsym 32-bit archive index count 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 --- diff --git a/libelf/elf_getarsym.c b/libelf/elf_getarsym.c index 03381059..346596d7 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 d3ecd35b..e5e03737 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 elf-from-memory ar-getarsym-mem \ $(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 @@ -786,6 +786,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..63f39137 --- /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 . */ + +/* 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 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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, "!\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; +}