Skip to content

Commit

Permalink
feat: add GNU_PROPERTY_X86_ISA_1_NEEDED detection
Browse files Browse the repository at this point in the history
ISA extensions usage is not defined by a PEP yet.

This first implementation fails to repair the wheel if the usage of x86-64-v[2-4] is required.

The check can be disabled with `--disable-isa-ext-check`.

The detection being related to a declaration when building, it will not detect the requirement for binaries where the declaration is missing.

All executables built on a manylinux_2_34  image will be detected as x86-64-v2.
  • Loading branch information
mayeut committed Feb 2, 2025
1 parent d4570da commit 4350408
Show file tree
Hide file tree
Showing 10 changed files with 542 additions and 254 deletions.
71 changes: 71 additions & 0 deletions src/auditwheel/architecture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from __future__ import annotations

import functools
import platform
import struct
import sys
from enum import Enum


class Architecture(Enum):
value: str

aarch64 = "aarch64"
armv7l = "armv7l"
i686 = "i686"
loongarch64 = "loongarch64"
ppc64 = "ppc64"
ppc64le = "ppc64le"
riscv64 = "riscv64"
s390x = "s390x"
x86_64 = "x86_64"
x86_64_v2 = "x86_64_v2"
x86_64_v3 = "x86_64_v3"
x86_64_v4 = "x86_64_v4"

def __str__(self):
return self.value

@property
def baseline(self):
if self.value.startswith("x86_64"):
return Architecture.x86_64
return self

@classmethod
@functools.lru_cache(None)
def _member_list(cls) -> list[Architecture]:
return list(cls)

def is_subset(self, other: Architecture) -> bool:
if self.baseline != other.baseline:
return False
member_list = Architecture._member_list()
return member_list.index(self) <= member_list.index(other)

def is_superset(self, other: Architecture) -> bool:
if self.baseline != other.baseline:
return False
return other.is_subset(self)

@staticmethod
def get_native_architecture(*, bits: int | None = None) -> Architecture:
machine = platform.machine()
if sys.platform.startswith("win"):
machine = {"AMD64": "x86_64", "ARM64": "aarch64", "x86": "i686"}.get(

Check warning on line 55 in src/auditwheel/architecture.py

View check run for this annotation

Codecov / codecov/patch

src/auditwheel/architecture.py#L55

Added line #L55 was not covered by tests
machine, machine
)
elif sys.platform.startswith("darwin"):
machine = {"arm64": "aarch64"}.get(machine, machine)

Check warning on line 59 in src/auditwheel/architecture.py

View check run for this annotation

Codecov / codecov/patch

src/auditwheel/architecture.py#L59

Added line #L59 was not covered by tests

if bits is None:
# c.f. https://github.com/pypa/packaging/pull/711
bits = 8 * struct.calcsize("P")

if machine in {"x86_64", "i686"}:
machine = {64: "x86_64", 32: "i686"}[bits]
elif machine in {"aarch64", "armv8l"}:
# use armv7l policy for 64-bit arm kernel in 32-bit mode (armv8l)
machine = {64: "aarch64", 32: "armv7l"}[bits]

return Architecture(machine)
242 changes: 160 additions & 82 deletions src/auditwheel/lddtree.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,58 @@
from fnmatch import fnmatch
from pathlib import Path

from elftools.elf.constants import E_FLAGS
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import NoteSection

from .architecture import Architecture
from .libc import Libc, get_libc

log = logging.getLogger(__name__)
__all__ = ["DynamicExecutable", "DynamicLibrary", "ldd"]


@dataclass(frozen=True)
class Platform:
_elf_osabi: str
_elf_class: int
_elf_little_endian: bool
_elf_machine: str
_base_arch: Architecture | None
_ext_arch: Architecture | None
_error_msg: str | None

