From c61742d34e8f82647a876c99da27006deaf856ce Mon Sep 17 00:00:00 2001 From: "Panagiotis \"Ivory\" Vasilopoulos" Date: Sun, 1 Dec 2024 14:04:20 +0100 Subject: [PATCH] feat(landlock): introduce UhyveLandlockWrapper --- src/isolation/landlock.rs | 211 +++++++++++++++++++------------------- src/vm.rs | 10 +- 2 files changed, 114 insertions(+), 107 deletions(-) diff --git a/src/isolation/landlock.rs b/src/isolation/landlock.rs index fe943712..ad1fc3fc 100644 --- a/src/isolation/landlock.rs +++ b/src/isolation/landlock.rs @@ -1,7 +1,4 @@ -use std::{sync::OnceLock, vec::Vec}; - -pub static WHITELISTED_PATHS: OnceLock> = OnceLock::new(); -pub static UHYVE_PATHS: OnceLock> = OnceLock::new(); +use std::vec::Vec; use landlock::{ Access, AccessFs, PathBeneath, PathFd, PathFdError, RestrictionStatus, Ruleset, RulesetAttr, @@ -11,74 +8,6 @@ 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_whitelist(mappings: &[String], kernel_path: &str, temp_dir: &str) { - #[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 = mappings - .iter() - .map(String::as_str) - .map(split_guest_and_host_path) - .map(|(guest_path, host_path)| { (guest_path, host_path) }.0) - .collect(); - let _ = *WHITELISTED_PATHS.get_or_init(|| paths); - - // This segment "whitelists" the following immediately before reading the kernel: - // - // - The kernel path. - // - /dev/urandom: For good measure. - // - /sys/devices/system, /proc/cpuinfo, /proc/stat: Useful for sysinfo. - // - // See: https://github.com/GuillaumeGomez/sysinfo/blob/8fd58b8/src/unix/linux/cpu.rs#L420 - // - // Primarily intended for Landlock: Useful for "process-wide" file isolation. - // It is not necessary to whitelist e.g. /dev/kvm, as the isolation will be - // enforced _after_ KVM is initialized. - // - // Given that we cannot enumerate all of these locations in advance, - // some problems may occur if... - // - sysinfo decides to read data from a different location in the future. - // - Uhyve is being run on a system with a non-"standard" directory structure. - - let uhyve_paths = vec![ - kernel_path.to_string(), - temp_dir.to_string(), - String::from("/dev/urandom"), - String::from("/sys/devices/system"), - String::from("/proc/cpuinfo"), - String::from("/proc/stat"), - ]; - - let _ = *UHYVE_PATHS.get_or_init(|| uhyve_paths); - } -} - -/// 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")] - { - let _status = match initialize_landlock() { - 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 { @@ -88,38 +17,112 @@ pub enum LandlockRestrictError { AddRule(#[from] PathFdError), } -/// Initializes Landlock by providing R/W-access to user-defined and -/// Uhyve-defined paths. -pub fn initialize_landlock() -> Result { - // This should be incremented regularly. - let abi = ABI::V5; - // Used for explicitly whitelisted files (read & write). - let access_all: landlock::BitFlags = 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::from_read(abi); +// Interface for Landlock crate. +#[derive(Clone, Debug)] +pub struct UhyveLandlockWrapper { + whitelisted_paths: Vec, + uhyve_paths: Vec, +} - Ok(Ruleset::default() - .handle_access(access_all)? - .create()? - .add_rules( - WHITELISTED_PATHS - .get() - .unwrap() - .as_slice() - .iter() - .map::, _>(|p| { - Ok(PathBeneath::new(PathFd::new(p)?, access_all)) - }), - )? - .add_rules( - UHYVE_PATHS - .get() - .unwrap() - .as_slice() +impl UhyveLandlockWrapper { + /// Adds host paths to WHITELISTED_PATHS and UHYVE_PATHS for isolation-related purposes. + pub fn new(mappings: &[String], kernel_path: &str, temp_dir: &str) -> 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::, _>(|p| { - Ok(PathBeneath::new(PathFd::new(p)?, access_read)) - }), - )? - .restrict_self()?) + .map(String::as_str) + .map(split_guest_and_host_path) + .map(|(guest_path, host_path)| { (guest_path, host_path) }.0) + .collect(); + + // This segment "whitelists" the following immediately before reading the kernel: + // + // - The kernel path. + // - /dev/urandom: For good measure. + // - /sys/devices/system, /proc/cpuinfo, /proc/stat: Useful for sysinfo. + // + // See: https://github.com/GuillaumeGomez/sysinfo/blob/8fd58b8/src/unix/linux/cpu.rs#L420 + // + // Primarily intended for Landlock: Useful for "process-wide" file isolation. + // It is not necessary to whitelist e.g. /dev/kvm, as the isolation will be + // enforced _after_ KVM is initialized. + // + // Given that we cannot enumerate all of these locations in advance, + // some problems may occur if... + // - sysinfo decides to read data from a different location in the future. + // - Uhyve is being run on a system with a non-"standard" directory structure. + + let uhyve_paths = vec![ + kernel_path.to_string(), + temp_dir.to_string(), + String::from("/dev/urandom"), + String::from("/sys/devices/system"), + String::from("/proc/cpuinfo"), + String::from("/proc/stat"), + ]; + + UhyveLandlockWrapper { + whitelisted_paths, + uhyve_paths, + } + } + } + + /// 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")] + { + #[cfg(target_os = "linux")] + { + let _status = match Self::initialize_landlock(self) { + Ok(status) => status, + Err(error) => panic!("Unable to initialize Landlock: {error:?}"), + }; + } + } + } + + /// Initializes Landlock by providing R/W-access to user-defined and + /// Uhyve-defined paths. + pub fn initialize_landlock(&self) -> Result { + // This should be incremented regularly. + let abi = ABI::V5; + // Used for explicitly whitelisted files (read & write). + let access_all: landlock::BitFlags = 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::from_read(abi); + + Ok(Ruleset::default() + .handle_access(access_all)? + .create()? + .add_rules( + self.whitelisted_paths + .as_slice() + .iter() + .map::, _>(|p| { + Ok(PathBeneath::new(PathFd::new(p)?, access_all)) + }), + )? + .add_rules( + self.uhyve_paths + .as_slice() + .iter() + .map::, _>(|p| { + Ok(PathBeneath::new(PathFd::new(p)?, access_read)) + }), + )? + .restrict_self()?) + } } diff --git a/src/vm.rs b/src/vm.rs index 205c0adf..5f9e8781 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -23,7 +23,7 @@ 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_whitelist}; +use crate::isolation::landlock::UhyveLandlockWrapper; use crate::{ arch::{self, FrequencyDetectionFailed}, consts::*, @@ -158,6 +158,8 @@ pub struct UhyveVm { pub(crate) virt_backend: VirtBackend, params: Params, pub output: Output, + #[cfg(feature = "landlock")] + pub(crate) landlock: UhyveLandlockWrapper, pub(crate) tempdir: TempDir, } impl UhyveVm { @@ -192,7 +194,7 @@ impl UhyveVm { let file_mapping = Mutex::new(UhyveFileMap::new(¶ms.file_mapping)); #[cfg(feature = "landlock")] - initialize_whitelist( + let landlock = UhyveLandlockWrapper::new( ¶ms.file_mapping, kernel_path.to_str().unwrap(), tempdir.path().to_str().unwrap(), @@ -235,6 +237,8 @@ impl UhyveVm { virt_backend, params, output, + #[cfg(feature = "landlock")] + landlock, tempdir, }; @@ -306,7 +310,7 @@ impl UhyveVm { 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)?;