diff --git a/tools/meson/patches/100-depfixer-zero-out-rpath-entry-string-on-removing-ent.patch b/tools/meson/patches/100-depfixer-zero-out-rpath-entry-string-on-removing-ent.patch new file mode 100644 index 0000000000..4ab9590a0c --- /dev/null +++ b/tools/meson/patches/100-depfixer-zero-out-rpath-entry-string-on-removing-ent.patch @@ -0,0 +1,161 @@ +From 8586a5eff0c117c627fe3f71003dd30e3785796a Mon Sep 17 00:00:00 2001 +From: Christian Marangi +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 +--- + 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] diff --git a/tools/meson/patches/101-01-interpreter-move-can_run_host_binaries-to-environmen.patch b/tools/meson/patches/101-01-interpreter-move-can_run_host_binaries-to-environmen.patch new file mode 100644 index 0000000000..ad30ef9a1f --- /dev/null +++ b/tools/meson/patches/101-01-interpreter-move-can_run_host_binaries-to-environmen.patch @@ -0,0 +1,53 @@ +From 08ef15e57709d2560b570ced9bc309ea2340d736 Mon Sep 17 00:00:00 2001 +From: Christian Marangi +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 +--- + 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 diff --git a/tools/meson/patches/101-02-linkers-don-t-include-absolue-RPATH-on-cross-compili.patch b/tools/meson/patches/101-02-linkers-don-t-include-absolue-RPATH-on-cross-compili.patch new file mode 100644 index 0000000000..692116ac54 --- /dev/null +++ b/tools/meson/patches/101-02-linkers-don-t-include-absolue-RPATH-on-cross-compili.patch @@ -0,0 +1,167 @@ +From 6d3899390bf75985eb79a106f6a487b335509114 Mon Sep 17 00:00:00 2001 +From: Christian Marangi +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 +--- + 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