1#!/usr/bin/env python3 2# Copyright 2017 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Checks the number of static initializers in an APK's library.""" 7 8 9import argparse 10import os 11import re 12import subprocess 13import sys 14import tempfile 15import zipfile 16 17from util import build_utils 18 19_DUMP_STATIC_INITIALIZERS_PATH = os.path.join(build_utils.DIR_SOURCE_ROOT, 20 'tools', 'linux', 21 'dump-static-initializers.py') 22 23 24def _RunReadelf(so_path, options, tool_prefix=''): 25 return subprocess.check_output( 26 [tool_prefix + 'readobj', '--elf-output-style=GNU'] + options + 27 [so_path]).decode('utf8') 28 29 30def _ParseLibBuildId(so_path, tool_prefix): 31 """Returns the Build ID of the given native library.""" 32 stdout = _RunReadelf(so_path, ['-n'], tool_prefix) 33 match = re.search(r'Build ID: (\w+)', stdout) 34 return match.group(1) if match else None 35 36 37def _VerifyLibBuildIdsMatch(tool_prefix, *so_files): 38 if len(set(_ParseLibBuildId(f, tool_prefix) for f in so_files)) > 1: 39 raise Exception('Found differing build ids in output directory and apk. ' 40 'Your output directory is likely stale.') 41 42 43def _DumpStaticInitializers(apk_so_name, unzipped_so, out_dir, tool_prefix): 44 so_with_symbols_path = os.path.join(out_dir, 'lib.unstripped', 45 os.path.basename(apk_so_name)) 46 if not os.path.exists(so_with_symbols_path): 47 raise Exception('Unstripped .so not found. Looked here: %s' % 48 so_with_symbols_path) 49 _VerifyLibBuildIdsMatch(tool_prefix, unzipped_so, so_with_symbols_path) 50 subprocess.check_call([_DUMP_STATIC_INITIALIZERS_PATH, so_with_symbols_path]) 51 52 53def _ReadInitArray(so_path, tool_prefix, expect_no_initializers): 54 stdout = _RunReadelf(so_path, ['-SW'], tool_prefix) 55 # Matches: .init_array INIT_ARRAY 000000000516add0 5169dd0 000010 00 WA 0 0 8 56 match = re.search(r'\.init_array.*$', stdout, re.MULTILINE) 57 if expect_no_initializers: 58 if match: 59 raise Exception( 60 'Expected no initializers for %s, yet some were found' % so_path) 61 return 0 62 if not match: 63 raise Exception('Did not find section: .init_array in {}:\n{}'.format( 64 so_path, stdout)) 65 size_str = re.split(r'\W+', match.group(0))[5] 66 return int(size_str, 16) 67 68 69def _CountStaticInitializers(so_path, tool_prefix, expect_no_initializers): 70 # Find the number of files with at least one static initializer. 71 # First determine if we're 32 or 64 bit 72 stdout = _RunReadelf(so_path, ['-h'], tool_prefix) 73 elf_class_line = re.search('Class:.*$', stdout, re.MULTILINE).group(0) 74 elf_class = re.split(r'\W+', elf_class_line)[1] 75 if elf_class == 'ELF32': 76 word_size = 4 77 else: 78 word_size = 8 79 80 # Then find the number of files with global static initializers. 81 # NOTE: this is very implementation-specific and makes assumptions 82 # about how compiler and linker implement global static initializers. 83 init_array_size = _ReadInitArray(so_path, tool_prefix, expect_no_initializers) 84 assert init_array_size % word_size == 0 85 return init_array_size // word_size 86 87 88def _AnalyzeStaticInitializers(apk_or_aab, tool_prefix, dump_sis, out_dir, 89 ignored_libs, no_initializers_libs): 90 with zipfile.ZipFile(apk_or_aab) as z: 91 so_files = [ 92 f for f in z.infolist() if f.filename.endswith('.so') 93 and f.file_size > 0 and os.path.basename(f.filename) not in ignored_libs 94 ] 95 # Skip checking static initializers for secondary abi libs. They will be 96 # checked by 32-bit bots. This avoids the complexity of finding 32 bit .so 97 # files in the output directory in 64 bit builds. 98 has_64 = any('64' in f.filename for f in so_files) 99 files_to_check = [f for f in so_files if not has_64 or '64' in f.filename] 100 101 # Do not check partitioned libs. They have no ".init_array" section since 102 # all SIs are considered "roots" by the linker, and so end up in the base 103 # module. 104 files_to_check = [ 105 f for f in files_to_check if not f.filename.endswith('_partition.so') 106 ] 107 108 si_count = 0 109 for f in files_to_check: 110 lib_basename = os.path.basename(f.filename) 111 expect_no_initializers = lib_basename in no_initializers_libs 112 with tempfile.NamedTemporaryFile(prefix=lib_basename) as temp: 113 temp.write(z.read(f)) 114 temp.flush() 115 si_count += _CountStaticInitializers(temp.name, tool_prefix, 116 expect_no_initializers) 117 if dump_sis: 118 _DumpStaticInitializers(f.filename, temp.name, out_dir, tool_prefix) 119 return si_count 120 121 122def main(): 123 parser = argparse.ArgumentParser() 124 parser.add_argument('--touch', help='File to touch upon success') 125 parser.add_argument('--tool-prefix', required=True, 126 help='Prefix for nm and friends') 127 parser.add_argument('--expected-count', required=True, type=int, 128 help='Fail if number of static initializers is not ' 129 'equal to this value.') 130 parser.add_argument('apk_or_aab', help='Path to .apk or .aab file.') 131 args = parser.parse_args() 132 133 # TODO(crbug.com/838414): add support for files included via loadable_modules. 134 ignored_libs = { 135 'libarcore_sdk_c.so', 'libcrashpad_handler_trampoline.so', 136 'libsketchology_native.so' 137 } 138 # The chromium linker doesn't have static initializers, which makes the 139 # regular check throw. It should not have any. 140 no_initializers_libs = ['libchromium_android_linker.so'] 141 142 si_count = _AnalyzeStaticInitializers(args.apk_or_aab, args.tool_prefix, 143 False, '.', ignored_libs, 144 no_initializers_libs) 145 if si_count != args.expected_count: 146 print('Expected {} static initializers, but found {}.'.format( 147 args.expected_count, si_count)) 148 if args.expected_count > si_count: 149 print('You have removed one or more static initializers. Thanks!') 150 print('To fix the build, update the expectation in:') 151 print(' //chrome/android/static_initializers.gni') 152 print() 153 154 print('Dumping static initializers via dump-static-initializers.py:') 155 sys.stdout.flush() 156 _AnalyzeStaticInitializers(args.apk_or_aab, args.tool_prefix, True, '.', 157 ignored_libs, no_initializers_libs) 158 print() 159 print('For more information:') 160 print(' https://chromium.googlesource.com/chromium/src/+/main/docs/' 161 'static_initializers.md') 162 sys.exit(1) 163 164 if args.touch: 165 open(args.touch, 'w') 166 167 168if __name__ == '__main__': 169 main() 170