--- /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]
--- /dev/null
+From 08ef15e57709d2560b570ced9bc309ea2340d736 Mon Sep 17 00:00:00 2001
+From: Christian Marangi <ansuelsmth@gmail.com>
+Date: Mon, 13 Oct 2025 14:00:55 +0200
+Subject: [PATCH 1/2] interpreter: move can_run_host_binaries() to environment
+
+To permit usage of can_run_host_binaries() in other location, move it to
+environment. This will be needed in linker to decide if external library
+should be included in RPATH entry.
+
+Signed-off-by: Christian Marangi <ansuelsmth@gmail.com>
+---
+ mesonbuild/environment.py | 7 +++++++
+ mesonbuild/interpreter/mesonmain.py | 11 ++---------
+ 2 files changed, 9 insertions(+), 9 deletions(-)
+
+--- a/mesonbuild/environment.py
++++ b/mesonbuild/environment.py
+@@ -999,3 +999,10 @@ class Environment:
+
+ def has_exe_wrapper(self) -> bool:
+ return self.exe_wrapper and self.exe_wrapper.found()
++
++ def can_run_host_binaries(self) -> bool:
++ return not (
++ self.is_cross_build() and
++ self.need_exe_wrapper() and
++ self.exe_wrapper is None
++ )
+--- a/mesonbuild/interpreter/mesonmain.py
++++ b/mesonbuild/interpreter/mesonmain.py
+@@ -277,20 +277,13 @@ class MesonMain(MesonInterpreterObject):
+ @noKwargs
+ @FeatureDeprecated('meson.has_exe_wrapper', '0.55.0', 'use meson.can_run_host_binaries instead.')
+ def has_exe_wrapper_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
+- return self._can_run_host_binaries_impl()
++ return self.build.environment.can_run_host_binaries()
+
+ @noPosargs
+ @noKwargs
+ @FeatureNew('meson.can_run_host_binaries', '0.55.0')
+ def can_run_host_binaries_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
+- return self._can_run_host_binaries_impl()
+-
+- def _can_run_host_binaries_impl(self) -> bool:
+- return not (
+- self.build.environment.is_cross_build() and
+- self.build.environment.need_exe_wrapper() and
+- self.build.environment.exe_wrapper is None
+- )
++ return self.build.environment.can_run_host_binaries()
+
+ @noPosargs
+ @noKwargs
--- /dev/null
+From 6d3899390bf75985eb79a106f6a487b335509114 Mon Sep 17 00:00:00 2001
+From: Christian Marangi <ansuelsmth@gmail.com>
+Date: Sun, 12 Oct 2025 13:57:15 +0200
+Subject: [PATCH] linkers: don't include absolue RPATH on cross-compiling
+
+There is currently a reproducible problem when cross-compiling with the
+inclusion of external shared library RPATH entry. Meson normally
+includes RPATH entry to permit the usage of the tool in the build process
+and later removes it on the intall phase. This might be ok and permits
+creating reproducible build to some degree when building on host (as we
+can expect the shared library are always placed on a standard directory
+path and have a consistent RPATH)
+
+This doesn't apply for cross-compilation scenario where the shared
+library might be provided from an arbritrary directory to be later
+packed in the final system (for example a squashfs image)
+
+On top of this on cross-compilation on 99% of the scenario, it's not
+really possible to run the just built tool for build usage as it
+probably target a different arch.
+
+To permit building REAL reproducible binary, add extra logic to skip the
+inclusion of such library path in RPATH if we detect a cross-compilation
+scenario and limit the inclusion of library path in RPATH only to
+relative path (expected to be the ones specific to the building
+binary/internal shared library)
+
+Signed-off-by: Christian Marangi <ansuelsmth@gmail.com>
+---
+ mesonbuild/linkers/linkers.py | 57 ++++++++++++++++++++++++-----------
+ 1 file changed, 40 insertions(+), 17 deletions(-)
+
+--- a/mesonbuild/linkers/linkers.py
++++ b/mesonbuild/linkers/linkers.py
+@@ -523,11 +523,11 @@ class MetrowerksStaticLinkerARM(Metrower
+ class MetrowerksStaticLinkerEmbeddedPowerPC(MetrowerksStaticLinker):
+ id = 'mwldeppc'
+
+-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]:
+ # The rpaths we write must be relative if they point to the build dir,
+ # because otherwise they have different length depending on the build
+ # directory. This breaks reproducible builds.
+- internal_format_rpaths = [evaluate_rpath(p, build_dir, from_dir) for p in raw_rpaths]
++ internal_format_rpaths = [evaluate_rpath(env, p, build_dir, from_dir) for p in raw_rpaths]
+ ordered_rpaths = order_rpaths(internal_format_rpaths)
+ return ordered_rpaths
+
+@@ -544,11 +544,16 @@ def order_rpaths(rpath_list: T.List[str]
+ return sorted(rpath_list, key=os.path.isabs)
+
+
+-def evaluate_rpath(p: str, build_dir: str, from_dir: str) -> str:
++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):
+- return p # These can be outside of build dir.
++ if env.can_run_host_binaries():
++ return p # These can be outside of build dir.
++ # Skip external library if we can't run binaries on host system.
++ # (cross-compilation and no exe_wrapper)
++ else:
++ return ''
+ 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
+ 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)
+ # 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
+ 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(':'):
+- rpath_dirs_to_remove.add(p.encode('utf8'))
++ paths = 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):
++ all_paths.add(p)
++ rpath_dirs_to_remove.add(p.encode('utf8'))
+
+ # TODO: should this actually be "for (dragonfly|open)bsd"?
+ if mesonlib.is_dragonflybsd() or mesonlib.is_openbsd():
+@@ -828,10 +837,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)
+ 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(':')
++ 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):
++ all_paths.add(p)
+ 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
+ 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)
+ 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(':')
++ 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):
++ all_paths.add(p)
+ 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:
+ return ([], set())
+- processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir)
++ processed_rpaths = prepare_rpaths(env, 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(':'):
+- rpath_dirs_to_remove.add(p.encode('utf8'))
++ paths = 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):
++ all_paths.add(p)
++ rpath_dirs_to_remove.add(p.encode('utf8'))
+
+ # 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(':')
++ 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):
++ all_paths.add(p)
+ for p in rpath_paths:
+ all_paths.add(os.path.join(build_dir, p))
+ # We should consider allowing the $LIBPATH environment variable