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