• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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