commit: 95d3e5e80ab9561db870858c2caf6e3bffbf47b0 Author: Zac Medico <zmedico <AT> gentoo <DOT> org> AuthorDate: Mon Jan 15 20:30:57 2024 +0000 Commit: Zac Medico <zmedico <AT> gentoo <DOT> org> CommitDate: Sat Jan 20 05:18:12 2024 +0000 URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=95d3e5e8
installed_dynlibs: Resolve *.so symlinks Resolve *.so symlinks to check if they point to regular files inside the top directory. If a symlink points outside the top directory then try to follow the corresponding file inside the top directory if it exists, and otherwise stop following. Bug: https://bugs.gentoo.org/921170 Signed-off-by: Zac Medico <zmedico <AT> gentoo.org> lib/portage/tests/util/dyn_libs/meson.build | 1 + .../tests/util/dyn_libs/test_installed_dynlibs.py | 65 ++++++++++++++++++++++ lib/portage/util/_dyn_libs/dyn_libs.py | 43 +++++++++++++- 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/lib/portage/tests/util/dyn_libs/meson.build b/lib/portage/tests/util/dyn_libs/meson.build index ddb08f5b1a..8f2c919c13 100644 --- a/lib/portage/tests/util/dyn_libs/meson.build +++ b/lib/portage/tests/util/dyn_libs/meson.build @@ -1,5 +1,6 @@ py.install_sources( [ + 'test_installed_dynlibs.py', 'test_soname_deps.py', '__init__.py', '__test__.py', diff --git a/lib/portage/tests/util/dyn_libs/test_installed_dynlibs.py b/lib/portage/tests/util/dyn_libs/test_installed_dynlibs.py new file mode 100644 index 0000000000..421dcf6061 --- /dev/null +++ b/lib/portage/tests/util/dyn_libs/test_installed_dynlibs.py @@ -0,0 +1,65 @@ +# Copyright 2024 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +import os +import tempfile + +from portage.const import BASH_BINARY +from portage.tests import TestCase +from portage.util import ensure_dirs +from portage.util._dyn_libs.dyn_libs import installed_dynlibs +from portage.util.file_copy import copyfile + + +class InstalledDynlibsTestCase(TestCase): + def testInstalledDynlibsRegular(self): + """ + Return True for *.so regular files. + """ + with tempfile.TemporaryDirectory() as directory: + bash_copy = os.path.join(directory, "lib", "libfoo.so") + ensure_dirs(os.path.dirname(bash_copy)) + copyfile(BASH_BINARY, bash_copy) + self.assertTrue(installed_dynlibs(directory)) + + def testInstalledDynlibsOnlySymlink(self): + """ + If a *.so symlink is installed but does not point to a regular + file inside the top directory, installed_dynlibs should return + False (bug 921170). + """ + with tempfile.TemporaryDirectory() as directory: + symlink_path = os.path.join(directory, "lib", "libfoo.so") + ensure_dirs(os.path.dirname(symlink_path)) + os.symlink(BASH_BINARY, symlink_path) + self.assertFalse(installed_dynlibs(directory)) + + def testInstalledDynlibsSymlink(self): + """ + Return True for a *.so symlink pointing to a regular file inside + the top directory. + """ + with tempfile.TemporaryDirectory() as directory: + bash_copy = os.path.join(directory, BASH_BINARY.lstrip(os.sep)) + ensure_dirs(os.path.dirname(bash_copy)) + copyfile(BASH_BINARY, bash_copy) + symlink_path = os.path.join(directory, "lib", "libfoo.so") + ensure_dirs(os.path.dirname(symlink_path)) + os.symlink(bash_copy, symlink_path) + self.assertTrue(installed_dynlibs(directory)) + + def testInstalledDynlibsAbsoluteSymlink(self): + """ + If a *.so symlink target is outside of the top directory, + traversal follows the corresponding file inside the top + directory if it exists, and otherwise stops following the + symlink. + """ + with tempfile.TemporaryDirectory() as directory: + bash_copy = os.path.join(directory, BASH_BINARY.lstrip(os.sep)) + ensure_dirs(os.path.dirname(bash_copy)) + copyfile(BASH_BINARY, bash_copy) + symlink_path = os.path.join(directory, "lib", "libfoo.so") + ensure_dirs(os.path.dirname(symlink_path)) + os.symlink(BASH_BINARY, symlink_path) + self.assertTrue(installed_dynlibs(directory)) diff --git a/lib/portage/util/_dyn_libs/dyn_libs.py b/lib/portage/util/_dyn_libs/dyn_libs.py index ee28e8839c..6f8a07d70d 100644 --- a/lib/portage/util/_dyn_libs/dyn_libs.py +++ b/lib/portage/util/_dyn_libs/dyn_libs.py @@ -1,14 +1,51 @@ -# Copyright 2021 Gentoo Authors +# Copyright 2021-2024 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 import os +import stat + +import portage def installed_dynlibs(directory): - for _dirpath, _dirnames, filenames in os.walk(directory): + """ + This traverses installed *.so symlinks to check if they point to + regular files. If a symlink target is outside of the top directory, + traversal follows the corresponding file inside the top directory + if it exists, and otherwise stops following the symlink. + """ + directory_prefix = f"{directory.rstrip(os.sep)}{os.sep}" + for parent, _dirnames, filenames in os.walk(directory): for filename in filenames: if filename.endswith(".so"): - return True + filename_abs = os.path.join(parent, filename) + target = filename_abs + levels = 0 + while True: + try: + st = os.lstat(target) + except OSError: + break + if stat.S_ISREG(st.st_mode): + return True + elif stat.S_ISLNK(st.st_mode): + levels += 1 + if levels == 40: + portage.writemsg( + f"too many levels of symbolic links: {filename_abs}\n", + noiselevel=-1, + ) + break + target = portage.abssymlink(target) + if not target.startswith(directory_prefix): + # If target is outside the top directory, then follow the + # corresponding file inside the top directory if it exists, + # and otherwise stop following. + target = os.path.join( + directory_prefix, target.lstrip(os.sep) + ) + else: + break return False
