[glibc/azanella/ifunc-malloc-reject] aarch64: Reject out-of-context malloc ifunc resolver calls
Adhemerval Zanella via Glibc-cvs <[email protected]> Thu, 2 Jul 2026 19:04:27 +0000 (GMT)
| Newsgroups | gmane.comp.lib.glibc.cvs |
|---|---|
| Message-ID | <[email protected]> |
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=294a13c7396cc3497a26edd9576802297864e965 commit 294a13c7396cc3497a26edd9576802297864e965 Author: Adhemerval Zanella <[email protected]> Date: Thu Jul 2 16:48:09 2026 +0000 aarch64: Reject out-of-context malloc ifunc resolver calls An STT_GNU_IFUNC resolver is only meant to run from within glibc. Some tools evaluate ifunc resolvers on their own, out of this context: for instance gdb resolves the ifunc when the user issues the "call malloc" command, invoking the resolver directly from a dummy frame in the inferior rather than through glibc. Detect this on aarch64 by checking, in the malloc resolver, that the return address belongs to a glibc mapping. The set of legitimate callers is larger than just the dynamic loader: - ld.so, while it relocates an object (the common case); - libc.so, when dlsym resolves the ifunc symbol, which runs the resolver via _dl_sym_post -> elf_ifunc_invoke. - With dlmopen this can be the libc.so of any namespace, so all namespaces are considered. The check is only enforced when the loader link map is available with valid bounds. That is not the case when a statically linked program dlopens a shared object: the resolver then runs against the program's own dormant _rtld_global_ro, where these fields are not set up, and the check is skipped. Checked on aarch64-linux-gnu. Diff: --- elf/rtld.c | 1 + sysdeps/aarch64/Makefile | 4 + sysdeps/aarch64/multiarch/malloc-ifuncs.c | 41 ++++- sysdeps/aarch64/tst-malloc-ifunc-resolver-mod.c | 36 +++++ sysdeps/aarch64/tst-malloc-ifunc-resolver.c | 189 ++++++++++++++++++++++++ sysdeps/generic/ldsodefs.h | 3 + 6 files changed, 273 insertions(+), 1 deletion(-) diff --git a/elf/rtld.c b/elf/rtld.c index e5ba71fef1..b8dbc1ef66 100644 --- a/elf/rtld.c +++ b/elf/rtld.c @@ -357,6 +357,7 @@ struct rtld_global_ro _rtld_global_ro attribute_relro = ._dl_fpu_control = _FPU_DEFAULT, ._dl_pagesize = EXEC_PAGESIZE, ._dl_inhibit_cache = 0, + ._dl_rtld_map = &_dl_rtld_map, /* Function pointers. */ ._dl_debug_printf = _dl_debug_printf, diff --git a/sysdeps/aarch64/Makefile b/sysdeps/aarch64/Makefile index 52ac85a75d..0af70f8182 100644 --- a/sysdeps/aarch64/Makefile +++ b/sysdeps/aarch64/Makefile @@ -51,13 +51,17 @@ tests-internal += \ # tests-internal tests += \ + tst-malloc-ifunc-resolver \ tst-vpcs \ # tests modules-names += \ + tst-malloc-ifunc-resolver-mod \ tst-vpcs-mod \ # modules-names LDFLAGS-tst-vpcs-mod.so = -Wl,-z,lazy $(objpfx)tst-vpcs: $(objpfx)tst-vpcs-mod.so +$(objpfx)tst-malloc-ifunc-resolver.out: \ + $(objpfx)tst-malloc-ifunc-resolver-mod.so endif ifeq ($(subdir),csu) diff --git a/sysdeps/aarch64/multiarch/malloc-ifuncs.c b/sysdeps/aarch64/multiarch/malloc-ifuncs.c index 648fb617de..e14372f3fd 100644 --- a/sysdeps/aarch64/multiarch/malloc-ifuncs.c +++ b/sysdeps/aarch64/multiarch/malloc-ifuncs.c @@ -22,8 +22,47 @@ #include <malloc-api.h> #include <shlib-compat.h> +#ifdef SHARED +static inline bool +addr_in_map (uintptr_t addr, const struct link_map *map) +{ + return map != NULL && addr >= map->l_map_start && addr < map->l_map_end; +} + +/* Return true if the code address RET is a legitimate caller of a malloc + ifunc resolver: the dynamic loader or any libc.so (including dlmopen). */ +static bool +called_from_glibc (uintptr_t ret) +{ + if (addr_in_map (ret, GLRO(dl_rtld_map))) + return true; + for (size_t ns = 0; ns < GL(dl_nns); ++ns) + if (addr_in_map (ret, GL(dl_ns)[ns].libc_map)) + return true; + return false; +} +#endif + +/* The malloc ifunc resolver is only meant to be executed from within glibc + (see called_from_glibc). Some tools, notably gdb evaluating 'call malloc' + command invoke ifunc resolvers directly from an out-of-context dummy + frame. */ +static __typeof (__libc_malloc) * +malloc_ifunc_resolve (uintptr_t ret) +{ +#ifdef SHARED + struct link_map *rtld_map = GLRO(dl_rtld_map); + if (rtld_map != NULL && rtld_map->l_map_end != 0 + && !called_from_glibc (ret)) + _dl_fatal_printf ("\ +Fatal glibc error: malloc ifunc resolver called outside the dynamic loader\n"); +#endif + return __libc_malloc; +} + libc_ifunc_hidden (__libc_malloc, __libc_malloc_redirect, - __libc_malloc) + malloc_ifunc_resolve + ((uintptr_t) __builtin_return_address (0))) strong_alias (__libc_malloc_redirect, malloc) libc_ifunc_hidden (__libc_calloc, __libc_calloc_redirect, diff --git a/sysdeps/aarch64/tst-malloc-ifunc-resolver-mod.c b/sysdeps/aarch64/tst-malloc-ifunc-resolver-mod.c new file mode 100644 index 0000000000..4ec55ead2b --- /dev/null +++ b/sysdeps/aarch64/tst-malloc-ifunc-resolver-mod.c @@ -0,0 +1,36 @@ +/* Module for tst-malloc-ifunc-resolver, loaded into a second namespace. + Copyright (C) 2026 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + <https://www.gnu.org/licenses/>. */ + +#include <dlfcn.h> +#include <stdlib.h> + +/* Called from tst-malloc-ifunc-resolver after this module has been loaded + with dlmopen into a fresh namespace. */ + +int +resolve_malloc_via_dlsym (void) +{ + void *(*malloc_p) (size_t) = (void *(*) (size_t)) dlsym (RTLD_DEFAULT, + "malloc"); + if (malloc_p == NULL) + return 0; + void *p = malloc_p (48); + int ok = p != NULL; + free (p); + return ok; +} diff --git a/sysdeps/aarch64/tst-malloc-ifunc-resolver.c b/sysdeps/aarch64/tst-malloc-ifunc-resolver.c new file mode 100644 index 0000000000..65a3a3a27a --- /dev/null +++ b/sysdeps/aarch64/tst-malloc-ifunc-resolver.c @@ -0,0 +1,189 @@ +/* Test the caller check in the AArch64 malloc ifunc resolver. + Copyright (C) 2026 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + <https://www.gnu.org/licenses/>. */ + +/* The malloc ifunc resolver only accepts being invoked from within glibc: + by the dynamic loader (relocation) or by libc.so (dlsym resolving the + ifunc through _dl_sym_post), in any namespace. Any other caller, such as + a debugger evaluating "call malloc (...)" from an out-of-context dummy + frame, is rejected with a fatal error. + + This test exercises both directions: + + - The rejection path is reproduced without a debugger by locating the + STT_GNU_IFUNC resolver for "malloc" in the loaded libc and calling it + directly from the test binary (which is neither ld.so nor libc.so). + + - The accepted paths must keep working: a plain malloc, a dlsym of the + malloc ifunc (resolver runs in libc.so), and the same dlsym from a + second namespace created with dlmopen (resolver runs in that + namespace's libc.so). */ + +#include <dlfcn.h> +#include <elf.h> +#include <link.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <sys/auxv.h> +#include <sys/ifunc.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <support/capture_subprocess.h> +#include <support/check.h> +#include <support/xdlfcn.h> + +struct resolver_search +{ + const char *name; + uintptr_t address; +}; + +static uintptr_t +dynamic_pointer (const struct dl_phdr_info *info, ElfW(Addr) value) +{ + return value < info->dlpi_addr ? info->dlpi_addr + value : value; +} + +/* dl_iterate_phdr callback: look up the STT_GNU_IFUNC symbol NAME in the + dynamic symbol table of each object and record the runtime address of its + resolver. Symbol values are relative to the object load address. */ +static int +find_malloc_resolver (struct dl_phdr_info *info, size_t size, void *closure) +{ + struct resolver_search *search = closure; + + const ElfW(Sym) *symtab = NULL; + const char *strtab = NULL; + const ElfW(Word) *hash = NULL; + + for (const ElfW(Phdr) *phdr = info->dlpi_phdr; + phdr < info->dlpi_phdr + info->dlpi_phnum; ++phdr) + if (phdr->p_type == PT_DYNAMIC) + for (ElfW(Dyn) *dyn = (ElfW(Dyn) *) (info->dlpi_addr + phdr->p_vaddr); + dyn->d_tag != DT_NULL; ++dyn) + switch (dyn->d_tag) + { + case DT_SYMTAB: + symtab = (const void *) dynamic_pointer (info, dyn->d_un.d_ptr); + break; + case DT_STRTAB: + strtab = (const void *) dynamic_pointer (info, dyn->d_un.d_ptr); + break; + case DT_HASH: + hash = (const void *) dynamic_pointer (info, dyn->d_un.d_ptr); + break; + } + + if (symtab == NULL || strtab == NULL || hash == NULL) + return 0; + + /* The second word of the SysV hash table is the number of symbol table + entries (nchain). */ + ElfW(Word) nsyms = hash[1]; + for (ElfW(Word) i = 0; i < nsyms; ++i) + if (ELF64_ST_TYPE (symtab[i].st_info) == STT_GNU_IFUNC + && strcmp (strtab + symtab[i].st_name, search->name) == 0) + { + search->address = info->dlpi_addr + symtab[i].st_value; + return 1; + } + + return 0; +} + +static uintptr_t malloc_resolver; + +/* Invoke the malloc ifunc resolver directly, i.e. from a call site that is + neither the dynamic loader nor libc.so. The resolver must not return. */ +static void +call_malloc_resolver (void *closure) +{ + __ifunc_arg_t arg = + { + sizeof (arg), + getauxval (AT_HWCAP), + getauxval (AT_HWCAP2), + getauxval (AT_HWCAP3), + getauxval (AT_HWCAP4), + }; + ElfW(Addr) (*resolver) (uint64_t, const __ifunc_arg_t *) + = (void *) malloc_resolver; + + resolver (getauxval (AT_HWCAP) | _IFUNC_ARG_HWCAP, &arg); + + /* Unreachable if the resolver correctly rejects the call. */ + _exit (0); +} + +/* Resolve the malloc ifunc through dlsym (which runs the resolver from + libc.so via _dl_sym_post) and use the result. Returns the allocation, or + NULL on failure. */ +static void * +malloc_via_dlsym (void) +{ + void *(*malloc_p) (size_t) = (void *(*) (size_t)) xdlsym (RTLD_DEFAULT, + "malloc"); + return malloc_p (32); +} + +static int +do_test (void) +{ + struct resolver_search search = { "malloc", 0 }; + if (dl_iterate_phdr (find_malloc_resolver, &search) == 0 + || search.address == 0) + FAIL_UNSUPPORTED ("could not locate the malloc ifunc resolver " + "(malloc not implemented as an ifunc?)"); + malloc_resolver = search.address; + + /* Accepted: the regular malloc, resolved by ld.so. */ + void *p = malloc (64); + TEST_VERIFY (p != NULL); + free (p); + + /* Accepted: dlsym resolving the malloc ifunc runs the resolver from + libc.so. */ + void *q = malloc_via_dlsym (); + TEST_VERIFY (q != NULL); + free (q); + + /* Accepted: the same dlsym from a second namespace created with dlmopen + runs the resolver in that namespace's libc.so. */ + void *h = xdlmopen (LM_ID_NEWLM, "$ORIGIN/tst-malloc-ifunc-resolver-mod.so", + RTLD_NOW); + int (*mod_resolve) (void) = (int (*) (void)) + xdlsym (h, "resolve_malloc_via_dlsym"); + TEST_VERIFY (mod_resolve () != 0); + xdlclose (h); + + /* Rejected: calling the resolver out of context must abort the process + through _dl_fatal_printf, which reports the error and calls + _exit (127). Run it in a subprocess so the test survives. */ + struct support_capture_subprocess proc + = support_capture_subprocess (call_malloc_resolver, NULL); + + TEST_VERIFY (WIFEXITED (proc.status)); + TEST_COMPARE (WEXITSTATUS (proc.status), 127); + TEST_VERIFY (strstr (proc.err.buffer, "malloc ifunc resolver") != NULL); + + support_capture_subprocess_free (&proc); + return 0; +} + +#include <support/test-driver.c> diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h index f94247ad9f..e615dd95a2 100644 --- a/sysdeps/generic/ldsodefs.h +++ b/sysdeps/generic/ldsodefs.h @@ -642,6 +642,9 @@ struct rtld_global_ro EXTERN enum dso_sort_algorithm _dl_dso_sort_algo; #ifdef SHARED + /* Link map of the dynamic linker itself. */ + struct link_map *_dl_rtld_map; + /* We add a function table to _rtld_global which is then used to call the function instead of going through the PLT. The result is that we can avoid exporting the functions and we do not jump