# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Runs a command inside an NsJail sandbox for building Android. NsJail creates a user namespace sandbox where Android can be built in an isolated process. If no command is provided then it will open an interactive bash shell. """ import argparse import collections import os import re import subprocess from . import config from .overlay import BindMount from .overlay import BindOverlay _DEFAULT_META_ANDROID_DIR = 'LINUX/android' _DEFAULT_COMMAND = '/bin/bash' _SOURCE_MOUNT_POINT = '/src' _OUT_MOUNT_POINT = '/src/out' _DIST_MOUNT_POINT = '/dist' _META_MOUNT_POINT = '/meta' _CHROOT_MOUNT_POINTS = [ 'bin', 'sbin', 'etc/alternatives', 'etc/default', 'etc/perl', 'etc/ssl', 'etc/xml', 'lib', 'lib32', 'lib64', 'libx32', 'usr', ] def run(command, build_target, nsjail_bin, chroot, overlay_config=None, source_dir=os.getcwd(), dist_dir=None, build_id=None, out_dir = None, meta_root_dir = None, meta_android_dir = _DEFAULT_META_ANDROID_DIR, mount_local_device = False, max_cpus=None, extra_bind_mounts=[], readonly_bind_mounts=[], extra_nsjail_args=[], dry_run=False, quiet=False, env=[], nsjail_wrapper=[], stdout=None, stderr=None, allow_network=False): """Run inside an NsJail sandbox. Args: command: A list of strings with the command to run. build_target: A string with the name of the build target to be prepared inside the container. nsjail_bin: A string with the path to the nsjail binary. chroot: A string with the path to the chroot. overlay_config: A string path to an overlay configuration file. source_dir: A string with the path to the Android platform source. dist_dir: A string with the path to the dist directory. build_id: A string with the build identifier. out_dir: An optional path to the Android build out folder. meta_root_dir: An optional path to a folder containing the META build. meta_android_dir: An optional path to the location where the META build expects the Android build. This path must be relative to meta_root_dir. mount_local_device: Whether to mount /dev/usb (and related) trees enabling adb to run inside the jail max_cpus: An integer with maximum number of CPUs. extra_bind_mounts: An array of extra mounts in the 'source' or 'source:dest' syntax. readonly_bind_mounts: An array of read only mounts in the 'source' or 'source:dest' syntax. extra_nsjail_args: A list of strings that contain extra arguments to nsjail. dry_run: If true, the command will be returned but not executed quiet: If true, the function will not display the command and will pass -quiet argument to nsjail env: An array of environment variables to define in the jail in the `var=val` syntax. nsjail_wrapper: A list of strings used to wrap the nsjail command. stdout: the standard output for all printed messages. Valid values are None, a file descriptor or file object. A None value means sys.stdout is used. stderr: the standard error for all printed messages. Valid values are None, a file descriptor or file object, and subprocess.STDOUT (which indicates that all stderr should be redirected to stdout). A None value means sys.stderr is used. allow_network: allow access to host network Returns: A list of strings with the command executed. """ nsjail_command = get_command( command=command, build_target=build_target, nsjail_bin=nsjail_bin, chroot=chroot, cfg=config.factory(overlay_config), source_dir=source_dir, dist_dir=dist_dir, build_id=build_id, out_dir=out_dir, meta_root_dir=meta_root_dir, meta_android_dir=meta_android_dir, mount_local_device=mount_local_device, max_cpus=max_cpus, extra_bind_mounts=extra_bind_mounts, readonly_bind_mounts=readonly_bind_mounts, extra_nsjail_args=extra_nsjail_args, quiet=quiet, env=env, nsjail_wrapper=nsjail_wrapper, allow_network=allow_network) run_command( nsjail_command=nsjail_command, mount_local_device=mount_local_device, dry_run=dry_run, quiet=quiet, stdout=stdout, stderr=stderr) return nsjail_command def get_command(command, build_target, nsjail_bin, chroot, cfg=None, source_dir=os.getcwd(), dist_dir=None, build_id=None, out_dir = None, meta_root_dir = None, meta_android_dir = _DEFAULT_META_ANDROID_DIR, mount_local_device = False, max_cpus=None, extra_bind_mounts=[], readonly_bind_mounts=[], extra_nsjail_args=[], quiet=False, env=[], nsjail_wrapper=[], allow_network=False): """Get command to run nsjail sandbox. Args: command: A list of strings with the command to run. build_target: A string with the name of the build target to be prepared inside the container. nsjail_bin: A string with the path to the nsjail binary. chroot: A string with the path to the chroot. cfg: A config.Config instance or None. source_dir: A string with the path to the Android platform source. dist_dir: A string with the path to the dist directory. build_id: A string with the build identifier. out_dir: An optional path to the Android build out folder. meta_root_dir: An optional path to a folder containing the META build. meta_android_dir: An optional path to the location where the META build expects the Android build. This path must be relative to meta_root_dir. max_cpus: An integer with maximum number of CPUs. extra_bind_mounts: An array of extra mounts in the 'source' or 'source:dest' syntax. readonly_bind_mounts: An array of read only mounts in the 'source' or 'source:dest' syntax. extra_nsjail_args: A list of strings that contain extra arguments to nsjail. quiet: If true, the function will not display the command and will pass -quiet argument to nsjail env: An array of environment variables to define in the jail in the `var=val` syntax. allow_network: allow access to host network Returns: A list of strings with the command to execute. """ script_dir = os.path.dirname(os.path.abspath(__file__)) config_file = os.path.join(script_dir, 'nsjail.cfg') # Run expects absolute paths if out_dir: out_dir = os.path.abspath(out_dir) if dist_dir: dist_dir = os.path.abspath(dist_dir) if meta_root_dir: meta_root_dir = os.path.abspath(meta_root_dir) if source_dir: source_dir = os.path.abspath(source_dir) if nsjail_bin: nsjail_bin = os.path.join(source_dir, nsjail_bin) if chroot: chroot = os.path.join(source_dir, chroot) if meta_root_dir: if not meta_android_dir or os.path.isabs(meta_android_dir): raise ValueError('error: the provided meta_android_dir is not a path' 'relative to meta_root_dir.') nsjail_command = nsjail_wrapper + [nsjail_bin, '--env', 'USER=nobody', '--config', config_file] # By mounting the points individually that we need we reduce exposure and # keep the chroot clean from artifacts if chroot: for mpoints in _CHROOT_MOUNT_POINTS: source = os.path.join(chroot, mpoints) dest = os.path.join('/', mpoints) if os.path.exists(source): nsjail_command.extend([ '--bindmount_ro', '%s:%s' % (source, dest) ]) if build_id: nsjail_command.extend(['--env', 'BUILD_NUMBER=%s' % build_id]) if max_cpus: nsjail_command.append('--max_cpus=%i' % max_cpus) if quiet: nsjail_command.append('--quiet') whiteout_list = set() if out_dir and ( os.path.dirname(out_dir) == source_dir) and ( os.path.basename(out_dir) != 'out'): whiteout_list.add(os.path.abspath(out_dir)) if not os.path.exists(out_dir): os.makedirs(out_dir) # Apply the overlay for the selected Android target to the source directory # from the supplied config.Config instance (which may be None). if cfg is not None: overlay = BindOverlay(build_target, source_dir, cfg, whiteout_list, _SOURCE_MOUNT_POINT, quiet=quiet) bind_mounts = overlay.GetBindMounts() else: bind_mounts = collections.OrderedDict() bind_mounts[_SOURCE_MOUNT_POINT] = BindMount(source_dir, False, False) if out_dir: bind_mounts[_OUT_MOUNT_POINT] = BindMount(out_dir, False, False) if dist_dir: bind_mounts[_DIST_MOUNT_POINT] = BindMount(dist_dir, False, False) nsjail_command.extend([ '--env', 'DIST_DIR=%s'%_DIST_MOUNT_POINT ]) if meta_root_dir: bind_mounts[_META_MOUNT_POINT] = BindMount(meta_root_dir, False, False) bind_mounts[os.path.join(_META_MOUNT_POINT, meta_android_dir)] = BindMount(source_dir, False, False) if out_dir: bind_mounts[os.path.join(_META_MOUNT_POINT, meta_android_dir, 'out')] = BindMount(out_dir, False, False) for bind_destination, bind_mount in bind_mounts.items(): if bind_mount.readonly: nsjail_command.extend([ '--bindmount_ro', bind_mount.source_dir + ':' + bind_destination ]) else: nsjail_command.extend([ '--bindmount', bind_mount.source_dir + ':' + bind_destination ]) if mount_local_device: # Mount /dev/bus/usb and several /sys/... paths, which adb will examine # while attempting to find the attached android device. These paths expose # a lot of host operating system device space, so it's recommended to use # the mount_local_device option only when you need to use adb (e.g., for # atest or some other purpose). nsjail_command.extend(['--bindmount', '/dev/bus/usb']) nsjail_command.extend(['--bindmount', '/sys/bus/usb/devices']) nsjail_command.extend(['--bindmount', '/sys/dev']) nsjail_command.extend(['--bindmount', '/sys/devices']) for mount in extra_bind_mounts: nsjail_command.extend(['--bindmount', mount]) for mount in readonly_bind_mounts: nsjail_command.extend(['--bindmount_ro', mount]) for var in env: nsjail_command.extend(['--env', var]) if allow_network: nsjail_command.extend(['--disable_clone_newnet', '--bindmount_ro', '/etc/resolv.conf']) nsjail_command.extend(extra_nsjail_args) nsjail_command.append('--') nsjail_command.extend(command) return nsjail_command def run_command(nsjail_command, mount_local_device=False, dry_run=False, quiet=False, stdout=None, stderr=None): """Run the provided nsjail command. Args: nsjail_command: A list of strings with the command to run. mount_local_device: Whether to mount /dev/usb (and related) trees enabling adb to run inside the jail dry_run: If true, the command will be returned but not executed quiet: If true, the function will not display the command and will pass -quiet argument to nsjail stdout: the standard output for all printed messages. Valid values are None, a file descriptor or file object. A None value means sys.stdout is used. stderr: the standard error for all printed messages. Valid values are None, a file descriptor or file object, and subprocess.STDOUT (which indicates that all stderr should be redirected to stdout). A None value means sys.stderr is used. """ if mount_local_device: # A device can only communicate with one adb server at a time, so the adb server is # killed on the host machine. for line in subprocess.check_output(['ps','-eo','cmd']).decode().split('\n'): if re.match(r'adb.*fork-server.*', line): print('An adb server is running on your host machine. This server must be ' 'killed to use the --mount_local_device flag.') print('Continue? [y/N]: ', end='') if input().lower() != 'y': exit() subprocess.check_call(['adb', 'kill-server']) if not quiet: print('NsJail command:', file=stdout) print(' '.join(nsjail_command), file=stdout) if not dry_run: try: subprocess.check_call(nsjail_command, stdout=stdout, stderr=stderr) except subprocess.CalledProcessError as error: if len(error.cmd) > 13: cmd = error.cmd[:6] + ['...elided...'] + error.cmd[-6:] else: cmd = error.cmd msg = 'nsjail command %s failed with return code %d' % (cmd, error.returncode) # Raise from None to avoid exception chaining. raise RuntimeError(msg) from None def parse_args(): """Parse command line arguments. Returns: An argparse.Namespace object. """ # Use the top level module docstring for the help description parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument( '--nsjail_bin', required=True, help='Path to NsJail binary.') parser.add_argument( '--chroot', help='Path to the chroot to be used for building the Android' 'platform. This will be mounted as the root filesystem in the' 'NsJail sandbox.') parser.add_argument( '--overlay_config', help='Path to the overlay configuration file.') parser.add_argument( '--source_dir', default=os.getcwd(), help='Path to Android platform source to be mounted as /src.') parser.add_argument( '--out_dir', help='Full path to the Android build out folder. If not provided, uses ' 'the standard \'out\' folder in the current path.') parser.add_argument( '--meta_root_dir', default='', help='Full path to META folder. Default to \'\'') parser.add_argument( '--meta_android_dir', default=_DEFAULT_META_ANDROID_DIR, help='Relative path to the location where the META build expects ' 'the Android build. This path must be relative to meta_root_dir. ' 'Defaults to \'%s\'' % _DEFAULT_META_ANDROID_DIR) parser.add_argument( '--command', default=_DEFAULT_COMMAND, help='Command to run after entering the NsJail.' 'If not set then an interactive Bash shell will be launched') parser.add_argument( '--build_target', required=True, help='Android target selected for building') parser.add_argument( '--dist_dir', help='Path to the Android dist directory. This is where' 'Android platform release artifacts will be written.' 'If unset then the Android platform default will be used.') parser.add_argument( '--build_id', help='Build identifier what will label the Android platform' 'release artifacts.') parser.add_argument( '--max_cpus', type=int, help='Limit of concurrent CPU cores that the NsJail sandbox' 'can use. Defaults to unlimited.') parser.add_argument( '--bindmount', type=str, default=[], action='append', help='List of mountpoints to be mounted. Can be specified multiple times. ' 'Syntax: \'source\' or \'source:dest\'') parser.add_argument( '--bindmount_ro', type=str, default=[], action='append', help='List of mountpoints to be mounted read-only. Can be specified multiple times. ' 'Syntax: \'source\' or \'source:dest\'') parser.add_argument( '--dry_run', action='store_true', help='Prints the command without executing') parser.add_argument( '--quiet', '-q', action='store_true', help='Suppress debugging output') parser.add_argument( '--mount_local_device', action='store_true', help='If provided, mount locally connected Android USB devices inside ' 'the container. WARNING: Using this flag will cause the adb server to be ' 'killed on the host machine. WARNING: Using this flag exposes parts of ' 'the host /sys/... file system. Use only when you need adb.') parser.add_argument( '--env', '-e', type=str, default=[], action='append', help='Specify an environment variable to the NSJail sandbox. Can be specified ' 'muliple times. Syntax: var_name=value') parser.add_argument( '--allow_network', action='store_true', help='If provided, allow access to the host network. WARNING: Using this ' 'flag exposes the network inside jail. Use only when needed.') return parser.parse_args() def run_with_args(args): """Run inside an NsJail sandbox. Use the arguments from an argspace namespace. Args: An argparse.Namespace object. Returns: A list of strings with the commands executed. """ run(chroot=args.chroot, nsjail_bin=args.nsjail_bin, overlay_config=args.overlay_config, source_dir=args.source_dir, command=args.command.split(), build_target=args.build_target, dist_dir=args.dist_dir, build_id=args.build_id, out_dir=args.out_dir, meta_root_dir=args.meta_root_dir, meta_android_dir=args.meta_android_dir, mount_local_device=args.mount_local_device, max_cpus=args.max_cpus, extra_bind_mounts=args.bindmount, readonly_bind_mounts=args.bindmount_ro, dry_run=args.dry_run, quiet=args.quiet, env=args.env, allow_network=args.allow_network) def main(): run_with_args(parse_args()) if __name__ == '__main__': main()