def is_compatible(self, other: Platform) -> bool:
os_abis = frozenset((self._elf_osabi, other._elf_osabi))
compat_sets = (
frozenset(f"ELFOSABI_{x}" for x in ("NONE", "SYSV", "GNU", "LINUX")),
)
return (
(len(os_abis) == 1 or any(os_abis.issubset(x) for x in compat_sets))
and self._elf_class == other._elf_class
and self._elf_little_endian == other._elf_little_endian
and self._elf_machine == other._elf_machine
)

@property
def baseline_architecture(self) -> Architecture:
if self._base_arch is not None:
return self._base_arch
raise ValueError(self._error_msg)

Check warning on line 62 in src/auditwheel/lddtree.py

View check run for this annotation

Codecov / codecov/patch

src/auditwheel/lddtree.py#L62

Added line #L62 was not covered by tests

@property
def extended_architecture(self) -> Architecture | None:
if self._error_msg is not None:
raise ValueError(self._error_msg)

Check warning on line 67 in src/auditwheel/lddtree.py

View check run for this annotation

Codecov / codecov/patch

src/auditwheel/lddtree.py#L67

Added line #L67 was not covered by tests
return self._ext_arch


@dataclass(frozen=True)
class DynamicLibrary:
soname: str
path: str | None
realpath: str | None
platform: Platform | None = None
needed: frozenset[str] = frozenset()


Expand All @@ -43,12 +82,80 @@ class DynamicExecutable:
interpreter: str | None
path: str
realpath: str
platform: Platform
needed: frozenset[str]
rpath: tuple[str, ...]
runpath: tuple[str, ...]
libraries: dict[str, DynamicLibrary]


def _get_platform(elf: ELFFile) -> Platform:
elf_osabi = elf.header["e_ident"]["EI_OSABI"]
elf_class = elf.elfclass
elf_little_endian = elf.little_endian
elf_machine = elf["e_machine"]
base_arch = {
("EM_386", 32, True): Architecture.i686,
("EM_X86_64", 64, True): Architecture.x86_64,
("EM_PPC64", 64, True): Architecture.ppc64le,
("EM_PPC64", 64, False): Architecture.ppc64,
("EM_RISCV", 64, True): Architecture.riscv64,
("EM_AARCH64", 64, True): Architecture.aarch64,
("EM_S390", 64, False): Architecture.s390x,
("EM_ARM", 32, True): Architecture.armv7l,
("EM_LOONGARCH", 64, True): Architecture.loongarch64,
}.get((elf_machine, elf_class, elf_little_endian), None)
ext_arch: Architecture | None = None
error_msg: str | None = None
flags = elf["e_flags"]
assert base_arch is None or base_arch.baseline == base_arch
if base_arch is None:
error_msg = "Unknown architecture"

Check warning on line 113 in src/auditwheel/lddtree.py

View check run for this annotation

Codecov / codecov/patch

src/auditwheel/lddtree.py#L113

Added line #L113 was not covered by tests
elif base_arch == Architecture.x86_64:
for section in elf.iter_sections():
if not isinstance(section, NoteSection):
continue
for note in section.iter_notes():
if note["n_type"] != "NT_GNU_PROPERTY_TYPE_0":
continue
if note["n_name"] != "GNU":
continue

Check warning on line 122 in src/auditwheel/lddtree.py

View check run for this annotation

Codecov / codecov/patch

src/auditwheel/lddtree.py#L122

Added line #L122 was not covered by tests
for prop in note["n_desc"]:
if prop.pr_type != "GNU_PROPERTY_X86_ISA_1_NEEDED":
continue
if prop.pr_datasz != 4:
continue

Check warning on line 127 in src/auditwheel/lddtree.py

View check run for this annotation

Codecov / codecov/patch

src/auditwheel/lddtree.py#L127

Added line #L127 was not covered by tests
data = prop.pr_data
data -= data & 1 # clear baseline
if data & 8 == 8:
ext_arch = Architecture.x86_64_v4
break

Check warning on line 132 in src/auditwheel/lddtree.py

View check run for this annotation

Codecov / codecov/patch

src/auditwheel/lddtree.py#L131-L132

