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 1, 2024
1 parent c83f9f7 commit c61742d
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 107 deletions.
211 changes: 107 additions & 104 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 landlock::{
Access, AccessFs, PathBeneath, PathFd, PathFdError, RestrictionStatus, Ruleset, RulesetAttr,
Expand All @@ -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 {
Expand All @@ -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<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);
// Interface for Landlock crate.
#[derive(Clone, Debug)]
pub struct UhyveLandlockWrapper {
whitelisted_paths: Vec<String>,
uhyve_paths: Vec<String>,
}

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()
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::<Result<_, LandlockRestrictError>, _>(|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<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()?)
}
}
10 changes: 7 additions & 3 deletions src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*,
Expand Down Expand Up @@ -158,6 +158,8 @@ pub struct UhyveVm<VirtBackend: VirtualizationBackend> {
pub(crate) virt_backend: VirtBackend,
params: Params,
pub output: Output,
#[cfg(feature = "landlock")]
pub(crate) landlock: UhyveLandlockWrapper,
pub(crate) tempdir: TempDir,
}
impl<VirtBackend: VirtualizationBackend> UhyveVm<VirtBackend> {
Expand Down Expand Up @@ -192,7 +194,7 @@ impl<VirtBackend: VirtualizationBackend> UhyveVm<VirtBackend> {

let file_mapping = Mutex::new(UhyveFileMap::new(&params.file_mapping));
#[cfg(feature = "landlock")]
initialize_whitelist(
let landlock = UhyveLandlockWrapper::new(
&params.file_mapping,
kernel_path.to_str().unwrap(),
tempdir.path().to_str().unwrap(),
Expand Down Expand Up @@ -235,6 +237,8 @@ impl<VirtBackend: VirtualizationBackend> UhyveVm<VirtBackend> {
virt_backend,
params,
output,
#[cfg(feature = "landlock")]
landlock,
tempdir,
};

Expand Down Expand Up @@ -306,7 +310,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 c61742d

Please sign in to comment.