1#!/usr/bin/env python3 2 3# Copyright 2022 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17from pathlib import Path 18import argparse 19import glob 20import logging 21import mini_parser 22import os 23import pkgutil 24import policy 25import shutil 26import subprocess 27import sys 28import tempfile 29import zipfile 30"""This tool generates a mapping file for {ver} core sepolicy.""" 31 32temp_dir = '' 33mapping_cil_footer = ";; mapping information from ToT policy's types to %s policy's types.\n" 34compat_cil_template = """;; complement CIL file for compatibility between ToT policy and %s vendors. 35;; will be compiled along with other normal policy files, on %s vendors. 36;; 37""" 38ignore_cil_template = """;; new_objects - a collection of types that have been introduced with ToT policy 39;; that have no analogue in %s policy. Thus, we do not need to map these types to 40;; previous ones. Add here to pass checkapi tests. 41(type new_objects) 42(typeattribute new_objects) 43(typeattributeset new_objects 44 ( new_objects 45 %s 46 )) 47""" 48 49SHARED_LIB_EXTENSION = '.dylib' if sys.platform == 'darwin' else '.so' 50 51def check_run(cmd, cwd=None): 52 if cwd: 53 logging.debug('Running cmd at %s: %s' % (cwd, cmd)) 54 else: 55 logging.debug('Running cmd: %s' % cmd) 56 subprocess.run(cmd, cwd=cwd, check=True) 57 58 59def check_output(cmd): 60 logging.debug('Running cmd: %s' % cmd) 61 return subprocess.run(cmd, check=True, stdout=subprocess.PIPE) 62 63 64def get_android_build_top(): 65 ANDROID_BUILD_TOP = os.getenv('ANDROID_BUILD_TOP') 66 if not ANDROID_BUILD_TOP: 67 sys.exit( 68 'Error: Missing ANDROID_BUILD_TOP env variable. Please run ' 69 '\'. build/envsetup.sh; lunch <build target>\'. Exiting script.') 70 return ANDROID_BUILD_TOP 71 72 73def fetch_artifact(branch, build, pattern, destination='.'): 74 """Fetches build artifacts from Android Build server. 75 76 Args: 77 branch: string, branch to pull build artifacts from 78 build: string, build ID or "latest" 79 pattern: string, pattern of build artifact file name 80 destination: string, destination to pull build artifact to 81 """ 82 fetch_artifact_path = '/google/data/ro/projects/android/fetch_artifact' 83 cmd = [ 84 fetch_artifact_path, '--branch', branch, '--target', 85 'aosp_arm64-userdebug' 86 ] 87 if build == 'latest': 88 cmd.append('--latest') 89 else: 90 cmd.extend(['--bid', build]) 91 cmd.extend([pattern, destination]) 92 check_run(cmd) 93 94 95def extract_mapping_file_from_img_zip(zip_path, ver, destination='.'): 96 """ Extracts system/etc/selinux/mapping/{ver}.cil from img.zip file. 97 98 Args: 99 zip_path: string, path to img.zip file 100 ver: string, version of designated mapping file 101 destination: string, destination to pull the mapping file to 102 103 Returns: 104 string, path to extracted mapping file 105 """ 106 with zipfile.ZipFile(zip_path) as zip_file: 107 logging.debug(f'Extracting system.img to {temp_dir}') 108 zip_file.extract('system.img', temp_dir) 109 110 system_img_path = os.path.join(temp_dir, 'system.img') 111 mapping_file_path = os.path.join(destination, f'{ver}.cil') 112 cmd = [ 113 'debugfs', '-R', 114 f'dump system/etc/selinux/mapping/{ver}.cil {mapping_file_path}', 115 system_img_path 116 ] 117 logging.debug(f'Extracting {ver}.cil to {destination}') 118 check_run(cmd) 119 return mapping_file_path 120 121 122def download_img_zip(branch, build, destination='.'): 123 """ Downloads system/etc/selinux/mapping/{ver}.cil from Android Build server. 124 125 Args: 126 branch: string, branch to pull build artifacts from (e.g. "sc-v2-dev") 127 build: string, build ID or "latest" 128 ver: string, version of designated mapping file (e.g. "32.0") 129 destination: string, destination to pull build artifact to 130 131 Returns: 132 string, path to img.zip file 133 """ 134 logging.info('Downloading %s mapping file from branch %s build %s...' % 135 (ver, branch, build)) 136 artifact_pattern = 'aosp_arm64-img-*.zip' 137 fetch_artifact(branch, build, artifact_pattern, temp_dir) 138 139 # glob must succeed 140 return glob.glob(os.path.join(temp_dir, artifact_pattern))[0] 141 142 143def build_base_files(target_version): 144 """ Builds needed base policy files from the source code. 145 146 Args: 147 target_version: string, target version to gerenate the mapping file 148 149 Returns: 150 (string, string, string), paths to base policy, old policy, and pub policy 151 cil 152 """ 153 logging.info('building base sepolicy files') 154 build_top = get_android_build_top() 155 156 cmd = [ 157 'build/soong/soong_ui.bash', 158 '--make-mode', 159 'dist', 160 'base-sepolicy-files-for-mapping', 161 'TARGET_PRODUCT=aosp_arm64', 162 'TARGET_BUILD_VARIANT=userdebug', 163 ] 164 check_run(cmd, cwd=build_top) 165 166 dist_dir = os.path.join(build_top, 'out', 'dist') 167 base_policy_path = os.path.join(dist_dir, 'base_plat_sepolicy') 168 old_policy_path = os.path.join(dist_dir, 169 '%s_plat_policy' % target_version) 170 pub_policy_cil_path = os.path.join(dist_dir, 'base_plat_pub_policy.cil') 171 172 return base_policy_path, old_policy_path, pub_policy_cil_path 173 174 175def change_api_level(versioned_type, api_from, api_to): 176 """ Verifies the API version of versioned_type, and changes it to new API level. 177 178 For example, change_api_level("foo_202404", "202404", "202504") will return 179 "foo_202504". 180 181 Args: 182 versioned_type: string, type with version suffix 183 api_from: string, api version of versioned_type 184 api_to: string, new api version for versioned_type 185 186 Returns: 187 string, a new versioned type 188 """ 189 if not versioned_type.endswith(api_from): 190 raise ValueError('Version of type %s is different from %s' % 191 (versioned_type, api_from)) 192 return versioned_type.removesuffix(api_from) + api_to 193 194 195def create_target_compat_modules(bp_path, target_ver): 196 """ Creates compat modules to Android.bp. 197 198 Args: 199 bp_path: string, path to Android.bp 200 target_ver: string, api version to generate 201 """ 202 203 module_template = """ 204se_build_files {{ 205 name: "{ver}.board.compat.map", 206 srcs: ["compat/{ver}/{ver}.cil"], 207}} 208 209se_build_files {{ 210 name: "{ver}.board.compat.cil", 211 srcs: ["compat/{ver}/{ver}.compat.cil"], 212}} 213 214se_build_files {{ 215 name: "{ver}.board.ignore.map", 216 srcs: ["compat/{ver}/{ver}.ignore.cil"], 217}} 218 219se_cil_compat_map {{ 220 name: "plat_{ver}.cil", 221 stem: "{ver}.cil", 222 bottom_half: [":{ver}.board.compat.map{{.plat_private}}"], 223 version: "{ver}", 224}} 225 226se_cil_compat_map {{ 227 name: "system_ext_{ver}.cil", 228 stem: "{ver}.cil", 229 bottom_half: [":{ver}.board.compat.map{{.system_ext_private}}"], 230 system_ext_specific: true, 231 version: "{ver}", 232}} 233 234se_cil_compat_map {{ 235 name: "product_{ver}.cil", 236 stem: "{ver}.cil", 237 bottom_half: [":{ver}.board.compat.map{{.product_private}}"], 238 product_specific: true, 239 version: "{ver}", 240}} 241 242se_cil_compat_map {{ 243 name: "{ver}.ignore.cil", 244 bottom_half: [":{ver}.board.ignore.map{{.plat_private}}"], 245 version: "{ver}", 246}} 247 248se_cil_compat_map {{ 249 name: "system_ext_{ver}.ignore.cil", 250 stem: "{ver}.ignore.cil", 251 bottom_half: [":{ver}.board.ignore.map{{.system_ext_private}}"], 252 system_ext_specific: true, 253 version: "{ver}", 254}} 255 256se_cil_compat_map {{ 257 name: "product_{ver}.ignore.cil", 258 stem: "{ver}.ignore.cil", 259 bottom_half: [":{ver}.board.ignore.map{{.product_private}}"], 260 product_specific: true, 261 version: "{ver}", 262}} 263 264se_compat_cil {{ 265 name: "{ver}.compat.cil", 266 srcs: [":{ver}.board.compat.cil{{.plat_private}}"], 267 version: "{ver}", 268}} 269 270se_compat_cil {{ 271 name: "system_ext_{ver}.compat.cil", 272 stem: "{ver}.compat.cil", 273 srcs: [":{ver}.board.compat.cil{{.system_ext_private}}"], 274 system_ext_specific: true, 275 version: "{ver}", 276}} 277""" 278 279 with open(bp_path, 'a') as f: 280 f.write(module_template.format(ver=target_ver)) 281 282 283def patch_top_half_of_latest_compat_modules(bp_path, latest_ver, target_ver): 284 """ Adds top_half property to latest compat modules in Android.bp. 285 286 Args: 287 bp_path: string, path to Android.bp 288 latest_ver: string, previous api version 289 target_ver: string, api version to generate 290 """ 291 292 modules_to_patch = [ 293 "plat_{ver}.cil", 294 "system_ext_{ver}.cil", 295 "product_{ver}.cil", 296 "{ver}.ignore.cil", 297 "system_ext_{ver}.ignore.cil", 298 "product_{ver}.ignore.cil", 299 ] 300 301 for module in modules_to_patch: 302 # set latest_ver module's top_half property to target_ver 303 # e.g. 304 # 305 # se_cil_compat_map { 306 # name: "plat_33.0.cil", 307 # top_half: "plat_34.0.cil", <== this 308 # ... 309 # } 310 check_run([ 311 "bpmodify", 312 "-m", module.format(ver=latest_ver), 313 "-property", "top_half", 314 "-str", module.format(ver=target_ver), 315 "-w", 316 bp_path 317 ]) 318 319def get_args(): 320 parser = argparse.ArgumentParser() 321 parser.add_argument( 322 '--branch', 323 help='Branch to pull build from. e.g. "sc-v2-dev"') 324 parser.add_argument('--build', 325 default='latest', 326 help='Build ID, or "latest"') 327 parser.add_argument( 328 '--target-version', 329 required=True, 330 help='Target version of designated mapping file. e.g. "202504"') 331 parser.add_argument( 332 '--latest-version', 333 required=True, 334 help='Latest version for mapping of newer types. e.g. "202404"') 335 parser.add_argument( 336 '--img-zip', 337 help='Pre-downloaded img.zip. e.g. "aosp_arm64-img-xxxxxxxx.zip"') 338 parser.add_argument( 339 '-v', 340 '--verbose', 341 action='count', 342 default=0, 343 help='Increase output verbosity, e.g. "-v", "-vv".') 344 return parser.parse_args() 345 346 347def main(): 348 args = get_args() 349 350 verbosity = min(args.verbose, 2) 351 logging.basicConfig( 352 format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s', 353 level=(logging.WARNING, logging.INFO, logging.DEBUG)[verbosity]) 354 355 global temp_dir 356 temp_dir = tempfile.mkdtemp() 357 358 try: 359 libname = "libsepolwrap" + SHARED_LIB_EXTENSION 360 libpath = os.path.join(temp_dir, libname) 361 with open(libpath, "wb") as f: 362 blob = pkgutil.get_data("sepolicy_generate_compat", libname) 363 if not blob: 364 sys.exit("Error: libsepolwrap does not exist. Is this binary corrupted?\n") 365 f.write(blob) 366 367 build_top = get_android_build_top() 368 sepolicy_path = os.path.join(build_top, 'system', 'sepolicy') 369 370 # Step 0. Create a placeholder files and compat modules 371 # These are needed to build base policy files below. 372 compat_bp_path = os.path.join(sepolicy_path, 'compat', 'Android.bp') 373 create_target_compat_modules(compat_bp_path, args.target_version) 374 patch_top_half_of_latest_compat_modules(compat_bp_path, args.latest_version, 375 args.target_version) 376 377 target_compat_path = os.path.join(sepolicy_path, 'private', 'compat', 378 args.target_version) 379 target_mapping_file = os.path.join(target_compat_path, 380 args.target_version + '.cil') 381 target_compat_file = os.path.join(target_compat_path, 382 args.target_version + '.compat.cil') 383 target_ignore_file = os.path.join(target_compat_path, 384 args.target_version + '.ignore.cil') 385 Path(target_compat_path).mkdir(parents=True, exist_ok=True) 386 Path(target_mapping_file).touch() 387 Path(target_compat_file).touch() 388 Path(target_ignore_file).touch() 389 390 # Step 1. Download system/etc/selinux/mapping/{ver}.cil, and remove types/typeattributes 391 if args.img_zip and args.branch: 392 sys.exit('Error: only one of --img-zip and --branch can be set') 393 elif args.img_zip: 394 img_zip = args.img_zip 395 elif args.branch: 396 img_zip = download_img_zip(args.branch, args.build, destination=temp_dir) 397 else: 398 sys.exit('Error: either one of --img-zip and --branch must be set') 399 mapping_file = extract_mapping_file_from_img_zip(img_zip, args.target_version, 400 destination=temp_dir) 401 mapping_file_cil = mini_parser.MiniCilParser(mapping_file) 402 mapping_file_cil.types = set() 403 mapping_file_cil.typeattributes = set() 404 405 # Step 2. Build base policy files and parse latest mapping files 406 base_policy_path, old_policy_path, pub_policy_cil_path = build_base_files( 407 args.target_version) 408 base_policy = policy.Policy(base_policy_path, None, libpath) 409 old_policy = policy.Policy(old_policy_path, None, libpath) 410 pub_policy_cil = mini_parser.MiniCilParser(pub_policy_cil_path) 411 412 all_types = base_policy.GetAllTypes(False) 413 old_all_types = old_policy.GetAllTypes(False) 414 pub_types = pub_policy_cil.types 415 416 # Step 3. Find new types and removed types 417 new_types = pub_types & (all_types - old_all_types) 418 removed_types = (mapping_file_cil.pubtypes - mapping_file_cil.types) & ( 419 old_all_types - all_types) 420 421 logging.info('new types: %s' % new_types) 422 logging.info('removed types: %s' % removed_types) 423 424 # Step 4. Map new types and removed types appropriately, based on the latest mapping 425 latest_compat_path = os.path.join(sepolicy_path, 'private', 'compat', 426 args.latest_version) 427 latest_mapping_cil = mini_parser.MiniCilParser( 428 os.path.join(latest_compat_path, args.latest_version + '.cil')) 429 latest_ignore_cil = mini_parser.MiniCilParser( 430 os.path.join(latest_compat_path, 431 args.latest_version + '.ignore.cil')) 432 433 latest_ignored_types = list(latest_ignore_cil.rTypeattributesets.keys()) 434 latest_removed_types = latest_mapping_cil.types 435 logging.debug('types ignored in latest policy: %s' % 436 latest_ignored_types) 437 logging.debug('types removed in latest policy: %s' % 438 latest_removed_types) 439 440 target_ignored_types = set() 441 target_removed_types = set() 442 invalid_new_types = set() 443 invalid_mapping_types = set() 444 invalid_removed_types = set() 445 446 logging.info('starting mapping') 447 for new_type in new_types: 448 # Either each new type should be in latest_ignore_cil, or mapped to existing types 449 if new_type in latest_ignored_types: 450 logging.debug('adding %s to ignore' % new_type) 451 target_ignored_types.add(new_type) 452 elif new_type in latest_mapping_cil.rTypeattributesets: 453 latest_mapped_types = latest_mapping_cil.rTypeattributesets[ 454 new_type] 455 target_mapped_types = {change_api_level(t, args.latest_version, 456 args.target_version) 457 for t in latest_mapped_types} 458 logging.debug('mapping %s to %s' % 459 (new_type, target_mapped_types)) 460 461 for t in target_mapped_types: 462 if t not in mapping_file_cil.typeattributesets: 463 logging.error( 464 'Cannot find desired type %s in mapping file' % t) 465 invalid_mapping_types.add(t) 466 continue 467 mapping_file_cil.typeattributesets[t].add(new_type) 468 else: 469 logging.error('no mapping information for new type %s' % 470 new_type) 471 invalid_new_types.add(new_type) 472 473 for removed_type in removed_types: 474 # Removed type should be in latest_mapping_cil 475 if removed_type in latest_removed_types: 476 logging.debug('adding %s to removed' % removed_type) 477 target_removed_types.add(removed_type) 478 else: 479 logging.error('no mapping information for removed type %s' % 480 removed_type) 481 invalid_removed_types.add(removed_type) 482 483 error_msg = '' 484 485 if invalid_new_types: 486 error_msg += ('The following new types were not in the latest ' 487 'mapping: %s\n') % sorted(invalid_new_types) 488 if invalid_mapping_types: 489 error_msg += ( 490 'The following existing types were not in the ' 491 'downloaded mapping file: %s\n') % sorted(invalid_mapping_types) 492 if invalid_removed_types: 493 error_msg += ('The following removed types were not in the latest ' 494 'mapping: %s\n') % sorted(invalid_removed_types) 495 496 if error_msg: 497 error_msg += '\n' 498 error_msg += ('Please make sure the source tree and the build ID is' 499 ' up to date.\n') 500 sys.exit(error_msg) 501 502 # Step 5. Write to system/sepolicy/private/compat 503 with open(target_mapping_file, 'w') as f: 504 logging.info('writing %s' % target_mapping_file) 505 if removed_types: 506 f.write(';; types removed from current policy\n') 507 f.write('\n'.join(f'(type {x})' for x in sorted(target_removed_types))) 508 f.write('\n\n') 509 f.write(mapping_cil_footer % args.target_version) 510 f.write(mapping_file_cil.unparse()) 511 512 with open(target_compat_file, 'w') as f: 513 logging.info('writing %s' % target_compat_file) 514 f.write(compat_cil_template % (args.target_version, args.target_version)) 515 516 with open(target_ignore_file, 'w') as f: 517 logging.info('writing %s' % target_ignore_file) 518 f.write(ignore_cil_template % 519 (args.target_version, '\n '.join(sorted(target_ignored_types)))) 520 521 # TODO(b/391513934): add treble tests 522 # TODO(b/391513934): add mapping files to phony modules like selinux_policy_system 523 finally: 524 logging.info('Deleting temporary dir: {}'.format(temp_dir)) 525 shutil.rmtree(temp_dir) 526 527 528if __name__ == '__main__': 529 main() 530