+++ /dev/null
-From 8586a5eff0c117c627fe3f71003dd30e3785796a Mon Sep 17 00:00:00 2001
-From: Christian Marangi <ansuelsmth@gmail.com>
-Date: Sat, 11 Oct 2025 01:48:51 +0200
-Subject: [PATCH] depfixer: zero-out rpath entry string on removing entry
-
-While investigating a reproducible problem with a binary compiled with
-Meson, it was notice that the RPATH entry was never removed.
-
-By comparing the binary from 2 different build system it was observed
-that altough the RPATH entry was removed (verified by the readelf -d
-command) the actual path was still present causing 2 different binary.
-
-Going deeper in the Meson build process, it was discovered that
-remove_rpath_entry only deletes the entry in the '.dynamic' section but
-never actually 'clean' (or better say zero-out) the path from the
-.dynstr section producing binary dependendt of the build system.
-
-To address this, introduce a new helper to to zero-out the entry from
-the .dynstr section permitting to produce REAL reproducible binary.
-
-Additional logic was needed to handle GCC linker optimization for dynstr
-table where the rpath string might be reused for other dym function
-string. The setion that is actually removed is filled with 'X' following
-patchelf behaviour.
-
-Signed-off-by: Christian Marangi <ansuelsmth@gmail.com>
----
- mesonbuild/scripts/depfixer.py | 79 ++++++++++++++++++++++++++++++++++
- 1 file changed, 79 insertions(+)
-
---- a/mesonbuild/scripts/depfixer.py
-+++ b/mesonbuild/scripts/depfixer.py
-@@ -31,8 +31,12 @@ class DataSizes:
- p = '<'
- else:
- p = '>'
-+ self.Char = p + 'c'
-+ self.CharSize = 1
- self.Half = p + 'h'
- self.HalfSize = 2
-+ self.Section = p + 'h'
-+ self.SectionSize = 2
- self.Word = p + 'I'
- self.WordSize = 4
- self.Sword = p + 'i'
-@@ -71,6 +75,24 @@ class DynamicEntry(DataSizes):
- ofile.write(struct.pack(self.Sword, self.d_tag))
- ofile.write(struct.pack(self.Word, self.val))
-
-+class DynsymEntry(DataSizes):
-+ def __init__(self, ifile: T.BinaryIO, ptrsize: int, is_le: bool) -> None:
-+ super().__init__(ptrsize, is_le)
-+ is_64 = ptrsize == 64
-+ self.st_name = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
-+ if is_64:
-+ self.st_info = struct.unpack(self.Char, ifile.read(self.CharSize))[0]
-+ self.st_other = struct.unpack(self.Char, ifile.read(self.CharSize))[0]
-+ self.st_shndx = struct.unpack(self.Section, ifile.read(self.SectionSize))[0]
-+ self.st_value = struct.unpack(self.Addr, ifile.read(self.AddrSize))[0]
-+ self.st_size = struct.unpack(self.XWord, ifile.read(self.XWordSize))[0]
-+ else:
-+ self.st_value = struct.unpack(self.Addr, ifile.read(self.AddrSize))[0]
-+ self.st_size = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
-+ self.st_info = struct.unpack(self.Char, ifile.read(self.CharSize))[0]
-+ self.st_other = struct.unpack(self.Char, ifile.read(self.CharSize))[0]
-+ self.st_shndx = struct.unpack(self.Section, ifile.read(self.SectionSize))[0]
-+
- class SectionHeader(DataSizes):
- def __init__(self, ifile: T.BinaryIO, ptrsize: int, is_le: bool) -> None:
- super().__init__(ptrsize, is_le)
-@@ -115,6 +137,8 @@ class Elf(DataSizes):
- self.verbose = verbose
- self.sections: T.List[SectionHeader] = []
- self.dynamic: T.List[DynamicEntry] = []
-+ self.dynsym: T.List[DynsymEntry] = []
-+ self.dynsym_strings: T.List[str] = []
- self.open_bf(bfile)
- try:
- (self.ptrsize, self.is_le) = self.detect_elf_type()
-@@ -122,6 +146,8 @@ class Elf(DataSizes):
- self.parse_header()
- self.parse_sections()
- self.parse_dynamic()
-+ self.parse_dynsym()
-+ self.parse_dynsym_strings()
- except (struct.error, RuntimeError):
- self.close_bf()
- raise
-@@ -232,6 +258,23 @@ class Elf(DataSizes):
- if e.d_tag == 0:
- break
-
-+ def parse_dynsym(self) -> None:
-+ sec = self.find_section(b'.dynsym')
-+ if sec is None:
-+ return
-+ self.bf.seek(sec.sh_offset)
-+ for i in range(sec.sh_size // sec.sh_entsize):
-+ e = DynsymEntry(self.bf, self.ptrsize, self.is_le)
-+ self.dynsym.append(e)
-+
-+ def parse_dynsym_strings(self) -> None:
-+ sec = self.find_section(b'.dynstr')
-+ if sec is None:
-+ return
-+ for i in self.dynsym:
-+ self.bf.seek(sec.sh_offset + i.st_name)
-+ self.dynsym_strings.append(self.read_str().decode())
-+
- @generate_list
- def get_section_names(self) -> T.Generator[str, None, None]:
- section_names = self.sections[self.e_shstrndx]
-@@ -353,12 +396,48 @@ class Elf(DataSizes):
- self.bf.write(new_rpath)
- self.bf.write(b'\0')
-
-+ def clean_rpath_entry_string(self, entrynum: int) -> None:
-+ # Get the rpath string
-+ offset = self.get_entry_offset(entrynum)
-+ self.bf.seek(offset)
-+ rpath_string = self.read_str().decode()
-+ reused_str = ''
-+
-+ # Inspect the dyn strings and check if our rpath string
-+ # ends with one of them.
-+ # This is to handle a subtle optimization of the linker
-+ # where one of the dyn function name offset in the dynstr
-+ # table might be set at the an offset of the rpath string.
-+ # Example:
-+ #
-+ # rpath offset = 1314 string = /usr/lib/foo
-+ # dym function offset = 1322 string = foo
-+ #
-+ # In the following case, the dym function string offset is
-+ # placed at the offset +10 of the rpath.
-+ # To correctly clear the rpath entry AND keep normal
-+ # functionality of this optimization (and the binary),
-+ # parse the maximum string we can remove from the rpath entry.
-+ #
-+ # Since strings MUST be null terminated, we can always check
-+ # if the rpath string ends with the dyn function string and
-+ # calculate what we can actually remove accordingly.
-+ for dynsym_string in self.dynsym_strings:
-+ if rpath_string.endswith(dynsym_string):
-+ if len(dynsym_string) > len(reused_str):
-+ reused_str = dynsym_string
-+
-+ # Seek back to start of string
-+ self.bf.seek(offset)
-+ self.bf.write(b'X' * (len(rpath_string) - len(reused_str)))
-+
- def remove_rpath_entry(self, entrynum: int) -> None:
- sec = self.find_section(b'.dynamic')
- if sec is None:
- return None
- for (i, entry) in enumerate(self.dynamic):
- if entry.d_tag == entrynum:
-+ self.clean_rpath_entry_string(entrynum)
- rpentry = self.dynamic[i]
- rpentry.d_tag = 0
- self.dynamic = self.dynamic[:i] + self.dynamic[i + 1:] + [rpentry]
--- a/mesonbuild/linkers/linkers.py
+++ b/mesonbuild/linkers/linkers.py
-@@ -523,11 +523,11 @@ class MetrowerksStaticLinkerARM(Metrower
- class MetrowerksStaticLinkerEmbeddedPowerPC(MetrowerksStaticLinker):
- id = 'mwldeppc'
+@@ -588,11 +588,11 @@ class EmxomfArLinker(ArLinker):
+ def get_std_link_args(self, env: 'Environment', is_thin: bool) -> T.List[str]:
+ return ['cr']
-def prepare_rpaths(raw_rpaths: T.Tuple[str, ...], build_dir: str, from_dir: str) -> T.List[str]:
+def prepare_rpaths(env: Environment, raw_rpaths: T.Tuple[str, ...], build_dir: str, from_dir: str) -> T.List[str]:
ordered_rpaths = order_rpaths(internal_format_rpaths)
return ordered_rpaths
-@@ -544,11 +544,16 @@ def order_rpaths(rpath_list: T.List[str]
+@@ -609,11 +609,16 @@ def order_rpaths(rpath_list: T.List[str]
return sorted(rpath_list, key=os.path.isabs)
+def evaluate_rpath(env: Environment, p: str, build_dir: str, from_dir: str) -> str:
if p == from_dir:
return '' # relpath errors out in this case
- elif os.path.isabs(p):
+ elif path_has_root(p):
- return p # These can be outside of build dir.
+ if env.can_run_host_binaries():
+ return p # These can be outside of build dir.
else:
return os.path.relpath(os.path.join(build_dir, p), os.path.join(build_dir, from_dir))
-@@ -673,7 +678,7 @@ class GnuLikeDynamicLinkerMixin(DynamicL
+@@ -752,7 +757,7 @@ class GnuLikeDynamicLinkerMixin(DynamicL
return ([], set())
args: T.List[str] = []
origin_placeholder = '$ORIGIN'
- processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir)
-+ processed_rpaths = prepare_rpaths(env, rpath_paths, build_dir, from_dir)
++ processed_rpaths = prepare_rpaths(self.environment, rpath_paths, build_dir, from_dir)
# Need to deduplicate rpaths, as macOS's install_name_tool
# is *very* allergic to duplicate -delete_rpath arguments
# when calling depfixer on installation.
-@@ -683,9 +688,13 @@ class GnuLikeDynamicLinkerMixin(DynamicL
+@@ -762,9 +767,13 @@ class GnuLikeDynamicLinkerMixin(DynamicL
rpath_dirs_to_remove.add(p.encode('utf8'))
# Build_rpath is used as-is (it is usually absolute).
- if build_rpath != '':
-- all_paths.add(build_rpath)
-- for p in build_rpath.split(':'):
+ if target.build_rpath != '':
+- all_paths.add(target.build_rpath)
+- for p in target.build_rpath.split(':'):
- rpath_dirs_to_remove.add(p.encode('utf8'))
-+ paths = build_rpath.split(':')
++ paths = target.build_rpath.split(':')
+ for p in paths:
+ # Only include relative paths if we can't run binaries on host system.
+ # (cross-compilation and no exe_wrapper)
-+ if env.can_run_host_binaries() or not os.path.isabs(p):
++ if self.environment.can_run_host_binaries() or not os.path.isabs(p):
+ all_paths.add(p)
+ rpath_dirs_to_remove.add(p.encode('utf8'))
+ if extra_paths:
+ all_paths.update(extra_paths)
- # TODO: should this actually be "for (dragonfly|open)bsd"?
- if mesonlib.is_dragonflybsd() or mesonlib.is_openbsd():
-@@ -828,10 +837,15 @@ class AppleDynamicLinker(PosixDynamicLin
+@@ -937,10 +946,15 @@ class AppleDynamicLinker(PosixDynamicLin
# @loader_path is the equivalent of $ORIGIN on macOS
# https://stackoverflow.com/q/26280738
origin_placeholder = '@loader_path'
- processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir)
-+ processed_rpaths = prepare_rpaths(env, rpath_paths, build_dir, from_dir)
++ processed_rpaths = prepare_rpaths(self.environment, rpath_paths, build_dir, from_dir)
all_paths = mesonlib.OrderedSet([os.path.join(origin_placeholder, p) for p in processed_rpaths])
- if build_rpath != '':
-- all_paths.update(build_rpath.split(':'))
-+ paths = build_rpath.split(':')
+ if target.build_rpath != '':
+- all_paths.update(target.build_rpath.split(':'))
++ paths = target.build_rpath.split(':')
+ for p in paths:
+ # Only include relative paths if we can't run binaries on host system.
+ # (cross-compilation and no exe_wrapper)
-+ if env.can_run_host_binaries() or not os.path.isabs(p):
++ if self.environment.can_run_host_binaries() or not os.path.isabs(p):
+ all_paths.add(p)
+ if extra_paths:
+ all_paths.update(extra_paths)
for rp in all_paths:
- rpath_dirs_to_remove.add(rp.encode('utf8'))
- args.extend(self._apply_prefix('-rpath,' + rp))
-@@ -1200,10 +1214,15 @@ class NAGDynamicLinker(PosixDynamicLinke
+@@ -1356,10 +1370,15 @@ class NAGDynamicLinker(PosixDynamicLinke
return ([], set())
args: T.List[str] = []
origin_placeholder = '$ORIGIN'
- processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir)
-+ processed_rpaths = prepare_rpaths(env, rpath_paths, build_dir, from_dir)
++ processed_rpaths = prepare_rpaths(self.environment, rpath_paths, build_dir, from_dir)
all_paths = mesonlib.OrderedSet([os.path.join(origin_placeholder, p) for p in processed_rpaths])
- if build_rpath != '':
-- all_paths.add(build_rpath)
-+ paths = build_rpath.split(':')
+ if target.build_rpath != '':
+- all_paths.add(target.build_rpath)
++ paths = target.build_rpath.split(':')
+ for p in paths:
+ # Only include relative paths if we can't run binaries on host system.
+ # (cross-compilation and no exe_wrapper)
-+ if env.can_run_host_binaries() or not os.path.isabs(p):
++ if self.environment.can_run_host_binaries() or not os.path.isabs(p):
+ all_paths.add(p)
+ if extra_paths:
+ all_paths.update(extra_paths)
for rp in all_paths:
- args.extend(self._apply_prefix('-Wl,-Wl,,-rpath,,' + rp))
-
-@@ -1454,15 +1473,19 @@ class SolarisDynamicLinker(PosixDynamicL
- install_rpath: str) -> T.Tuple[T.List[str], T.Set[bytes]]:
- if not rpath_paths and not install_rpath and not build_rpath:
+@@ -1627,15 +1646,19 @@ class SolarisDynamicLinker(PosixDynamicL
+ rpath_paths = target.determine_rpath_dirs()
+ if not rpath_paths and not target.install_rpath and not target.build_rpath and not extra_paths:
return ([], set())
- processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir)
-+ processed_rpaths = prepare_rpaths(env, rpath_paths, build_dir, from_dir)
++ processed_rpaths = prepare_rpaths(self.environment, rpath_paths, build_dir, from_dir)
all_paths = mesonlib.OrderedSet([os.path.join('$ORIGIN', p) for p in processed_rpaths])
rpath_dirs_to_remove: T.Set[bytes] = set()
for p in all_paths:
rpath_dirs_to_remove.add(p.encode('utf8'))
- if build_rpath != '':
-- all_paths.add(build_rpath)
-- for p in build_rpath.split(':'):
+ if target.build_rpath != '':
+- all_paths.add(target.build_rpath)
+- for p in target.build_rpath.split(':'):
- rpath_dirs_to_remove.add(p.encode('utf8'))
-+ paths = build_rpath.split(':')
++ paths = target.build_rpath.split(':')
+ for p in paths:
+ # Only include relative paths if we can't run binaries on host system.
+ # (cross-compilation and no exe_wrapper)
-+ if env.can_run_host_binaries() or not os.path.isabs(p):
++ if self.environment.can_run_host_binaries() or not os.path.isabs(p):
+ all_paths.add(p)
+ rpath_dirs_to_remove.add(p.encode('utf8'))
+ if extra_paths:
+ all_paths.update(extra_paths)
- # In order to avoid relinking for RPATH removal, the binary needs to contain just
- # enough space in the ELF header to hold the final installation RPATH.
-@@ -1525,7 +1548,12 @@ class AIXDynamicLinker(PosixDynamicLinke
- if install_rpath != '':
- all_paths.add(install_rpath)
- if build_rpath != '':
-- all_paths.add(build_rpath)
-+ paths = build_rpath.split(':')
+@@ -1703,7 +1726,12 @@ class AIXDynamicLinker(PosixDynamicLinke
+ if target.install_rpath != '':
+ all_paths.add(target.install_rpath)
+ if target.build_rpath != '':
+- all_paths.add(target.build_rpath)
++ paths = target.build_rpath.split(':')
+ for p in paths:
+ # Only include relative paths if we can't run binaries on host system.
+ # (cross-compilation and no exe_wrapper)
-+ if env.can_run_host_binaries() or not os.path.isabs(p):
++ if self.environment.can_run_host_binaries() or not os.path.isabs(p):
+ all_paths.add(p)
- for p in rpath_paths:
+ for p in target.determine_rpath_dirs():
all_paths.add(os.path.join(build_dir, p))
# We should consider allowing the $LIBPATH environment variable