diff --git a/fuzz/fuzz_targets/http_api.rs b/fuzz/fuzz_targets/http_api.rs index d801c90f1..553c338d5 100644 --- a/fuzz/fuzz_targets/http_api.rs +++ b/fuzz/fuzz_targets/http_api.rs @@ -191,6 +191,7 @@ impl RequestHandler for StubApiRequestHandler { tpm: None, preserved_fds: None, landlock_enable: false, + landlock_config: None, })), state: VmState::Running, memory_actual_size: 0, diff --git a/option_parser/src/lib.rs b/option_parser/src/lib.rs index ac3dd3885..5a8399a94 100644 --- a/option_parser/src/lib.rs +++ b/option_parser/src/lib.rs @@ -23,6 +23,7 @@ pub enum OptionParserError { UnknownOption(String), InvalidSyntax(String), Conversion(String, String), + InvalidValue(String), } impl fmt::Display for OptionParserError { @@ -33,6 +34,7 @@ impl fmt::Display for OptionParserError { OptionParserError::Conversion(field, value) => { write!(f, "unable to convert {value} for {field}") } + OptionParserError::InvalidValue(s) => write!(f, "invalid value: {s}"), } } } diff --git a/src/main.rs b/src/main.rs index 2639ea2a4..0cf5b0fa0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -282,6 +282,13 @@ fn create_app(default_vcpus: String, default_memory: String, default_rng: String .default_value("false") .group("vm-config"), ) + .arg( + Arg::new("landlock-rules") + .long("landlock-rules") + .help(config::LandlockConfig::SYNTAX) + .num_args(1..) + .group("vm-config"), + ) .arg( Arg::new("net") .long("net") @@ -1044,6 +1051,7 @@ mod unit_tests { tpm: None, preserved_fds: None, landlock_enable: false, + landlock_config: None, }; assert_eq!(expected_vm_config, result_vm_config); diff --git a/vmm/src/config.rs b/vmm/src/config.rs index b47ca37dd..a676dabe8 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -110,6 +110,10 @@ pub enum Error { ParseTpm(OptionParserError), /// Missing path for TPM device ParseTpmPathMissing, + /// Error parsing Landlock rules + ParseLandlockRules(OptionParserError), + /// Missing fields in Landlock rules + ParseLandlockMissingFields, } #[derive(Debug, PartialEq, Eq, Error)] @@ -205,6 +209,8 @@ pub enum ValidationError { RestoreMissingRequiredNetId(String), /// Number of FDs passed during Restore are incorrect to the NetConfig RestoreNetFdCountMismatch(String, usize, usize), + /// Path provided in landlock-rules doesn't exist + LandlockPathDoesNotExist(PathBuf), } type ValidationResult = std::result::Result; @@ -356,6 +362,13 @@ impl fmt::Display for ValidationError { "Number of Net FDs passed for '{s}' during Restore: {u1}. Expected: {u2}" ) } + LandlockPathDoesNotExist(s) => { + write!( + f, + "Path {:?} provided in landlock-rules does not exist", + s.as_path() + ) + } } } } @@ -421,6 +434,11 @@ impl fmt::Display for Error { ParseVdpaPathMissing => write!(f, "Error parsing --vdpa: path missing"), ParseTpm(o) => write!(f, "Error parsing --tpm: {o}"), ParseTpmPathMissing => write!(f, "Error parsing --tpm: path missing"), + ParseLandlockRules(o) => write!(f, "Error parsing --landlock-rules: {o}"), + ParseLandlockMissingFields => write!( + f, + "Error parsing --landlock-rules: path/access field missing" + ), } } } @@ -473,6 +491,7 @@ pub struct VmParams<'a> { #[cfg(feature = "sev_snp")] pub host_data: Option<&'a str>, pub landlock_enable: bool, + pub landlock_config: Option>, } impl<'a> VmParams<'a> { @@ -539,6 +558,10 @@ impl<'a> VmParams<'a> { #[cfg(feature = "sev_snp")] let host_data = args.get_one::("host-data").map(|x| x as &str); let landlock_enable = args.get_flag("landlock"); + let landlock_config: Option> = args + .get_many::("landlock-rules") + .map(|x| x.map(|y| y as &str).collect()); + VmParams { cpus, memory, @@ -577,6 +600,7 @@ impl<'a> VmParams<'a> { #[cfg(feature = "sev_snp")] host_data, landlock_enable, + landlock_config, } } } @@ -2304,6 +2328,43 @@ impl TpmConfig { } } +impl LandlockConfig { + pub const SYNTAX: &'static str = "Landlock parameters \ + \"path=,access=[rw]\""; + + pub fn parse(landlock_rule: &str) -> Result { + let mut parser = OptionParser::new(); + parser.add("path").add("access"); + parser + .parse(landlock_rule) + .map_err(Error::ParseLandlockRules)?; + + let path = parser + .get("path") + .map(PathBuf::from) + .ok_or(Error::ParseLandlockMissingFields)?; + + let access = parser + .get("access") + .ok_or(Error::ParseLandlockMissingFields)?; + + if access.chars().count() > 2 { + return Err(Error::ParseLandlockRules(OptionParserError::InvalidValue( + access.to_string(), + ))); + } + + Ok(LandlockConfig { path, access }) + } + + pub fn validate(&self) -> ValidationResult<()> { + if !self.path.exists() { + return Err(ValidationError::LandlockPathDoesNotExist(self.path.clone())); + } + Ok(()) + } +} + impl VmConfig { fn validate_identifier( id_list: &mut BTreeSet, @@ -2656,6 +2717,12 @@ impl VmConfig { .map(|p| p.iommu_segments.is_some()) .unwrap_or_default(); + if let Some(landlock_configs) = &self.landlock_config { + for landlock_config in landlock_configs { + landlock_config.validate()?; + } + } + Ok(id_list) } @@ -2826,6 +2893,16 @@ impl VmConfig { #[cfg(feature = "guest_debug")] let gdb = vm_params.gdb; + let mut landlock_config: Option> = None; + if let Some(ll_config) = vm_params.landlock_config { + landlock_config = Some( + ll_config + .iter() + .map(|rule| LandlockConfig::parse(rule)) + .collect::>>()?, + ); + } + let mut config = VmConfig { cpus: CpusConfig::parse(vm_params.cpus)?, memory: MemoryConfig::parse(vm_params.memory, vm_params.memory_zones)?, @@ -2858,6 +2935,7 @@ impl VmConfig { tpm, preserved_fds: None, landlock_enable: vm_params.landlock_enable, + landlock_config, }; config.validate().map_err(Error::Validation)?; Ok(config) @@ -2984,6 +3062,7 @@ impl Clone for VmConfig { .as_ref() // SAFETY: FFI call with valid FDs .map(|fds| fds.iter().map(|fd| unsafe { libc::dup(*fd) }).collect()), + landlock_config: self.landlock_config.clone(), ..*self } } @@ -3783,6 +3862,7 @@ mod tests { }, ]), landlock_enable: false, + landlock_config: None, }; let valid_config = RestoreConfig { @@ -3972,6 +4052,7 @@ mod tests { tpm: None, preserved_fds: None, landlock_enable: false, + landlock_config: None, }; assert!(valid_config.validate().is_ok()); @@ -4529,4 +4610,20 @@ mod tests { } let _still_valid_config = still_valid_config.clone(); } + #[test] + fn test_landlock_parsing() -> Result<()> { + // should not be empty + assert!(LandlockConfig::parse("").is_err()); + // access should not be empty + assert!(LandlockConfig::parse("path=/dir/path1").is_err()); + assert!(LandlockConfig::parse("path=/dir/path1,access=rwr").is_err()); + assert_eq!( + LandlockConfig::parse("path=/dir/path1,access=rw")?, + LandlockConfig { + path: PathBuf::from("/dir/path1"), + access: "rw".to_string(), + } + ); + Ok(()) + } } diff --git a/vmm/src/landlock.rs b/vmm/src/landlock.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/vmm/src/landlock.rs @@ -0,0 +1 @@ + diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index cd38060ca..fa0ea4a15 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -75,6 +75,7 @@ mod gdb; #[cfg(feature = "igvm")] mod igvm; pub mod interrupt; +pub mod landlock; pub mod memory_manager; pub mod migration; mod pci_segment; @@ -2190,6 +2191,7 @@ mod unit_tests { tpm: None, preserved_fds: None, landlock_enable: false, + landlock_config: None, })) } diff --git a/vmm/src/vm_config.rs b/vmm/src/vm_config.rs index ea3a588cc..194d19f8f 100644 --- a/vmm/src/vm_config.rs +++ b/vmm/src/vm_config.rs @@ -598,6 +598,12 @@ pub struct TpmConfig { pub socket: PathBuf, } +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct LandlockConfig { + pub path: PathBuf, + pub access: String, +} + #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct VmConfig { #[serde(default)] @@ -647,4 +653,5 @@ pub struct VmConfig { pub preserved_fds: Option>, #[serde(default)] pub landlock_enable: bool, + pub landlock_config: Option>, }