diff --git a/.gitignore b/.gitignore index 423e8eec..6b0ea347 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ Makefile /tests/contiguous-note-sections /tests/simple-pie /tests/simple-execstack +/tests/symver .direnv/ .vscode/ diff --git a/patchelf.1 b/patchelf.1 index 7bb94f7f..89c18ef6 100644 --- a/patchelf.1 +++ b/patchelf.1 @@ -114,6 +114,11 @@ This means that when a shared library has an entry point (so that it can be run as an executable), the debugger does not connect to it correctly and symbols are not resolved. +.IP "--remove-needed-version LIBRARY VERSION_SYMBOL" +Removes VERSION_SYMBOL from LIBRARY in .gnu.version_r and resets entries referenced +the version in .gnu.version, could be used to remove symbol versioning. LIBRARY and +VERSION_SYMBOL can be retrieved from the output of "readelf -V". + .IP "--print-execstack" Prints the state of the executable flag of the GNU_STACK program header, if present. diff --git a/src/patchelf.cc b/src/patchelf.cc index 14c0802b..c0332d18 100644 --- a/src/patchelf.cc +++ b/src/patchelf.cc @@ -2269,6 +2269,174 @@ void ElfFile::renameDynamicSymbols(const std::unordered_maprewriteSections(); } +template +void ElfFile::removeNeededVersion(const std::map> & verMap) +{ + if (verMap.empty()) return; + + auto idxVersionR = getSectionIndex(".gnu.version_r"); + if (!idxVersionR) { + error("no .gnu.version_r section found\n"); + return; + } + auto & shdrVersionR = shdrs.at(idxVersionR); + Elf_Shdr & shdrVersionRStrings = shdrs.at(rdi(shdrVersionR.sh_link)); + + auto mapOffset = rdi(shdrVersionR.sh_addr) - rdi(shdrVersionR.sh_offset); + + size_t verNeedNum = rdi(shdrVersionR.sh_info); + std::set removedVersionIds; + + // file names and versions here + char * verStrTab = (char *) fileContents->data() + rdi(shdrVersionRStrings.sh_offset); + + debug("found .gnu.version_r with %i entries\n", verNeedNum); + + bool removeEntireSection = false; + + auto vn = (Elf_Verneed *)(fileContents->data() + rdi(shdrVersionR.sh_offset)); + Elf_Verneed * prevVn = nullptr; + for (size_t i = 0; i < verNeedNum; ++i) { + char * file = verStrTab + rdi(vn->vn_file); + + if (verMap.count(file)) { + size_t verNauxNum = rdi(vn->vn_cnt); + size_t newVerNauxNum = 0; + + auto va = (follow(vn, rdi(vn->vn_aux))); + Elf_Vernaux * prevVa = nullptr; + + auto & versions = verMap.at(file); + + for (size_t j = 0; j < verNauxNum; ++j) { + char * name = verStrTab + rdi(va->vna_name); + + auto version = rdi(va->vna_other); + off_t next = rdi(va->vna_next); + if (versions.count(name)) { + debug("removing symbol %s with version %d in entry %s\n", name, version, file); + // 1 for unversioned symbol + removedVersionIds.insert(version); + + if (prevVa) { + wri(prevVa->vna_next, next ? rdi(prevVa->vna_next) + next : 0); + } else { + wri(vn->vn_aux, next ? rdi(vn->vn_aux) + next : 0); + } + changed = true; + + auto nextVa = follow(va, next); + // remove the version data + memset(va, 0, sizeof(Elf_Vernaux)); + va = nextVa; + } else { + debug("keeping symbol %s with version %d in entry %s\n", name, version, file); + ++newVerNauxNum; + prevVa = va; + va = follow(va, next); + } + } + + off_t next = rdi(vn->vn_next); + // there are versions left + if (prevVa) { + wri(vn->vn_cnt, newVerNauxNum); + prevVn = vn; + vn = follow(vn, next); + } else { + // remove entire file entry + if (prevVn) { + wri(prevVn->vn_next, next ? rdi(prevVn->vn_next) + next : 0); + } else { + // there are file entries left + if (next) { + wri(shdrVersionR.sh_offset, rdi(shdrVersionR.sh_offset) + next); + } else { + wri(shdrVersionR.sh_size, 0); + + removeEntireSection = true; + } + } + wri(shdrVersionR.sh_info, rdi(shdrVersionR.sh_info) - 1); + + Elf_Verneed * nextVn = follow(vn, next); + memset(vn, 0, sizeof(Elf_Verneed)); + vn = nextVn; + } + } else { + off_t next = rdi(vn->vn_next); + prevVn = vn; + vn = follow(vn, next); + } + } + + // virtual address and file offset need to be the same + if (!removeEntireSection) { + wri(shdrVersionR.sh_addr, mapOffset + rdi(shdrVersionR.sh_offset)); + } + + if (auto shdrDynamic = tryFindSectionHeader(".dynamic")) { + auto dyn = (Elf_Dyn *)(fileContents->data() + rdi(shdrDynamic->get().sh_offset)); + + // keep DT_VERNEED and DT_VERNEEDNUM in sync, DT_VERNEED handled by rewriteHeaders + Elf_Dyn * last = dyn; + for ( ; rdi(dyn->d_tag) != DT_NULL; dyn++) { + if (rdi(dyn->d_tag) == DT_VERNEEDNUM) { + if (!removeEntireSection) { + dyn->d_un.d_val = shdrVersionR.sh_info; + *last++ = *dyn; + } + } else if (rdi(dyn->d_tag) == DT_VERNEED) { + if (!removeEntireSection) { + *last++ = *dyn; + } + } else if (rdi(dyn->d_tag) == DT_VERSYM) { + if (!removeEntireSection) { + *last++ = *dyn; + } + } else { + *last++ = *dyn; + } + } + memset(last, 0, sizeof(Elf_Dyn) * (dyn - last)); + } + + auto spanVersyms = tryGetSectionSpan(".gnu.version"); + + if (spanVersyms) { + for (auto & versym : spanVersyms) { + if (removedVersionIds.count(versym)) { + wri(versym, 1); + } + } + } + + if (removeEntireSection) { + debug("removing .gnu.version_r and .gnu.version section\n"); + wri(shdrVersionR.sh_type, SHT_NULL); + auto idxVersion = getSectionIndex(".gnu.version"); + if (idxVersion) { + auto & shdrVersion = shdrs.at(idxVersion); + wri(shdrVersion.sh_type, SHT_NULL); + memset(fileContents->data() + rdi(shdrVersion.sh_offset), 0, rdi(shdrVersion.sh_size)); + wri(shdrVersion.sh_size, 0); + } + } else { + debug("remaining entries in .gnu.version_r: %i\n", rdi(shdrVersionR.sh_info)); + } + + // we did not change phdrAddress, rewrite it in place + Elf_Addr phdrAddress; + for (auto & phdr : phdrs) { + if (rdi(phdr.p_type) == PT_PHDR) { + phdrAddress = rdi(phdr.p_vaddr); + break; + } + } + + rewriteHeaders(phdrAddress); +} + template void ElfFile::clearSymbolVersions(const std::set & syms) { @@ -2430,6 +2598,7 @@ static bool addDebugTag = false; static bool renameDynamicSymbols = false; static bool printRPath = false; static std::string newRPath; +static std::map> neededVersionsToRemove; static std::set neededLibsToRemove; static std::map neededLibsToReplace; static std::set neededLibsToAdd; @@ -2484,6 +2653,7 @@ static void patchElf2(ElfFile && elfFile, const FileContents & fileContents, con if (printNeeded) elfFile.printNeededLibs(); + elfFile.removeNeededVersion(neededVersionsToRemove); elfFile.removeNeeded(neededLibsToRemove); elfFile.replaceNeeded(neededLibsToReplace); elfFile.addNeeded(neededLibsToAdd); @@ -2559,6 +2729,7 @@ static void showHelp(const std::string & progName) [--clear-symbol-version SYMBOL]\n\ [--add-debug-tag]\n\ [--print-execstack]\t\tPrints whether the object requests an executable stack\n\ + [--remove-needed-version LIBRARY VERSION_SYMBOL]\n\ [--clear-execstack]\n\ [--set-execstack]\n\ [--rename-dynamic-symbols NAME_MAP_FILE]\tRenames dynamic symbols. The map file should contain two symbols (old_name new_name) per line\n\ @@ -2667,6 +2838,11 @@ static int mainWrapped(int argc, char * * argv) neededLibsToReplace[ argv[i+1] ] = argv[i+2]; i += 2; } + else if (arg == "--remove-needed-version") { + if (i+2 >= argc) error("missing argument(s)"); + neededVersionsToRemove[ argv[i+1] ].insert(resolveArgument(argv[i+2])); + i += 2; + } else if (arg == "--clear-symbol-version") { if (++i == argc) error("missing argument"); symbolsToClearVersion.insert(resolveArgument(argv[i])); diff --git a/src/patchelf.h b/src/patchelf.h index 5ed1b585..7739bba9 100644 --- a/src/patchelf.h +++ b/src/patchelf.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "elf.h" @@ -170,6 +171,8 @@ class ElfFile void renameDynamicSymbols(const std::unordered_map&); + void removeNeededVersion(const std::map> & verMap); + void clearSymbolVersions(const std::set & syms); enum class ExecstackMode { print, set, clear }; diff --git a/tests/Makefile.am b/tests/Makefile.am index b08929f1..f28f6736 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -1,6 +1,7 @@ LIBS = -check_PROGRAMS = simple-pie simple simple-execstack main too-many-strtab main-scoped big-dynstr no-rpath contiguous-note-sections +check_PROGRAMS = simple-pie simple simple-execstack main too-many-strtab main-scoped big-dynstr no-rpath contiguous-note-sections \ + symver no_rpath_arch_TESTS = \ no-rpath-alpha.sh \ @@ -53,6 +54,7 @@ src_TESTS = \ shared-rpath.sh \ short-first-segment.sh \ empty-note.sh \ + remove-needed-version.sh \ set-interpreter-same.sh build_TESTS = \ @@ -125,7 +127,8 @@ check_DATA = libbig-dynstr.debug # - with libtool, it is difficult to control options # - with libtool, it is not possible to compile convenience *dynamic* libraries :-( check_PROGRAMS += libfoo.so libfoo-scoped.so libbar.so libbar-scoped.so libsimple.so libsimple-execstack.so libbuildid.so libtoomanystrtab.so \ - phdr-corruption.so many-syms-main libmany-syms.so liboveralign.so libshared-rpath.so + phdr-corruption.so many-syms-main libmany-syms.so liboveralign.so libshared-rpath.so \ + libsymver.so libsymver-old.so libbuildid_so_SOURCES = simple.c libbuildid_so_LDFLAGS = $(LDFLAGS_sharedlib) -Wl,--build-id @@ -183,6 +186,18 @@ phdr_corruption_so_SOURCES = void.c phdr-corruption.ld phdr_corruption_so_LDFLAGS = -nostdlib -shared -Wl,-T$(srcdir)/phdr-corruption.ld phdr_corruption_so_CFLAGS = +libsymver_so_SOURCES = libsymver.c +libsymver_so_LDFLAGS = $(LDFLAGS_sharedlib) -Wl,--version-script=libsymver.map +libsymver_so_CFLAGS = $(AM_CFLAGS) +libsymver_old_so_SOURCES = libsymver-old.c +libsymver_old_so_LDFLAGS = $(LDFLAGS_sharedlib) -Wl,--version-script=libsymver-old.map +libsymver_old_so_CFLAGS = $(AM_CFLAGS) +symver_SOURCES = symver.c +symver_LDFLAGS = $(LDFLAGS_local) +symver_LDADD = -lsymver $(AM_LDADD) +symver_DEPENDENCIES = libsymver.so libsymver-old.so +symver_CFLAGS = $(AM_CFLAGS) + many-syms.c: i=1; while [ $$i -le 2000 ]; do echo "void f$$i() {};"; i=$$(($$i + 1)); done > $@ diff --git a/tests/libsymver-old.c b/tests/libsymver-old.c new file mode 100644 index 00000000..6deff366 --- /dev/null +++ b/tests/libsymver-old.c @@ -0,0 +1,11 @@ +#include + +__asm__(".symver foo_v1, foo@@V1"); + +void foo_v1() { + printf("foo_v1\n"); +} + +void bar() { + printf("bar_unver\n"); +} diff --git a/tests/libsymver-old.map b/tests/libsymver-old.map new file mode 100644 index 00000000..41d6fd70 --- /dev/null +++ b/tests/libsymver-old.map @@ -0,0 +1,4 @@ +V1 { + global: foo; + local: foo_v1; +}; \ No newline at end of file diff --git a/tests/libsymver.c b/tests/libsymver.c new file mode 100644 index 00000000..53fca67e --- /dev/null +++ b/tests/libsymver.c @@ -0,0 +1,18 @@ +#include + +__asm__(".symver foo_v1, foo@V1"); +__asm__(".symver foo_v2, foo@@V2"); + +void foo_v1() { + printf("foo_v1\n"); +} + +void foo_v2() { + printf("foo_v2\n"); +} + + +// version defined in version script +void bar() { + printf("bar_ver\n"); +} \ No newline at end of file diff --git a/tests/libsymver.map b/tests/libsymver.map new file mode 100644 index 00000000..2ef0ac22 --- /dev/null +++ b/tests/libsymver.map @@ -0,0 +1,11 @@ +V1 { + global: foo; + local: foo_v1; +}; +V2 { + global: foo; + local: foo_v2; +}; +VER { + global: bar; +}; \ No newline at end of file diff --git a/tests/remove-needed-version.sh b/tests/remove-needed-version.sh new file mode 100755 index 00000000..1b8264c6 --- /dev/null +++ b/tests/remove-needed-version.sh @@ -0,0 +1,50 @@ +#! /bin/sh -e +SCRATCH="scratch/$(basename "$0" .sh)" +PATCHELF="$(readlink -f "../src/patchelf")" +READELF="${READELF:-readelf}" +MAIN=symver +LIBNEW=libsymver.so +LIBOLD=libsymver-old.so + + +rm -rf "$SCRATCH" +mkdir -p "$SCRATCH" + +cp $MAIN $LIBNEW $LIBOLD "${SCRATCH}/" + +cd "$SCRATCH" + +fail() { + echo $1 + "$READELF" -a -W $MAIN + "$READELF" -a -W $LIBNEW + "$READELF" -a -W $LIBOLD + exit $2 +} + +# sanity check +exit_code=0 +LD_LIBRARY_PATH="$PWD" ./${MAIN} || exit_code=$? +if [ $exit_code -ne 0 ]; then + fail "basic check" $exit_code +fi + +# replace with old version +mv $LIBOLD $LIBNEW + +# should NOT run before patch +exit_code=0 +LD_LIBRARY_PATH="$PWD" ./${MAIN} || exit_code=$? +if [ $exit_code -eq 0 ]; then + fail "patch check" 1 +fi + +${PATCHELF} --remove-needed-version $LIBNEW V2 \ + --remove-needed-version $LIBNEW VER $MAIN || fail "patchelf" -1 + +# should run after removing version +exit_code=0 +LD_LIBRARY_PATH="$PWD" ./${MAIN} || exit_code=$? +if [ $exit_code -ne 0 ]; then + fail "patch check" $exit_code +fi diff --git a/tests/symver.c b/tests/symver.c new file mode 100644 index 00000000..aa9dad8b --- /dev/null +++ b/tests/symver.c @@ -0,0 +1,7 @@ +void foo(); +void bar(); + +int main() { + foo(); + bar(); +} \ No newline at end of file