D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
proc
/
self
/
root
/
opt
/
saltstack
/
salt
/
lib
/
python3.10
/
site-packages
/
relenv
/
Filename :
relocate.py
back
Copy
# Copyright 2022-2026 Broadcom. # SPDX-License-Identifier: Apache-2.0 """ A script to ensure the proper rpaths are in place for the relenv environment. """ from __future__ import annotations import logging import os as _os import pathlib import shutil as _shutil import subprocess as _subprocess import sys as _sys from typing import Optional log = logging.getLogger(__name__) os = _os shutil = _shutil subprocess = _subprocess sys = _sys __all__ = [ "is_macho", "is_elf", "parse_otool_l", "parse_readelf_d", "parse_macho", "parse_rpath", "handle_macho", "is_in_dir", "handle_elf", "patch_rpath", "main", "os", "shutil", "subprocess", ] LIBCLIBS = [ "linux-vdso.so.1", "libc.so.6", "librt.so.1", "libm.so.6", "libmd.so.0", "libpthread.so.0", # Adjust rpath of libcrypt because we include libxcrypt # "libcrypt.so.1", "libdl.so.2", "libmemusage.so", "libnsl.so.1", "libnss_compat.so.2", "libnss_db.so.2", "libnss_dns.so.2", "libnss_files.so.2", "libnss_hesiod.so.2", "libpcprofile.so.2", "libresolv.so.2", "librt.so.1", "libthread_db.so.1", "libutil.so.1", "libutil.so.2", # libgcc is not technically glibc but shares the same kind of backwards # compatablity guarantees. "libgcc_s.so.2", "libgcc_s.so.1", ] LC_ID_DYLIB = "LC_ID_DYLIB" LC_LOAD_DYLIB = "LC_LOAD_DYLIB" LC_RPATH = "LC_RPATH" # Cache for readelf binary path _READELF_BINARY: Optional[str] = None # Cache for patchelf binary path _PATCHELF_BINARY: Optional[str] = None def _get_readelf_binary() -> str: """ Get the path to readelf binary, preferring toolchain version. Returns the cached value if already computed. On Linux, prefers the toolchain's readelf over the system version. Falls back to "readelf" from PATH if toolchain is unavailable. :return: Path to readelf binary :rtype: str """ global _READELF_BINARY if _READELF_BINARY is not None: return _READELF_BINARY # Only Linux has the toolchain with readelf if sys.platform == "linux": try: from relenv.common import get_toolchain, get_triplet toolchain = get_toolchain() if toolchain: triplet = get_triplet() toolchain_readelf = toolchain / "bin" / f"{triplet}-readelf" if toolchain_readelf.exists(): _READELF_BINARY = str(toolchain_readelf) return _READELF_BINARY except Exception: # Fall through to system readelf pass # Fall back to system readelf _READELF_BINARY = "readelf" return _READELF_BINARY def _get_patchelf_binary() -> str: """ Get the path to patchelf binary, preferring toolchain version. Returns the cached value if already computed. On Linux, prefers the toolchain's patchelf over the system version. Falls back to "patchelf" from PATH if toolchain is unavailable. :return: Path to patchelf binary :rtype: str """ global _PATCHELF_BINARY if _PATCHELF_BINARY is not None: return _PATCHELF_BINARY # Only Linux has the toolchain with patchelf if sys.platform == "linux": try: from relenv.common import get_toolchain toolchain = get_toolchain() if toolchain: toolchain_patchelf = toolchain / "bin" / "patchelf" if toolchain_patchelf.exists(): _PATCHELF_BINARY = str(toolchain_patchelf) return _PATCHELF_BINARY except Exception: # Fall through to system patchelf pass # Fall back to system patchelf _PATCHELF_BINARY = "patchelf" return _PATCHELF_BINARY def is_macho(path: str | os.PathLike[str]) -> bool: """ Determines whether the given file is a macho file. :param path: The path to the file to check :type path: str :return: Whether the file is a macho file :rtype: bool """ with open(path, "rb") as fp: magic = fp.read(4) # XXX: Handle 64bit, 32bit, ppc, arm return magic in [b"\xcf\xfa\xed\xfe"] def is_elf(path: str | os.PathLike[str]) -> bool: """ Determines whether the given file is an ELF file. :param path: The path to the file to check :type path: str :return: Whether the file is an ELF file :rtype: bool """ with open(path, "rb") as fp: magic = fp.read(4) return magic == b"\x7f\x45\x4c\x46" def parse_otool_l(stdout: str) -> dict[str, list[str]]: """ Parse the output of ``otool -l <path>``. :param stdout: The output of the ``otool -l <path>`` command :type stdout: str :return: The parsed relevant output with command keys and path values :rtype: dict """ in_cmd = False cmd: Optional[str] = None name: Optional[str] = None data: dict[str, list[str]] = {} for line in [x.strip() for x in stdout.split("\n")]: if not line: continue if line.split()[0] == "cmd": in_cmd = False if cmd is not None and name is not None: data.setdefault(cmd, []).append(name) cmd = None name = None command = line.split()[-1] if command in (LC_ID_DYLIB, LC_LOAD_DYLIB, "LC_RPATH"): cmd = command in_cmd = True if in_cmd: parts = line.split() if parts[0] == "name" and len(parts) > 1: name = parts[1] if parts[0] == "path" and len(parts) > 1: name = parts[1] if in_cmd and cmd is not None and name is not None: data.setdefault(cmd, []).append(name) return data def parse_readelf_d(stdout: str) -> list[str]: """ Parse the output of ``readelf -d <path>``. :param stdout: The output of the ``readelf -d <path>`` command :type stdout: str :return: The RPATH values :rtype: list """ for line in stdout.splitlines(): # Find either RPATH or READPATH if line.find("PATH") == -1: continue return line.split(":", 1)[-1].strip().strip("[").strip("]").split(":") return [] def parse_macho(path: str | os.PathLike[str]) -> dict[str, list[str]] | None: """ Run ``otool -l <path>`` and return its parsed output. :param path: The path to the file :type path: str :return: The parsed relevant RPATH content, or None if it isn't an object file :rtype: dict or None """ proc = subprocess.run( ["otool", "-l", path], stderr=subprocess.PIPE, stdout=subprocess.PIPE ) stdout = proc.stdout.decode() if stdout.find("is not an object file") != -1: return None return parse_otool_l(stdout) def parse_rpath(path: str | os.PathLike[str]) -> list[str]: """ Run ``readelf -d <path>`` and return its parsed output. :param path: The path to the file :type path: str :return: The RPATH's found. :rtype: list """ readelf = _get_readelf_binary() proc = subprocess.run( [readelf, "-d", path], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) return parse_readelf_d(proc.stdout.decode()) def handle_macho( path: str | os.PathLike[str], root_dir: str | os.PathLike[str], rpath_only: bool, ) -> dict[str, list[str]] | None: """ Ensure the given macho file has the correct rpath and is in th correct location. :param path: The path to a macho file :type path: str :param root_dir: The directory the file needs to reside under :type root_dir: str :param rpath_only: If true, only ensure the correct rpaths are present and don't copy the file :type rpath_only: bool :return: The information from ``parse_macho`` on the macho file. """ path_obj = pathlib.Path(path) obj = parse_macho(path_obj) path_str = str(path_obj) log.info("Processing file %s %r", path_str, obj) if not obj: return None if LC_LOAD_DYLIB in obj: for x in obj[LC_LOAD_DYLIB]: if x.startswith("@"): log.info("Skipping dynamic load: %s", x) continue if os.path.exists(x): target_dir = pathlib.Path(root_dir).resolve() y = target_dir / os.path.basename(x) if not os.path.exists(y): if rpath_only: log.warning("In `rpath_only mode` but %s is not in %s", x, y) continue else: shutil.copy(x, y) shutil.copymode(x, y) log.info("Copied %s to %s", x, y) log.info("Use %s to %s", y, path_str) z = pathlib.Path("@loader_path") / os.path.relpath( y, path_obj.resolve().parent ) cmd = ["install_name_tool", "-change", x, str(z), path_str] subprocess.run(cmd) log.info("Changed %s to %s in %s", x, z, path_str) return obj def is_in_dir( filepath: str | os.PathLike[str], directory: str | os.PathLike[str] ) -> bool: """ Determines whether a file is contained within a directory. :param filepath: The path to the file to check :type filepath: str :param directory: The directory to check within :type directory: str :return: Whether the file is contained within the given directory :rtype: bool """ 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) patchelf = _get_patchelf_binary() 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, only_relative: bool = True, ) -> str | bool: """ Patch the rpath of a given ELF file. :param path: The path to an ELF file :type path: str :param new_rpath: The new rpath to add :type new_rpath: str :param only_relative: Whether or not to remove non-relative rpaths, defaults to True :type only_relative: bool, optional :return: The new rpath, or False if patching failed :rtype: str or bool """ old_rpath = parse_rpath(path) # Remove non-relative rpaths if needed if only_relative: old_rpath = [rpath for rpath in old_rpath if rpath.startswith("$ORIGIN")] if new_rpath not in old_rpath: patched_rpath = ":".join([new_rpath] + old_rpath) log.info("Set RPATH=%s %s", patched_rpath, path) patchelf = _get_patchelf_binary() proc = subprocess.run( [patchelf, "--force-rpath", "--set-rpath", patched_rpath, path], stderr=subprocess.PIPE, stdout=subprocess.PIPE, ) if proc.returncode: return False return patched_rpath return ":".join(old_rpath) def handle_elf( path: str | os.PathLike[str], libs: str | os.PathLike[str], rpath_only: bool, root: str | os.PathLike[str] | None = None, ) -> None: """ Handle the parsing and pathcing of an ELF file. :param path: The path of the ELF file :type path: str :param libs: The libs directory :type libs: str :param rpath_only: If true, only ensure the correct rpaths are present and don't copy the file :type rpath_only: bool :param root: The directory to ensure the file is under, defaults to None :type root: str, optional """ if root is None: 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) continue lib_name, location_info = [_.strip() for _ in line.split("=>", 1)] if location_info == "not found": # It is likely that something was not compiled correctly log.warning("Unable to find library %s linked from %s", lib_name, path) continue linked_lib = location_info.rsplit(" ", 1)[0].strip() lib_basename = os.path.basename(linked_lib) if lib_name in LIBCLIBS: log.debug("Skipping glibc lib %s", lib_name) continue if is_in_dir(linked_lib, root): needs_rpath = True log.info("File already within root directory: %s", linked_lib) continue relocated_path = os.path.join(libs, lib_basename) if os.path.exists(relocated_path): log.debug("Relocated library exists: %s", relocated_path) elif rpath_only: log.warning("In `rpath_only mode` but %s is not in %s", linked_lib, root) else: # If we aren't in `rpath_only` mode, we can copy log.info("Copy %s to %s", linked_lib, relocated_path) shutil.copy(linked_lib, relocated_path) shutil.copymode(linked_lib, relocated_path) needs_rpath = True if needs_rpath: relpart = os.path.relpath(libs, os.path.dirname(path)) if relpart == ".": relpath = "$ORIGIN" else: relpath = str(pathlib.Path("$ORIGIN") / relpart) log.info("Adjust rpath of %s to %s", path, relpath) patch_rpath(path, relpath) else: # No relenv libraries are linked, so RPATH is not needed # Remove any existing RPATH to avoid security/correctness issues remove_rpath(path) def main( root: str | os.PathLike[str], libs_dir: str | os.PathLike[str] | None = None, rpath_only: bool = True, log_level: str = "DEBUG", log_file_name: str = "<stdout>", ) -> None: """ The entrypoint into the relocate script. :param root: The root directory to operate traverse for files to be patched :type root: str :param libs_dir: The directory to place the libraries in, defaults to None :type libs_dir: str, optional :param rpath_only: If true, only ensure the correct rpaths are present and don't copy the file, defaults to True :type rpath_only: bool, optional :param log_level: The level to log at, defaults to "INFO" :type log_level: str, optional """ level = logging.getLevelName(log_level.upper()) if log_file_name != "<stdout>": logging.basicConfig( level=level, format="%(asctime)s %(message)s", filename=log_file_name, filemode="w", ) else: logging.basicConfig( level=level, format="%(asctime)s %(message)s", ) root_dir = str(pathlib.Path(root).resolve()) if libs_dir is None: libs_dir = pathlib.Path(root_dir, "lib") libs_dir = str(pathlib.Path(libs_dir).resolve()) processed: dict[str, dict[str, list[str]] | None] = {} found = True while found: found = False for root, dirs, files in os.walk(root_dir): for file in files: path = os.path.join(root, file) 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) if _ is not None: processed[path] = _ found = True elif is_elf(path): log.info("Found ELF %s", path) handle_elf(path, libs_dir, rpath_only, root_dir) if __name__ == "__main__": import sys if not hasattr(sys, "RELENV"): raise RuntimeError("Not in a relenv environment") main(sys.RELENV)