autoPatchelfHook: add support for .note.dlopen

also retain libc in rpath if it was there originally
This commit is contained in:
Arian van Putten
2024-07-21 06:25:51 -04:00
committed by Will Fancher
parent ffbecb3c19
commit 80be92660e
+64 -21
View File
@@ -5,16 +5,18 @@ import os
import pprint import pprint
import subprocess import subprocess
import sys import sys
import json
from fnmatch import fnmatch from fnmatch import fnmatch
from collections import defaultdict from collections import defaultdict
from contextlib import contextmanager from contextlib import contextmanager
from dataclasses import dataclass from dataclasses import dataclass
from itertools import chain from itertools import chain
from pathlib import Path, PurePath from pathlib import Path, PurePath
from typing import DefaultDict, Iterator, Optional from typing import DefaultDict, Generator, Iterator, Optional
from elftools.common.exceptions import ELFError # type: ignore from elftools.common.exceptions import ELFError # type: ignore
from elftools.elf.dynamic import DynamicSection # type: ignore from elftools.elf.dynamic import DynamicSection # type: ignore
from elftools.elf.sections import NoteSection # type: ignore
from elftools.elf.elffile import ELFFile # type: ignore from elftools.elf.elffile import ELFFile # type: ignore
from elftools.elf.enums import ENUM_E_TYPE, ENUM_EI_OSABI # type: ignore from elftools.elf.enums import ENUM_E_TYPE, ENUM_EI_OSABI # type: ignore
@@ -38,7 +40,7 @@ def is_dynamic_executable(elf: ELFFile) -> bool:
return bool(elf.get_section_by_name(".interp")) return bool(elf.get_section_by_name(".interp"))
def get_dependencies(elf: ELFFile) -> list[str]: def get_dependencies(elf: ELFFile) -> list[list[Path]]:
dependencies = [] dependencies = []
# This convoluted code is here on purpose. For some reason, using # This convoluted code is here on purpose. For some reason, using
# elf.get_section_by_name(".dynamic") does not always return an # elf.get_section_by_name(".dynamic") does not always return an
@@ -46,12 +48,33 @@ def get_dependencies(elf: ELFFile) -> list[str]:
for section in elf.iter_sections(): for section in elf.iter_sections():
if isinstance(section, DynamicSection): if isinstance(section, DynamicSection):
for tag in section.iter_tags('DT_NEEDED'): for tag in section.iter_tags('DT_NEEDED'):
dependencies.append(tag.needed) dependencies.append([Path(tag.needed)])
break # There is only one dynamic section break # There is only one dynamic section
return dependencies return dependencies
def get_dlopen_dependencies(elf: ELFFile) -> list[list[Path]]:
"""
Extracts dependencies from the `.note.dlopen` section.
This is a FreeDesktop standard to annotate binaries with libraries that it may `dlopen`.
See https://systemd.io/ELF_DLOPEN_METADATA/
"""
dependencies = []
for section in elf.iter_sections():
if not isinstance(section, NoteSection) or section.name != ".note.dlopen":
continue
for note in section.iter_notes():
if note["n_type"] != 0x407C0C0A or note["n_name"] != "FDO":
continue
note_desc = note["n_desc"]
text = note_desc.decode("utf-8").rstrip("\0")
j = json.loads(text)
for d in j:
dependencies.append([Path(soname) for soname in d["soname"]])
return dependencies
def get_rpath(elf: ELFFile) -> list[str]: def get_rpath(elf: ELFFile) -> list[str]:
# This convoluted code is here on purpose. For some reason, using # This convoluted code is here on purpose. For some reason, using
# elf.get_section_by_name(".dynamic") does not always return an # elf.get_section_by_name(".dynamic") does not always return an
@@ -204,7 +227,7 @@ def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list
file_is_dynamic_executable = is_dynamic_executable(elf) file_is_dynamic_executable = is_dynamic_executable(elf)
file_dependencies = map(Path, get_dependencies(elf)) file_dependencies = get_dependencies(elf) + get_dlopen_dependencies(elf)
except ELFError: except ELFError:
return [] return []
@@ -223,24 +246,44 @@ def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list
# failing at the first one, because it's more useful when working # failing at the first one, because it's more useful when working
# on a new package where you don't yet know the dependencies. # on a new package where you don't yet know the dependencies.
for dep in file_dependencies: for dep in file_dependencies:
if dep.is_absolute() and dep.is_file(): was_found = False
# This is an absolute path. If it exists, just use it. for candidate in dep:
# Otherwise, we probably want this to produce an error when
# checked (because just updating the rpath won't satisfy
# it).
continue
elif (libc_lib / dep).is_file():
# This library exists in libc, and will be correctly
# resolved by the linker.
continue
if found_dependency := find_dependency(dep.name, file_arch, file_osabi): # This loop determines which candidate for a given
rpath.append(found_dependency) # dependency can be found, and how. There may be multiple
dependencies.append(Dependency(path, dep, True)) # candidates for a dep because of '.note.dlopen'
print(f" {dep} -> found: {found_dependency}") # dependencies.
else: #
dependencies.append(Dependency(path, dep, False)) # 1. If a candidate is an absolute path, it is already a
print(f" {dep} -> not found!") # valid dependency if that path exists, and nothing needs
# to be done. It should be an error if that path does not exist.
# 2. If a candidate is found in our library dependencies, that
# dependency should be added to rpath.
# 3. If a candidate is found in libc, it will be correctly
# resolved by the dynamic linker automatically.
#
# These conditions are checked in this order, because #2
# and #3 may both be true. In that case, we still want to
# add the dependency to rpath, as the original binary
# presumably had it and this should be preserved.
if candidate.is_absolute() and candidate.is_file():
was_found = True
break
elif found_dependency := find_dependency(candidate.name, file_arch, file_osabi):
rpath.append(found_dependency)
dependencies.append(Dependency(path, candidate, found=True))
print(f" {candidate} -> found: {found_dependency}")
was_found = True
break
elif (libc_lib / candidate).is_file():
was_found = True
break
if not was_found:
dep_name = dep[0] if len(dep) == 1 else f"any({', '.join(map(str, dep))})"
dependencies.append(Dependency(path, dep_name, found=False))
print(f" {dep_name} -> not found!")
rpath.extend(append_rpaths) rpath.extend(append_rpaths)