diff --git a/src/track/heaptrack_inject.cpp b/src/track/heaptrack_inject.cpp index c24a266..717d391 100644 --- a/src/track/heaptrack_inject.cpp +++ b/src/track/heaptrack_inject.cpp @@ -1,292 +1,301 @@ /* * Copyright 2014-2017 Milian Wolff * * This 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. * * This 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 this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "libheaptrack.h" #include "util/config.h" #include #include #include #include #include #include /** * @file heaptrack_inject.cpp * * @brief Experimental support for symbol overloading after runtime injection. */ #if __WORDSIZE == 64 #define ELF_R_SYM(i) ELF64_R_SYM(i) #elif __WORDSIZE == 32 #define ELF_R_SYM(i) ELF32_R_SYM(i) #else #error unsupported word size #endif namespace { namespace Elf { using Addr = ElfW(Addr); using Dyn = ElfW(Dyn); using Rela = ElfW(Rela); using Sym = ElfW(Sym); using Sxword = ElfW(Sxword); using Xword = ElfW(Xword); } void overwrite_symbols() noexcept; namespace hooks { struct malloc { static constexpr auto name = "malloc"; static constexpr auto original = &::malloc; static void* hook(size_t size) noexcept { auto ptr = original(size); heaptrack_malloc(ptr, size); return ptr; } }; struct free { static constexpr auto name = "free"; static constexpr auto original = &::free; static void hook(void* ptr) noexcept { heaptrack_free(ptr); original(ptr); } }; struct realloc { static constexpr auto name = "realloc"; static constexpr auto original = &::realloc; static void* hook(void* ptr, size_t size) noexcept { auto ret = original(ptr, size); heaptrack_realloc(ptr, size, ret); return ret; } }; struct calloc { static constexpr auto name = "calloc"; static constexpr auto original = &::calloc; static void* hook(size_t num, size_t size) noexcept { auto ptr = original(num, size); heaptrack_malloc(ptr, num * size); return ptr; } }; #if HAVE_CFREE struct cfree { static constexpr auto name = "cfree"; static constexpr auto original = &::cfree; static void hook(void* ptr) noexcept { heaptrack_free(ptr); original(ptr); } }; #endif struct dlopen { static constexpr auto name = "dlopen"; static constexpr auto original = &::dlopen; static void* hook(const char* filename, int flag) noexcept { auto ret = original(filename, flag); if (ret) { heaptrack_invalidate_module_cache(); overwrite_symbols(); } return ret; } }; struct dlclose { static constexpr auto name = "dlclose"; static constexpr auto original = &::dlclose; static int hook(void* handle) noexcept { auto ret = original(handle); if (!ret) { heaptrack_invalidate_module_cache(); } return ret; } }; struct posix_memalign { static constexpr auto name = "posix_memalign"; static constexpr auto original = &::posix_memalign; static int hook(void** memptr, size_t alignment, size_t size) noexcept { auto ret = original(memptr, alignment, size); if (!ret) { heaptrack_malloc(*memptr, size); } return ret; } }; template bool hook(const char* symname, Elf::Addr addr, bool restore) { static_assert(std::is_convertible::value, "hook is not compatible to original function"); if (strcmp(Hook::name, symname) != 0) { return false; } // try to make the page read/write accessible, which is hackish // but apparently required for some shared libraries auto page = reinterpret_cast(addr & ~(0x1000 - 1)); mprotect(page, 0x1000, PROT_READ | PROT_WRITE); // now write to the address auto typedAddr = reinterpret_cast::type*>(addr); if (restore) { // restore the original address on shutdown *typedAddr = Hook::original; } else { // now actually inject our hook *typedAddr = &Hook::hook; } return true; } void apply(const char* symname, Elf::Addr addr, bool restore) { // TODO: use std::apply once we can rely on C++17 hook(symname, addr, restore) || hook(symname, addr, restore) || hook(symname, addr, restore) || hook(symname, addr, restore) #if HAVE_CFREE || hook(symname, addr, restore) #endif || hook(symname, addr, restore) || hook(symname, addr, restore) || hook(symname, addr, restore); } } template struct elftable { T* table = nullptr; Elf::Xword size = {}; bool consume(const Elf::Dyn* dyn) noexcept { if (dyn->d_tag == AddrTag) { table = reinterpret_cast(dyn->d_un.d_ptr); return true; } else if (dyn->d_tag == SizeTag) { size = dyn->d_un.d_val; return true; } return false; } }; using elf_string_table = elftable; +using elf_rela_table = elftable; using elf_jmprel_table = elftable; using elf_symbol_table = elftable; +template +void try_overwrite_elftable(const T& jumps, const elf_string_table& strings, const elf_symbol_table& symbols, + const Elf::Addr base, const bool restore) noexcept +{ + const auto rela_end = reinterpret_cast(reinterpret_cast(jumps.table) + jumps.size); + for (auto rela = jumps.table; rela < rela_end; rela++) { + const auto index = ELF_R_SYM(rela->r_info); + const char* symname = strings.table + symbols.table[index].st_name; + auto addr = rela->r_offset + base; + hooks::apply(symname, addr, restore); + } +} + void try_overwrite_symbols(const Elf::Dyn* dyn, const Elf::Addr base, const bool restore) noexcept { elf_symbol_table symbols; + elf_rela_table relas; elf_jmprel_table jmprels; elf_string_table strings; // initialize the elf tables for (; dyn->d_tag != DT_NULL; ++dyn) { - symbols.consume(dyn) || jmprels.consume(dyn) || strings.consume(dyn); + symbols.consume(dyn) || relas.consume(dyn) || jmprels.consume(dyn) || strings.consume(dyn); } // find symbols to overwrite - const auto rela_end = reinterpret_cast(reinterpret_cast(jmprels.table) + jmprels.size); - for (auto rela = jmprels.table; rela < rela_end; rela++) { - const auto index = ELF_R_SYM(rela->r_info); - const char* symname = strings.table + symbols.table[index].st_name; - auto addr = rela->r_offset + base; - - hooks::apply(symname, addr, restore); - } + try_overwrite_elftable(relas, strings, symbols, base, restore); + try_overwrite_elftable(jmprels, strings, symbols, base, restore); } int iterate_phdrs(dl_phdr_info* info, size_t /*size*/, void* data) noexcept { if (strstr(info->dlpi_name, "/libheaptrack_inject.so")) { // prevent infinite recursion: do not overwrite our own symbols return 0; } else if (strstr(info->dlpi_name, "/ld-linux")) { // prevent strange crashes due to overwriting the free symbol in ld-linux return 0; } for (auto phdr = info->dlpi_phdr, end = phdr + info->dlpi_phnum; phdr != end; ++phdr) { if (phdr->p_type == PT_DYNAMIC) { try_overwrite_symbols(reinterpret_cast(phdr->p_vaddr + info->dlpi_addr), info->dlpi_addr, data != nullptr); } } return 0; } void overwrite_symbols() noexcept { dl_iterate_phdr(&iterate_phdrs, nullptr); } } extern "C" { void heaptrack_inject(const char* outputFileName) noexcept { heaptrack_init(outputFileName, []() { overwrite_symbols(); }, [](FILE* out) { fprintf(out, "A\n"); }, []() { bool do_shutdown = true; dl_iterate_phdr(&iterate_phdrs, &do_shutdown); }); } } diff --git a/tests/manual/run_linkage_tests.sh b/tests/manual/run_linkage_tests.sh new file mode 100755 index 0000000..bf65607 --- /dev/null +++ b/tests/manual/run_linkage_tests.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +input_file=$(readlink -f test_linkage.c) +cd /tmp + +function run { + echo "compiling with $@" + gcc $@ -O0 -g $input_file -o test_linkage || return + readelf -a test_linkage | grep free + ./test_linkage & + p=$! + sleep .1 + heaptrack -p $p |& tee test_linkage.log + if ! grep -qP "^\s+allocations:\s+10$" test_linkage.log; then + echo "FAILED: wrong allocation count (compiled with $@)" + fi + if ! grep -qP "^\s+leaked allocations:\s+0$" test_linkage.log; then + echo "FAILED: wrong allocation count (compiled with $@)" + fi + if ! grep -qP "^\s+temporary allocations:\s+10$" test_linkage.log; then + echo "FAILED: wrong temporary allocation count (compiled with $@)" + fi +} + +# set -o xtrace + +for linker in bfd gold; do + run -fuse-ld=$linker + run -fuse-ld=$linker -Wl,-z,now + run -fuse-ld=$linker -DTAKE_ADDR + run -fuse-ld=$linker -DUSE_FREEPTR +done + +rm heaptrack.test_linkage.*.gz test_linkage test_linkage.log diff --git a/tests/manual/test_linkage.c b/tests/manual/test_linkage.c new file mode 100644 index 0000000..8792e73 --- /dev/null +++ b/tests/manual/test_linkage.c @@ -0,0 +1,102 @@ +/* + * Copyright 2018 Ivan Middleton + * Copyright 2018 Milian Wolff + * + * This 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. + * + * This 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* + By default, the linker uses lazy binding (function calls aren't + resolved until the first time the function is called). + + Relevant sections of the executable for lazy binding: + .plt (trampoline code) + .got.plt (function addresses cached here) + .rela.plt (relocation entries associating each function name + with its storage location in .got.plt) + + But symbols can also be bound right away when the executable or + shared library is started or loaded. + + Relevant sections for immediate binding: + .plt.got (trampoline code) + .got (function addresses stored here) + .rela.dyn (relocation entries) + + Immediate binding can be triggered in a couple different ways: + + (1) The linker option "-z now" makes all symbols use immediate + binding. Compile this file as follows to see this in action: + + gcc -Wl,-z,now -o testbind testbind.c + + Why might this linker option be used? See: + + https://wiki.debian.org/Hardening#DEB_BUILD_HARDENING_BINDNOW_.28ld_-z_now.29 + + Note that this seems to be platform dependant and is not always reproducible. + + (2) If a particular function has a pointer to it passed around, + then it must be bound immediately. Define TAKE_ADDR, i.e. compile with + + gcc -g -O0 -fuse-ld=bfd -DTAKE_ADDR -o testbind testbind.c + + to see this behavior. Note that ld.gold does not show this behavior. + + The heaptrack_inject function needs to look in both .rela.plt + (DT_JMPREL) and .rela.dyn (DT_RELA) in order to find all + malloc/free function pointers, lazily-bound or no. + + There is also another option which is currently not handled by heaptrack: + When do not rewrite data segments, which would be required to catch accessing + a given symbol through a function pointer (-DUSE_FREEPTR). + + Use the run_linkage_tests.sh bash script to check the behavior in an automated fashion. +*/ + +#include +#include + +void escape(void *p) +{ + asm volatile("" : : "g"(p) : "memory"); +} + +int main() +{ + int i = 0; + +#if defined(TAKE_ADDR) || defined(USE_FREEPTR) + void (*freePtr)(void*) = &free; + + escape(freePtr); +#endif + + sleep(1); + + for (i = 0; i < 10; ++i) { + void* foo = malloc(256); + escape(foo); + + usleep(200); +#if defined(USE_FREEPTR) + freePtr(foo); +#else + free(foo); +#endif + usleep(200); + } + return 0; +}