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