1# Copyright 2020 Google LLC 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# https://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"""Runs a command inside an NsJail sandbox for building Android. 16 17NsJail creates a user namespace sandbox where 18Android can be built in an isolated process. 19If no command is provided then it will open 20an interactive bash shell. 21""" 22 23import argparse 24import collections 25import os 26import re 27import subprocess 28from .overlay import BindMount 29from .overlay import BindOverlay 30 31_DEFAULT_META_ANDROID_DIR = 'LINUX/android' 32_DEFAULT_COMMAND = '/bin/bash' 33 34_SOURCE_MOUNT_POINT = '/src' 35_OUT_MOUNT_POINT = '/src/out' 36_DIST_MOUNT_POINT = '/dist' 37_META_MOUNT_POINT = '/meta' 38 39_CHROOT_MOUNT_POINTS = [ 40 'bin', 'sbin', 41 'etc/alternatives', 'etc/default' 'etc/perl', 42 'etc/ssl', 'etc/xml', 43 'lib', 'lib32', 'lib64', 'libx32', 44 'usr', 45] 46 47def load_rw_whitelist(rw_whitelist_config): 48 """Loads a read/write whitelist configuration file. 49 50 The read/write whitelist configuration file is a text file that contains a 51 list of source_dir relative paths which should be mounted read/write inside 52 the build sandbox. Empty lines and lines begnning with a comment marker ('#') 53 will be ignored. An empty whitelist implies that all source paths are mounted 54 read-only. An empty rw_whitelist_config argument implies that all source 55 paths are mounted read/write. 56 57 Args: 58 rw_whitelist_config: A string path to a read/write whitelist file. 59 60 Returns: 61 A set of whitelist path strings. 62 """ 63 if not rw_whitelist_config: 64 return None 65 66 if not os.path.exists(rw_whitelist_config): 67 return None 68 69 ret = set() 70 with open(rw_whitelist_config, 'r') as f: 71 for p in f.read().splitlines(): 72 p = p.strip() 73 if not p or p.startswith('#'): 74 continue 75 ret.add(p) 76 77 return ret 78 79 80def run(command, 81 android_target, 82 nsjail_bin, 83 chroot, 84 overlay_config=None, 85 rw_whitelist_config=None, 86 source_dir=os.getcwd(), 87 out_dirname_for_whiteout=None, 88 dist_dir=None, 89 build_id=None, 90 out_dir = None, 91 meta_root_dir = None, 92 meta_android_dir = _DEFAULT_META_ANDROID_DIR, 93 mount_local_device = False, 94 max_cpus=None, 95 extra_bind_mounts=[], 96 readonly_bind_mounts=[], 97 extra_nsjail_args=[], 98 dry_run=False, 99 quiet=False, 100 env=[], 101 stdout=None, 102 stderr=None): 103 """Run inside an NsJail sandbox. 104 105 Args: 106 command: A list of strings with the command to run. 107 android_target: A string with the name of the target to be prepared 108 inside the container. 109 nsjail_bin: A string with the path to the nsjail binary. 110 chroot: A string with the path to the chroot. 111 overlay_config: A string path to an overlay configuration file. 112 rw_whitelist_config: A string path to a read/write whitelist configuration file. 113 source_dir: A string with the path to the Android platform source. 114 out_dirname_for_whiteout: The optional name of the folder within 115 source_dir that is the Android build out folder *as seen from outside 116 the Docker container*. 117 dist_dir: A string with the path to the dist directory. 118 build_id: A string with the build identifier. 119 out_dir: An optional path to the Android build out folder. 120 meta_root_dir: An optional path to a folder containing the META build. 121 meta_android_dir: An optional path to the location where the META build expects 122 the Android build. This path must be relative to meta_root_dir. 123 mount_local_device: Whether to mount /dev/usb (and related) trees enabling 124 adb to run inside the jail 125 max_cpus: An integer with maximum number of CPUs. 126 extra_bind_mounts: An array of extra mounts in the 'source' or 'source:dest' syntax. 127 readonly_bind_mounts: An array of read only mounts in the 'source' or 'source:dest' syntax. 128 extra_nsjail_args: A list of strings that contain extra arguments to nsjail. 129 dry_run: If true, the command will be returned but not executed 130 quiet: If true, the function will not display the command and 131 will pass -quiet argument to nsjail 132 env: An array of environment variables to define in the jail in the `var=val` syntax. 133 stdout: the standard output for all printed messages. Valid values are None, a file 134 descriptor or file object. A None value means sys.stdout is used. 135 stderr: the standard error for all printed messages. Valid values are None, a file 136 descriptor or file object, and subprocess.STDOUT (which indicates that all stderr 137 should be redirected to stdout). A None value means sys.stderr is used. 138 139 Returns: 140 A list of strings with the command executed. 141 """ 142 143 144 nsjail_command = get_command( 145 command=command, 146 android_target=android_target, 147 nsjail_bin=nsjail_bin, 148 chroot=chroot, 149 overlay_config=overlay_config, 150 rw_whitelist_config=rw_whitelist_config, 151 source_dir=source_dir, 152 out_dirname_for_whiteout=out_dirname_for_whiteout, 153 dist_dir=dist_dir, 154 build_id=build_id, 155 out_dir=out_dir, 156 meta_root_dir=meta_root_dir, 157 meta_android_dir=meta_android_dir, 158 mount_local_device=mount_local_device, 159 max_cpus=max_cpus, 160 extra_bind_mounts=extra_bind_mounts, 161 readonly_bind_mounts=readonly_bind_mounts, 162 extra_nsjail_args=extra_nsjail_args, 163 quiet=quiet, 164 env=env) 165 166 run_command( 167 nsjail_command=nsjail_command, 168 mount_local_device=mount_local_device, 169 dry_run=dry_run, 170 quiet=quiet, 171 stdout=stdout, 172 stderr=stderr) 173 174 return nsjail_command 175 176def get_command(command, 177 android_target, 178 nsjail_bin, 179 chroot, 180 overlay_config=None, 181 rw_whitelist_config=None, 182 source_dir=os.getcwd(), 183 out_dirname_for_whiteout=None, 184 dist_dir=None, 185 build_id=None, 186 out_dir = None, 187 meta_root_dir = None, 188 meta_android_dir = _DEFAULT_META_ANDROID_DIR, 189 mount_local_device = False, 190 max_cpus=None, 191 extra_bind_mounts=[], 192 readonly_bind_mounts=[], 193 extra_nsjail_args=[], 194 quiet=False, 195 env=[]): 196 """Get command to run nsjail sandbox. 197 198 Args: 199 command: A list of strings with the command to run. 200 android_target: A string with the name of the target to be prepared 201 inside the container. 202 nsjail_bin: A string with the path to the nsjail binary. 203 chroot: A string with the path to the chroot. 204 overlay_config: A string path to an overlay configuration file. 205 rw_whitelist_config: A string path to a read/write whitelist configuration file. 206 source_dir: A string with the path to the Android platform source. 207 out_dirname_for_whiteout: The optional name of the folder within 208 source_dir that is the Android build out folder *as seen from outside 209 the Docker container*. 210 dist_dir: A string with the path to the dist directory. 211 build_id: A string with the build identifier. 212 out_dir: An optional path to the Android build out folder. 213 meta_root_dir: An optional path to a folder containing the META build. 214 meta_android_dir: An optional path to the location where the META build expects 215 the Android build. This path must be relative to meta_root_dir. 216 max_cpus: An integer with maximum number of CPUs. 217 extra_bind_mounts: An array of extra mounts in the 'source' or 'source:dest' syntax. 218 readonly_bind_mounts: An array of read only mounts in the 'source' or 'source:dest' syntax. 219 extra_nsjail_args: A list of strings that contain extra arguments to nsjail. 220 quiet: If true, the function will not display the command and 221 will pass -quiet argument to nsjail 222 env: An array of environment variables to define in the jail in the `var=val` syntax. 223 224 Returns: 225 A list of strings with the command to execute. 226 """ 227 script_dir = os.path.dirname(os.path.abspath(__file__)) 228 config_file = os.path.join(script_dir, 'nsjail.cfg') 229 230 # Run expects absolute paths 231 if out_dir: 232 out_dir = os.path.abspath(out_dir) 233 if dist_dir: 234 dist_dir = os.path.abspath(dist_dir) 235 if meta_root_dir: 236 meta_root_dir = os.path.abspath(meta_root_dir) 237 if source_dir: 238 source_dir = os.path.abspath(source_dir) 239 240 if nsjail_bin: 241 nsjail_bin = os.path.join(source_dir, nsjail_bin) 242 243 if chroot: 244 chroot = os.path.join(source_dir, chroot) 245 246 if meta_root_dir: 247 if not meta_android_dir or os.path.isabs(meta_android_dir): 248 raise ValueError('error: the provided meta_android_dir is not a path' 249 'relative to meta_root_dir.') 250 251 nsjail_command = [nsjail_bin, 252 '--env', 'USER=nobody', 253 '--config', config_file] 254 255 # By mounting the points individually that we need we reduce exposure and 256 # keep the chroot clean from artifacts 257 if chroot: 258 for mpoints in _CHROOT_MOUNT_POINTS: 259 source = os.path.join(chroot, mpoints) 260 dest = os.path.join('/', mpoints) 261 if os.path.exists(source): 262 nsjail_command.extend([ 263 '--bindmount_ro', '%s:%s' % (source, dest) 264 ]) 265 266 if build_id: 267 nsjail_command.extend(['--env', 'BUILD_NUMBER=%s' % build_id]) 268 if max_cpus: 269 nsjail_command.append('--max_cpus=%i' % max_cpus) 270 if quiet: 271 nsjail_command.append('--quiet') 272 273 whiteout_list = set() 274 if out_dirname_for_whiteout: 275 whiteout_list.add(os.path.join(source_dir, out_dirname_for_whiteout)) 276 if out_dir and ( 277 os.path.dirname(out_dir) == source_dir) and ( 278 os.path.basename(out_dir) != 'out'): 279 whiteout_list.add(os.path.abspath(out_dir)) 280 if not os.path.exists(out_dir): 281 os.makedirs(out_dir) 282 283 rw_whitelist = load_rw_whitelist(rw_whitelist_config) 284 285 # Apply the overlay for the selected Android target to the source 286 # directory if an overlay configuration was provided 287 if overlay_config and os.path.exists(overlay_config): 288 overlay = BindOverlay(android_target, 289 source_dir, 290 overlay_config, 291 whiteout_list, 292 _SOURCE_MOUNT_POINT, 293 rw_whitelist) 294 bind_mounts = overlay.GetBindMounts() 295 else: 296 bind_mounts = collections.OrderedDict() 297 bind_mounts[_SOURCE_MOUNT_POINT] = BindMount(source_dir, False) 298 299 if out_dir: 300 bind_mounts[_OUT_MOUNT_POINT] = BindMount(out_dir, False) 301 302 if dist_dir: 303 bind_mounts[_DIST_MOUNT_POINT] = BindMount(dist_dir, False) 304 nsjail_command.extend([ 305 '--env', 'DIST_DIR=%s'%_DIST_MOUNT_POINT 306 ]) 307 308 if meta_root_dir: 309 bind_mounts[_META_MOUNT_POINT] = BindMount(meta_root_dir, False) 310 bind_mounts[os.path.join(_META_MOUNT_POINT, meta_android_dir)] = BindMount(source_dir, False) 311 if out_dir: 312 bind_mounts[os.path.join(_META_MOUNT_POINT, meta_android_dir, 'out')] = BindMount(out_dir, False) 313 314 for bind_destination, bind_mount in bind_mounts.items(): 315 if bind_mount.readonly: 316 nsjail_command.extend([ 317 '--bindmount_ro', bind_mount.source_dir + ':' + bind_destination 318 ]) 319 else: 320 nsjail_command.extend([ 321 '--bindmount', bind_mount.source_dir + ':' + bind_destination 322 ]) 323 324 if mount_local_device: 325 # Mount /dev/bus/usb and several /sys/... paths, which adb will examine 326 # while attempting to find the attached android device. These paths expose 327 # a lot of host operating system device space, so it's recommended to use 328 # the mount_local_device option only when you need to use adb (e.g., for 329 # atest or some other purpose). 330 nsjail_command.extend(['--bindmount', '/dev/bus/usb']) 331 nsjail_command.extend(['--bindmount', '/sys/bus/usb/devices']) 332 nsjail_command.extend(['--bindmount', '/sys/dev']) 333 nsjail_command.extend(['--bindmount', '/sys/devices']) 334 335 for mount in extra_bind_mounts: 336 nsjail_command.extend(['--bindmount', mount]) 337 for mount in readonly_bind_mounts: 338 nsjail_command.extend(['--bindmount_ro', mount]) 339 340 for var in env: 341 nsjail_command.extend(['--env', var]) 342 343 nsjail_command.extend(extra_nsjail_args) 344 345 nsjail_command.append('--') 346 nsjail_command.extend(command) 347 348 return nsjail_command 349 350def run_command(nsjail_command, 351 mount_local_device=False, 352 dry_run=False, 353 quiet=False, 354 stdout=None, 355 stderr=None): 356 """Run the provided nsjail command. 357 358 Args: 359 nsjail_command: A list of strings with the command to run. 360 mount_local_device: Whether to mount /dev/usb (and related) trees enabling 361 adb to run inside the jail 362 dry_run: If true, the command will be returned but not executed 363 quiet: If true, the function will not display the command and 364 will pass -quiet argument to nsjail 365 stdout: the standard output for all printed messages. Valid values are None, a file 366 descriptor or file object. A None value means sys.stdout is used. 367 stderr: the standard error for all printed messages. Valid values are None, a file 368 descriptor or file object, and subprocess.STDOUT (which indicates that all stderr 369 should be redirected to stdout). A None value means sys.stderr is used. 370 """ 371 372 if mount_local_device: 373 # A device can only communicate with one adb server at a time, so the adb server is 374 # killed on the host machine. 375 for line in subprocess.check_output(['ps','-eo','cmd']).decode().split('\n'): 376 if re.match(r'adb.*fork-server.*', line): 377 print('An adb server is running on your host machine. This server must be ' 378 'killed to use the --mount_local_device flag.') 379 print('Continue? [y/N]: ', end='') 380 if input().lower() != 'y': 381 exit() 382 subprocess.check_call(['adb', 'kill-server']) 383 384 if not quiet: 385 print('NsJail command:', file=stdout) 386 print(' '.join(nsjail_command), file=stdout) 387 388 if not dry_run: 389 subprocess.check_call(nsjail_command, stdout=stdout, stderr=stderr) 390 391def parse_args(): 392 """Parse command line arguments. 393 394 Returns: 395 An argparse.Namespace object. 396 """ 397 398 # Use the top level module docstring for the help description 399 parser = argparse.ArgumentParser( 400 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) 401 parser.add_argument( 402 '--nsjail_bin', 403 required=True, 404 help='Path to NsJail binary.') 405 parser.add_argument( 406 '--chroot', 407 help='Path to the chroot to be used for building the Android' 408 'platform. This will be mounted as the root filesystem in the' 409 'NsJail sandbox.') 410 parser.add_argument( 411 '--overlay_config', 412 help='Path to the overlay configuration file.') 413 parser.add_argument( 414 '--rw_whitelist_config', 415 help='Path to the read/write whitelist configuration file.') 416 parser.add_argument( 417 '--source_dir', 418 default=os.getcwd(), 419 help='Path to Android platform source to be mounted as /src.') 420 parser.add_argument( 421 '--out_dir', 422 help='Full path to the Android build out folder. If not provided, uses ' 423 'the standard \'out\' folder in the current path.') 424 parser.add_argument( 425 '--meta_root_dir', 426 default='', 427 help='Full path to META folder. Default to \'\'') 428 parser.add_argument( 429 '--meta_android_dir', 430 default=_DEFAULT_META_ANDROID_DIR, 431 help='Relative path to the location where the META build expects ' 432 'the Android build. This path must be relative to meta_root_dir. ' 433 'Defaults to \'%s\'' % _DEFAULT_META_ANDROID_DIR) 434 parser.add_argument( 435 '--out_dirname_for_whiteout', 436 help='The optional name of the folder within source_dir that is the ' 437 'Android build out folder *as seen from outside the Docker ' 438 'container*.') 439 parser.add_argument( 440 '--whiteout', 441 action='append', 442 default=[], 443 help='Optional glob filter of directories to add to the whiteout. The ' 444 'directories will not appear in the container. ' 445 'Can be specified multiple times.') 446 parser.add_argument( 447 '--command', 448 default=_DEFAULT_COMMAND, 449 help='Command to run after entering the NsJail.' 450 'If not set then an interactive Bash shell will be launched') 451 parser.add_argument( 452 '--android_target', 453 required=True, 454 help='Android target selected for building') 455 parser.add_argument( 456 '--dist_dir', 457 help='Path to the Android dist directory. This is where' 458 'Android platform release artifacts will be written.' 459 'If unset then the Android platform default will be used.') 460 parser.add_argument( 461 '--build_id', 462 help='Build identifier what will label the Android platform' 463 'release artifacts.') 464 parser.add_argument( 465 '--max_cpus', 466 type=int, 467 help='Limit of concurrent CPU cores that the NsJail sandbox' 468 'can use. Defaults to unlimited.') 469 parser.add_argument( 470 '--bindmount', 471 type=str, 472 default=[], 473 action='append', 474 help='List of mountpoints to be mounted. Can be specified multiple times. ' 475 'Syntax: \'source\' or \'source:dest\'') 476 parser.add_argument( 477 '--bindmount_ro', 478 type=str, 479 default=[], 480 action='append', 481 help='List of mountpoints to be mounted read-only. Can be specified multiple times. ' 482 'Syntax: \'source\' or \'source:dest\'') 483 parser.add_argument( 484 '--dry_run', 485 action='store_true', 486 help='Prints the command without executing') 487 parser.add_argument( 488 '--quiet', '-q', 489 action='store_true', 490 help='Suppress debugging output') 491 parser.add_argument( 492 '--mount_local_device', 493 action='store_true', 494 help='If provided, mount locally connected Android USB devices inside ' 495 'the container. WARNING: Using this flag will cause the adb server to be ' 496 'killed on the host machine. WARNING: Using this flag exposes parts of ' 497 'the host /sys/... file system. Use only when you need adb.') 498 parser.add_argument( 499 '--env', '-e', 500 type=str, 501 default=[], 502 action='append', 503 help='Specify an environment variable to the NSJail sandbox. Can be specified ' 504 'muliple times. Syntax: var_name=value') 505 return parser.parse_args() 506 507def run_with_args(args): 508 """Run inside an NsJail sandbox. 509 510 Use the arguments from an argspace namespace. 511 512 Args: 513 An argparse.Namespace object. 514 515 Returns: 516 A list of strings with the commands executed. 517 """ 518 run(chroot=args.chroot, 519 nsjail_bin=args.nsjail_bin, 520 overlay_config=args.overlay_config, 521 rw_whitelist_config=args.rw_whitelist_config, 522 source_dir=args.source_dir, 523 command=args.command.split(), 524 android_target=args.android_target, 525 out_dirname_for_whiteout=args.out_dirname_for_whiteout, 526 dist_dir=args.dist_dir, 527 build_id=args.build_id, 528 out_dir=args.out_dir, 529 meta_root_dir=args.meta_root_dir, 530 meta_android_dir=args.meta_android_dir, 531 mount_local_device=args.mount_local_device, 532 max_cpus=args.max_cpus, 533 extra_bind_mounts=args.bindmount, 534 readonly_bind_mounts=args.bindmount_ro, 535 dry_run=args.dry_run, 536 quiet=args.quiet, 537 env=args.env) 538 539def main(): 540 run_with_args(parse_args()) 541 542if __name__ == '__main__': 543 main() 544