Added lines #L131 - L132 were not covered by tests
if data & 4 == 4:
ext_arch = Architecture.x86_64_v3
break

Check warning on line 135 in src/auditwheel/lddtree.py

View check run for this annotation

Codecov / codecov/patch

src/auditwheel/lddtree.py#L134-L135

Added lines #L134 - L135 were not covered by tests
if data & 2 == 2:
ext_arch = Architecture.x86_64_v2
break
if data != 0:
error_msg = "unknown x86_64 ISA"

Check warning on line 140 in src/auditwheel/lddtree.py

View check run for this annotation

Codecov / codecov/patch

src/auditwheel/lddtree.py#L140

Added line #L140 was not covered by tests
break
elif base_arch == Architecture.armv7l:
if (flags & E_FLAGS.EF_ARM_EABIMASK) != E_FLAGS.EF_ARM_EABI_VER5:
error_msg = "Invalid ARM EABI version for armv7l"

Check warning on line 144 in src/auditwheel/lddtree.py

View check run for this annotation

Codecov / codecov/patch

src/auditwheel/lddtree.py#L144

Added line #L144 was not covered by tests
elif (flags & E_FLAGS.EF_ARM_ABI_FLOAT_HARD) != E_FLAGS.EF_ARM_ABI_FLOAT_HARD:
error_msg = "armv7l shall use hard-float"

Check warning on line 146 in src/auditwheel/lddtree.py

View check run for this annotation

Codecov / codecov/patch

src/auditwheel/lddtree.py#L146

Added line #L146 was not covered by tests

return Platform(
elf_osabi,
elf_class,
elf_little_endian,
elf_machine,
base_arch,
ext_arch,
error_msg,
)


