depfixer: add RPATH handling for AIX's XCOFF object files.

Ensures install_rpath used in shared library and shared modules targets
is set at install time on AIX.
This commit is contained in:
Aditya Vidyadhar Kamath
2025-01-27 05:45:08 -06:00
committed by Paolo Bonzini
parent 40023e00ea
commit 271e1e50d9
4 changed files with 315 additions and 20 deletions

View File

@@ -144,6 +144,7 @@ class TargetInstallData:
# TODO: install_mode should just always be a FileMode object
install_mode: T.Optional['FileMode']
subproject: str
system: str
optional: bool = False
tag: T.Optional[str] = None
can_strip: bool = False
@@ -1749,7 +1750,8 @@ class Backend:
first_outdir_name,
should_strip, mappings, t.rpath_dirs_to_remove,
t.install_rpath, install_mode, t.subproject,
tag=tag, can_strip=can_strip)
tag=tag, can_strip=can_strip,
system=self.environment.machines[t.for_machine].system)
d.targets.append(i)
for alias, to, tag in t.get_aliases():
@@ -1774,7 +1776,8 @@ class Backend:
implib_install_dir, first_outdir_name,
False, {}, set(), '', install_mode,
t.subproject, optional=isinstance(t, build.SharedModule),
tag='devel')
tag='devel',
system=self.environment.machines[t.for_machine].system)
d.targets.append(i)
if not should_strip and t.get_debug_filename():
@@ -1783,7 +1786,8 @@ class Backend:
first_outdir_name,
False, {}, set(), '',
install_mode, t.subproject,
optional=True, tag='devel')
optional=True, tag='devel',
system=self.environment.machines[t.for_machine].system)
d.targets.append(i)
# Install secondary outputs. Only used for Vala right now.
if num_outdirs > 1:
@@ -1794,7 +1798,8 @@ class Backend:
f = os.path.join(self.get_target_dir(t), output)
i = TargetInstallData(f, outdir, outdir_name, False, {}, set(), None,
install_mode, t.subproject,
tag=tag)
tag=tag,
system=self.environment.machines[t.for_machine].system)
d.targets.append(i)
elif isinstance(t, build.CustomTarget):
# If only one install_dir is specified, assume that all
@@ -1815,7 +1820,8 @@ class Backend:
i = TargetInstallData(f, first_outdir, first_outdir_name,
False, {}, set(), None, install_mode,
t.subproject, optional=not t.build_by_default,
tag=tag)
tag=tag,
system=self.environment.machines[t.for_machine].system)
d.targets.append(i)
else:
for output, outdir, outdir_name, tag in zip(t.get_outputs(), outdirs, install_dir_names, t.install_tag):
@@ -1827,7 +1833,8 @@ class Backend:
i = TargetInstallData(f, outdir, outdir_name,
False, {}, set(), None, install_mode,
t.subproject, optional=not t.build_by_default,
tag=tag)
tag=tag,
system=self.environment.machines[t.for_machine].system)
d.targets.append(i)
def generate_custom_install_script(self, d: InstallData) -> None:

View File

@@ -1698,14 +1698,24 @@ class AIXDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker):
def build_rpath_args(self, build_dir: str, from_dir: str, target: BuildTarget,
extra_paths: T.Optional[T.List[str]] = None
) -> T.Tuple[T.List[str], T.Set[bytes]]:
# Extract rpath information from target
rpath_paths = target.determine_rpath_dirs()
install_rpath = target.install_rpath
build_rpath = target.build_rpath
all_paths: mesonlib.OrderedSet[str] = mesonlib.OrderedSet()
rpath_dirs_to_remove: T.Set[bytes] = set()
# install_rpath first, followed by other paths, and the system path last
if target.install_rpath != '':
all_paths.add(target.install_rpath)
if target.build_rpath != '':
all_paths.add(target.build_rpath)
for p in target.determine_rpath_dirs():
all_paths.add(os.path.join(build_dir, p))
if install_rpath != '':
all_paths.add(install_rpath)
if build_rpath != '':
all_paths.add(build_rpath)
for p in build_rpath.split(':'):
rpath_dirs_to_remove.add(p.encode('utf8'))
for p in rpath_paths:
path = os.path.join(build_dir, p)
all_paths.add(path)
rpath_dirs_to_remove.add(path.encode('utf8'))
# We should consider allowing the $LIBPATH environment variable
# to override sys_path.
sys_path = self.environment.get_compiler_system_lib_dirs(self.for_machine)
@@ -1720,7 +1730,7 @@ class AIXDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker):
all_paths.add(p)
if extra_paths:
all_paths.update(extra_paths)
return (self._apply_prefix('-blibpath:' + ':'.join(all_paths)), set())
return (self._apply_prefix('-blibpath:' + ':'.join(all_paths)), rpath_dirs_to_remove)
def thread_flags(self) -> T.List[str]:
return ['-pthread']

