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