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