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
 
 

Reply via email to