Skip to content

Commit

Permalink
feat(landlock): introduce UhyveLandlockWrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
n0toose committed Dec 18, 2024
1 parent 8868305 commit 103e92f
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 108 deletions.
207 changes: 105 additions & 102 deletions src/isolation/landlock.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
use std::{sync::OnceLock, vec::Vec};

pub static WHITELISTED_PATHS: OnceLock<Vec<String>> = OnceLock::new();
pub static UHYVE_PATHS: OnceLock<Vec<String>> = OnceLock::new();
use std::vec::Vec;

use std::{ffi::OsString, path::PathBuf};

Expand All @@ -13,114 +10,120 @@ use thiserror::Error;

use crate::isolation::split_guest_and_host_path;

/// Adds host paths to WHITELISTED_PATHS and UHYVE_PATHS for isolation-related purposes.
pub fn initialize_landlock(mappings: &[String], uhyve_paths: &[String]) {
#[cfg(not(target_os = "linux"))]
#[cfg(feature = "landlock")]
compile_error!("Landlock is only available on Linux.");

// TODO: Check whether host OS (Linux, of course) actually supports Landlock.
// TODO: Introduce parameter that lets the user manually disable Landlock.
// TODO: Reduce code repetition (wrt. `crate::isolation::filemap`).
// TODO: What to do with files that don't exist yet?
// TODO: Don't use OnceLock to pass params between UhyveVm::new and UhyveVm::load_kernel
#[cfg(target_os = "linux")]
#[cfg(feature = "landlock")]
{
let paths: Vec<String> = mappings
.iter()
.map(String::as_str)
.map(split_guest_and_host_path)
.map(Result::unwrap)
.map(|(guest_path, host_path)| { (guest_path, host_path) }.1)
.map(get_parent_directory)
.collect();
error!("{:#?}", paths.clone());
let _ = *WHITELISTED_PATHS.get_or_init(|| paths);
/// Contains types of errors that may occur during Landlock's initialization.
#[derive(Debug, Error)]
pub enum LandlockRestrictError {
#[error(transparent)]
Ruleset(#[from] RulesetError),
#[error(transparent)]
AddRule(#[from] PathFdError),
}

let _ = *UHYVE_PATHS.get_or_init(|| uhyve_paths.to_vec());
}
/// Interface for Landlock crate.
#[derive(Clone, Debug)]
pub struct UhyveLandlockWrapper {
whitelisted_paths: Vec<String>,
uhyve_paths: Vec<String>,
}

/// If the file does not exist, we add the parent directory instead. This might have practical
/// security implications, however, combined with the other security measures implemented into
/// Uhyve, this should be fine.
///
/// TODO: Inform the user in the docs.
/// TODO: Make the amount of iterations configurable.
pub fn get_parent_directory(host_path: OsString) -> String {
let iterations = 2;
let mut host_pathbuf: PathBuf = host_path.into();
for _i in 0..iterations {
if host_pathbuf.exists() {
return host_pathbuf.to_str().unwrap().to_owned();
} else {
host_pathbuf.pop();
impl UhyveLandlockWrapper {
pub fn new(mappings: &[String], uhyve_paths: &[String]) -> UhyveLandlockWrapper {
#[cfg(not(target_os = "linux"))]
#[cfg(feature = "landlock")]
compile_error!("Landlock is only available on Linux.");

// TODO: Check whether host OS (Linux, of course) actually supports Landlock.
// TODO: Introduce parameter that lets the user manually disable Landlock.
// TODO: Reduce code repetition (wrt. `crate::isolation::filemap`).
// TODO: What to do with files that don't exist yet?
#[cfg(target_os = "linux")]
#[cfg(feature = "landlock")]
{
let whitelisted_paths = mappings
.iter()
.map(String::as_str)
.map(split_guest_and_host_path)
.map(Result::unwrap)
.map(|(guest_path, host_path)| { (guest_path, host_path).1 })
.map(Self::get_parent_directory)
.collect();

UhyveLandlockWrapper {
whitelisted_paths,
uhyve_paths: uhyve_paths.to_vec(),
}
}
}
panic!(
"The mapped file's parent directory wasn't found within {} iteration(s).",
iterations
);
}

/// This function attempts to enforce different layers of file-related isolation.
/// This is currently only used for Landlock. It can be extended for other isolation
/// layers, as well as operating system-specific implementations.
pub fn enforce_isolation() {
#[cfg(feature = "landlock")]
{
#[cfg(target_os = "linux")]
/// This function attempts to enforce different layers of file-related isolation.
/// This is currently only used for Landlock. It can be extended for other isolation
/// layers, as well as operating system-specific implementations.
pub fn enforce_isolation(&self) {
#[cfg(feature = "landlock")]
{
let _status = match enforce_landlock() {
Ok(status) => status,
Err(error) => panic!("Unable to initialize Landlock: {error:?}"),
};
#[cfg(target_os = "linux")]
{
let _status = match Self::enforce_landlock(self) {
Ok(status) => status,
Err(error) => panic!("Unable to initialize Landlock: {error:?}"),
};
}
}
}
}

/// Contains types of errors that may occur during Landlock's initialization.
#[derive(Debug, Error)]
pub enum LandlockRestrictError {
#[error(transparent)]
Ruleset(#[from] RulesetError),
#[error(transparent)]
AddRule(#[from] PathFdError),
}

/// Initializes Landlock by providing R/W-access to user-defined and
/// Uhyve-defined paths.
pub fn enforce_landlock() -> Result<RestrictionStatus, LandlockRestrictError> {
// This should be incremented regularly.
let abi = ABI::V5;
// Used for explicitly whitelisted files (read & write).
let access_all: landlock::BitFlags<AccessFs, u64> = AccessFs::from_all(abi);
// Used for the kernel itself, as well as "system directories" that we only read from.
let access_read: landlock::BitFlags<AccessFs, u64> = AccessFs::from_read(abi);
/// If the file does not exist, we add the parent directory instead. This might have practical
/// security implications, however, combined with the other security measures implemented into
/// Uhyve, this should be fine.
///
/// TODO: Inform the user in the docs.
/// TODO: Make the amount of iterations configurable.
pub fn get_parent_directory(host_path: OsString) -> String {
let iterations = 2;
let mut host_pathbuf: PathBuf = host_path.into();
for _i in 0..iterations {
if host_pathbuf.exists() {
return host_pathbuf.to_str().unwrap().to_owned();
} else {
host_pathbuf.pop();
}
}
panic!(
"The mapped file's parent directory wasn't found within {} iteration(s).",
iterations
);
}

Ok(Ruleset::default()
.handle_access(access_all)?
.create()?
.add_rules(
WHITELISTED_PATHS
.get()
.unwrap()
.as_slice()
.iter()
.map::<Result<_, LandlockRestrictError>, _>(|p| {
Ok(PathBeneath::new(PathFd::new(p)?, access_all))
}),
)?
.add_rules(
UHYVE_PATHS
.get()
.unwrap()
.as_slice()
.iter()
.map::<Result<_, LandlockRestrictError>, _>(|p| {
Ok(PathBeneath::new(PathFd::new(p)?, access_read))
}),
)?
.restrict_self()?)

/// Initializes Landlock by providing R/W-access to user-defined and
/// Uhyve-defined paths.
pub fn enforce_landlock(&self) -> Result<RestrictionStatus, LandlockRestrictError> {
// This should be incremented regularly.
let abi = ABI::V5;
// Used for explicitly whitelisted files (read & write).
let access_all: landlock::BitFlags<AccessFs, u64> = AccessFs::from_all(abi);
// Used for the kernel itself, as well as "system directories" that we only read from.
let access_read: landlock::BitFlags<AccessFs, u64> = AccessFs::from_read(abi);

Ok(Ruleset::default()
.handle_access(access_all)?
.create()?
.add_rules(
self.whitelisted_paths
.as_slice()
.iter()
.map::<Result<_, LandlockRestrictError>, _>(|p| {
Ok(PathBeneath::new(PathFd::new(p)?, access_all))
}),
)?
.add_rules(
self.uhyve_paths
.as_slice()
.iter()
.map::<Result<_, LandlockRestrictError>, _>(|p| {
Ok(PathBeneath::new(PathFd::new(p)?, access_read))
}),
)?
.restrict_self()?)
}
}
15 changes: 9 additions & 6 deletions src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ use uhyve_interface::GuestPhysAddr;
use crate::arch::x86_64::{
detect_freq_from_cpuid, detect_freq_from_cpuid_hypervisor_info, get_cpu_frequency_from_os,
};

#[cfg(feature = "landlock")]
use crate::isolation::landlock::{enforce_isolation, initialize_landlock};
use crate::isolation::landlock::UhyveLandlockWrapper;

use crate::{
arch::{self, FrequencyDetectionFailed},
consts::*,
Expand Down Expand Up @@ -158,6 +160,8 @@ pub struct UhyveVm<VirtBackend: VirtualizationBackend> {
pub(crate) virt_backend: VirtBackend,
params: Params,
pub output: Output,
#[cfg(feature = "landlock")]
pub(crate) landlock: UhyveLandlockWrapper,
}
impl<VirtBackend: VirtualizationBackend> UhyveVm<VirtBackend> {
pub fn new(kernel_path: PathBuf, params: Params) -> HypervisorResult<UhyveVm<VirtBackend>> {
Expand Down Expand Up @@ -238,10 +242,7 @@ impl<VirtBackend: VirtualizationBackend> UhyveVm<VirtBackend> {
};

#[cfg(feature = "landlock")]
{
initialize_landlock(&params.file_mapping, &uhyve_paths);
file_mapping.lock().unwrap().get_temp_dir();
}
let landlock = UhyveLandlockWrapper::new(&params.file_mapping, &uhyve_paths);

let mut vm = Self {
kernel_address: GuestPhysAddr::zero(),
Expand All @@ -256,6 +257,8 @@ impl<VirtBackend: VirtualizationBackend> UhyveVm<VirtBackend> {
virt_backend,
params,
output,
#[cfg(feature = "landlock")]
landlock,
};

vm.init_guest_mem();
Expand Down Expand Up @@ -322,7 +325,7 @@ impl<VirtBackend: VirtualizationBackend> UhyveVm<VirtBackend> {

pub fn load_kernel(&mut self) -> LoadKernelResult<()> {
#[cfg(feature = "landlock")]
enforce_isolation();
self.landlock.enforce_isolation();
let elf = fs::read(self.kernel_path())?;
let object = KernelObject::parse(&elf).map_err(LoadKernelError::ParseKernelError)?;

Expand Down

0 comments on commit 103e92f

Please sign in to comment.