From 405980f77af0ca7ed74c627d0a0937404c75697e Mon Sep 17 00:00:00 2001 From: 1998LJ Date: Sun, 27 Sep 2026 20:11:40 +0800 Subject: [PATCH 1/2] Make AUDITWHEEL_LD_LIBRARY_PATH override DT_RUNPATH --- src/auditwheel/lddtree.py | 28 +++++----- tests/unit/test_lddtree.py | 109 +++++++++++++++++++++++++++++++++++-- 2 files changed, 119 insertions(+), 18 deletions(-) diff --git a/src/auditwheel/lddtree.py b/src/auditwheel/lddtree.py index af46a8e0..de2ee89d 100644 --- a/src/auditwheel/lddtree.py +++ b/src/auditwheel/lddtree.py @@ -334,24 +334,24 @@ def load_ld_paths( dict containing library paths to search """ - ldpaths: dict[str, list[str]] = {"conf": [], "env": [], "interp": []} + ldpaths: dict[str, list[str]] = { + "conf": [], + "auditwheel": [], + "env": [], + "interp": [], + } + + auditwheel_ld_library_path = os.environ.get("AUDITWHEEL_LD_LIBRARY_PATH") + if auditwheel_ld_library_path: + ldpaths["auditwheel"] = parse_ld_paths(auditwheel_ld_library_path, path="") ld_library_path = os.environ.get("LD_LIBRARY_PATH") if root != "/" and ld_library_path is not None: log.warning("ignoring LD_LIBRARY_PATH due to ROOT usage") ld_library_path = None - # Load up $AUDITWHEEL_LD_LIBRARY_PATH and $LD_LIBRARY_PATH - env_ldpath = ":".join( - filter(None, (os.environ.get("AUDITWHEEL_LD_LIBRARY_PATH"), ld_library_path)), - ) - - if env_ldpath: - # TODO: If this contains $ORIGIN, we probably have to parse this - # on a per-ELF basis so it can get turned into the right thing. - # don't pass root: in case root != "/", only AUDITWHEEL_LD_LIBRARY_PATH is checked - # it shall already contain fully resolved paths - ldpaths["env"] = parse_ld_paths(env_ldpath, path="") + if ld_library_path: + ldpaths["env"] = parse_ld_paths(ld_library_path, path="") if libc == Libc.MUSL: # from https://git.musl-libc.org/cgit/musl/tree/ldso @@ -389,7 +389,8 @@ def ld_paths_from_arg(args_ldpaths: str | None) -> dict[str, list[str]] | None: return { "conf": parse_ld_paths(args_ldpaths), - "env": parse_ld_paths(os.environ.get("AUDITWHEEL_LD_LIBRARY_PATH", "")), + "auditwheel": parse_ld_paths(os.environ.get("AUDITWHEEL_LD_LIBRARY_PATH", "")), + "env": [], "interp": [], } @@ -590,6 +591,7 @@ def ldd( all_ldpaths = ( ldpaths["rpath"] + rpaths + + ldpaths.get("auditwheel", []) + runpaths + ldpaths["env"] + ldpaths["runpath"] diff --git a/tests/unit/test_lddtree.py b/tests/unit/test_lddtree.py index 99047edd..483aad3d 100644 --- a/tests/unit/test_lddtree.py +++ b/tests/unit/test_lddtree.py @@ -3,8 +3,9 @@ import pytest +from auditwheel import lddtree from auditwheel.architecture import Architecture -from auditwheel.lddtree import LIBPYTHON_RE, ld_paths_from_arg, ldd, parse_ld_paths +from auditwheel.lddtree import LIBPYTHON_RE, ld_paths_from_arg, ldd, load_ld_paths, parse_ld_paths from auditwheel.libc import Libc from auditwheel.tools import zip2dir @@ -114,13 +115,13 @@ def test_parse_ld_paths_origin(origin): [ (None, "", None), (None, str(HERE.parent), None), - ("", "", {"conf": [], "env": [], "interp": []}), - (str(HERE), "", {"conf": [str(HERE)], "env": [], "interp": []}), - ("", str(HERE), {"conf": [], "env": [str(HERE)], "interp": []}), + ("", "", {"conf": [], "auditwheel": [], "env": [], "interp": []}), + (str(HERE), "", {"conf": [str(HERE)], "auditwheel": [], "env": [], "interp": []}), + ("", str(HERE), {"conf": [], "auditwheel": [str(HERE)], "env": [], "interp": []}), ( str(HERE), str(HERE.parent), - {"conf": [str(HERE)], "env": [str(HERE.parent)], "interp": []}, + {"conf": [str(HERE)], "auditwheel": [str(HERE.parent)], "env": [], "interp": []}, ), ], ) @@ -144,3 +145,101 @@ def test_libc_no_detect_musl_cp310(tmp_path: Path) -> None: assert result.rpath == () assert result.runpath == () assert not result.libraries + + +def test_load_ld_paths_root_scenario(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """root != '/' should ignore standard LD_LIBRARY_PATH but retain AUDITWHEEL_LD_LIBRARY_PATH.""" + auditwheel_dir = tmp_path / "auditwheel" + auditwheel_dir.mkdir() + ld_dir = tmp_path / "ld_library_path" + ld_dir.mkdir() + + monkeypatch.setitem(os.environ, "AUDITWHEEL_LD_LIBRARY_PATH", str(auditwheel_dir)) + monkeypatch.setitem(os.environ, "LD_LIBRARY_PATH", str(ld_dir)) + + # root is non-root + fake_root = tmp_path / "fake_root" + fake_root.mkdir() + + res = load_ld_paths(Libc.GLIBC, root=str(fake_root)) + assert res["auditwheel"] == [str(auditwheel_dir)] + assert res["env"] == [] + + +def test_runpath_vs_auditwheel_and_ld_library_path_precedence( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Verify precedence order: AUDITWHEEL_LD_LIBRARY_PATH > RUNPATH > LD_LIBRARY_PATH.""" + wheel = BUNDLED_WHEELS / "testzlib-0.0.1-cp310-cp310-linux_x86_64.whl" + so = tmp_path / "testzlib.cpython-310-x86_64-linux-gnu.so" + zip2dir(wheel, tmp_path) + + # Prepare directories with fake libz.so.1 (use valid ELF bytes from testzlib so) + valid_elf_bytes = so.read_bytes() + + dir_auditwheel = tmp_path / "dir_auditwheel" + dir_auditwheel.mkdir() + (dir_auditwheel / "libz.so.1").write_bytes(valid_elf_bytes) + + dir_runpath = tmp_path / "dir_runpath" + dir_runpath.mkdir() + (dir_runpath / "libz.so.1").write_bytes(valid_elf_bytes) + + dir_ld_lib = tmp_path / "dir_ld_lib" + dir_ld_lib.mkdir() + (dir_ld_lib / "libz.so.1").write_bytes(valid_elf_bytes) + + orig_iter_segments = lddtree.ELFFile.iter_segments + + def mocked_iter_segments(self): + for seg in orig_iter_segments(self): + if seg.header.p_type == "PT_DYNAMIC": + orig_iter_tags = getattr(seg, "iter_tags") # noqa: B009 + + def mocked_iter_tags(tags_fn=orig_iter_tags): + yield from tags_fn() + entry = type("Entry", (), {"d_tag": "DT_RUNPATH"})() + yield type( + "MockTag", + (), + {"entry": entry, "runpath": str(dir_runpath)}, + )() + + seg.iter_tags = mocked_iter_tags # type: ignore[attr-defined] + yield seg + + monkeypatch.setattr(lddtree.ELFFile, "iter_segments", mocked_iter_segments) + + # 1. RUNPATH vs normal LD_LIBRARY_PATH (PR #4 historical behavior: RUNPATH must win) + lddtree.load_ld_paths.cache_clear() + monkeypatch.delenv("AUDITWHEEL_LD_LIBRARY_PATH", raising=False) + monkeypatch.setitem(os.environ, "LD_LIBRARY_PATH", str(dir_ld_lib)) + res1 = ldd(so) + assert res1.libraries["libz.so.1"].path == str(dir_runpath / "libz.so.1") + + # 2. RUNPATH vs AUDITWHEEL_LD_LIBRARY_PATH (Issue #737: AUDITWHEEL must win) + lddtree.load_ld_paths.cache_clear() + monkeypatch.setitem(os.environ, "AUDITWHEEL_LD_LIBRARY_PATH", str(dir_auditwheel)) + monkeypatch.delenv("LD_LIBRARY_PATH", raising=False) + res2 = ldd(so) + assert res2.libraries["libz.so.1"].path == str(dir_auditwheel / "libz.so.1") + + # 3. Triple precedence: AUDITWHEEL_LD_LIBRARY_PATH > RUNPATH > LD_LIBRARY_PATH + lddtree.load_ld_paths.cache_clear() + monkeypatch.setitem(os.environ, "AUDITWHEEL_LD_LIBRARY_PATH", str(dir_auditwheel)) + monkeypatch.setitem(os.environ, "LD_LIBRARY_PATH", str(dir_ld_lib)) + res3 = ldd(so) + assert res3.libraries["libz.so.1"].path == str(dir_auditwheel / "libz.so.1") + + # 4. Backward compatibility: custom ldpaths dict without 'auditwheel' key + # Ensure no KeyError is raised and original search behavior is preserved + custom_ldpaths = { + "rpath": [], + "runpath": [], + "conf": [str(dir_runpath)], + "env": [str(dir_ld_lib)], + "interp": [], + } + res4 = ldd(so, ldpaths=custom_ldpaths) + assert res4.libraries["libz.so.1"].path == str(dir_runpath / "libz.so.1") From ff3833ea3e52e19b1d2bc395169768160025a865 Mon Sep 17 00:00:00 2001 From: 1998LJ Date: Wed, 30 Sep 2026 13:56:26 +0800 Subject: [PATCH 2/2] Clarify and avoid duplicate top-level loader paths --- src/auditwheel/lddtree.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/auditwheel/lddtree.py b/src/auditwheel/lddtree.py index de2ee89d..d9dc2c60 100644 --- a/src/auditwheel/lddtree.py +++ b/src/auditwheel/lddtree.py @@ -588,13 +588,20 @@ def ldd( assert ldpaths is not None # noqa: S101 + # On recursive calls, ldpaths["rpath"] and ldpaths["runpath"] are the + # top-level ELF's paths propagated for dependency resolution, while rpaths + # and runpaths belong to the current ELF. On the first call they are the + # same lists, so omit the inherited copies to avoid searching duplicates. + inherited_rpaths = [] if _first else ldpaths["rpath"] + inherited_runpaths = [] if _first else ldpaths["runpath"] + all_ldpaths = ( - ldpaths["rpath"] + inherited_rpaths + rpaths + ldpaths.get("auditwheel", []) + runpaths + ldpaths["env"] - + ldpaths["runpath"] + + inherited_runpaths + ldpaths["conf"] + ldpaths["interp"] )