From c83f9f70b1041ae26da46daec90ebf83a3c2a6e3 Mon Sep 17 00:00:00 2001 From: "Panagiotis \"Ivory\" Vasilopoulos" Date: Fri, 29 Nov 2024 16:54:17 +0100 Subject: [PATCH] feat(landlock): initial proof of concept --- Cargo.lock | 34 ++++++++++- Cargo.toml | 4 +- src/bin/uhyve.rs | 27 +++++--- src/isolation/filemap.rs | 53 +--------------- src/isolation/landlock.rs | 125 ++++++++++++++++++++++++++++++++++++++ src/isolation/mod.rs | 52 ++++++++++++++++ src/vm.rs | 11 ++++ 7 files changed, 244 insertions(+), 62 deletions(-) create mode 100644 src/isolation/landlock.rs diff --git a/Cargo.lock b/Cargo.lock index 037ec55e..31bc4067 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aarch64" @@ -466,6 +466,26 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "enumflags2" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "env_filter" version = "0.1.2" @@ -736,6 +756,17 @@ dependencies = [ "vmm-sys-util", ] +[[package]] +name = "landlock" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18738c5d4c7fae6727a96adb94722ef7ce82f3eafea0a11777e258a93816537e" +dependencies = [ + "enumflags2", + "libc", + "thiserror 1.0.69", +] + [[package]] name = "libc" version = "0.2.165" @@ -1486,6 +1517,7 @@ dependencies = [ "hermit-entry", "kvm-bindings", "kvm-ioctls", + "landlock", "libc", "log", "mac_address", diff --git a/Cargo.toml b/Cargo.toml index 167895ac..34b2b32e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,8 @@ path = "benches/benchmarks.rs" harness = false [features] -default = [] +default = ["landlock"] +landlock = ["dep:landlock"] instrument = ["rftrace", "rftrace-frontend"] [dependencies] @@ -64,6 +65,7 @@ sysinfo = { version = "0.32.0", default-features = false, features = ["system"] vm-fdt = "0.3" tempfile = "3.14.0" uuid = { version = "1.11.0", features = ["fast-rng", "v4"]} +landlock = { version = "0.4.1", optional = true } [target.'cfg(target_os = "linux")'.dependencies] kvm-bindings = "0.10" diff --git a/src/bin/uhyve.rs b/src/bin/uhyve.rs index b471678d..3cc11122 100644 --- a/src/bin/uhyve.rs +++ b/src/bin/uhyve.rs @@ -1,6 +1,8 @@ #![warn(rust_2018_idioms)] -use std::{iter, num::ParseIntError, ops::RangeInclusive, path::PathBuf, process, str::FromStr}; +use std::{ + iter, num::ParseIntError, ops::RangeInclusive, path::PathBuf, process, str::FromStr, thread, +}; use clap::{error::ErrorKind, Command, CommandFactory, Parser}; use core_affinity::CoreId; @@ -301,17 +303,22 @@ fn run_uhyve() -> i32 { let affinity = args.cpu_args.clone().get_affinity(&mut app); let params = Params::from(args); - let vm = UhyveVm::new(kernel, params) - .expect("Unable to create VM! Is the hypervisor interface (e.g. KVM) activated?"); + let vm_handle = thread::spawn(move || { + let vm = UhyveVm::new(kernel, params) + .expect("Unable to create VM! Is the hypervisor interface (e.g. KVM) activated?"); - let res = vm.run(affinity); - if stats { - if let Some(stats) = res.stats { - println!("Run statistics:"); - println!("{stats}"); + let res = vm.run(affinity); + if stats { + if let Some(stats) = res.stats { + println!("Run statistics:"); + println!("{stats}"); + } } - } - res.code + + res.code + }); + + vm_handle.join().unwrap() } fn main() { diff --git a/src/isolation/filemap.rs b/src/isolation/filemap.rs index 4766835e..80ddeb4d 100644 --- a/src/isolation/filemap.rs +++ b/src/isolation/filemap.rs @@ -6,6 +6,8 @@ use std::{ path::PathBuf, }; +use crate::isolation::split_guest_and_host_path; + /// Wrapper around a `HashMap` to map guest paths to arbitrary host paths. #[derive(Debug, Clone)] pub struct UhyveFileMap { @@ -21,7 +23,7 @@ impl UhyveFileMap { files: mappings .iter() .map(String::as_str) - .map(Self::split_guest_and_host_path) + .map(split_guest_and_host_path) .map(|(guest_path, host_path)| { ( guest_path, @@ -32,18 +34,6 @@ impl UhyveFileMap { } } - /// Separates a string of the format "./host_dir/host_path.txt:guest_path.txt" - /// into a guest_path (String) and host_path (OsString) respectively. - /// - /// * `mapping` - A mapping of the format `./host_path.txt:guest.txt`. - fn split_guest_and_host_path(mapping: &str) -> (String, OsString) { - let mut mappingiter = mapping.split(":"); - let host_path = OsString::from(mappingiter.next().unwrap()); - let guest_path = mappingiter.next().unwrap().to_owned(); - - (guest_path, host_path) - } - /// Returns the host_path on the host filesystem given a requested guest_path, if it exists. /// /// * `guest_path` - The guest path that is to be looked up in the map. @@ -104,43 +94,6 @@ impl UhyveFileMap { mod tests { use super::*; - #[test] - fn test_split_guest_and_host_path() { - let host_guest_strings = vec![ - "./host_string.txt:guest_string.txt", - "/home/user/host_string.txt:guest_string.md.txt", - ":guest_string.conf", - ":", - "exists.txt:also_exists.txt:should_not_exist.txt", - ]; - - // Mind the inverted order. - let results = vec![ - ( - String::from("guest_string.txt"), - OsString::from("./host_string.txt"), - ), - ( - String::from("guest_string.md.txt"), - OsString::from("/home/user/host_string.txt"), - ), - (String::from("guest_string.conf"), OsString::from("")), - (String::from(""), OsString::from("")), - ( - String::from("also_exists.txt"), - OsString::from("exists.txt"), - ), - ]; - - for (i, host_and_guest_string) in host_guest_strings - .into_iter() - .map(UhyveFileMap::split_guest_and_host_path) - .enumerate() - { - assert_eq!(host_and_guest_string, results[i]); - } - } - #[test] fn test_uhyvefilemap() { // Our files are in `$CARGO_MANIFEST_DIR/data/fixtures/fs`. diff --git a/src/isolation/landlock.rs b/src/isolation/landlock.rs new file mode 100644 index 00000000..fe943712 --- /dev/null +++ b/src/isolation/landlock.rs @@ -0,0 +1,125 @@ +use std::{sync::OnceLock, vec::Vec}; + +pub static WHITELISTED_PATHS: OnceLock> = OnceLock::new(); +pub static UHYVE_PATHS: OnceLock> = OnceLock::new(); + +use landlock::{ + Access, AccessFs, PathBeneath, PathFd, PathFdError, RestrictionStatus, Ruleset, RulesetAttr, + RulesetCreatedAttr, RulesetError, ABI, +}; +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 { + #[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 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); + + 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() + .iter() + .map::, _>(|p| { + Ok(PathBeneath::new(PathFd::new(p)?, access_read)) + }), + )? + .restrict_self()?) +} diff --git a/src/isolation/mod.rs b/src/isolation/mod.rs index 701a81cc..30f7c602 100644 --- a/src/isolation/mod.rs +++ b/src/isolation/mod.rs @@ -1,2 +1,54 @@ pub mod filemap; +pub mod landlock; pub mod tempdir; + +use std::ffi::OsString; + +/// Separates a string of the format "./host_dir/host_path.txt:guest_path.txt" +/// into a guest_path (String) and host_path (OsString) respectively. +/// +/// * `mapping` - A mapping of the format `./host_path.txt:guest.txt`. +pub fn split_guest_and_host_path(mapping: &str) -> (String, OsString) { + let mut mappingiter = mapping.split(":"); + let host_path = OsString::from(mappingiter.next().unwrap()); + let guest_path = mappingiter.next().unwrap().to_owned(); + + (guest_path, host_path) +} + +#[test] +fn test_split_guest_and_host_path() { + let host_guest_strings = vec![ + "./host_string.txt:guest_string.txt", + "/home/user/host_string.txt:guest_string.md.txt", + ":guest_string.conf", + ":", + "exists.txt:also_exists.txt:should_not_exist.txt", + ]; + + // Mind the inverted order. + let results = vec![ + ( + String::from("guest_string.txt"), + OsString::from("./host_string.txt"), + ), + ( + String::from("guest_string.md.txt"), + OsString::from("/home/user/host_string.txt"), + ), + (String::from("guest_string.conf"), OsString::from("")), + (String::from(""), OsString::from("")), + ( + String::from("also_exists.txt"), + OsString::from("exists.txt"), + ), + ]; + + for (i, host_and_guest_string) in host_guest_strings + .into_iter() + .map(split_guest_and_host_path) + .enumerate() + { + assert_eq!(host_and_guest_string, results[i]); + } +} diff --git a/src/vm.rs b/src/vm.rs index af010d3e..205c0adf 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -22,6 +22,8 @@ use thiserror::Error; 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::{ arch::{self, FrequencyDetectionFailed}, consts::*, @@ -187,7 +189,14 @@ impl UhyveVm { ); let tempdir = create_temp_dir(); + let file_mapping = Mutex::new(UhyveFileMap::new(¶ms.file_mapping)); + #[cfg(feature = "landlock")] + initialize_whitelist( + ¶ms.file_mapping, + kernel_path.to_str().unwrap(), + tempdir.path().to_str().unwrap(), + ); let output = match params.output { params::Output::None => Output::None, @@ -296,6 +305,8 @@ impl UhyveVm { } pub fn load_kernel(&mut self) -> LoadKernelResult<()> { + #[cfg(feature = "landlock")] + enforce_isolation(); let elf = fs::read(self.kernel_path())?; let object = KernelObject::parse(&elf).map_err(LoadKernelError::ParseKernelError)?;