summaryrefslogtreecommitdiffstats
path: root/tools/meson/patches/100-depfixer-zero-out-rpath-entry-string-on-removing-ent.patch
blob: 4ab9590a0cf77e3b5bd3cc9fab169b7a72e4dfab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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]