aboutsummaryrefslogtreecommitdiff
path: root/build/sandbox/nsjail.py
blob: c388d0beeaed4c70c0f5075bff7d97015f15ab79 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
# 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:
    subprocess.check_call(nsjail_command, stdout=stdout, stderr=stderr)

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()