1# Copyright (C) 2020 The Android Open Source Project 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# Licensed under the Apache License, Version 2.0 (the "License"); 16# you may not use this file except in compliance with the License. 17# You may obtain a copy of the License at 18# 19# http://www.apache.org/licenses/LICENSE-2.0 20# 21# Unless required by applicable law or agreed to in writing, software 22# distributed under the License is distributed on an "AS IS" BASIS, 23# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24# See the License for the specific language governing permissions and 25# limitations under the License. 26"""Repacking tool for Shared Libs APEX testing.""" 27 28import argparse 29import hashlib 30import logging 31import os 32import shutil 33import subprocess 34import sys 35import tempfile 36from zipfile import ZipFile 37 38import apex_build_info_pb2 39import apex_manifest_pb2 40 41logger = logging.getLogger(__name__) 42 43def comma_separated_list(arg): 44 return arg.split(',') 45 46 47def parse_args(argv): 48 parser = argparse.ArgumentParser( 49 description='Repacking tool for Shared Libs APEX testing') 50 51 parser.add_argument('--input', required=True, help='Input file') 52 parser.add_argument('--output', required=True, help='Output file') 53 parser.add_argument( 54 '--key', required=True, help='Path to the private avb key file') 55 parser.add_argument( 56 '--pk8key', 57 required=True, 58 help='Path to the private apk key file in pk8 format') 59 parser.add_argument( 60 '--pubkey', required=True, help='Path to the public avb key file') 61 parser.add_argument( 62 '--tmpdir', required=True, help='Temporary directory to use') 63 parser.add_argument( 64 '--x509key', 65 required=True, 66 help='Path to the public apk key file in x509 format') 67 parser.add_argument( 68 '--mode', default='strip', choices=['strip', 'sharedlibs']) 69 parser.add_argument( 70 '--libs', 71 default='libc++.so,libsharedlibtest.so', 72 type=comma_separated_list, 73 help='Libraries to strip/repack. Expects comma separated values.') 74 return parser.parse_args(argv) 75 76 77def run(args, verbose=None, **kwargs): 78 """Creates and returns a subprocess.Popen object. 79 80 Args: 81 args: The command represented as a list of strings. 82 verbose: Whether the commands should be shown. Default to the global 83 verbosity if unspecified. 84 kwargs: Any additional args to be passed to subprocess.Popen(), such as env, 85 stdin, etc. stdout and stderr will default to subprocess.PIPE and 86 subprocess.STDOUT respectively unless caller specifies any of them. 87 universal_newlines will default to True, as most of the users in 88 releasetools expect string output. 89 90 Returns: 91 A subprocess.Popen object. 92 """ 93 if 'stdout' not in kwargs and 'stderr' not in kwargs: 94 kwargs['stdout'] = subprocess.PIPE 95 kwargs['stderr'] = subprocess.STDOUT 96 if 'universal_newlines' not in kwargs: 97 kwargs['universal_newlines'] = True 98 if verbose: 99 logger.info(' Running: \"%s\"', ' '.join(args)) 100 return subprocess.Popen(args, **kwargs) 101 102 103def run_and_check_output(args, verbose=None, **kwargs): 104 """Runs the given command and returns the output. 105 106 Args: 107 args: The command represented as a list of strings. 108 verbose: Whether the commands should be shown. Default to the global 109 verbosity if unspecified. 110 kwargs: Any additional args to be passed to subprocess.Popen(), such as env, 111 stdin, etc. stdout and stderr will default to subprocess.PIPE and 112 subprocess.STDOUT respectively unless caller specifies any of them. 113 114 Returns: 115 The output string. 116 117 Raises: 118 ExternalError: On non-zero exit from the command. 119 """ 120 proc = run(args, verbose=verbose, **kwargs) 121 output, _ = proc.communicate() 122 if output is None: 123 output = '' 124 # Don't log any if caller explicitly says so. 125 if verbose: 126 logger.info('%s', output.rstrip()) 127 if proc.returncode != 0: 128 raise RuntimeError( 129 'Failed to run command \'{}\' (exit code {}):\n{}'.format( 130 args, proc.returncode, output)) 131 return output 132 133 134def get_container_files(apex_file_path, tmpdir): 135 dir_name = tempfile.mkdtemp(prefix='container_files_', dir=tmpdir) 136 with ZipFile(apex_file_path, 'r') as zip_obj: 137 zip_obj.extractall(path=dir_name) 138 files = {} 139 for i in [ 140 'apex_manifest.json', 'apex_manifest.pb', 'apex_build_info.pb', 'assets', 141 'apex_payload.img', 'apex_payload.zip' 142 ]: 143 file_path = os.path.join(dir_name, i) 144 if os.path.exists(file_path): 145 files[i] = file_path 146 147 image_file = files.get('apex_payload.img') 148 if image_file is None: 149 image_file = files.get('apex_payload.zip') 150 151 files['apex_payload'] = image_file 152 153 return files 154 155 156def extract_payload_from_img(img_file_path, tmpdir): 157 dir_name = tempfile.mkdtemp(prefix='extracted_payload_', dir=tmpdir) 158 cmd = [ 159 _get_host_tools_path('debugfs_static'), '-R', 160 'rdump ./ %s' % dir_name, img_file_path 161 ] 162 run_and_check_output(cmd) 163 164 # Remove payload files added by apexer and e2fs tools. 165 for i in ['apex_manifest.json', 'apex_manifest.pb']: 166 if os.path.exists(os.path.join(dir_name, i)): 167 os.remove(os.path.join(dir_name, i)) 168 if os.path.isdir(os.path.join(dir_name, 'lost+found')): 169 shutil.rmtree(os.path.join(dir_name, 'lost+found')) 170 return dir_name 171 172 173def run_apexer(container_files, payload_dir, key_path, pubkey_path, tmpdir): 174 apexer_cmd = _get_host_tools_path('apexer') 175 cmd = [ 176 apexer_cmd, '--force', '--include_build_info', '--do_not_check_keyname' 177 ] 178 cmd.extend([ 179 '--apexer_tool_path', 180 os.path.dirname(apexer_cmd) + ':prebuilts/sdk/tools/linux/bin' 181 ]) 182 cmd.extend(['--manifest', container_files['apex_manifest.pb']]) 183 if 'apex_manifest.json' in container_files: 184 cmd.extend(['--manifest_json', container_files['apex_manifest.json']]) 185 cmd.extend(['--build_info', container_files['apex_build_info.pb']]) 186 if 'assets' in container_files: 187 cmd.extend(['--assets_dir', container_files['assets']]) 188 cmd.extend(['--key', key_path]) 189 cmd.extend(['--pubkey', pubkey_path]) 190 191 # Decide on output file name 192 apex_suffix = '.apex.unsigned' 193 fd, fn = tempfile.mkstemp(prefix='repacked_', suffix=apex_suffix, dir=tmpdir) 194 os.close(fd) 195 cmd.extend([payload_dir, fn]) 196 197 run_and_check_output(cmd) 198 return fn 199 200 201def _get_java_toolchain(): 202 java_toolchain = 'java' 203 if os.path.isfile('prebuilts/jdk/jdk11/linux-x86/bin/java'): 204 java_toolchain = 'prebuilts/jdk/jdk11/linux-x86/bin/java' 205 206 java_dep_lib = ( 207 os.path.join(os.path.dirname(_get_host_tools_path()), 'lib64') + ':' + 208 os.path.join(os.path.dirname(_get_host_tools_path()), 'lib')) 209 210 return [java_toolchain, java_dep_lib] 211 212 213def _get_host_tools_path(tool_name=None): 214 # This script is located at e.g. 215 # out/soong/host/linux-x86/bin/shared_libs_repack/shared_libs_repack.py. 216 # Find the host tools dir by going up two directories. 217 dirname = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 218 if tool_name: 219 return os.path.join(dirname, tool_name) 220 return dirname 221 222 223def sign_apk_container(unsigned_apex, x509key_path, pk8key_path, tmpdir): 224 fd, fn = tempfile.mkstemp(prefix='repacked_', suffix='.apex', dir=tmpdir) 225 os.close(fd) 226 java_toolchain, java_dep_lib = _get_java_toolchain() 227 228 cmd = [ 229 java_toolchain, '-Djava.library.path=' + java_dep_lib, '-jar', 230 os.path.join( 231 os.path.dirname(_get_host_tools_path()), 'framework', 'signapk.jar'), 232 '-a', '4096', x509key_path, pk8key_path, unsigned_apex, fn 233 ] 234 run_and_check_output(cmd) 235 return fn 236 237 238def compute_sha512(file_path): 239 block_size = 65536 240 hashbuf = hashlib.sha512() 241 with open(file_path, 'rb') as f: 242 fb = f.read(block_size) 243 while len(fb) > 0: 244 hashbuf.update(fb) 245 fb = f.read(block_size) 246 return hashbuf.hexdigest() 247 248 249def parse_fs_config(fs_config): 250 configs = fs_config.splitlines() 251 # Result is set of configurations. 252 # Each configuration is set of items as [file path, uid, gid, mode]. 253 # All items are stored as string. 254 result = [] 255 for config in configs: 256 result.append(config.split()) 257 return result 258 259 260def config_to_str(configs): 261 result = '' 262 for config in configs: 263 result += ' '.join(config) + '\n' 264 return result 265 266 267def _extract_lib_or_lib64(payload_dir, lib_full_path): 268 # Figure out if this is lib or lib64: 269 # Strip out the payload_dir and split by / 270 libpath = lib_full_path[len(payload_dir):].lstrip('/').split('/') 271 return libpath[0] 272 273 274def main(argv): 275 args = parse_args(argv) 276 apex_file_path = args.input 277 278 container_files = get_container_files(apex_file_path, args.tmpdir) 279 payload_dir = extract_payload_from_img(container_files['apex_payload.img'], 280 args.tmpdir) 281 libs = args.libs 282 assert len(libs)> 0 283 284 lib_paths = [os.path.join(payload_dir, lib_dir, lib) 285 for lib_dir in ['lib', 'lib64'] 286 for lib in libs 287 if os.path.exists(os.path.join(payload_dir, lib_dir, lib))] 288 289 assert len(lib_paths) > 0 290 291 lib_paths_hashes = [(lib, compute_sha512(lib)) for lib in lib_paths] 292 293 if args.mode == 'strip': 294 # Stripping mode. Add a reference to the version of libc++.so to the 295 # requireSharedApexLibs entry in the manifest, and remove lib64/libc++.so 296 # from the payload. 297 pb = apex_manifest_pb2.ApexManifest() 298 with open(container_files['apex_manifest.pb'], 'rb') as f: 299 pb.ParseFromString(f.read()) 300 for lib_path_hash in lib_paths_hashes: 301 basename = os.path.basename(lib_path_hash[0]) 302 libpath = _extract_lib_or_lib64(payload_dir, lib_path_hash[0]) 303 assert libpath in ('lib', 'lib64') 304 pb.requireSharedApexLibs.append(os.path.join(libpath, basename) + ':' 305 + lib_path_hash[1]) 306 # Replace existing library with symlink 307 symlink_dst = os.path.join('/', 'apex', 'sharedlibs', 308 libpath, basename, lib_path_hash[1], 309 basename) 310 os.remove(lib_path_hash[0]) 311 os.system('ln -s {0} {1}'.format(symlink_dst, lib_path_hash[0])) 312 # 313 # Example of resulting manifest: 314 # --- 315 # name: "com.android.apex.test.foo" 316 # version: 1 317 # requireNativeLibs: "libc.so" 318 # requireNativeLibs: "libdl.so" 319 # requireNativeLibs: "libm.so" 320 # requireSharedApexLibs: "lib/libc++.so:23c5dd..." 321 # requireSharedApexLibs: "lib/libsharedlibtest.so:870f38..." 322 # requireSharedApexLibs: "lib64/libc++.so:72a584..." 323 # requireSharedApexLibs: "lib64/libsharedlibtest.so:109015..." 324 # -- 325 # To print uncomment the following: 326 # from google.protobuf import text_format 327 # print(text_format.MessageToString(pb)) 328 with open(container_files['apex_manifest.pb'], 'wb') as f: 329 f.write(pb.SerializeToString()) 330 331 if args.mode == 'sharedlibs': 332 # Sharedlibs mode. Mark in the APEX manifest that this package contains 333 # shared libraries. 334 pb = apex_manifest_pb2.ApexManifest() 335 with open(container_files['apex_manifest.pb'], 'rb') as f: 336 pb.ParseFromString(f.read()) 337 del pb.requireNativeLibs[:] 338 pb.provideSharedApexLibs = True 339 with open(container_files['apex_manifest.pb'], 'wb') as f: 340 f.write(pb.SerializeToString()) 341 342 pb = apex_build_info_pb2.ApexBuildInfo() 343 with open(container_files['apex_build_info.pb'], 'rb') as f: 344 pb.ParseFromString(f.read()) 345 346 canned_fs_config = parse_fs_config(pb.canned_fs_config.decode('utf-8')) 347 348 # Remove the bin directory from payload dir and from the canned_fs_config. 349 shutil.rmtree(os.path.join(payload_dir, 'bin')) 350 canned_fs_config = [config for config in canned_fs_config 351 if not config[0].startswith('/bin')] 352 353 # Remove from the canned_fs_config the entries we are about to relocate in 354 # different dirs. 355 source_lib_paths = [os.path.join('/', libpath, lib) 356 for libpath in ['lib', 'lib64'] 357 for lib in libs] 358 # We backup the fs config lines for the libraries we are going to relocate, 359 # so we can set the same permissions later. 360 canned_fs_config_original_lib = {config[0] : config 361 for config in canned_fs_config 362 if config[0] in source_lib_paths} 363 364 canned_fs_config = [config for config in canned_fs_config 365 if config[0] not in source_lib_paths] 366 367 # We move any targeted library in lib64/ or lib/ to a directory named 368 # /lib64/libNAME.so/${SHA512_OF_LIBCPP}/ or 369 # /lib/libNAME.so/${SHA512_OF_LIBCPP}/ 370 # 371 for lib_path_hash in lib_paths_hashes: 372 basename = os.path.basename(lib_path_hash[0]) 373 libpath = _extract_lib_or_lib64(payload_dir, lib_path_hash[0]) 374 tmp_lib = os.path.join(payload_dir, libpath, basename + '.bak') 375 shutil.move(lib_path_hash[0], tmp_lib) 376 destdir = os.path.join(payload_dir, libpath, basename, lib_path_hash[1]) 377 os.makedirs(destdir) 378 shutil.move(tmp_lib, os.path.join(destdir, basename)) 379 380 canned_fs_config.append( 381 ['/' + libpath + '/' + basename, '0', '2000', '0755']) 382 canned_fs_config.append( 383 ['/' + libpath + '/' + basename + '/' + lib_path_hash[1], 384 '0', '2000', '0755']) 385 386 if os.path.join('/', libpath, basename) in canned_fs_config_original_lib: 387 config = canned_fs_config_original_lib[os.path.join( 388 '/', 389 libpath, 390 basename)] 391 canned_fs_config.append([os.path.join('/', libpath, basename, 392 lib_path_hash[1], basename), 393 config[1], config[2], config[3]]) 394 else: 395 canned_fs_config.append([os.path.join('/', libpath, basename, 396 lib_path_hash[1], basename), 397 '1000', '1000', '0644']) 398 399 pb.canned_fs_config = config_to_str(canned_fs_config).encode('utf-8') 400 with open(container_files['apex_build_info.pb'], 'wb') as f: 401 f.write(pb.SerializeToString()) 402 403 try: 404 for lib in lib_paths: 405 os.rmdir(os.path.dirname(lib)) 406 except OSError: 407 # Directory not empty, that's OK. 408 pass 409 410 repack_apex_file_path = run_apexer(container_files, payload_dir, args.key, 411 args.pubkey, args.tmpdir) 412 413 resigned_apex_file_path = sign_apk_container(repack_apex_file_path, 414 args.x509key, args.pk8key, 415 args.tmpdir) 416 417 shutil.copyfile(resigned_apex_file_path, args.output) 418 419 420if __name__ == '__main__': 421 main(sys.argv[1:]) 422