def normpath(path: str) -> str:
"""Normalize a path
Expand Down Expand Up @@ -243,43 +350,16 @@ def load_ld_paths(root: str = "/", prefix: str = "") -> dict[str, list[str]]:
return ldpaths


def compatible_elfs(elf1: ELFFile, elf2: ELFFile) -> bool:
"""See if two ELFs are compatible
This compares the aspects of the ELF to see if they're compatible:
bit size, endianness, machine type, and operating system.
Parameters
----------
elf1 : ELFFile
elf2 : ELFFile
Returns
-------
True if compatible, False otherwise
"""
osabis = frozenset(e.header["e_ident"]["EI_OSABI"] for e in (elf1, elf2))
compat_sets = (
frozenset(f"ELFOSABI_{x}" for x in ("NONE", "SYSV", "GNU", "LINUX")),
)
return (
(len(osabis) == 1 or any(osabis.issubset(x) for x in compat_sets))
and elf1.elfclass == elf2.elfclass
and elf1.little_endian == elf2.little_endian
and elf1.header["e_machine"] == elf2.header["e_machine"]
)


def find_lib(
elf: ELFFile, lib: str, ldpaths: list[str], root: str = "/"
platform: Platform, lib: str, ldpaths: list[str], root: str = "/"
) -> tuple[str | None, str | None]:
"""Try to locate a ``lib`` that is compatible to ``elf`` in the given
``ldpaths``
Parameters
----------
elf : ELFFile
The elf which the library should be compatible with (ELF wise)
platform : Platform
The platform which the library should be compatible with (ELF wise)
lib : str
The library (basename) to search for
ldpaths : list[str]
Expand All @@ -299,7 +379,7 @@ def find_lib(
if os.path.exists(target):
with open(target, "rb") as f:
libelf = ELFFile(f)
if compatible_elfs(elf, libelf):
if platform.is_compatible(_get_platform(libelf)):
return target, path

return None, None
Expand Down Expand Up @@ -371,7 +451,6 @@ def ldd(

with open(path, "rb") as f:
elf = ELFFile(f)

# If this is the first ELF, extract the interpreter.
if _first:
for segment in elf.iter_segments():
Expand All @@ -391,6 +470,9 @@ def ldd(
log.debug(" ldpaths[interp] = %s", ldpaths["interp"])
break

# get the platform
platform = _get_platform(elf)

# Parse the ELF's dynamic tags.
for segment in elf.iter_segments():
if segment.header.p_type != "PT_DYNAMIC":
Expand All @@ -411,67 +493,63 @@ def ldd(
# probably fine since the runtime ldso does the same.
break

if _first:
# Propagate the rpaths used by the main ELF since those will be
# used at runtime to locate things.
ldpaths["rpath"] = rpaths
ldpaths["runpath"] = runpaths
log.debug(" ldpaths[rpath] = %s", rpaths)
log.debug(" ldpaths[runpath] = %s", runpaths)

# Search for the libs this ELF uses.
all_ldpaths = (
ldpaths["rpath"]
+ rpaths
+ runpaths
+ ldpaths["env"]
+ ldpaths["runpath"]
+ ldpaths["conf"]
+ ldpaths["interp"]
)
for soname in needed:
if soname in _all_libs:
continue
if soname in _excluded_libs:
continue
if any(fnmatch(soname, e) for e in exclude):
log.info("Excluding %s", soname)
_excluded_libs.add(soname)
continue
# TODO we should avoid keeping elf here, related to compat
realpath, fullpath = find_lib(elf, soname, all_ldpaths, root)
if realpath is not None and any(fnmatch(realpath, e) for e in exclude):
log.info("Excluding %s", realpath)
_excluded_libs.add(soname)
continue
_all_libs[soname] = DynamicLibrary(soname, fullpath, realpath)
if realpath is None or fullpath is None:
continue
lret = ldd(
realpath,
root,
prefix,
ldpaths,
display=fullpath,
exclude=exclude,
_all_libs=_all_libs,
)
_all_libs[soname] = DynamicLibrary(
soname, fullpath, realpath, lret.needed
)

del elf

if _first:
# Propagate the rpaths used by the main ELF since those will be
# used at runtime to locate things.
ldpaths["rpath"] = rpaths
ldpaths["runpath"] = runpaths
log.debug(" ldpaths[rpath] = %s", rpaths)
log.debug(" ldpaths[runpath] = %s", runpaths)

# Search for the libs this ELF uses.
all_ldpaths = (
ldpaths["rpath"]
+ rpaths
+ runpaths
+ ldpaths["env"]
+ ldpaths["runpath"]
+ ldpaths["conf"]
+ ldpaths["interp"]
)
for soname in needed:
if soname in _all_libs:
continue
if soname in _excluded_libs:
continue

Check warning on line 520 in src/auditwheel/lddtree.py

View check run for this annotation

Codecov / codecov/patch

src/auditwheel/lddtree.py#L520

Added line #L520 was not covered by tests
if any(fnmatch(soname, e) for e in exclude):
log.info("Excluding %s", soname)
_excluded_libs.add(soname)
continue
realpath, fullpath = find_lib(platform, soname, all_ldpaths, root)
if realpath is not None and any(fnmatch(realpath, e) for e in exclude):
log.info("Excluding %s", realpath)
_excluded_libs.add(soname)
continue
_all_libs[soname] = DynamicLibrary(soname, fullpath, realpath)
if realpath is None or fullpath is None:
continue
dependency = ldd(realpath, root, prefix, ldpaths, fullpath, exclude, _all_libs)
_all_libs[soname] = DynamicLibrary(
soname,
fullpath,
realpath,
dependency.platform,
dependency.needed,
)

if interpreter is not None:
soname = os.path.basename(interpreter)
_all_libs[soname] = DynamicLibrary(
soname, interpreter, readlink(interpreter, root, prefixed=True)
soname, interpreter, readlink(interpreter, root, prefixed=True), platform
)

return DynamicExecutable(
interpreter,
path if display is None else display,
path,
platform,
frozenset(needed - _excluded_libs),
tuple(rpaths),
tuple(runpaths),
Expand Down
Loading

0 comments on commit 4350408

Please sign in to comment.