use std::{
ffi::OsStr,
io::{Error, ErrorKind},
os::fd::{AsFd, AsRawFd, RawFd},
path::PathBuf,
sync::Mutex,
vec::Vec,
};
use landlock::{
ABI, Access, AccessFs, CompatLevel, Compatible, PathBeneath, PathFd, PathFdError,
RestrictionStatus, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetError, RulesetStatus,
};
use thiserror::Error;
use crate::{net::tap::TUN_PATH, params::FileSandboxMode};
static LANDLOCK_ENABLED: Mutex<bool> = Mutex::new(false);
#[derive(Debug, Error)]
pub(crate) enum RestrictError {
#[error(transparent)]
Ruleset(#[from] RulesetError),
#[error(transparent)]
AddRule(#[from] PathFdError),
}
#[derive(Clone, Debug)]
pub(crate) struct UhyveLandlockWrapper {
rw_paths: Vec<PathBuf>,
ro_paths: Vec<PathBuf>,
ro_dirs: Vec<PathBuf>,
compat_level: landlock::CompatLevel,
}
impl UhyveLandlockWrapper {
fn new(
sandbox_mode: &crate::params::FileSandboxMode,
rw_paths: Vec<PathBuf>,
ro_paths: Vec<PathBuf>,
ro_dirs: Vec<PathBuf>,
) -> UhyveLandlockWrapper {
#[cfg(not(target_os = "linux"))]
compile_error!("Landlock is only available on Linux.");
UhyveLandlockWrapper {
rw_paths,
ro_paths,
ro_dirs,
compat_level: Self::determine_compat_level(*sandbox_mode),
}
}
pub(crate) fn apply_landlock_restrictions(&self) {
debug!(
"Enabling Landlock path isolation with following compatibility mode: {:?}",
self.compat_level
);
let mut is_already_enabled = LANDLOCK_ENABLED.lock().unwrap();
if *is_already_enabled {
if self.compat_level == CompatLevel::HardRequirement {
panic!("Landlock has been enabled already. Failing because of strict sandbox mode.")
} else {
warn!(
"Landlock has been enabled already. Further policies will not affect existing ones."
);
}
} else {
*is_already_enabled = true;
}
Self::enforce_landlock(self)
.unwrap_or_else(|error| panic!("Unable to initialize Landlock: {error:?}"));
}
fn determine_compat_level(sandbox_mode: FileSandboxMode) -> CompatLevel {
match sandbox_mode {
FileSandboxMode::None => unreachable!(),
FileSandboxMode::Normal => CompatLevel::BestEffort,
FileSandboxMode::Strict => CompatLevel::HardRequirement,
}
}
fn enforce_landlock(&self) -> Result<RestrictionStatus, RestrictError> {
let abi = ABI::V4;
let access_all: landlock::BitFlags<AccessFs, u64> = AccessFs::from_all(abi);
let access_dir_with_rm: landlock::BitFlags<AccessFs, u64> =
AccessFs::ReadDir | AccessFs::RemoveDir | AccessFs::RemoveFile;
let res_status = Ruleset::default()
.handle_access(access_all)?
.create()?
.add_rules(
self.rw_paths
.as_slice()
.iter()
.map::<Result<_, RestrictError>, _>(|p| {
debug!("Adding read-write path ruleset for {:#?}", *p);
Self::determine_ruleset(PathFd::new(p)?, abi)
}),
)?
.set_compatibility(self.compat_level)
.add_rules(
self.ro_paths
.as_slice()
.iter()
.map::<Result<_, RestrictError>, _>(|p| {
debug!("Adding read-only path ruleset for {:#?}", *p);
Self::determine_ruleset(PathFd::new(p)?, abi)
}),
)?
.set_compatibility(self.compat_level)
.add_rules(
self.ro_dirs
.as_slice()
.iter()
.map::<Result<_, RestrictError>, _>(|p| {
debug!("Adding read-only directory ruleset for {:#?}", *p);
Ok(PathBeneath::new(PathFd::new(p)?, access_dir_with_rm))
}),
)?
.set_compatibility(self.compat_level)
.set_no_new_privs(true)
.restrict_self()?;
debug!(
"Landlock ruleset status: {:#?} (no_new_privs: {:#?})",
res_status.ruleset, res_status.no_new_privs
);
if res_status.ruleset == RulesetStatus::NotEnforced {
panic!(
"Landlock is not supported by the running kernel. You can disable Landlock using `--file-isolation none`."
);
}
Ok(res_status)
}
fn determine_ruleset(fd: PathFd, abi: ABI) -> Result<PathBeneath<PathFd>, RestrictError> {
let is_file = is_file(fd.as_fd().as_raw_fd());
match is_file {
true => Ok(PathBeneath::new(fd, AccessFs::from_file(abi))),
false => Ok(PathBeneath::new(fd, AccessFs::from_all(abi))),
}
}
}
fn get_file_or_parent(mut host_pathbuf: PathBuf) -> Result<PathBuf, Error> {
let iterations = 2;
for i in 0..iterations {
if !host_pathbuf.exists() {
warn!("Mapped file {host_pathbuf:#?} not found. Popping...");
host_pathbuf.pop();
continue;
}
return if host_pathbuf.is_dir() {
debug!("Adding directory {host_pathbuf:#?}.");
Ok(host_pathbuf)
} else if !host_pathbuf.is_dir() && i == 0 {
debug!("Mapped file {host_pathbuf:#?} found.");
Ok(host_pathbuf)
} else {
Err(Error::new(
ErrorKind::NotADirectory,
"Found parent is not a directory.",
))
};
}
Err(Error::new(
ErrorKind::NotFound,
format!("Mapped file's parent directory not found within {iterations} iterations."),
))
}
fn is_file(rawfd: RawFd) -> bool {
unsafe {
let mut stat = std::mem::zeroed();
match libc::fstat(rawfd, &mut stat) {
0 => (stat.st_mode & libc::S_IFMT) != libc::S_IFDIR,
_ => panic!("stat against fd {rawfd:?} failed"),
}
}
}
pub(crate) fn initialize<'hpi, HPI>(
sandbox_mode: &crate::params::FileSandboxMode,
kernel_path: PathBuf,
output: &crate::params::Output,
host_paths: HPI,
temp_dir: PathBuf,
#[cfg(feature = "instrument")] trace: &Option<PathBuf>,
) -> UhyveLandlockWrapper
where
HPI: Iterator<Item = &'hpi OsStr>,
{
#[allow(unused_mut, reason = "Useful for 'instrument' feature.")]
let mut uhyve_ro_paths = vec![
kernel_path,
PathBuf::from("/etc/"),
PathBuf::from("/sys/devices/system"),
PathBuf::from("/proc/cpuinfo"),
PathBuf::from("/proc/stat"),
];
let mut uhyve_rw_paths: Vec<PathBuf> = vec![PathBuf::from("/dev/kvm"), PathBuf::from(TUN_PATH)];
#[cfg(feature = "instrument")]
if let Some(trace) = trace {
uhyve_ro_paths.push(PathBuf::from("/proc/self/maps"));
uhyve_rw_paths.push(PathBuf::from(trace));
}
let mut uhyve_ro_dirs = Vec::new();
for host_path in host_paths {
let host_pathbuf = PathBuf::from(host_path);
if host_pathbuf.exists() {
if let Some(parent_dir) = host_pathbuf.parent() {
uhyve_ro_dirs.push(parent_dir.to_path_buf());
}
uhyve_rw_paths.push(host_pathbuf);
} else {
uhyve_rw_paths.push(get_file_or_parent(host_pathbuf).unwrap());
}
}
if let crate::params::Output::File(path) = output {
uhyve_rw_paths.push(path.to_owned());
}
if let Some(tmp) = temp_dir.parent() {
uhyve_ro_dirs.push(tmp.to_owned());
}
uhyve_rw_paths.push(temp_dir);
UhyveLandlockWrapper::new(sandbox_mode, uhyve_rw_paths, uhyve_ro_paths, uhyve_ro_dirs)
}
#[cfg(test)]
mod tests {
use std::panic;
use super::*;
#[test]
fn test_get_file_or_parent() {
assert_eq!(
get_file_or_parent(PathBuf::from("/dev/zero"))
.unwrap()
.to_str()
.unwrap(),
"/dev/zero"
);
assert_eq!(
get_file_or_parent(PathBuf::from("/dev/doesntexist"))
.unwrap()
.to_str()
.unwrap(),
"/dev"
);
assert_eq!(
get_file_or_parent(PathBuf::from("/dev/zero/doesntexist"))
.err()
.unwrap()
.kind(),
ErrorKind::NotADirectory
);
}
#[test]
fn test_landlock_strict_mode() {
let landlock = UhyveLandlockWrapper::new(
&FileSandboxMode::Strict,
vec![PathBuf::from("/dev/null")],
vec![PathBuf::from("/dev/zero")],
vec![PathBuf::from("/dev/")],
);
landlock.apply_landlock_restrictions();
let result = panic::catch_unwind(|| landlock.apply_landlock_restrictions());
assert!(result.is_err());
}
}