• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python3
2#
3# Copyright (C) 2022 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import json
18import os
19import textwrap
20
21
22def _bool(value):
23    """Convert string to one of None, True, False."""
24    if isinstance(value, bool):
25        return value
26    if value is None:
27        return None
28    if value.to_lower() in ('', 'none', 'null'):
29        return None
30    if value in ('False', 'false'):
31        return False
32    return bool(value)
33
34
35class Envar():
36    """Environment variables."""
37
38    def __init__(self, *args, name=None, value=None):
39        assert not args, "Envar only accepts kwargs"
40        self.name = name
41        self.value = value
42
43    def __str__(self):
44        if self.value is None:
45            # Use the current value of the variable.
46            return f'envar: "{self.name}"\n'
47        return f'envar: "{self.name}={self.value}"\n'
48
49
50class MountPt(object):
51    def __init__(self,
52                 _kw_only=(),
53                 src="",
54                 prefix_src_env="",
55                 src_content="",
56                 dst="",
57                 prefix_dst_env="",
58                 fstype="",
59                 options="",
60                 is_bind=None,
61                 rw=None,
62                 is_dir=None,
63                 mandatory=None,
64                 is_symlink=None,
65                 nosuid=None,
66                 nodev=None,
67                 noexec=None):
68        assert _kw_only == (), "MountPt only accepts kwargs"
69        # These asserts may need to be revisited if we ever use prefix_src_env
70        # or prefix_dst_env.
71        assert not src or os.path.abspath(src) == src, "Paths must be absolute"
72        assert not dst or os.path.abspath(dst) == dst, "Paths must be absolute"
73        self.src = src
74        self.prefix_src_env = prefix_src_env
75        self.src_content = src_content
76        self.dst = dst
77        self.prefix_dst_env = prefix_dst_env
78        self.fstype = fstype
79        self.options = options
80        self.is_bind = _bool(is_bind)
81        self.rw = _bool(rw)
82        self.is_dir = _bool(is_dir)
83        self.mandatory = _bool(mandatory)
84        self.is_symlink = _bool(is_symlink)
85        self.nosuid = _bool(nosuid)
86        self.nodev = _bool(nodev)
87        self.noexec = _bool(noexec)
88
89    def __str__(self):
90        ret = "mount {\n"
91        if self.src:
92            ret += f"  src: {json.dumps(self.src)}\n"
93        if self.prefix_src_env:
94            ret += f"  prefix_src_env: {json.dumps(self.prefix_src_env)}\n"
95        if self.src_content:
96            ret += f"  src_content: {json.dumps(self.src_content)}\n"
97        if self.dst:
98            ret += f"  dst: {json.dumps(self.dst)}\n"
99        if self.prefix_dst_env:
100            ret += f"  prefix_dst_env: {json.dumps(self.prefix_dst_env)}\n"
101        if self.fstype:
102            ret += f"  fstype: {json.dumps(self.fstype)}\n"
103        if self.options:
104            ret += f"  options: {json.dumps(self.options)}\n"
105        if self.is_bind is not None:
106            ret += f"  is_bind: {json.dumps(self.is_bind)}\n"
107        if self.rw is not None:
108            ret += f"  rw: {json.dumps(self.rw)}\n"
109        if self.is_dir is not None:
110            ret += f"  is_dir: {json.dumps(self.is_dir)}\n"
111        if self.mandatory is not None:
112            ret += f"  mandatory: {json.dumps(self.mandatory)}\n"
113        if self.is_symlink is not None:
114            ret += f"  is_symlink: {json.dumps(self.is_symlink)}\n"
115        if self.nosuid is not None:
116            ret += f"  nosuid: {json.dumps(self.nosuid)}\n"
117        if self.nodev is not None:
118            ret += f"  nodev: {json.dumps(self.nodev)}\n"
119        if self.noexec is not None:
120            ret += f"  noexec: {json.dumps(self.noexec)}\n"
121        ret += "}\n\n"
122        return ret
123
124    def __eq__(self, other):
125        return (isinstance(other, MountPt) and self.src == other.src
126                and self.prefix_src_env == other.prefix_src_env
127                and self.src_content == other.src_content
128                and self.dst == other.dst
129                and self.prefix_dst_env == other.prefix_dst_env
130                and self.fstype == other.fstype
131                and self.options == other.options
132                and self.is_bind == other.is_bind and self.rw == other.rw
133                and self.is_dir == other.is_dir
134                and self.mandatory == other.mandatory
135                and self.is_symlink == other.is_symlink
136                and self.nosuid == other.nosuid and self.nodev == other.nodev
137                and self.noexec == other.noexec)
138
139    def copy(self):
140        return MountPt(src=self.src,
141                       prefix_src_env=self.prefix_src_env,
142                       src_content=self.src_content,
143                       dst=self.dst,
144                       prefix_dst_env=self.prefix_dst_env,
145                       fstype=self.fstype,
146                       options=self.options,
147                       is_bind=self.is_bind,
148                       rw=self.rw,
149                       is_dir=self.is_dir,
150                       mandatory=self.mandatory,
151                       is_symlink=self.is_symlink,
152                       nosuid=self.nosuid,
153                       nodev=self.nodev,
154                       noexec=self.noexec)
155
156
157class NsjailConfigOption(object):
158    """Options for nsjail configuration."""
159
160    def __init__(self, name, value, comment=""):
161        self.name = name
162        self.value = value
163        self.comment = comment
164
165    def __str__(self):
166        return f"{self.comment}\n{self.name}: {self.value}"
167
168
169class Nsjail(object):
170    def __init__(self, cwd, verbose=False):
171        self.cwd = cwd
172        self.verbose = verbose
173        # Add the mount points that we always need.
174        self.mounts = [
175            # Mount proc so that the PID namespace can be shared between the
176            # parent and child process.
177            MountPt(src="/proc",
178                    dst="/proc",
179                    is_bind=True,
180                    rw=True,
181                    mandatory=True),
182            # Some commands may need /etc/alternatives to reach the correct
183            # binary.
184            MountPt(src="/etc/alternatives",
185                    dst="/etc/alternatives",
186                    is_bind=True,
187                    rw=False,
188                    mandatory=False),
189
190            # TODO: we may need to use something other than tmpfs for this,
191            # because of some tests, etc.
192            MountPt(dst="/tmp",
193                    fstype="tmpfs",
194                    rw=True,
195                    is_bind=False,
196                    noexec=False,
197                    nodev=True,
198                    nosuid=True),
199
200            # Some tools need /dev/shm to created a named semaphore. Use a new
201            # tmpfs to limit access to the external environment.
202            MountPt(dst="/dev/shm", fstype="tmpfs", rw=True, is_bind=False),
203
204            # Add the expected tty devices.
205            MountPt(src="/dev/tty", dst="/dev/tty", rw=True, is_bind=True),
206            # These are symlinks to /proc/self/fd/{0,1,2}.
207            MountPt(src="/proc/self/fd/0", dst="/dev/stdin", is_symlink=True),
208            MountPt(src="/proc/self/fd/1", dst="/dev/stdout", is_symlink=True),
209            MountPt(src="/proc/self/fd/2", dst="/dev/stderr", is_symlink=True),
210
211            # Map the working User ID to a username
212            # Some tools like Java need a valid username
213            # Inner trees building with Soong also expect the nobody UID to be
214            # available to setup its own nsjail.
215            MountPt(
216                src_content="user:x:999999:65533:user:/tmp:/bin/bash\n"
217                "nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n",
218                dst="/etc/passwd",
219                mandatory=False),
220
221            # Define default group
222            MountPt(src_content="group::65533:user\n"
223                    "nogroup::65534:nobody\n",
224                    dst="/etc/group",
225                    mandatory=False),
226
227            # Empty mtab file needed for some build scripts that check for
228            # images being mounted
229            MountPt(src_content="\n", dst="/etc/mtab", mandatory=False),
230
231            # Explicitly mount required device file nodes
232            #
233            # This will enable a chroot based NsJail sandbox. A chroot does not
234            # provide device file nodes. So just mount the required device file
235            # nodes directly from the host.
236            #
237            # Note that this has no effect in a docker container, since in that
238            # case NsJail will just mount the container device nodes. When we
239            # use NsJail in a docker container we mount the full file system
240            # root. So the container device nodes were already mounted in the
241            # NsJail.
242
243            # Some tools (like llvm-link) look for file descriptors in /dev/fd
244            MountPt(src="/proc/self/fd",
245                    dst="/dev/fd",
246                    is_symlink=True,
247                    mandatory=False),
248
249            # /dev/null is a very commonly used for silencing output
250            MountPt(src="/dev/null", dst="/dev/null", rw=True, is_bind=True),
251
252            # /dev/urandom used during the creation of system.img
253            MountPt(src="/dev/urandom",
254                    dst="/dev/urandom",
255                    rw=False,
256                    is_bind=True),
257
258            # /dev/random used by test scripts
259            MountPt(src="/dev/random",
260                    dst="/dev/random",
261                    rw=False,
262                    is_bind=True),
263
264            # /dev/zero is required to make vendor-qemu.img
265            MountPt(src="/dev/zero", dst="/dev/zero", is_bind=True),
266            MountPt(src="/lib", dst="/lib", is_bind=True, rw=False),
267            MountPt(src="/bin", dst="/bin", is_bind=True, rw=False),
268            MountPt(src="/sbin", dst="/sbin", is_bind=True, rw=False),
269            MountPt(src="/usr", dst="/usr", is_bind=True, rw=False),
270            MountPt(src="/lib64",
271                    dst="/lib64",
272                    is_bind=True,
273                    rw=False,
274                    mandatory=False),
275            MountPt(src="/lib32",
276                    dst="/lib32",
277                    is_bind=True,
278                    rw=False,
279                    mandatory=False),
280        ]
281
282        self.envars = [
283            # Some tools in the build toolchain expect a $HOME to be set Point
284            # $HOME to /tmp in case the toolchain needs to write something out
285            # there
286            Envar(name="HOME", value="/tmp"),
287            # By default nsjail does not propagate the environment into the
288            # jail. We need the path to be set up. There are a few ways to solve
289            # this problem, but to avoid an undocumented dependency we are
290            # explicit about the path we inject.
291            Envar(name="PATH", value="/usr/bin:/usr/sbin:/bin:/sbin"),
292        ]
293        self.options = []
294
295    def make_cwd_writable(self):
296        """Mark things under cwd writable."""
297        for mount in self.mounts:
298            if mount.dst.startswith(self.cwd) and not mount.rw:
299                if self.verbose:
300                    print(f"Marking {mount.dst} r/w")
301                mount.rw = True
302
303    def add_mountpt(self, **kwargs):
304        """Add a mountpoint to the config."""
305        self.mounts.append(MountPt(**kwargs))
306
307    def add_envar(self, **kwargs):
308        """Add an envar to the config."""
309        self.envars.append(Envar(**kwargs))
310
311    def add_option(self, **kwargs):
312        """Add an option to the njsail config."""
313        self.options.append(NsjailConfigOption(**kwargs))
314
315    def copy(self):
316        """Return a copy of ourselves."""
317        ret = Nsjail(self.cwd, verbose=self.verbose)
318        ret.mounts = [x.copy() for x in self.mounts()]
319        return ret
320
321    @property
322    def mount_points(self):
323        """Return the list of mount points.
324
325      Returns a list of mount points (destinations) for the nsjail.
326      """
327        return (x.dst for x in self.mounts)
328
329    def add_nsjail(self, other):
330        """Add another Nsjail object to this one."""
331        # WARNING: clone_newnet option of inner tree should not be merged into
332        # the combined nsjsail.cfg.
333        assert other.cwd.startswith(self.cwd), "Must be a subdir"
334        our_mounts = {x.dst: x for x in self.mounts}
335        for mount in other.mounts:
336            if mount.dst not in our_mounts:
337                self.mounts.append(mount)
338            else:
339                assert mount == our_mounts[mount.dst]
340
341    def generate_config(self, fn):
342        """Generate the nsjail config file.
343
344        Args:
345          fn: (str) The name of the file to write, or None to not create.
346
347        Returns:
348          (str) The configuration written.
349        """
350        data = textwrap.dedent(f"""\
351            name: "android-build-sandbox"
352            description: "Sandboxed Android Platform Build."
353            description: "No network access and a limited access to local host resources."
354
355            log_level: {"INFO" if self.verbose else "WARNING"}
356            # All configuration options are described in
357            # https://github.com/google/nsjail/blob/master/config.proto
358
359            # Run once then exit
360            mode: ONCE
361
362            # No time limit
363            time_limit: 0
364
365            # Limits memory usage
366            rlimit_as_type: SOFT
367            # Maximum size of core dump files
368            rlimit_core_type: SOFT
369            # Limits use of CPU time
370            rlimit_cpu_type: SOFT
371            # Maximum file size
372            rlimit_fsize_type: SOFT
373            # Maximum number of file descriptors opened
374            rlimit_nofile_type: SOFT
375            # Maximum stack size
376            rlimit_stack_type: SOFT
377            # Maximum number of threads
378            rlimit_nproc_type: SOFT
379
380            # Allow terminal control
381            # This let's users cancel jobs with CTRL-C without exiting the
382            # jail.
383            skip_setsid: true
384
385            # Below are all the host paths that shall be mounted
386            # to the sandbox
387
388            # TODO: Determine if we need to have /proc from outside of the jail.
389            mount_proc: false
390
391            # The user must mount the source to /src using --bindmount
392            # It will be set as the initial working directory
393            cwd: "{self.cwd}"
394
395            # The sandbox User ID was chosen arbitrarily
396            uidmap {{
397              inside_id: "999999"
398              outside_id: ""
399              count: 1
400            }}
401
402            # The sandbox Group ID was chosen arbitrarily
403            gidmap {{
404              inside_id: "65533"
405              outside_id: ""
406              count: 1
407            }}
408
409            # Share PID namespace between parent and child process.
410            # Sharing the PID namespace ensures that the Bazel daemon does not
411            # get killed after every invocation.
412            clone_newpid: false
413
414            """)
415        for envar in self.envars:
416            data += f'{envar}'
417        data += '\n'
418        for mount in self.mounts:
419            data += f'{mount}'
420        data += '\n'
421        for option in self.options:
422            data += f"{option}"
423
424        if fn:
425            os.makedirs(os.path.dirname(fn), exist_ok=True)
426            with open(fn, "w", encoding="iso-8859-1") as f:
427                f.write(data)
428        return data
429