diff --git a/Cargo.lock b/Cargo.lock index dcd037157..173a77ad6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1118,6 +1118,17 @@ dependencies = [ "vmm-sys-util", ] +[[package]] +name = "landlock" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dafb8a4afee64f167eb2b52d32f0eea002e41a7a6450e68c799c8ec3a81a634c" +dependencies = [ + "enumflags2", + "libc", + "thiserror", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2450,6 +2461,7 @@ dependencies = [ "hypervisor", "igvm", "igvm_defs", + "landlock", "libc", "linux-loader", "log", diff --git a/vmm/Cargo.toml b/vmm/Cargo.toml index ceccbd36e..a05927346 100644 --- a/vmm/Cargo.toml +++ b/vmm/Cargo.toml @@ -39,6 +39,7 @@ hex = { version = "0.4.3", optional = true } hypervisor = { path = "../hypervisor" } igvm = { version = "0.3.1", optional = true } igvm_defs = { version = "0.3.1", optional = true } +landlock = "0.4.0" libc = "0.2.153" linux-loader = { version = "0.11.0", features = ["bzimage", "elf", "pe"] } log = "0.4.21" diff --git a/vmm/src/config.rs b/vmm/src/config.rs index a676dabe8..6a9665e66 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // +use crate::landlock::LandlockAccess; pub use crate::vm_config::*; use clap::ArgMatches; use option_parser::{ @@ -211,6 +212,8 @@ pub enum ValidationError { RestoreNetFdCountMismatch(String, usize, usize), /// Path provided in landlock-rules doesn't exist LandlockPathDoesNotExist(PathBuf), + /// Access provided in landlock-rules in invalid + InvalidLandlockAccess(String), } type ValidationResult = std::result::Result; @@ -369,6 +372,9 @@ impl fmt::Display for ValidationError { s.as_path() ) } + InvalidLandlockAccess(s) => { + write!(f, "{s}") + } } } } @@ -2361,6 +2367,8 @@ impl LandlockConfig { if !self.path.exists() { return Err(ValidationError::LandlockPathDoesNotExist(self.path.clone())); } + LandlockAccess::try_from(self.access.as_str()) + .map_err(|e| ValidationError::InvalidLandlockAccess(e.to_string()))?; Ok(()) } } diff --git a/vmm/src/landlock.rs b/vmm/src/landlock.rs index 8b1378917..aa31bb2bf 100644 --- a/vmm/src/landlock.rs +++ b/vmm/src/landlock.rs @@ -1 +1,149 @@ +// Copyright © 2024 Microsoft Corporation +// +// SPDX-License-Identifier: Apache-2.0 +#[cfg(test)] +use landlock::make_bitflags; +use landlock::{ + path_beneath_rules, Access, AccessFs, BitFlags, Ruleset, RulesetAttr, RulesetCreated, + RulesetCreatedAttr, RulesetError, ABI, +}; +use std::convert::TryFrom; +use std::io::Error as IoError; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum LandlockError { + /// All RulesetErrors from Landlock library are wrapped in this error + #[error("Error creating/adding/restricting ruleset: {0}")] + ManageRuleset(#[source] RulesetError), + + /// Error opening path + #[error("Error opening path: {0}")] + OpenPath(#[source] IoError), + + /// Invalid Landlock access + #[error("Invalid Landlock access: {0}")] + InvalidLandlockAccess(String), +} + +// https://docs.rs/landlock/latest/landlock/enum.ABI.html for more info on ABI +static ABI: ABI = ABI::V3; + +pub struct LandlockAccess { + access: BitFlags, +} + +impl TryFrom<&str> for LandlockAccess { + type Error = LandlockError; + + fn try_from(s: &str) -> Result { + if s.is_empty() { + return Err(LandlockError::InvalidLandlockAccess( + "Access cannot be empty".to_string(), + )); + } + + let mut access = BitFlags::::empty(); + for c in s.chars() { + match c { + 'r' => access |= AccessFs::from_read(ABI), + 'w' => access |= AccessFs::from_write(ABI), + _ => { + return Err(LandlockError::InvalidLandlockAccess( + format!("Invalid access: {c}").to_string(), + )) + } + }; + } + Ok(LandlockAccess { access }) + } +} +pub struct Landlock { + ruleset: RulesetCreated, +} + +impl Landlock { + pub fn new() -> Result { + let file_access = AccessFs::from_all(ABI); + + let def_ruleset = Ruleset::default() + .handle_access(file_access) + .map_err(LandlockError::ManageRuleset)?; + + // By default, rulesets are created in `BestEffort` mode. This lets Landlock + // to enable all the supported rules and silently ignore the unsupported ones. + let ruleset = def_ruleset.create().map_err(LandlockError::ManageRuleset)?; + + Ok(Landlock { ruleset }) + } + + pub fn add_rule( + &mut self, + path: PathBuf, + access: BitFlags, + ) -> Result<(), LandlockError> { + // path_beneath_rules in landlock crate handles file and directory access rules. + // Incoming path/s are passed to path_beneath_rules, so that we don't + // have to worry about the type of the path. + let paths = vec![path.clone()]; + let path_beneath_rules = path_beneath_rules(paths, access); + self.ruleset + .as_mut() + .add_rules(path_beneath_rules) + .map_err(LandlockError::ManageRuleset)?; + Ok(()) + } + + pub fn add_rule_with_access( + &mut self, + path: PathBuf, + access: &str, + ) -> Result<(), LandlockError> { + self.add_rule(path, LandlockAccess::try_from(access)?.access)?; + Ok(()) + } + + pub fn restrict_self(self) -> Result<(), LandlockError> { + self.ruleset + .restrict_self() + .map_err(LandlockError::ManageRuleset)?; + Ok(()) + } +} + +#[test] +fn test_try_from_access() { + // These access rights could change in future versions of Landlock. Listing + // them here explicitly to raise their visibility during code reviews. + let read_access = make_bitflags!(AccessFs::{ + Execute + | ReadFile + | ReadDir + }); + let write_access = make_bitflags!(AccessFs::{ + WriteFile + | RemoveDir + | RemoveFile + | MakeChar + | MakeDir + | MakeReg + | MakeSock + | MakeFifo + | MakeBlock + | MakeSym + | Refer + | Truncate + }); + let landlock_access = LandlockAccess::try_from("rw").unwrap(); + assert!(landlock_access.access == read_access | write_access); + + let landlock_access = LandlockAccess::try_from("r").unwrap(); + assert!(landlock_access.access == read_access); + + let landlock_access = LandlockAccess::try_from("w").unwrap(); + assert!(landlock_access.access == write_access); + + assert!(LandlockAccess::try_from("").is_err()); +}