Skip to content

Commit

Permalink
feat(landlock): initial proof of concept
Browse files Browse the repository at this point in the history
  • Loading branch information
n0toose committed Nov 30, 2024
1 parent 9134d61 commit c83f9f7
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 62 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
27 changes: 17 additions & 10 deletions src/bin/uhyve.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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() {
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
125 changes: 125 additions & 0 deletions src/isolation/landlock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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, 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<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]);
}
}
Loading

0 comments on commit c83f9f7

Please sign in to comment.