Skip to content

Commit

Permalink
feat(landlock): initial support
Browse files Browse the repository at this point in the history
  • Loading branch information
n0toose committed Nov 29, 2024
1 parent 9134d61 commit fcfa50c
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 52 deletions.
34 changes: 33 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ path = "benches/benchmarks.rs"
harness = false

[features]
default = []
default = ["landlock"]
landlock = ["dep:landlock"]
instrument = ["rftrace", "rftrace-frontend"]

[dependencies]
Expand Down Expand Up @@ -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"
Expand Down
53 changes: 3 additions & 50 deletions src/isolation/filemap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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`.
Expand Down
123 changes: 123 additions & 0 deletions src/isolation/landlock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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 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) {
#[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 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(),
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<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(
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()?)
}
52 changes: 52 additions & 0 deletions src/isolation/mod.rs
Original file line number Diff line number Diff line change
@@ -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]);
}
}
7 changes: 7 additions & 0 deletions src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*,
Expand Down Expand Up @@ -187,7 +189,10 @@ impl<VirtBackend: VirtualizationBackend> UhyveVm<VirtBackend> {
);

let tempdir = create_temp_dir();

let file_mapping = Mutex::new(UhyveFileMap::new(&params.file_mapping));
#[cfg(feature = "landlock")]
initialize_whitelist(&params.file_mapping, kernel_path.to_str().unwrap());

let output = match params.output {
params::Output::None => Output::None,
Expand Down Expand Up @@ -296,6 +301,8 @@ impl<VirtBackend: VirtualizationBackend> UhyveVm<VirtBackend> {
}

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)?;

Expand Down

0 comments on commit fcfa50c

Please # to comment.