Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion relenv/build/common/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
34 changes: 33 additions & 1 deletion relenv/relocate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
85 changes: 85 additions & 0 deletions tests/test_relocate.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
main,
parse_readelf_d,
patch_rpath,
remove_rpath,
)

pytestmark = [
Expand Down Expand Up @@ -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/../..")
Loading