mirror of
				https://github.com/openwrt/openwrt.git
				synced 2025-10-30 07:49:23 +08:00 
			
		
		
		
	tools/meson: add pending patch to improve binary reproducibility
Add 3 pending patch that improve binary reproducibility. The first address a problem with RPATH string not getting cleared on removal of RPATH entry from ELF section. The other 2 skip including external shared library in RPATH in meson build phase. This follows the logic that on cross-compiling we can't run the binary anyway as it does target a different arch hence it doesn't make sense to include those extra path in RPATH causing reproducibility problems (as path for those external library will depend on the build system path) Link: https://github.com/openwrt/openwrt/pull/20389 Signed-off-by: Christian Marangi <ansuelsmth@gmail.com>
This commit is contained in:
		| @ -0,0 +1,161 @@ | ||||
| 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] | ||||
| @ -0,0 +1,53 @@ | ||||
| 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 | ||||
| @ -0,0 +1,167 @@ | ||||
| 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 | ||||
		Reference in New Issue
	
	Block a user
	 Christian Marangi
					Christian Marangi