vmm: Introduce landlock-rules cmdline param

Users can use this parameter to pass extra paths that 'vmm' and its
child threads can use at runtime. Hotplug is the primary usecase for
this parameter.

In order to hotplug devices that use local files: disks, memory zones,
pmem devices etc, users can use this option to pass the path/s that will
be used during hotplug while starting cloud-hypervisor. Doing this will
allow landlock to add required rules to grant access to these paths when
cloud-hypervisor process starts.

Signed-off-by: Praveen K Paladugu <prapal@linux.microsoft.com>
Signed-off-by: Wei Liu <liuwe@microsoft.com>
This commit is contained in:
Praveen K Paladugu 2024-02-15 23:36:23 +00:00 committed by Liu Wei
parent 287dbd4fc9
commit 1d89f98edf
7 changed files with 118 additions and 0 deletions

View File

@ -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,

View File

@ -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}"),
}
}
}

View File

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

View File

@ -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<T> = std::result::Result<T, ValidationError>;
@ -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<Vec<&'a str>>,
}
impl<'a> VmParams<'a> {
@ -539,6 +558,10 @@ impl<'a> VmParams<'a> {
#[cfg(feature = "sev_snp")]
let host_data = args.get_one::<String>("host-data").map(|x| x as &str);
let landlock_enable = args.get_flag("landlock");
let landlock_config: Option<Vec<&str>> = args
.get_many::<String>("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=<path/to/{file/dir}>,access=[rw]\"";
pub fn parse(landlock_rule: &str) -> Result<Self> {
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<String>,
@ -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<Vec<LandlockConfig>> = None;
if let Some(ll_config) = vm_params.landlock_config {
landlock_config = Some(
ll_config
.iter()
.map(|rule| LandlockConfig::parse(rule))
.collect::<Result<Vec<LandlockConfig>>>()?,
);
}
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(())
}
}

1
vmm/src/landlock.rs Normal file
View File

@ -0,0 +1 @@

View File

@ -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,
}))
}

View File

@ -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<Vec<i32>>,
#[serde(default)]
pub landlock_enable: bool,
pub landlock_config: Option<Vec<LandlockConfig>>,
}