429 lines
16 KiB
Python
429 lines
16 KiB
Python
#!/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
|