1# Copyright 2013-2016 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
16import sys, struct
17import shutil, subprocess
18
19from ..mesonlib import OrderedSet
20
21SHT_STRTAB = 3
22DT_NEEDED = 1
23DT_RPATH = 15
24DT_RUNPATH = 29
25DT_STRTAB = 5
26DT_SONAME = 14
27DT_MIPS_RLD_MAP_REL = 1879048245
28
29# Global cache for tools
30INSTALL_NAME_TOOL = False
31
32class DataSizes:
33    def __init__(self, ptrsize, is_le):
34        if is_le:
35            p = '<'
36        else:
37            p = '>'
38        self.Half = p + 'h'
39        self.HalfSize = 2
40        self.Word = p + 'I'
41        self.WordSize = 4
42        self.Sword = p + 'i'
43        self.SwordSize = 4
44        if ptrsize == 64:
45            self.Addr = p + 'Q'
46            self.AddrSize = 8
47            self.Off = p + 'Q'
48            self.OffSize = 8
49            self.XWord = p + 'Q'
50            self.XWordSize = 8
51            self.Sxword = p + 'q'
52            self.SxwordSize = 8
53        else:
54            self.Addr = p + 'I'
55            self.AddrSize = 4
56            self.Off = p + 'I'
57            self.OffSize = 4
58
59class DynamicEntry(DataSizes):
60    def __init__(self, ifile, ptrsize, is_le):
61        super().__init__(ptrsize, is_le)
62        self.ptrsize = ptrsize
63        if ptrsize == 64:
64            self.d_tag = struct.unpack(self.Sxword, ifile.read(self.SxwordSize))[0]
65            self.val = struct.unpack(self.XWord, ifile.read(self.XWordSize))[0]
66        else:
67            self.d_tag = struct.unpack(self.Sword, ifile.read(self.SwordSize))[0]
68            self.val = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
69
70    def write(self, ofile):
71        if self.ptrsize == 64:
72            ofile.write(struct.pack(self.Sxword, self.d_tag))
73            ofile.write(struct.pack(self.XWord, self.val))
74        else:
75            ofile.write(struct.pack(self.Sword, self.d_tag))
76            ofile.write(struct.pack(self.Word, self.val))
77
78class SectionHeader(DataSizes):
79    def __init__(self, ifile, ptrsize, is_le):
80        super().__init__(ptrsize, is_le)
81        if ptrsize == 64:
82            is_64 = True
83        else:
84            is_64 = False
85# Elf64_Word
86        self.sh_name = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
87# Elf64_Word
88        self.sh_type = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
89# Elf64_Xword
90        if is_64:
91            self.sh_flags = struct.unpack(self.XWord, ifile.read(self.XWordSize))[0]
92        else:
93            self.sh_flags = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
94# Elf64_Addr
95        self.sh_addr = struct.unpack(self.Addr, ifile.read(self.AddrSize))[0]
96# Elf64_Off
97        self.sh_offset = struct.unpack(self.Off, ifile.read(self.OffSize))[0]
98# Elf64_Xword
99        if is_64:
100            self.sh_size = struct.unpack(self.XWord, ifile.read(self.XWordSize))[0]
101        else:
102            self.sh_size = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
103# Elf64_Word
104        self.sh_link = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
105# Elf64_Word
106        self.sh_info = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
107# Elf64_Xword
108        if is_64:
109            self.sh_addralign = struct.unpack(self.XWord, ifile.read(self.XWordSize))[0]
110        else:
111            self.sh_addralign = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
112# Elf64_Xword
113        if is_64:
114            self.sh_entsize = struct.unpack(self.XWord, ifile.read(self.XWordSize))[0]
115        else:
116            self.sh_entsize = struct.unpack(self.Word, ifile.read(self.WordSize))[0]
117
118class Elf(DataSizes):
119    def __init__(self, bfile, verbose=True):
120        self.bfile = bfile
121        self.verbose = verbose
122        self.bf = open(bfile, 'r+b')
123        try:
124            (self.ptrsize, self.is_le) = self.detect_elf_type()
125            super().__init__(self.ptrsize, self.is_le)
126            self.parse_header()
127            self.parse_sections()
128            self.parse_dynamic()
129        except (struct.error, RuntimeError):
130            self.bf.close()
131            raise
132
133    def __enter__(self):
134        return self
135
136    def __del__(self):
137        if self.bf:
138            self.bf.close()
139
140    def __exit__(self, exc_type, exc_value, traceback):
141        self.bf.close()
142        self.bf = None
143
144    def detect_elf_type(self):
145        data = self.bf.read(6)
146        if data[1:4] != b'ELF':
147            # This script gets called to non-elf targets too
148            # so just ignore them.
149            if self.verbose:
150                print('File "%s" is not an ELF file.' % self.bfile)
151            sys.exit(0)
152        if data[4] == 1:
153            ptrsize = 32
154        elif data[4] == 2:
155            ptrsize = 64
156        else:
157            sys.exit('File "%s" has unknown ELF class.' % self.bfile)
158        if data[5] == 1:
159            is_le = True
160        elif data[5] == 2:
161            is_le = False
162        else:
163            sys.exit('File "%s" has unknown ELF endianness.' % self.bfile)
164        return ptrsize, is_le
165
166    def parse_header(self):
167        self.bf.seek(0)
168        self.e_ident = struct.unpack('16s', self.bf.read(16))[0]
169        self.e_type = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
170        self.e_machine = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
171        self.e_version = struct.unpack(self.Word, self.bf.read(self.WordSize))[0]
172        self.e_entry = struct.unpack(self.Addr, self.bf.read(self.AddrSize))[0]
173        self.e_phoff = struct.unpack(self.Off, self.bf.read(self.OffSize))[0]
174        self.e_shoff = struct.unpack(self.Off, self.bf.read(self.OffSize))[0]
175        self.e_flags = struct.unpack(self.Word, self.bf.read(self.WordSize))[0]
176        self.e_ehsize = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
177        self.e_phentsize = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
178        self.e_phnum = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
179        self.e_shentsize = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
180        self.e_shnum = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
181        self.e_shstrndx = struct.unpack(self.Half, self.bf.read(self.HalfSize))[0]
182
183    def parse_sections(self):
184        self.bf.seek(self.e_shoff)
185        self.sections = []
186        for _ in range(self.e_shnum):
187            self.sections.append(SectionHeader(self.bf, self.ptrsize, self.is_le))
188
189    def read_str(self):
190        arr = []
191        x = self.bf.read(1)
192        while x != b'\0':
193            arr.append(x)
194            x = self.bf.read(1)
195            if x == b'':
196                raise RuntimeError('Tried to read past the end of the file')
197        return b''.join(arr)
198
199    def find_section(self, target_name):
200        section_names = self.sections[self.e_shstrndx]
201        for i in self.sections:
202            self.bf.seek(section_names.sh_offset + i.sh_name)
203            name = self.read_str()
204            if name == target_name:
205                return i
206
207    def parse_dynamic(self):
208        sec = self.find_section(b'.dynamic')
209        self.dynamic = []
210        if sec is None:
211            return
212        self.bf.seek(sec.sh_offset)
213        while True:
214            e = DynamicEntry(self.bf, self.ptrsize, self.is_le)
215            self.dynamic.append(e)
216            if e.d_tag == 0:
217                break
218
219    def print_section_names(self):
220        section_names = self.sections[self.e_shstrndx]
221        for i in self.sections:
222            self.bf.seek(section_names.sh_offset + i.sh_name)
223            name = self.read_str()
224            print(name.decode())
225
226    def print_soname(self):
227        soname = None
228        strtab = None
229        for i in self.dynamic:
230            if i.d_tag == DT_SONAME:
231                soname = i
232            if i.d_tag == DT_STRTAB:
233                strtab = i
234        if soname is None or strtab is None:
235            print("This file does not have a soname")
236            return
237        self.bf.seek(strtab.val + soname.val)
238        print(self.read_str())
239
240    def get_entry_offset(self, entrynum):
241        sec = self.find_section(b'.dynstr')
242        for i in self.dynamic:
243            if i.d_tag == entrynum:
244                return sec.sh_offset + i.val
245        return None
246
247    def print_rpath(self):
248        offset = self.get_entry_offset(DT_RPATH)
249        if offset is None:
250            print("This file does not have an rpath.")
251        else:
252            self.bf.seek(offset)
253            print(self.read_str())
254
255    def print_runpath(self):
256        offset = self.get_entry_offset(DT_RUNPATH)
257        if offset is None:
258            print("This file does not have a runpath.")
259        else:
260            self.bf.seek(offset)
261            print(self.read_str())
262
263    def print_deps(self):
264        sec = self.find_section(b'.dynstr')
265        deps = []
266        for i in self.dynamic:
267            if i.d_tag == DT_NEEDED:
268                deps.append(i)
269        for i in deps:
270            offset = sec.sh_offset + i.val
271            self.bf.seek(offset)
272            name = self.read_str()
273            print(name)
274
275    def fix_deps(self, prefix):
276        sec = self.find_section(b'.dynstr')
277        deps = []
278        for i in self.dynamic:
279            if i.d_tag == DT_NEEDED:
280                deps.append(i)
281        for i in deps:
282            offset = sec.sh_offset + i.val
283            self.bf.seek(offset)
284            name = self.read_str()
285            if name.startswith(prefix):
286                basename = name.split(b'/')[-1]
287                padding = b'\0' * (len(name) - len(basename))
288                newname = basename + padding
289                assert(len(newname) == len(name))
290                self.bf.seek(offset)
291                self.bf.write(newname)
292
293    def fix_rpath(self, rpath_dirs_to_remove, new_rpath):
294        # The path to search for can be either rpath or runpath.
295        # Fix both of them to be sure.
296        self.fix_rpathtype_entry(rpath_dirs_to_remove, new_rpath, DT_RPATH)
297        self.fix_rpathtype_entry(rpath_dirs_to_remove, new_rpath, DT_RUNPATH)
298
299    def fix_rpathtype_entry(self, rpath_dirs_to_remove, new_rpath, entrynum):
300        if isinstance(new_rpath, str):
301            new_rpath = new_rpath.encode('utf8')
302        rp_off = self.get_entry_offset(entrynum)
303        if rp_off is None:
304            if self.verbose:
305                print('File does not have rpath. It should be a fully static executable.')
306            return
307        self.bf.seek(rp_off)
308
309        old_rpath = self.read_str()
310        # Some rpath entries may come from multiple sources.
311        # Only add each one once.
312        new_rpaths = OrderedSet()
313        if new_rpath:
314            new_rpaths.add(new_rpath)
315        if old_rpath:
316            # Filter out build-only rpath entries
317            # added by get_link_dep_subdirs() or
318            # specified by user with build_rpath.
319            for rpath_dir in old_rpath.split(b':'):
320                if not (rpath_dir in rpath_dirs_to_remove or
321                        rpath_dir == (b'X' * len(rpath_dir))):
322                    if rpath_dir:
323                        new_rpaths.add(rpath_dir)
324
325        # Prepend user-specified new entries while preserving the ones that came from pkgconfig etc.
326        new_rpath = b':'.join(new_rpaths)
327
328        if len(old_rpath) < len(new_rpath):
329            msg = "New rpath must not be longer than the old one.\n Old: {}\n New: {}".format(old_rpath, new_rpath)
330            sys.exit(msg)
331        # The linker does read-only string deduplication. If there is a
332        # string that shares a suffix with the rpath, they might get
333        # dedupped. This means changing the rpath string might break something
334        # completely unrelated. This has already happened once with X.org.
335        # Thus we want to keep this change as small as possible to minimize
336        # the chance of obliterating other strings. It might still happen
337        # but our behavior is identical to what chrpath does and it has
338        # been in use for ages so based on that this should be rare.
339        if not new_rpath:
340            self.remove_rpath_entry(entrynum)
341        else:
342            self.bf.seek(rp_off)
343            self.bf.write(new_rpath)
344            self.bf.write(b'\0')
345
346    def remove_rpath_entry(self, entrynum):
347        sec = self.find_section(b'.dynamic')
348        if sec is None:
349            return None
350        for (i, entry) in enumerate(self.dynamic):
351            if entry.d_tag == entrynum:
352                rpentry = self.dynamic[i]
353                rpentry.d_tag = 0
354                self.dynamic = self.dynamic[:i] + self.dynamic[i + 1:] + [rpentry]
355                break
356        # DT_MIPS_RLD_MAP_REL is relative to the offset of the tag. Adjust it consequently.
357        for entry in self.dynamic[i:]:
358            if entry.d_tag == DT_MIPS_RLD_MAP_REL:
359                entry.val += 2 * (self.ptrsize // 8)
360                break
361        self.bf.seek(sec.sh_offset)
362        for entry in self.dynamic:
363            entry.write(self.bf)
364        return None
365
366def fix_elf(fname, rpath_dirs_to_remove, new_rpath, verbose=True):
367    with Elf(fname, verbose) as e:
368        if new_rpath is None:
369            e.print_rpath()
370            e.print_runpath()
371        else:
372            e.fix_rpath(rpath_dirs_to_remove, new_rpath)
373
374def get_darwin_rpaths_to_remove(fname):
375    out = subprocess.check_output(['otool', '-l', fname],
376                                  universal_newlines=True,
377                                  stderr=subprocess.DEVNULL)
378    result = []
379    current_cmd = 'FOOBAR'
380    for line in out.split('\n'):
381        line = line.strip()
382        if ' ' not in line:
383            continue
384        key, value = line.strip().split(' ', 1)
385        if key == 'cmd':
386            current_cmd = value
387        if key == 'path' and current_cmd == 'LC_RPATH':
388            rp = value.split('(', 1)[0].strip()
389            result.append(rp)
390    return result
391
392def fix_darwin(fname, new_rpath, final_path, install_name_mappings):
393    try:
394        rpaths = get_darwin_rpaths_to_remove(fname)
395    except subprocess.CalledProcessError:
396        # Otool failed, which happens when invoked on a
397        # non-executable target. Just return.
398        return
399    try:
400        args = []
401        if rpaths:
402            # TODO: fix this properly, not totally clear how
403            #
404            # removing rpaths from binaries on macOS has tons of
405            # weird edge cases. For instance, if the user provided
406            # a '-Wl,-rpath' argument in LDFLAGS that happens to
407            # coincide with an rpath generated from a dependency,
408            # this would cause installation failures, as meson would
409            # generate install_name_tool calls with two identical
410            # '-delete_rpath' arguments, which install_name_tool
411            # fails on. Because meson itself ensures that it never
412            # adds duplicate rpaths, duplicate rpaths necessarily
413            # come from user variables. The idea of using OrderedSet
414            # is to remove *at most one* duplicate RPATH entry. This
415            # is not optimal, as it only respects the user's choice
416            # partially: if they provided a non-duplicate '-Wl,-rpath'
417            # argument, it gets removed, if they provided a duplicate
418            # one, it remains in the final binary. A potentially optimal
419            # solution would split all user '-Wl,-rpath' arguments from
420            # LDFLAGS, and later add them back with '-add_rpath'.
421            for rp in OrderedSet(rpaths):
422                args += ['-delete_rpath', rp]
423            subprocess.check_call(['install_name_tool', fname] + args,
424                                  stdout=subprocess.DEVNULL,
425                                  stderr=subprocess.DEVNULL)
426        args = []
427        if new_rpath:
428            args += ['-add_rpath', new_rpath]
429        # Rewrite -install_name @rpath/libfoo.dylib to /path/to/libfoo.dylib
430        if fname.endswith('dylib'):
431            args += ['-id', final_path]
432        if install_name_mappings:
433            for old, new in install_name_mappings.items():
434                args += ['-change', old, new]
435        if args:
436            subprocess.check_call(['install_name_tool', fname] + args,
437                                  stdout=subprocess.DEVNULL,
438                                  stderr=subprocess.DEVNULL)
439    except Exception as err:
440        raise SystemExit(err)
441
442def fix_jar(fname):
443    subprocess.check_call(['jar', 'xfv', fname, 'META-INF/MANIFEST.MF'])
444    with open('META-INF/MANIFEST.MF', 'r+') as f:
445        lines = f.readlines()
446        f.seek(0)
447        for line in lines:
448            if not line.startswith('Class-Path:'):
449                f.write(line)
450        f.truncate()
451    subprocess.check_call(['jar', 'ufm', fname, 'META-INF/MANIFEST.MF'])
452
453def fix_rpath(fname, rpath_dirs_to_remove, new_rpath, final_path, install_name_mappings, verbose=True):
454    global INSTALL_NAME_TOOL
455    # Static libraries, import libraries, debug information, headers, etc
456    # never have rpaths
457    # DLLs and EXE currently do not need runtime path fixing
458    if fname.endswith(('.a', '.lib', '.pdb', '.h', '.hpp', '.dll', '.exe')):
459        return
460    try:
461        if fname.endswith('.jar'):
462            fix_jar(fname)
463            return
464        fix_elf(fname, rpath_dirs_to_remove, new_rpath, verbose)
465        return
466    except SystemExit as e:
467        if isinstance(e.code, int) and e.code == 0:
468            pass
469        else:
470            raise
471    # We don't look for this on import because it will do a useless PATH lookup
472    # on non-mac platforms. That can be expensive on some Windows machines
473    # (upto 30ms), which is significant with --only-changed. For details, see:
474    # https://github.com/mesonbuild/meson/pull/6612#discussion_r378581401
475    if INSTALL_NAME_TOOL is False:
476        INSTALL_NAME_TOOL = shutil.which('install_name_tool')
477    if INSTALL_NAME_TOOL:
478        fix_darwin(fname, new_rpath, final_path, install_name_mappings)
479