From 01a4f023f6a453510cb3d2ffbc2a794b973814bf Mon Sep 17 00:00:00 2001 From: chitao1234 Date: Fri, 2 Aug 2024 22:46:20 +0800 Subject: [PATCH 1/4] feat: add --remove-needed-version switch Should fix #252, #284, --remove-needed-version works by removing specified version in .gnu.version_r, and resets version in referenced entry in .gnu.version. --- src/patchelf.cc | 132 ++++++++++++++++++++++++++++++++++++++++++++++++ src/patchelf.h | 3 ++ 2 files changed, 135 insertions(+) diff --git a/src/patchelf.cc b/src/patchelf.cc index 66d0b99a..a76c62c4 100644 --- a/src/patchelf.cc +++ b/src/patchelf.cc @@ -2215,6 +2215,130 @@ 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 spanVersyms = tryGetSectionSpan(".gnu.version"); + + 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); + + 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 { + // FIXME: remove entire section? + wri(shdrVersionR.sh_offset, 0); + wri(shdrVersionR.sh_size, 0); + } + } + 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 for .gnu.version_r + shdrVersionR.sh_addr = 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_VERNEEDNUM handled by rewriteHeaders + for ( ; rdi(dyn->d_tag) != DT_NULL; dyn++) { + if (rdi(dyn->d_tag) == DT_VERNEEDNUM) { + dyn->d_un.d_val = shdrVersionR.sh_info; + } + } + } + + if (spanVersyms) { + for (auto & versym : spanVersyms) { + if (removedVersionIds.count(versym)) { + wri(versym, 1); + } + } + } + + debug("remaining entries in .gnu.version_r: %i\n", rdi(shdrVersionR.sh_info)); + this->rewriteSections(true); +} + template void ElfFile::clearSymbolVersions(const std::set & syms) { @@ -2376,6 +2500,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; @@ -2430,6 +2555,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); @@ -2505,6 +2631,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\ @@ -2613,6 +2740,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 4e229d67..f85816f7 100644 --- a/src/patchelf.h +++ b/src/patchelf.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "elf.h" @@ -169,6 +170,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 }; From aaecc8ca838c295438f917516663215803629761 Mon Sep 17 00:00:00 2001 From: chitao1234 Date: Fri, 2 Aug 2024 23:00:25 +0800 Subject: [PATCH 2/4] doc: document the --remove-needed-version switch --- patchelf.1 | 5 +++++ 1 file changed, 5 insertions(+) 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. From 7e07d97dfff2cd76d9f32efcc04388219e133863 Mon Sep 17 00:00:00 2001 From: chitao1234 Date: Sat, 3 Aug 2024 15:20:57 +0800 Subject: [PATCH 3/4] test: add test for --remove-needed-version --- .gitignore | 1 + tests/Makefile.am | 21 ++++++++++++-- tests/libsymver-old.c | 11 ++++++++ tests/libsymver-old.map | 4 +++ tests/libsymver.c | 18 ++++++++++++ tests/libsymver.map | 11 ++++++++ tests/remove-needed-version.sh | 50 ++++++++++++++++++++++++++++++++++ tests/symver.c | 7 +++++ 8 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 tests/libsymver-old.c create mode 100644 tests/libsymver-old.map create mode 100644 tests/libsymver.c create mode 100644 tests/libsymver.map create mode 100755 tests/remove-needed-version.sh create mode 100644 tests/symver.c 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/tests/Makefile.am b/tests/Makefile.am index 8bbded7a..e6fbd681 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-amd64.sh \ @@ -51,7 +52,8 @@ src_TESTS = \ overlapping-segments-after-rounding.sh \ shared-rpath.sh \ short-first-segment.sh \ - empty-note.sh + empty-note.sh \ + remove-needed-version.sh build_TESTS = \ $(no_rpath_arch_TESTS) @@ -123,7 +125,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 @@ -180,6 +183,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 From 65e14792061c298f1d2bc44becd48a10cbf0bc81 Mon Sep 17 00:00:00 2001 From: chitao1234 Date: Tue, 3 Sep 2024 13:52:36 +0800 Subject: [PATCH 4/4] fix: make section as null when removing entire section, use rewriteHeaders rather than rewriteSection --- src/patchelf.cc | 62 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/src/patchelf.cc b/src/patchelf.cc index a76c62c4..934d9d3c 100644 --- a/src/patchelf.cc +++ b/src/patchelf.cc @@ -2228,7 +2228,7 @@ void ElfFile::removeNeededVersion(const std::map(".gnu.version"); + auto mapOffset = rdi(shdrVersionR.sh_addr) - rdi(shdrVersionR.sh_offset); size_t verNeedNum = rdi(shdrVersionR.sh_info); std::set removedVersionIds; @@ -2238,6 +2238,8 @@ void ElfFile::removeNeededVersion(const std::mapdata() + rdi(shdrVersionR.sh_offset)); Elf_Verneed * prevVn = nullptr; for (size_t i = 0; i < verNeedNum; ++i) { @@ -2296,9 +2298,9 @@ void ElfFile::removeNeededVersion(const std::map::removeNeededVersion(const std::map(vn, next); } } - // virtual address and file offset need to be the same for .gnu.version_r - shdrVersionR.sh_addr = shdrVersionR.sh_offset; + + // 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_VERNEEDNUM handled by rewriteHeaders + // 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) { - dyn->d_un.d_val = shdrVersionR.sh_info; + 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)) { @@ -2335,8 +2357,30 @@ void ElfFile::removeNeededVersion(const std::maprewriteSections(true); + 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