blob: 9a5601afbc011082e497f18c0c57cf8c062ebd5c [file]
// Copyright 2017 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#![deny(missing_docs)]
#![allow(dead_code)]
use std::path::Path;
use std::str;
use std::sync::LazyLock;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
#[cfg(feature = "seccomp_trace")]
use base::debug;
use base::getegid;
use base::geteuid;
#[cfg(feature = "seccomp_trace")]
use base::warn;
use libc::c_ulong;
use minijail::Minijail;
#[cfg(feature = "seccomp_trace")]
use static_assertions::const_assert;
#[cfg(feature = "seccomp_trace")]
use zerocopy::Immutable;
#[cfg(feature = "seccomp_trace")]
use zerocopy::IntoBytes;
use crate::config::JailConfig;
static EMBEDDED_BPFS: LazyLock<std::collections::HashMap<&str, Vec<u8>>> =
LazyLock::new(|| include!(concat!(env!("OUT_DIR"), "/bpf_includes.in")));
/// Most devices don't need to open many fds. However, an implementation detail of minijail is that
/// after applying this limit, it opens an additional file descriptor to scan the /proc/self/fd
/// directory to choose which file descriptors to close in the child process. The open files limit
/// therefore has to be higher than the number file descriptors that the parent thread holds open
/// before the jail is started.
pub const MAX_OPEN_FILES_DEFAULT: u64 = 4096;
/// The max open files for gpu processes.
const MAX_OPEN_FILES_FOR_GPU: u64 = 32768;
/// The max open files for jail warden, matching FD_RAW_FAILURE.
pub const MAX_OPEN_FILES_FOR_JAIL_WARDEN: u64 = 65536;
/// The user in the jail to run as.
pub enum RunAsUser {
/// Do not specify the user
Unspecified,
/// Runs as the same user in the jail as the current user.
CurrentUser,
/// Runs as the root user in the jail.
Root,
/// Runs as the specified uid and gid.
/// This requires `SandboxConfig::ugid_map` to be set.
Specified(u32, u32),
}
/// Config for the sandbox to be created by [Minijail].
pub struct SandboxConfig<'a> {
/// Whether or not to drop all capabilities in the sandbox.
pub limit_caps: bool,
log_failures: bool,
seccomp_policy_dir: Option<&'a Path>,
seccomp_policy_name: &'a str,
/// The pair of `uid_map` and `gid_map`.
pub ugid_map: Option<(&'a str, &'a str)>,
/// The remount mode instead of default MS_PRIVATE.
pub remount_mode: Option<c_ulong>,
/// Whether to use empty net namespace. Enabled by default.
pub namespace_net: bool,
/// Whether or not to configure the jail to support bind-mounts.
///
/// Note that most device processes deny `open(2)` and `openat(2)` by seccomp policy and just
/// returns `ENOENT`. Passing opened file descriptors is recommended over opening files in the
/// sandbox.
pub bind_mounts: bool,
/// Specify the user in the jail to run as.
pub run_as: RunAsUser,
}
impl<'a> SandboxConfig<'a> {
/// Creates [SandboxConfig].
pub fn new(jail_config: &'a JailConfig, policy: &'a str) -> Self {
Self {
limit_caps: true,
log_failures: jail_config.seccomp_log_failures,
seccomp_policy_dir: jail_config.seccomp_policy_dir.as_ref().map(Path::new),
seccomp_policy_name: policy,
ugid_map: None,
remount_mode: None,
namespace_net: true,
bind_mounts: false,
run_as: RunAsUser::Unspecified,
}
}
}
/// Wrapper that cleans up a [Minijail] when it is dropped
pub struct ScopedMinijail(pub Minijail);
impl Drop for ScopedMinijail {
fn drop(&mut self) {
let _ = self.0.kill();
}
}
/// Creates a [Minijail] instance which just changes the root using pivot_root(2) path and
/// `max_open_files` using `RLIMIT_NOFILE`.
///
/// If `root` path is "/", the minijail don't change the root.
///
/// # Arguments
///
/// * `root` - The root path to be changed to by minijail.
/// * `max_open_files` - The maximum number of file descriptors to allow a jailed process to open.
#[allow(clippy::unnecessary_cast)]
pub fn create_base_minijail(root: &Path, max_open_files: u64) -> Result<Minijail> {
// Validate new root directory. Path::is_dir() also checks the existence.
if !root.is_dir() {
bail!("{:?} is not a directory, cannot create jail", root);
}
// chroot accepts absolute path only.
if !root.is_absolute() {
bail!("{:?} is not absolute path", root);
}
let mut jail = Minijail::new().context("failed to jail device")?;
// Only pivot_root if we are not re-using the current root directory.
if root != Path::new("/") {
// Run in a new mount namespace.
jail.namespace_vfs();
jail.enter_pivot_root(root)
.context("failed to pivot root device")?;
}
jail.set_rlimit(libc::RLIMIT_NOFILE as i32, max_open_files, max_open_files)
.context("error setting max open files")?;
Ok(jail)
}
/// Creates a [Minijail] instance which just invokes a jail process and sets
/// `max_open_files` using `RLIMIT_NOFILE`. This is helpful with crosvm process
/// runs as a non-root user without SYS_ADMIN capabilities.
///
/// Unlike `create_base_minijail`, this function doesn't call `pivot_root`
/// and `mount namespace`. So, it runs as a non-root user without
/// SYS_ADMIN capabilities.
///
/// Note that since there is no file system isolation provided by this function,
/// caller of this function should enforce other security mechanisum such as selinux
/// on the host to protect directories.
///
/// # Arguments
///
/// * `root` - The root path to checked before the process is jailed
/// * `max_open_files` - The maximum number of file descriptors to allow a jailed process to open.
#[allow(clippy::unnecessary_cast)]
pub fn create_base_minijail_without_pivot_root(
root: &Path,
max_open_files: u64,
) -> Result<Minijail> {
// Validate new root directory. Path::is_dir() also checks the existence.
if !root.is_dir() {
bail!("{:?} is not a directory, cannot create jail", root);
}
if !root.is_absolute() {
bail!("{:?} is not absolute path", root);
}
let mut jail = Minijail::new().context("failed to jail device")?;
jail.set_rlimit(libc::RLIMIT_NOFILE as i32, max_open_files, max_open_files)
.context("error setting max open files")?;
Ok(jail)
}
/// Creates a [Minijail] instance which creates a sandbox.
///
/// # Arguments
///
/// * `root` - The root path to be changed to by minijail.
/// * `max_open_files` - The maximum number of file descriptors to allow a jailed process to open.
/// * `config` - The [SandboxConfig] to control details of the sandbox.
pub fn create_sandbox_minijail(
root: &Path,
max_open_files: u64,
config: &SandboxConfig,
) -> Result<Minijail> {
let mut jail = create_base_minijail(root, max_open_files)?;
jail.namespace_pids();
jail.namespace_user();
jail.namespace_user_disable_setgroups();
if config.limit_caps {
// Don't need any capabilities.
jail.use_caps(0);
}
match config.run_as {
RunAsUser::Unspecified => {
if config.bind_mounts && config.ugid_map.is_none() {
// Minijail requires to set user/group map to mount extra directories.
add_current_user_to_jail(&mut jail)?;
}
}
RunAsUser::CurrentUser => {
add_current_user_to_jail(&mut jail)?;
}
RunAsUser::Root => {
// Add the current user as root in the jail.
let crosvm_uid = geteuid();
let crosvm_gid = getegid();
jail.uidmap(&format!("0 {crosvm_uid} 1"))
.context("error setting UID map")?;
jail.gidmap(&format!("0 {crosvm_gid} 1"))
.context("error setting GID map")?;
}
RunAsUser::Specified(uid, gid) => {
if uid != 0 {
jail.change_uid(uid)
}
if gid != 0 {
jail.change_gid(gid)
}
}
}
if config.bind_mounts {
// Create a tmpfs in the device's root directory so that we can bind mount files.
// The size=67108864 is size=64*1024*1024 or size=64MB.
// TODO(b/267581374): Use appropriate size for tmpfs.
jail.mount_with_data(
Path::new("none"),
Path::new("/"),
"tmpfs",
(libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC) as usize,
"size=67108864",
)?;
}
if let Some((uid_map, gid_map)) = config.ugid_map {
jail.uidmap(uid_map).context("error setting UID map")?;
jail.gidmap(gid_map).context("error setting GID map")?;
}
// Run in a new mount namespace.
jail.namespace_vfs();
if config.namespace_net {
// Run in an empty network namespace.
jail.namespace_net();
}
// Don't allow the device to gain new privileges.
jail.no_new_privs();
#[cfg(feature = "seccomp_trace")]
{
#[repr(C)]
#[derive(Immutable, IntoBytes)]
struct sock_filter {
/* Filter block */
code: u16, /* Actual filter code */
jt: u8, /* Jump true */
jf: u8, /* Jump false */
k: u32, /* Generic multiuse field */
}
// BPF constant is defined in https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/bpf_common.h
// BPF parser/assembler is defined in https://elixir.bootlin.com/linux/v4.9/source/tools/net/bpf_exp.y
const SECCOMP_RET_TRACE: u32 = 0x7ff00000;
const SECCOMP_RET_LOG: u32 = 0x7ffc0000;
const BPF_RET: u16 = 0x06;
const BPF_K: u16 = 0x00;
// return SECCOMP_RET_LOG for all syscalls
const FILTER_RET_LOG_BLOCK: sock_filter = sock_filter {
code: BPF_RET | BPF_K,
jt: 0,
jf: 0,
k: SECCOMP_RET_LOG,
};
warn!("The running crosvm is compiled with seccomp_trace feature, and is striclty used for debugging purpose only. DO NOT USE IN PRODUCTION!!!");
debug!(
"seccomp_trace {{\"event\": \"minijail_create\", \"name\": \"{}\", \"jail_addr\": \"0x{:x}\"}}",
config.seccomp_policy_name,
read_jail_addr(&jail),
);
jail.parse_seccomp_bytes(FILTER_RET_LOG_BLOCK.as_bytes())
.unwrap();
}
#[cfg(not(feature = "seccomp_trace"))]
if let Some(seccomp_policy_dir) = config.seccomp_policy_dir {
let seccomp_policy_path = seccomp_policy_dir.join(config.seccomp_policy_name);
// By default we'll prioritize using the pre-compiled .bpf over the .policy file (the .bpf
// is expected to be compiled using "trap" as the failure behavior instead of the default
// "kill" behavior) when a policy path is supplied in the command line arugments. Otherwise
// the built-in pre-compiled policies will be used.
// Refer to the code comment for the "seccomp-log-failures" command-line parameter for an
// explanation about why the |log_failures| flag forces the use of .policy files (and the
// build-time alternative to this run-time flag).
let bpf_policy_file = seccomp_policy_path.with_extension("bpf");
if bpf_policy_file.exists() && !config.log_failures {
jail.parse_seccomp_program(&bpf_policy_file)
.with_context(|| {
format!(
"failed to parse precompiled seccomp policy: {}",
bpf_policy_file.display()
)
})?;
} else {
// Use TSYNC only for the side effect of it using SECCOMP_RET_TRAP, which will correctly
// kill the entire device process if a worker thread commits a seccomp violation.
jail.set_seccomp_filter_tsync();
if config.log_failures {
jail.log_seccomp_filter_failures();
}
let bpf_policy_file = seccomp_policy_path.with_extension("policy");
jail.parse_seccomp_filters(&bpf_policy_file)
.with_context(|| {
format!(
"failed to parse seccomp policy: {}",
bpf_policy_file.display()
)
})?;
}
} else {
set_embedded_bpf_program(&mut jail, config.seccomp_policy_name)?;
}
jail.use_seccomp_filter();
// Don't do init setup.
jail.run_as_init();
// Set up requested remount mode instead of default MS_PRIVATE.
if let Some(mode) = config.remount_mode {
jail.set_remount_mode(mode);
}
Ok(jail)
}
/// Creates a basic [Minijail] if `jail_config` is present.
///
/// Returns `None` if `jail_config` is none.
pub fn simple_jail(jail_config: Option<&JailConfig>, policy: &str) -> Result<Option<Minijail>> {
if let Some(jail_config) = jail_config {
let config = SandboxConfig::new(jail_config, policy);
Ok(Some(create_sandbox_minijail(
&jail_config.pivot_root,
MAX_OPEN_FILES_DEFAULT,
&config,
)?))
} else {
Ok(None)
}
}
/// Creates [Minijail] for gpu processes.
pub fn create_gpu_minijail(
root: &Path,
config: &SandboxConfig,
render_node_only: bool,
snapshot_scratch_directory: Option<&Path>,
) -> Result<Minijail> {
let mut jail = create_sandbox_minijail(root, MAX_OPEN_FILES_FOR_GPU, config)?;
// Device nodes required for DRM.
let sys_dev_char_path = Path::new("/sys/dev/char");
jail.mount_bind(sys_dev_char_path, sys_dev_char_path, false)?;
// Necessary for CGROUP control of the vGPU threads
// This is not necessary UNLESS one wants to make use
// of the gpu cgroup command line options.
let sys_cpuset_path = Path::new("/sys/fs/cgroup/cpuset");
if sys_cpuset_path.exists() {
jail.mount_bind(sys_cpuset_path, sys_cpuset_path, true)?;
}
let sys_devices_path = Path::new("/sys/devices");
jail.mount_bind(sys_devices_path, sys_devices_path, false)?;
jail_mount_bind_drm(&mut jail, render_node_only)?;
// If the ARM specific devices exist on the host, bind mount them in.
let mali0_path = Path::new("/dev/mali0");
if mali0_path.exists() {
jail.mount_bind(mali0_path, mali0_path, true)?;
}
let pvr_sync_path = Path::new("/dev/pvr_sync");
if pvr_sync_path.exists() {
jail.mount_bind(pvr_sync_path, pvr_sync_path, true)?;
}
// If the udmabuf driver exists on the host, bind mount it in.
let udmabuf_path = Path::new("/dev/udmabuf");
if udmabuf_path.exists() {
jail.mount_bind(udmabuf_path, udmabuf_path, true)?;
}
// Libraries that are required when mesa drivers are dynamically loaded.
jail_mount_bind_if_exists(
&mut jail,
&[
"/usr/lib",
"/usr/lib64",
"/lib",
"/lib64",
"/usr/share/drirc.d",
"/usr/share/glvnd",
"/usr/share/libdrm",
"/usr/share/vulkan",
],
)?;
// pvr driver requires read access to /proc/self/task/*/comm.
mount_proc(&mut jail)?;
// To enable perfetto tracing, we need to give access to the perfetto service IPC
// endpoints.
let perfetto_path = Path::new("/run/perfetto");
if perfetto_path.exists() {
jail.mount_bind(perfetto_path, perfetto_path, true)?;
}
// Provide scratch space for the GPU device to build or unpack snapshots.
if let Some(snapshot_scratch_directory) = snapshot_scratch_directory {
jail.mount_with_data(
Path::new("none"),
snapshot_scratch_directory,
"tmpfs",
(libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC) as usize,
"size=4294967296",
)?;
}
Ok(jail)
}
/// Selectively bind mount drm nodes into `jail` based on `render_node_only`
///
/// This function will not return an error if drm nodes don't exist
pub fn jail_mount_bind_drm(jail: &mut Minijail, render_node_only: bool) -> Result<()> {
if render_node_only {
const DRM_NUM_NODES: u32 = 63;
const DRM_RENDER_NODE_START: u32 = 128;
for offset in 0..DRM_NUM_NODES {
let path_str = format!("/dev/dri/renderD{}", DRM_RENDER_NODE_START + offset);
let drm_dri_path = Path::new(&path_str);
if !drm_dri_path.exists() {
break;
}
jail.mount_bind(drm_dri_path, drm_dri_path, false)?;
}
} else {
let drm_dri_path = Path::new("/dev/dri");
if drm_dri_path.exists() {
jail.mount_bind(drm_dri_path, drm_dri_path, false)?;
}
}
Ok(())
}
/// Mirror-mount all the directories in `dirs` into `jail` on a best-effort basis.
///
/// This function will not return an error if any of the directories in `dirs` is missing.
pub fn jail_mount_bind_if_exists<P: AsRef<std::ffi::OsStr>>(
jail: &mut Minijail,
dirs: &[P],
) -> Result<()> {
for dir in dirs {
let dir_path = Path::new(dir);
if dir_path.exists() {
jail.mount_bind(dir_path, dir_path, false)?;
}
}
Ok(())
}
/// Mount proc in the sandbox.
pub fn mount_proc(jail: &mut Minijail) -> Result<()> {
jail.mount(
Path::new("proc"),
Path::new("/proc"),
"proc",
(libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC | libc::MS_RDONLY) as usize,
)?;
Ok(())
}
/// Read minijail internal struct address for uniquely identifying and tracking jail's lifetime
#[cfg(feature = "seccomp_trace")]
pub fn read_jail_addr(jail: &Minijail) -> usize {
// We can only hope minijail's rust object will always contain a pointer to C jail struct as the
// first field.
const_assert!(std::mem::size_of::<Minijail>() >= std::mem::size_of::<usize>());
// Safe because it's only doing a read within bound checked by static assert
unsafe { *(jail as *const Minijail as *const usize) }
}
/// Set the uid/gid for the jailed process and give a basic id map. This is
/// required for bind mounts to work.
fn add_current_user_to_jail(jail: &mut Minijail) -> Result<()> {
let crosvm_uid = geteuid();
let crosvm_gid = getegid();
jail.uidmap(&format!("{crosvm_uid} {crosvm_uid} 1"))
.context("error setting UID map")?;
jail.gidmap(&format!("{crosvm_gid} {crosvm_gid} 1"))
.context("error setting GID map")?;
if crosvm_uid != 0 {
jail.change_uid(crosvm_uid);
}
if crosvm_gid != 0 {
jail.change_gid(crosvm_gid);
}
Ok(())
}
/// Set the seccomp policy for a jail from embedded bpfs
pub fn set_embedded_bpf_program(jail: &mut Minijail, seccomp_policy_name: &str) -> Result<()> {
let bpf_program = EMBEDDED_BPFS.get(seccomp_policy_name).with_context(|| {
format!("failed to find embedded seccomp policy: {seccomp_policy_name}")
})?;
jail.parse_seccomp_bytes(bpf_program).with_context(|| {
format!("failed to parse embedded seccomp policy: {seccomp_policy_name}")
})?;
Ok(())
}