View File

@@ -786,7 +786,7 @@ class Installer:
self.did_install_something = True
try:
self.fix_rpath(outname, t.rpath_dirs_to_remove, install_rpath, final_path,
install_name_mappings, verbose=False)
install_name_mappings, t.system, verbose=False)
except SystemExit as e:
if isinstance(e.code, int) and e.code == 0:
pass

View File

@@ -25,6 +25,277 @@ DT_MIPS_RLD_MAP_REL = 1879048245
# Global cache for tools
INSTALL_NAME_TOOL = False
# -------------------AIX-------------------------
# Reference documents:
# https://www.ibm.com/docs/en/aix/7.1?topic=formats-xcoff-object-file-format
# https://www.ibm.com/docs/en/aix/7.2?topic=formats-ar-file-format-big
# Size of magic number
XCOFF_MAGIC_SIZE = 2
def align(value: int, align_bytes: int) -> int:
# Function used by AIX to align value to align_bytes
align_bytes = 1 << align_bytes
return ((value + align_bytes - 1) // align_bytes) * align_bytes
class XcoffFixedLengthHeader:
# AIX archive fixed length header (struct fl_hdr)
FL_HDR_SIZE = 128
FL_HDR_FORMAT = '8s 20s 20s 20s 20s 20s 20s'
def __init__(self, file: T.BinaryIO) -> None:
header_data = file.read(self.FL_HDR_SIZE)
fl_magic, fl_memoff, fl_gstoff, fl_gst64off, fl_fstmoff, fl_lstmoff, fl_freeoff = struct.unpack(
self.FL_HDR_FORMAT, header_data
)
self.fl_magic = fl_magic
self.fl_fstmoff = int(fl_fstmoff)
class XcoffArchiveHeader:
# AIX archive header (struct ar_hdr)
AR_HDR_SIZE = 114
AR_HDR_FORMAT = '20s 20s 20s 12s 12s 12s 12s 4s 2s'
def __init__(self, file: T.BinaryIO) -> None:
header_data = file.read(self.AR_HDR_SIZE)
ar_size, ar_nxtmem, ar_prvmem, ar_date, ar_uid, ar_gid, ar_mode, ar_namlen, _ar_name = struct.unpack(
self.AR_HDR_FORMAT, header_data
)
ar_namlen_int = int(ar_namlen.strip())
# The Magic number always starts at the even byte boundary
self.ar_namlen = ar_namlen_int if ar_namlen_int % 2 == 0 else ar_namlen_int + 1
# Seek past the archive member name
file.seek(self.ar_namlen, os.SEEK_CUR)
class XcoffCompositeFileHeader:
# XCOFF composite file header
CFH_HDR_SIZE = 24
CFH_HDR_SIZE_32 = 20
CFH_HDR_FORMAT = '2s 2s 4s 8s 2s 2s 4s'
CFH_HDR_FORMAT_32 = '2s 2s 4s 4s 4s 2s 2s'
def __init__(self, file: T.BinaryIO, magic: int) -> None:
if magic == 0x01DF:
cfh_header_data = file.read(self.CFH_HDR_SIZE_32)
f_magic, f_nscns, f_timdat, f_symptr, f_nsyms, f_opthdr, f_flags = struct.unpack(
self.CFH_HDR_FORMAT_32, cfh_header_data
)
else:
cfh_header_data = file.read(self.CFH_HDR_SIZE)
f_magic, f_nscns, f_timdat, f_symptr, f_opthdr, f_flags, f_nsyms = struct.unpack(
self.CFH_HDR_FORMAT, cfh_header_data
)
self.f_flags = int.from_bytes(f_flags, byteorder='big')
self.f_opthdr = int.from_bytes(f_opthdr, byteorder='big')
class XcoffAuxiliaryHeader:
# XCOFF auxiliary header
AUX_HDR_SIZE = 120
AUX_HDR_SIZE_32 = 72
AUX_HDR_FORMAT = '2s 2s 4s 8s 8s 8s 2s 2s 2s 2s 2s 2s 2s 2s 2s 1s 1s 1s 1s 1s 1s 8s 8s 8s 8s 8s 8s 2s 2s 2s 10s'
AUX_HDR_FORMAT_32 = '2s 2s 4s 4s 4s 4s 4s 4s 4s 2s 2s 2s 2s 2s 2s 2s 2s 2s 1s 1s 4s 4s 4s 1s 1s 1s 1s 2s 2s'
def __init__(self, file: T.BinaryIO, magic: int, opthdr_size: int) -> None:
if magic == 0x01DF:
if opthdr_size < self.AUX_HDR_SIZE_32:
sys.exit(f'Error: Auxiliary header size {opthdr_size} is too small for 32-bit XCOFF')
aux_header_data = file.read(self.AUX_HDR_SIZE_32)
if len(aux_header_data) < self.AUX_HDR_SIZE_32:
sys.exit(f'Error: Could not read auxiliary header, got {len(aux_header_data)} bytes, expected {self.AUX_HDR_SIZE_32}')
(o_mflags, o_vstamp, o_tsize, o_dsize, o_bsize, o_entry, o_text_start, o_data_start, o_toc,
o_snentry, o_sntext, o_sndata, o_sntoc, o_snloader, o_snbss, o_algntext, o_algndata, o_modtype,
o_cpuflag, o_cputype, o_maxstack, o_maxdata, o_debugger, o_textpsize, o_datapsize, o_stackpsize,
o_flags, o_sntdata, o_sntbss) = struct.unpack(self.AUX_HDR_FORMAT_32, aux_header_data)
else:
if opthdr_size < self.AUX_HDR_SIZE:
sys.exit(f'Error: Auxiliary header size {opthdr_size} is too small for 64-bit XCOFF')
aux_header_data = file.read(self.AUX_HDR_SIZE)
if len(aux_header_data) < self.AUX_HDR_SIZE:
sys.exit(f'Error: Could not read auxiliary header, got {len(aux_header_data)} bytes, expected {self.AUX_HDR_SIZE}')
(o_mflags, o_vstamp, o_debugger, o_text_start, o_data_start, o_toc, o_snentry, o_sntext, o_sndata,
o_sntoc, o_snloader, o_snbss, o_algntext, o_algndata, o_modtype, o_cpuflag, o_cputype, o_textpsize,
o_datapsize, o_stackpsize, o_flags, o_tsize, o_dsize, o_bsize, o_entry, o_maxstack, o_maxdata,
o_sntdata, o_sntbss, o_x64flags, dummy) = struct.unpack(self.AUX_HDR_FORMAT, aux_header_data)
self.o_algntext = int.from_bytes(o_algntext, byteorder='big')
self.o_algndata = int.from_bytes(o_algndata, byteorder='big')
self.o_snloader = int.from_bytes(o_snloader, byteorder='big')
class XcoffSectionHeader:
# XCOFF section header
SCN_HDR_SIZE = 72
SCN_HDR_SIZE_32 = 40
SCN_HDR_FORMAT = '8s 8s 8s 8s 8s 8s 8s 4s 4s 4s 4s'
SCN_HDR_FORMAT_32 = '8s 4s 4s 4s 4s 4s 4s 2s 2s 2s 2s'
def __init__(self, file: T.BinaryIO, magic: int) -> None:
if magic == 0x01DF:
scn_header_data = file.read(self.SCN_HDR_SIZE_32)
if len(scn_header_data) < self.SCN_HDR_SIZE_32:
raise EOFError('End of archive member reached')
(s_name, s_paddr, s_vaddr, s_size, s_scnptr, s_relptr, s_lnnoptr, s_nreloc, s_nlnno, s_flags,
dummy) = struct.unpack(self.SCN_HDR_FORMAT_32, scn_header_data)
else:
scn_header_data = file.read(self.SCN_HDR_SIZE)
if len(scn_header_data) < self.SCN_HDR_SIZE:
raise EOFError('End of archive member reached')
(s_name, s_paddr, s_vaddr, s_size, s_scnptr, s_relptr, s_lnnoptr, s_nreloc, s_nlnno, s_flags,
dummy) = struct.unpack(self.SCN_HDR_FORMAT, scn_header_data)
self.s_scnptr = int.from_bytes(s_scnptr, byteorder='big')
class XcoffLoaderHeader:
# XCOFF loader header
LDR_HDR_SIZE = 56
LDR_HDR_SIZE_32 = 32
LDR_HDR_FORMAT = '4s 4s 4s 4s 4s 4s 8s 8s 8s 8s'
LDR_HDR_FORMAT_32 = '4s 4s 4s 4s 4s 4s 4s 4s'
def __init__(self, file: T.BinaryIO, magic: int) -> None:
if magic == 0x01DF:
ldr_header_data = file.read(self.LDR_HDR_SIZE_32)
(l_version, l_nsyms, l_nreloc, l_istlen, l_nimpid, l_impoff, l_stlen,
l_stoff) = struct.unpack(self.LDR_HDR_FORMAT_32, ldr_header_data)
else:
ldr_header_data = file.read(self.LDR_HDR_SIZE)
(l_version, l_nsyms, l_nreloc, l_istlen, l_nimpid, l_stlen, l_impoff, l_stoff, l_symoff,
l_rldoff) = struct.unpack(self.LDR_HDR_FORMAT, ldr_header_data)
self.l_impoff = int.from_bytes(l_impoff, byteorder='big')
self.l_istlen = int.from_bytes(l_istlen, byteorder='big')
def traverse_xcoff(file: T.BinaryIO, rpath_dirs_to_remove: T.Set[bytes], new_rpath: T.Optional[bytes], verbose: bool = True) -> None:
"""Traverse XCOFF file or archive and modify Libpath entries.
Handles both standalone XCOFF shared objects (.so) and XCOFF archives (.a).
Archives are detected by magic number and processed member by member.
"""
def dummy_print(*args: object) -> None:
pass
log_msg: T.Callable[..., None]
log_msg = print if verbose else dummy_print # type: ignore[assignment]
# Detect if this is an archive by checking for magic number
magic_check = file.read(8)
file.seek(0)
# Read archive headers if this is an archive
ar_header: T.Optional[XcoffArchiveHeader] = None
if magic_check == b'<bigaf>\n':
fl_header = XcoffFixedLengthHeader(file)
file.seek(fl_header.fl_fstmoff)
ar_header = XcoffArchiveHeader(file)
composite_header_pos = file.tell()
# Read XCOFF magic number
magic_data = file.read(XCOFF_MAGIC_SIZE)
if len(magic_data) != XCOFF_MAGIC_SIZE:
sys.exit(0)
magic_number = struct.unpack('>H', magic_data)[0] # Big-endian 16-bit integer
if magic_number == 0x01DF:
log_msg(' Changing Libpath for a 32-bit Shared Object')
elif magic_number == 0x01F7:
log_msg(' Changing Libpath for a 64-bit Shared Object')
else:
log_msg(' Not a shared object')
sys.exit(0)
# Reposition to start of XCOFF headers
file.seek(composite_header_pos)
cfh_header = XcoffCompositeFileHeader(file, magic_number)
if not cfh_header.f_flags & 0x2000:
log_msg('Did not change rpath since not a shared library/archive')
return
# Read auxiliary header
aux_header = XcoffAuxiliaryHeader(file, magic_number, cfh_header.f_opthdr)
# Calculate the bytes_to_align
bytes_to_align = max(aux_header.o_algntext, aux_header.o_algndata)
# Seek to section header and read it
if magic_number == 0x01DF:
file.seek((aux_header.o_snloader - 1) * XcoffSectionHeader.SCN_HDR_SIZE_32, os.SEEK_CUR)
else:
file.seek((aux_header.o_snloader - 1) * XcoffSectionHeader.SCN_HDR_SIZE, os.SEEK_CUR)
try:
scn_header = XcoffSectionHeader(file, magic_number)
except EOFError:
return # End of archive member, nothing to fix
header_len = 0
if ar_header:
header_len = XcoffFixedLengthHeader.FL_HDR_SIZE + XcoffArchiveHeader.AR_HDR_SIZE + ar_header.ar_namlen
scnptrFromArchiveStart = scn_header.s_scnptr + align(header_len, bytes_to_align)
else:
scnptrFromArchiveStart = scn_header.s_scnptr
file.seek(scnptrFromArchiveStart)
# Read loader header
ldr_header = XcoffLoaderHeader(file, magic_number)
if ar_header:
scnptrFromArchiveStartPlusOff = scn_header.s_scnptr + ldr_header.l_impoff + align(header_len, bytes_to_align)
else:
scnptrFromArchiveStartPlusOff = scn_header.s_scnptr + ldr_header.l_impoff
file.seek(scnptrFromArchiveStartPlusOff)
# Read and update libpath
libpath = file.read(ldr_header.l_istlen)
build_libpath = libpath.split(b'\x00')[0]
# Build the new rpath by merging new_rpath with existing paths (excluding removed ones)
new_rpaths: OrderedSet[bytes] = OrderedSet()
if new_rpath:
new_rpaths.update(new_rpath.split(b':'))
new_rpaths.update(
path
for path in build_libpath.split(b':')
if path and path not in rpath_dirs_to_remove
)
install_rpath = b':'.join(new_rpaths)
# If the length of the build_rpath length is < install_rpath length then we do not have space to write
# the same hence exit. Otherwise, we can and the length has to be the same as build_rpath.
# If the install_rpath length is less than build_rpath length then pad the difference
if len(install_rpath) > len(build_libpath):
sys.exit('Error: install_rpath is bigger than build_rpath')
# Pad with colon bytes to match build_libpath length
install_rpath = install_rpath + (b':' * (len(build_libpath) - len(install_rpath)))
file.seek(scnptrFromArchiveStartPlusOff)
file.write(install_rpath)
log_msg(f'Successfully changed libpath from {build_libpath!r} to {install_rpath!r}')
def fix_aix(fname: str, rpath_dirs_to_remove: T.Set[bytes], new_rpath: T.Optional[bytes], verbose: bool = True) -> None:
"""Writes Libpath to an xcoff shared object.
In AIX, shared modules are .so files and shared libraries are in .a archives.
Follows the same calling convention as fix_elf:
- Errors: sys.exit('error message')
- Wrong file type: sys.exit(0)
- Success: return normally
"""
if new_rpath is None:
return
try:
with open(fname, 'r+b') as file:
traverse_xcoff(file, rpath_dirs_to_remove, new_rpath, verbose)
except FileNotFoundError:
sys.exit(f'Error: File not found: {fname}')
except Exception as e:
sys.exit(f'Unexpected error processing {fname}: {e}')
# ----------------ELF------------------
class DataSizes:
def __init__(self, ptrsize: int, is_le: bool) -> None:
if is_le:
@@ -379,7 +650,7 @@ class Elf(DataSizes):
new_rpath = b':'.join(new_rpaths)
if len(old_rpath) < len(new_rpath):
msg = "New rpath must not be longer than the old one.\n Old: {}\n New: {}".format(old_rpath.decode('utf-8'), new_rpath.decode('utf-8'))
msg = 'New rpath must not be longer than the old one.\n Old: {}\n New: {}'.format(old_rpath.decode('utf-8'), new_rpath.decode('utf-8'))
sys.exit(msg)
# The linker does read-only string deduplication. If there is a
# string that shares a suffix with the rpath, they might get
@@ -534,20 +805,27 @@ def fix_jar(fname: str) -> None:
# than the beginning, but the spec doesn't forbid that.
subprocess.check_call(['jar', 'ufM', fname, 'META-INF/MANIFEST.MF'])
def fix_rpath(fname: str, rpath_dirs_to_remove: T.Set[bytes], new_rpath: T.Union[str, bytes], final_path: str, install_name_mappings: T.Dict[str, str], verbose: bool = True) -> None:
def fix_rpath(fname: str, rpath_dirs_to_remove: T.Set[bytes], new_rpath: T.Union[str, bytes], final_path: str, install_name_mappings: T.Dict[str, str], system: str, verbose: bool = True) -> None:
global INSTALL_NAME_TOOL # pylint: disable=global-statement
# Static libraries, import libraries, debug information, headers, etc
# never have rpaths
# DLLs and EXE currently do not need runtime path fixing
if fname.endswith(('.a', '.lib', '.pdb', '.h', '.hpp', '.dll', '.exe')):
if fname.endswith(('.lib', '.pdb', '.h', '.hpp', '.dll', '.exe')):
return
# Shared libraries are .a files on AIX
if fname.endswith('.a') and system != 'aix':
return
if isinstance(new_rpath, str):
new_rpath = new_rpath.encode('utf8')
try:
if fname.endswith('.jar'):
fix_jar(fname)
return
if isinstance(new_rpath, str):
new_rpath = new_rpath.encode('utf8')
fix_elf(fname, rpath_dirs_to_remove, new_rpath, verbose)
if system == 'aix':
fix_aix(fname, rpath_dirs_to_remove, new_rpath, verbose)
else:
fix_elf(fname, rpath_dirs_to_remove, new_rpath, verbose)
return
except SystemExit as e:
if isinstance(e.code, int) and e.code == 0: