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 subprocess.check_call(nsjail_command, stdout=stdout, stderr=stderr) 355 356def parse_args(): 357 """Parse command line arguments. 358 359 Returns: 360 An argparse.Namespace object. 361 """ 362 363 # Use the top level module docstring for the help description 364 parser = argparse.ArgumentParser( 365 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) 366 parser.add_argument( 367 '--nsjail_bin', 368 required=True, 369 help='Path to NsJail binary.') 370 parser.add_argument( 371 '--chroot', 372 help='Path to the chroot to be used for building the Android' 373 'platform. This will be mounted as the root filesystem in the' 374 'NsJail sandbox.') 375 parser.add_argument( 376 '--overlay_config', 377 help='Path to the overlay configuration file.') 378 parser.add_argument( 379 '--source_dir', 380 default=os.getcwd(), 381 help='Path to Android platform source to be mounted as /src.') 382 parser.add_argument( 383 '--out_dir', 384 help='Full path to the Android build out folder. If not provided, uses ' 385 'the standard \'out\' folder in the current path.') 386 parser.add_argument( 387 '--meta_root_dir', 388 default='', 389 help='Full path to META folder. Default to \'\'') 390 parser.add_argument( 391 '--meta_android_dir', 392 default=_DEFAULT_META_ANDROID_DIR, 393 help='Relative path to the location where the META build expects ' 394 'the Android build. This path must be relative to meta_root_dir. ' 395 'Defaults to \'%s\'' % _DEFAULT_META_ANDROID_DIR) 396 parser.add_argument( 397 '--command', 398 default=_DEFAULT_COMMAND, 399 help='Command to run after entering the NsJail.' 400 'If not set then an interactive Bash shell will be launched') 401 parser.add_argument( 402 '--build_target', 403 required=True, 404 help='Android target selected for building') 405 parser.add_argument( 406 '--dist_dir', 407 help='Path to the Android dist directory. This is where' 408 'Android platform release artifacts will be written.' 409 'If unset then the Android platform default will be used.') 410 parser.add_argument( 411 '--build_id', 412 help='Build identifier what will label the Android platform' 413 'release artifacts.') 414 parser.add_argument( 415 '--max_cpus', 416 type=int, 417 help='Limit of concurrent CPU cores that the NsJail sandbox' 418 'can use. Defaults to unlimited.') 419 parser.add_argument( 420 '--bindmount', 421 type=str, 422 default=[], 423 action='append', 424 help='List of mountpoints to be mounted. Can be specified multiple times. ' 425 'Syntax: \'source\' or \'source:dest\'') 426 parser.add_argument( 427 '--bindmount_ro', 428 type=str, 429 default=[], 430 action='append', 431 help='List of mountpoints to be mounted read-only. Can be specified multiple times. ' 432 'Syntax: \'source\' or \'source:dest\'') 433 parser.add_argument( 434 '--dry_run', 435 action='store_true', 436 help='Prints the command without executing') 437 parser.add_argument( 438 '--quiet', '-q', 439 action='store_true', 440 help='Suppress debugging output') 441 parser.add_argument( 442 '--mount_local_device', 443 action='store_true', 444 help='If provided, mount locally connected Android USB devices inside ' 445 'the container. WARNING: Using this flag will cause the adb server to be ' 446 'killed on the host machine. WARNING: Using this flag exposes parts of ' 447 'the host /sys/... file system. Use only when you need adb.') 448 parser.add_argument( 449 '--env', '-e', 450 type=str, 451 default=[], 452 action='append', 453 help='Specify an environment variable to the NSJail sandbox. Can be specified ' 454 'muliple times. Syntax: var_name=value') 455 parser.add_argument( 456 '--allow_network', action='store_true', 457 help='If provided, allow access to the host network. WARNING: Using this ' 458 'flag exposes the network inside jail. Use only when needed.') 459 return parser.parse_args() 460 461def run_with_args(args): 462 """Run inside an NsJail sandbox. 463 464 Use the arguments from an argspace namespace. 465 466 Args: 467 An argparse.Namespace object. 468 469 Returns: 470 A list of strings with the commands executed. 471 """ 472 run(chroot=args.chroot, 473 nsjail_bin=args.nsjail_bin, 474 overlay_config=args.overlay_config, 475 source_dir=args.source_dir, 476 command=args.command.split(), 477 build_target=args.build_target, 478 dist_dir=args.dist_dir, 479 build_id=args.build_id, 480 out_dir=args.out_dir, 481 meta_root_dir=args.meta_root_dir, 482 meta_android_dir=args.meta_android_dir, 483 mount_local_device=args.mount_local_device, 484 max_cpus=args.max_cpus, 485 extra_bind_mounts=args.bindmount, 486 readonly_bind_mounts=args.bindmount_ro, 487 dry_run=args.dry_run, 488 quiet=args.quiet, 489 env=args.env, 490 allow_network=args.allow_network) 491 492def main(): 493 run_with_args(parse_args()) 494 495if __name__ == '__main__': 496 main() 497