blob: 3a1b8d2117733343d8607db50560b8f2b9f837c4 [file] [log] [blame]
#!/usr/bin/python3
#
# Copyright (C) 2022 The Android Open Source Project
#
# 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
#
# http://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.
import json
import os
import textwrap
def _bool(value):
"""Convert string to one of None, True, False."""
if isinstance(value, bool):
return value
if value is None:
return None
if value.to_lower() in ('', 'none', 'null'):
return None
if value in ('False', 'false'):
return False
return bool(value)
class Envar():
"""Environment variables."""
def __init__(self, *args, name=None, value=None):
assert not args, "Envar only accepts kwargs"
self.name = name
self.value = value
def __str__(self):
if self.value is None:
# Use the current value of the variable.
return f'envar: "{self.name}"\n'
return f'envar: "{self.name}={self.value}"\n'
class MountPt(object):
def __init__(self,
_kw_only=(),
src="",
prefix_src_env="",
src_content="",
dst="",
prefix_dst_env="",
fstype="",
options="",
is_bind=None,
rw=None,
is_dir=None,
mandatory=None,
is_symlink=None,
nosuid=None,
nodev=None,
noexec=None):
assert _kw_only == (), "MountPt only accepts kwargs"
# These asserts may need to be revisited if we ever use prefix_src_env
# or prefix_dst_env.
assert not src or os.path.abspath(src) == src, "Paths must be absolute"
assert not dst or os.path.abspath(dst) == dst, "Paths must be absolute"
self.src = src
self.prefix_src_env = prefix_src_env
self.src_content = src_content
self.dst = dst
self.prefix_dst_env = prefix_dst_env
self.fstype = fstype
self.options = options
self.is_bind = _bool(is_bind)
self.rw = _bool(rw)
self.is_dir = _bool(is_dir)
self.mandatory = _bool(mandatory)
self.is_symlink = _bool(is_symlink)
self.nosuid = _bool(nosuid)
self.nodev = _bool(nodev)
self.noexec = _bool(noexec)
def __str__(self):
ret = "mount {\n"
if self.src:
ret += f" src: {json.dumps(self.src)}\n"
if self.prefix_src_env:
ret += f" prefix_src_env: {json.dumps(self.prefix_src_env)}\n"
if self.src_content:
ret += f" src_content: {json.dumps(self.src_content)}\n"
if self.dst:
ret += f" dst: {json.dumps(self.dst)}\n"
if self.prefix_dst_env:
ret += f" prefix_dst_env: {json.dumps(self.prefix_dst_env)}\n"
if self.fstype:
ret += f" fstype: {json.dumps(self.fstype)}\n"
if self.options:
ret += f" options: {json.dumps(self.options)}\n"
if self.is_bind is not None:
ret += f" is_bind: {json.dumps(self.is_bind)}\n"
if self.rw is not None:
ret += f" rw: {json.dumps(self.rw)}\n"
if self.is_dir is not None:
ret += f" is_dir: {json.dumps(self.is_dir)}\n"
if self.mandatory is not None:
ret += f" mandatory: {json.dumps(self.mandatory)}\n"
if self.is_symlink is not None:
ret += f" is_symlink: {json.dumps(self.is_symlink)}\n"
if self.nosuid is not None:
ret += f" nosuid: {json.dumps(self.nosuid)}\n"
if self.nodev is not None:
ret += f" nodev: {json.dumps(self.nodev)}\n"
if self.noexec is not None:
ret += f" noexec: {json.dumps(self.noexec)}\n"
ret += "}\n\n"
return ret
def __eq__(self, other):
return (isinstance(other, MountPt) and self.src == other.src
and self.prefix_src_env == other.prefix_src_env
and self.src_content == other.src_content
and self.dst == other.dst
and self.prefix_dst_env == other.prefix_dst_env
and self.fstype == other.fstype
and self.options == other.options
and self.is_bind == other.is_bind and self.rw == other.rw
and self.is_dir == other.is_dir
and self.mandatory == other.mandatory
and self.is_symlink == other.is_symlink
and self.nosuid == other.nosuid and self.nodev == other.nodev
and self.noexec == other.noexec)
def copy(self):
return MountPt(src=self.src,
prefix_src_env=self.prefix_src_env,
src_content=self.src_content,
dst=self.dst,
prefix_dst_env=self.prefix_dst_env,
fstype=self.fstype,
options=self.options,
is_bind=self.is_bind,
rw=self.rw,
is_dir=self.is_dir,
mandatory=self.mandatory,
is_symlink=self.is_symlink,
nosuid=self.nosuid,
nodev=self.nodev,
noexec=self.noexec)
class NsjailConfigOption(object):
"""Options for nsjail configuration."""
def __init__(self, name, value, comment=""):
self.name = name
self.value = value
self.comment = comment
def __str__(self):
return f"{self.comment}\n{self.name}: {self.value}"
class Nsjail(object):
def __init__(self, cwd, verbose=False):
self.cwd = cwd
self.verbose = verbose
# Add the mount points that we always need.
self.mounts = [
# Mount proc so that the PID namespace can be shared between the
# parent and child process.
MountPt(src="/proc",
dst="/proc",
is_bind=True,
rw=True,
mandatory=True),
# Some commands may need /etc/alternatives to reach the correct
# binary.
MountPt(src="/etc/alternatives",
dst="/etc/alternatives",
is_bind=True,
rw=False,
mandatory=False),
# TODO: we may need to use something other than tmpfs for this,
# because of some tests, etc.
MountPt(dst="/tmp",
fstype="tmpfs",
rw=True,
is_bind=False,
noexec=False,
nodev=True,
nosuid=True),
# Some tools need /dev/shm to created a named semaphore. Use a new
# tmpfs to limit access to the external environment.
MountPt(dst="/dev/shm", fstype="tmpfs", rw=True, is_bind=False),
# Add the expected tty devices.
MountPt(src="/dev/tty", dst="/dev/tty", rw=True, is_bind=True),
# These are symlinks to /proc/self/fd/{0,1,2}.
MountPt(src="/proc/self/fd/0", dst="/dev/stdin", is_symlink=True),
MountPt(src="/proc/self/fd/1", dst="/dev/stdout", is_symlink=True),
MountPt(src="/proc/self/fd/2", dst="/dev/stderr", is_symlink=True),
# Map the working User ID to a username
# Some tools like Java need a valid username
# Inner trees building with Soong also expect the nobody UID to be
# available to setup its own nsjail.
MountPt(
src_content="user:x:999999:65533:user:/tmp:/bin/bash\n"
"nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n",
dst="/etc/passwd",
mandatory=False),
# Define default group
MountPt(src_content="group::65533:user\n"
"nogroup::65534:nobody\n",
dst="/etc/group",
mandatory=False),
# Empty mtab file needed for some build scripts that check for
# images being mounted
MountPt(src_content="\n", dst="/etc/mtab", mandatory=False),
# Explicitly mount required device file nodes
#
# This will enable a chroot based NsJail sandbox. A chroot does not
# provide device file nodes. So just mount the required device file
# nodes directly from the host.
#
# Note that this has no effect in a docker container, since in that
# case NsJail will just mount the container device nodes. When we
# use NsJail in a docker container we mount the full file system
# root. So the container device nodes were already mounted in the
# NsJail.
# Some tools (like llvm-link) look for file descriptors in /dev/fd
MountPt(src="/proc/self/fd",
dst="/dev/fd",
is_symlink=True,
mandatory=False),
# /dev/null is a very commonly used for silencing output
MountPt(src="/dev/null", dst="/dev/null", rw=True, is_bind=True),
# /dev/urandom used during the creation of system.img
MountPt(src="/dev/urandom",
dst="/dev/urandom",
rw=False,
is_bind=True),
# /dev/random used by test scripts
MountPt(src="/dev/random",
dst="/dev/random",
rw=False,
is_bind=True),
# /dev/zero is required to make vendor-qemu.img
MountPt(src="/dev/zero", dst="/dev/zero", is_bind=True),
MountPt(src="/lib", dst="/lib", is_bind=True, rw=False),
MountPt(src="/bin", dst="/bin", is_bind=True, rw=False),
MountPt(src="/sbin", dst="/sbin", is_bind=True, rw=False),
MountPt(src="/usr", dst="/usr", is_bind=True, rw=False),
MountPt(src="/lib64",
dst="/lib64",
is_bind=True,
rw=False,
mandatory=False),
MountPt(src="/lib32",
dst="/lib32",
is_bind=True,
rw=False,
mandatory=False),
]
self.envars = [
# Some tools in the build toolchain expect a $HOME to be set Point
# $HOME to /tmp in case the toolchain needs to write something out
# there
Envar(name="HOME", value="/tmp"),
# By default nsjail does not propagate the environment into the
# jail. We need the path to be set up. There are a few ways to solve
# this problem, but to avoid an undocumented dependency we are
# explicit about the path we inject.
Envar(name="PATH", value="/usr/bin:/usr/sbin:/bin:/sbin"),
]
self.options = []
def make_cwd_writable(self):
"""Mark things under cwd writable."""
for mount in self.mounts:
if mount.dst.startswith(self.cwd) and not mount.rw:
if self.verbose:
print(f"Marking {mount.dst} r/w")
mount.rw = True
def add_mountpt(self, **kwargs):
"""Add a mountpoint to the config."""
self.mounts.append(MountPt(**kwargs))
def add_envar(self, **kwargs):
"""Add an envar to the config."""
self.envars.append(Envar(**kwargs))
def add_option(self, **kwargs):
"""Add an option to the njsail config."""
self.options.append(NsjailConfigOption(**kwargs))
def copy(self):
"""Return a copy of ourselves."""
ret = Nsjail(self.cwd, verbose=self.verbose)
ret.mounts = [x.copy() for x in self.mounts()]
return ret
@property
def mount_points(self):
"""Return the list of mount points.
Returns a list of mount points (destinations) for the nsjail.
"""
return (x.dst for x in self.mounts)
def add_nsjail(self, other):
"""Add another Nsjail object to this one."""
# WARNING: clone_newnet option of inner tree should not be merged into
# the combined nsjsail.cfg.
assert other.cwd.startswith(self.cwd), "Must be a subdir"
our_mounts = {x.dst: x for x in self.mounts}
for mount in other.mounts:
if mount.dst not in our_mounts:
self.mounts.append(mount)
else:
assert mount == our_mounts[mount.dst]
def generate_config(self, fn):
"""Generate the nsjail config file.
Args:
fn: (str) The name of the file to write, or None to not create.
Returns:
(str) The configuration written.
"""
data = textwrap.dedent(f"""\
name: "android-build-sandbox"
description: "Sandboxed Android Platform Build."
description: "No network access and a limited access to local host resources."
log_level: {"INFO" if self.verbose else "WARNING"}
# All configuration options are described in
# https://github.com/google/nsjail/blob/master/config.proto
# Run once then exit
mode: ONCE
# No time limit
time_limit: 0
# Limits memory usage
rlimit_as_type: SOFT
# Maximum size of core dump files
rlimit_core_type: SOFT
# Limits use of CPU time
rlimit_cpu_type: SOFT
# Maximum file size
rlimit_fsize_type: SOFT
# Maximum number of file descriptors opened
rlimit_nofile_type: SOFT
# Maximum stack size
rlimit_stack_type: SOFT
# Maximum number of threads
rlimit_nproc_type: SOFT
# Allow terminal control
# This let's users cancel jobs with CTRL-C without exiting the
# jail.
skip_setsid: true
# Below are all the host paths that shall be mounted
# to the sandbox
# TODO: Determine if we need to have /proc from outside of the jail.
mount_proc: false
# The user must mount the source to /src using --bindmount
# It will be set as the initial working directory
cwd: "{self.cwd}"
# The sandbox User ID was chosen arbitrarily
uidmap {{
inside_id: "999999"
outside_id: ""
count: 1
}}
# The sandbox Group ID was chosen arbitrarily
gidmap {{
inside_id: "65533"
outside_id: ""
count: 1
}}
# Share PID namespace between parent and child process.
# Sharing the PID namespace ensures that the Bazel daemon does not
# get killed after every invocation.
clone_newpid: false
""")
for envar in self.envars:
data += f'{envar}'
data += '\n'
for mount in self.mounts:
data += f'{mount}'
data += '\n'
for option in self.options:
data += f"{option}"
if fn:
os.makedirs(os.path.dirname(fn), exist_ok=True)
with open(fn, "w", encoding="iso-8859-1") as f:
f.write(data)
return data