From 1342ef7e544bb539475ecaee7328dd9982f44ba3 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Sat, 20 Dec 2025 16:08:04 -0700 Subject: [PATCH] Remove unused rpath settings Prior to this change if a module didn't link to anything under our root we left the rpath from the build un-modified. This lead to rpaths pointint to directories that may not exist. This update will clear any un-used rpath settings, only modules that require an rpath will have one set. --- relenv/build/common/install.py | 6 ++- relenv/relocate.py | 34 +++++++++++++- tests/test_relocate.py | 85 ++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/relenv/build/common/install.py b/relenv/build/common/install.py index 4ab30d37..5d6c4677 100644 --- a/relenv/build/common/install.py +++ b/relenv/build/common/install.py @@ -427,7 +427,11 @@ def finalize( :type logfp: file """ # Run relok8 to make sure the rpaths are relocatable. - relenv.relocate.main(dirs.prefix, log_file_name=str(dirs.logs / "relocate.py.log")) + # Modules that don't link to relenv libs will have their RPATH removed + relenv.relocate.main( + dirs.prefix, + log_file_name=str(dirs.logs / "relocate.py.log"), + ) # Install relenv-sysconfigdata module libdir = pathlib.Path(dirs.prefix) / "lib" diff --git a/relenv/relocate.py b/relenv/relocate.py index b13eb0d7..58a4fbdb 100755 --- a/relenv/relocate.py +++ b/relenv/relocate.py @@ -264,6 +264,34 @@ def is_in_dir( return os.path.realpath(filepath).startswith(os.path.realpath(directory) + os.sep) +def remove_rpath(path: str | os.PathLike[str]) -> bool: + """ + Remove the rpath from a given ELF file. + + :param path: The path to an ELF file + :type path: str + + :return: True if successful, False otherwise + :rtype: bool + """ + old_rpath = parse_rpath(path) + if not old_rpath: + # No RPATH to remove + return True + + log.info("Remove RPATH from %s (was: %s)", path, old_rpath) + proc = subprocess.run( + ["patchelf", "--remove-rpath", path], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + if proc.returncode: + log.error("Failed to remove RPATH from %s: %s", path, proc.stderr.decode()) + return False + return True + + def patch_rpath( path: str | os.PathLike[str], new_rpath: str, @@ -325,6 +353,7 @@ def handle_elf( root = libs proc = subprocess.run(["ldd", path], stderr=subprocess.PIPE, stdout=subprocess.PIPE) needs_rpath = False + for line in proc.stdout.decode().splitlines(): if line.find("=>") == -1: log.debug("Skip ldd output line: %s", line) @@ -371,7 +400,9 @@ def handle_elf( log.info("Adjust rpath of %s to %s", path, relpath) patch_rpath(path, relpath) else: - log.info("Do not adjust rpath of %s", path) + # No relenv libraries are linked, so RPATH is not needed + # Remove any existing RPATH to avoid security/correctness issues + remove_rpath(path) def main( @@ -420,6 +451,7 @@ def main( if path in processed: continue log.debug("Checking %s", path) + if is_macho(path): log.info("Found Mach-O %s", path) _ = handle_macho(path, libs_dir, rpath_only) diff --git a/tests/test_relocate.py b/tests/test_relocate.py index 0a9054c4..094d2544 100644 --- a/tests/test_relocate.py +++ b/tests/test_relocate.py @@ -16,6 +16,7 @@ main, parse_readelf_d, patch_rpath, + remove_rpath, ) pytestmark = [ @@ -276,3 +277,87 @@ def test_handle_elf_rpath_only(tmp_path: pathlib.Path) -> None: assert not (proj.libs_dir / "fake.so.2").exists() assert patch_rpath_mock.call_count == 1 patch_rpath_mock.assert_called_with(str(pybin), "$ORIGIN/../lib") + + +def test_remove_rpath_with_existing_rpath(tmp_path: pathlib.Path) -> None: + """Test that remove_rpath removes an existing RPATH.""" + path = str(tmp_path / "test.so") + with patch("subprocess.run", return_value=MagicMock(returncode=0)): + with patch( + "relenv.relocate.parse_rpath", + return_value=["/some/absolute/path"], + ): + assert remove_rpath(path) is True + + +def test_remove_rpath_no_existing_rpath(tmp_path: pathlib.Path) -> None: + """Test that remove_rpath succeeds when there's no RPATH to remove.""" + path = str(tmp_path / "test.so") + with patch("relenv.relocate.parse_rpath", return_value=[]): + assert remove_rpath(path) is True + + +def test_remove_rpath_failed(tmp_path: pathlib.Path) -> None: + """Test that remove_rpath returns False when patchelf fails.""" + path = str(tmp_path / "test.so") + with patch("subprocess.run", return_value=MagicMock(returncode=1)): + with patch( + "relenv.relocate.parse_rpath", + return_value=["/some/absolute/path"], + ): + assert remove_rpath(path) is False + + +def test_handle_elf_removes_rpath_when_no_relenv_libs(tmp_path: pathlib.Path) -> None: + """Test that handle_elf removes RPATH for binaries linking only to system libs.""" + proj = LinuxProject(tmp_path / "proj") + module = proj.add_simple_elf("array.so", "lib", "python3.10", "lib-dynload") + + # ldd output showing only system libraries + ldd_ret = """ + linux-vdso.so.1 => linux-vdso.so.1 (0x0123456789) + libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x0123456789) + libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0123456789) + """.encode() + + with proj: + with patch("subprocess.run", return_value=MagicMock(stdout=ldd_ret)): + with patch("relenv.relocate.remove_rpath") as remove_rpath_mock: + with patch("relenv.relocate.patch_rpath") as patch_rpath_mock: + handle_elf( + str(module), str(proj.libs_dir), True, str(proj.root_dir) + ) + # Should remove RPATH, not patch it + assert remove_rpath_mock.call_count == 1 + assert patch_rpath_mock.call_count == 0 + remove_rpath_mock.assert_called_with(str(module)) + + +def test_handle_elf_sets_rpath_when_relenv_libs_present(tmp_path: pathlib.Path) -> None: + """Test that handle_elf sets RPATH for binaries linking to relenv libs.""" + proj = LinuxProject(tmp_path / "proj") + module = proj.add_simple_elf("_ssl.so", "lib", "python3.10", "lib-dynload") + libssl = proj.libs_dir / "libssl.so.3" + libssl.touch() + + # ldd output showing relenv-built library + ldd_ret = """ + linux-vdso.so.1 => linux-vdso.so.1 (0x0123456789) + libssl.so.3 => {libssl} (0x0123456789) + libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x0123456789) + libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0123456789) + """.format( + libssl=libssl + ).encode() + + with proj: + with patch("subprocess.run", return_value=MagicMock(stdout=ldd_ret)): + with patch("relenv.relocate.remove_rpath") as remove_rpath_mock: + with patch("relenv.relocate.patch_rpath") as patch_rpath_mock: + handle_elf( + str(module), str(proj.libs_dir), True, str(proj.root_dir) + ) + # Should patch RPATH, not remove it + assert patch_rpath_mock.call_count == 1 + assert remove_rpath_mock.call_count == 0 + patch_rpath_mock.assert_called_with(str(module), "$ORIGIN/../..")