diff --git a/src/tools/kata-ctl/src/arch/aarch64/mod.rs b/src/tools/kata-ctl/src/arch/aarch64/mod.rs index 65e3ed93f2..a6137856fa 100644 --- a/src/tools/kata-ctl/src/arch/aarch64/mod.rs +++ b/src/tools/kata-ctl/src/arch/aarch64/mod.rs @@ -20,7 +20,7 @@ mod arch_specific { // List of check functions static CHECK_LIST: &[CheckItem] = &[CheckItem { - name: CheckType::CheckCpu, + name: CheckType::Cpu, descr: "This parameter performs the host check", fp: check, perm: PermissionType::NonPrivileged, diff --git a/src/tools/kata-ctl/src/arch/s390x/mod.rs b/src/tools/kata-ctl/src/arch/s390x/mod.rs index 276c75a9eb..7a9940dcf9 100644 --- a/src/tools/kata-ctl/src/arch/s390x/mod.rs +++ b/src/tools/kata-ctl/src/arch/s390x/mod.rs @@ -60,7 +60,7 @@ mod arch_specific { // List of check functions static CHECK_LIST: &[CheckItem] = &[CheckItem { - name: CheckType::CheckCpu, + name: CheckType::Cpu, descr: "This parameter performs the cpu check", fp: check, perm: PermissionType::NonPrivileged, diff --git a/src/tools/kata-ctl/src/arch/x86_64/mod.rs b/src/tools/kata-ctl/src/arch/x86_64/mod.rs index 9c8782fa7b..0263126249 100644 --- a/src/tools/kata-ctl/src/arch/x86_64/mod.rs +++ b/src/tools/kata-ctl/src/arch/x86_64/mod.rs @@ -21,16 +21,43 @@ mod arch_specific { const CPUINFO_FLAGS_TAG: &str = "flags"; const CPU_FLAGS_INTEL: &[&str] = &["lm", "sse4_1", "vmx"]; const CPU_ATTRIBS_INTEL: &[&str] = &["GenuineIntel"]; + const VMM_FLAGS: &[&str] = &["hypervisor"]; + pub const ARCH_CPU_VENDOR_FIELD: &str = check::GENERIC_CPU_VENDOR_FIELD; pub const ARCH_CPU_MODEL_FIELD: &str = check::GENERIC_CPU_MODEL_FIELD; // List of check functions - static CHECK_LIST: &[CheckItem] = &[CheckItem { - name: CheckType::CheckCpu, - descr: "This parameter performs the cpu check", - fp: check_cpu, - perm: PermissionType::NonPrivileged, - }]; + static CHECK_LIST: &[CheckItem] = &[ + CheckItem { + name: CheckType::Cpu, + descr: "This parameter performs the cpu check", + fp: check_cpu, + perm: PermissionType::NonPrivileged, + }, + CheckItem { + name: CheckType::KernelModules, + descr: "This parameter performs the kvm check", + fp: check_kernel_modules, + perm: PermissionType::NonPrivileged, + }, + ]; + + static MODULE_LIST: &[KernelModule] = &[ + KernelModule { + name: "kvm", + parameter: KernelParam { + name: "kvmclock_periodic_sync", + value: KernelParamType::Simple("Y"), + }, + }, + KernelModule { + name: "kvm_intel", + parameter: KernelParam { + name: "unrestricted_guest", + value: KernelParamType::Predicate(unrestricted_guest_param_check), + }, + }, + ]; pub fn get_checks() -> Option<&'static [CheckItem<'static>]> { Some(CHECK_LIST) @@ -141,6 +168,120 @@ mod arch_specific { Ok(GuestProtection::NoProtection) } + + fn running_on_vmm() -> Result { + match check::get_single_cpu_info(check::PROC_CPUINFO, CPUINFO_DELIMITER) { + Ok(cpu_info) => { + // check if the 'hypervisor' flag exist in the cpu features + let missing_hypervisor_flag = check::check_cpu_attribs(&cpu_info, VMM_FLAGS)?; + + if missing_hypervisor_flag.is_empty() { + return Ok(true); + } + } + Err(e) => { + return Err(anyhow!( + "Unable to determine if the OS is running on a VM: {}: {}", + e, + check::PROC_CPUINFO + )); + } + } + + Ok(false) + } + + // check the host kernel parameter value is valid + // and check if we are running inside a VMM + fn unrestricted_guest_param_check( + module: &str, + param_name: &str, + param_value_host: &str, + ) -> Result<()> { + let expected_param_value: char = 'Y'; + + let running_on_vmm_alt = running_on_vmm()?; + + if running_on_vmm_alt { + let msg = format!("You are running in a VM, where the kernel module '{}' parameter '{:}' has a value '{:}'. This causes conflict when running kata.", + module, + param_name, + param_value_host + ); + return Err(anyhow!(msg)); + } + + if param_value_host == expected_param_value.to_string() { + Ok(()) + } else { + let error_msg = format!( + "Kernel Module: '{:}' parameter '{:}' should have value '{:}', but found '{:}.'.", + module, param_name, expected_param_value, param_value_host + ); + + let action_msg = format!("Remove the '{:}' module using `rmmod` and then reload using `modprobe`, setting '{:}={:}'", + module, + param_name, + expected_param_value + ); + + return Err(anyhow!("{} {}", error_msg, action_msg)); + } + } + + fn check_kernel_param( + module: &str, + param_name: &str, + param_value_host: &str, + param_type: KernelParamType, + ) -> Result<()> { + match param_type { + KernelParamType::Simple(param_value_req) => { + if param_value_host != param_value_req { + return Err(anyhow!( + "Kernel module '{}': parameter '{}' should have value '{}', but found '{}'", + module, + param_name, + param_value_req, + param_value_host + )); + } + Ok(()) + } + KernelParamType::Predicate(pred_func) => { + pred_func(module, param_name, param_value_host) + } + } + } + + fn check_kernel_modules(_args: &str) -> Result<()> { + println!("INFO: check kernel modules for: x86_64"); + + for module in MODULE_LIST { + let module_loaded = + check::check_kernel_module_loaded(module.name, module.parameter.name); + + match module_loaded { + Ok(param_value_host) => { + let parameter_check = check_kernel_param( + module.name, + module.parameter.name, + ¶m_value_host, + module.parameter.value.clone(), + ); + + match parameter_check { + Ok(_v) => println!("{} Ok", module.name), + Err(e) => return Err(e), + } + } + Err(err) => { + eprintln!("WARNING {:}", err.replace('\n', "")) + } + } + } + Ok(()) + } } #[cfg(target_arch = "x86_64")] diff --git a/src/tools/kata-ctl/src/check.rs b/src/tools/kata-ctl/src/check.rs index 81298d1a20..dfb9a3b7b6 100644 --- a/src/tools/kata-ctl/src/check.rs +++ b/src/tools/kata-ctl/src/check.rs @@ -9,6 +9,10 @@ use anyhow::{anyhow, Result}; use reqwest::header::{CONTENT_TYPE, USER_AGENT}; use serde::{Deserialize, Serialize}; use thiserror::Error; + +#[cfg(any(target_arch = "x86_64"))] +use std::process::{Command, Stdio}; + #[derive(Debug, Deserialize, Serialize, PartialEq)] struct Release { tag_name: String, @@ -17,6 +21,12 @@ struct Release { tarball_url: String, } +#[allow(dead_code)] +const MODPROBE_PATH: &str = "/sbin/modprobe"; + +#[allow(dead_code)] +const MODINFO_PATH: &str = "/sbin/modinfo"; + const KATA_GITHUB_RELEASE_URL: &str = "https://api.github.com/repos/kata-containers/kata-containers/releases"; @@ -29,6 +39,7 @@ const ERR_NO_CPUINFO: &str = "cpu_info string is empty"; #[allow(dead_code)] pub const GENERIC_CPU_VENDOR_FIELD: &str = "vendor_id"; + #[allow(dead_code)] pub const GENERIC_CPU_MODEL_FIELD: &str = "model name"; @@ -36,8 +47,8 @@ pub const GENERIC_CPU_MODEL_FIELD: &str = "model name"; pub const PROC_CPUINFO: &str = "/proc/cpuinfo"; #[cfg(any(target_arch = "s390x", target_arch = "x86_64"))] -fn get_cpu_info(cpu_info_file: &str) -> Result { - let contents = std::fs::read_to_string(cpu_info_file)?; +fn read_file_contents(file_path: &str) -> Result { + let contents = std::fs::read_to_string(file_path)?; Ok(contents) } @@ -45,7 +56,7 @@ fn get_cpu_info(cpu_info_file: &str) -> Result { // the specified cpuinfo file by parsing based on a specified delimiter #[cfg(any(target_arch = "s390x", target_arch = "x86_64"))] pub fn get_single_cpu_info(cpu_info_file: &str, substring: &str) -> Result { - let contents = get_cpu_info(cpu_info_file)?; + let contents = read_file_contents(cpu_info_file)?; if contents.is_empty() { return Err(anyhow!(ERR_NO_CPUINFO)); @@ -57,7 +68,6 @@ pub fn get_single_cpu_info(cpu_info_file: &str, substring: &str) -> Result Result { } if cpu_flags_tag.is_empty() { - return Err(anyhow!("cpu flags delimiter string is empty")); + return Err(anyhow!("cpu flags delimiter string is empty"))?; } let subcontents: Vec<&str> = cpu_info.split('\n').collect(); @@ -222,6 +232,86 @@ pub fn check_official_releases() -> Result<()> { Ok(()) } +#[cfg(any(target_arch = "x86_64"))] +pub fn check_kernel_module_loaded(module: &str, parameter: &str) -> Result { + const MODPROBE_PARAMETERS_DRY_RUN: &str = "--dry-run"; + const MODPROBE_PARAMETERS_FIRST_TIME: &str = "--first-time"; + const MODULES_PATH: &str = "/sys/module"; + + let status_modinfo_success; + + // Partial check w/ modinfo + // verifies that the module exists + match Command::new(MODINFO_PATH) + .arg(module) + .stdout(Stdio::piped()) + .output() + { + Ok(v) => { + status_modinfo_success = v.status.success(); + + // The module is already not loaded. + if !status_modinfo_success { + let msg = String::from_utf8_lossy(&v.stderr).replace('\n', ""); + return Err(msg); + } + } + Err(_e) => { + let msg = format!( + "Command {:} not found, verify that `kmod` package is already installed.", + MODINFO_PATH, + ); + return Err(msg); + } + } + + // Partial check w/ modprobe + // check that the module is already loaded + match Command::new(MODPROBE_PATH) + .arg(MODPROBE_PARAMETERS_DRY_RUN) + .arg(MODPROBE_PARAMETERS_FIRST_TIME) + .arg(module) + .stdout(Stdio::piped()) + .output() + { + Ok(v) => { + // a successful simulated modprobe insert, means the module is not already loaded + let status_modprobe_success = v.status.success(); + + if status_modprobe_success && status_modinfo_success { + // This condition is true in the case that the module exist, but is not already loaded + let msg = format!("The kernel module `{:}` exist but is not already loaded. Try reloading it using 'modprobe {:}=Y'", + module, module + ); + return Err(msg); + } + } + + Err(_e) => { + let msg = format!( + "Command {:} not found, verify that `kmod` package is already installed.", + MODPROBE_PATH, + ); + return Err(msg); + } + } + + let module_path = format!("{}/{}/parameters/{}", MODULES_PATH, module, parameter); + + // Here the currently loaded kernel parameter value + // is retrieved and returned on success + match read_file_contents(&module_path) { + Ok(result) => Ok(result.replace('\n', "")), + Err(_e) => { + let msg = format!( + "'{:}' kernel module parameter `{:}` not found.", + module, parameter + ); + Err(msg) + } + } +} + #[cfg(any(target_arch = "s390x", target_arch = "x86_64"))] #[cfg(test)] mod tests { @@ -413,4 +503,64 @@ mod tests { assert!(!v.minor.to_string().is_empty()); assert!(!v.patch.to_string().is_empty()); } + + #[cfg(any(target_arch = "x86_64"))] + #[test] + fn check_module_loaded() { + #[allow(dead_code)] + #[derive(Debug)] + struct TestData<'a> { + module_name: &'a str, + param_name: &'a str, + param_value: &'a str, + result: Result, + } + + let tests = &[ + // Failure scenarios + TestData { + module_name: "", + param_name: "", + param_value: "", + result: Err(anyhow!("modinfo: ERROR: Module {} not found.", "")), + }, + TestData { + module_name: "kvm", + param_name: "", + param_value: "", + result: Err(anyhow!( + "'{:}' kernel module parameter `{:}` not found.", + "kvm", + "" + )), + }, + // Success scenarios + TestData { + module_name: "kvm", + param_name: "kvmclock_periodic_sync", + param_value: "Y", + result: Ok("Y".to_string()), + }, + ]; + + for (i, d) in tests.iter().enumerate() { + let msg = format!("test[{}]: {:?}", i, d); + let result = check_kernel_module_loaded(d.module_name, d.param_name); + let msg = format!("{}, result: {:?}", msg, result); + + if d.result.is_ok() { + assert_eq!( + result.as_ref().unwrap(), + d.result.as_ref().unwrap(), + "{}", + msg + ); + continue; + } + + let expected_error = format!("{}", &d.result.as_ref().unwrap_err()); + let actual_error = result.unwrap_err().to_string(); + assert!(actual_error == expected_error, "{}", msg); + } + } } diff --git a/src/tools/kata-ctl/src/ops/check_ops.rs b/src/tools/kata-ctl/src/ops/check_ops.rs index fa3aa688b1..f2dbea7025 100644 --- a/src/tools/kata-ctl/src/ops/check_ops.rs +++ b/src/tools/kata-ctl/src/ops/check_ops.rs @@ -73,15 +73,18 @@ pub fn handle_check(checkcmd: CheckArgument) -> Result<()> { match command { CheckSubCommand::All => { // run architecture-specific tests - handle_builtin_check(CheckType::CheckCpu, "")?; + handle_builtin_check(CheckType::Cpu, "")?; // run code that uses network checks check::run_network_checks()?; + + // run kernel module checks + handle_builtin_check(CheckType::KernelModules, "")?; } CheckSubCommand::NoNetworkChecks => { // run architecture-specific tests - handle_builtin_check(CheckType::CheckCpu, "")?; + handle_builtin_check(CheckType::Cpu, "")?; } CheckSubCommand::CheckVersionOnly => { diff --git a/src/tools/kata-ctl/src/types.rs b/src/tools/kata-ctl/src/types.rs index 483e5bce70..26f5954d80 100644 --- a/src/tools/kata-ctl/src/types.rs +++ b/src/tools/kata-ctl/src/types.rs @@ -12,8 +12,9 @@ pub type BuiltinCmdFp = fn(args: &str) -> Result<()>; // CheckType encodes the name of each check provided by kata-ctl. #[derive(Debug, strum_macros::Display, EnumString, PartialEq)] pub enum CheckType { - CheckCpu, - CheckNetwork, + Cpu, + Network, + KernelModules, } // PermissionType is used to show whether a check needs to run with elevated (super-user) @@ -33,3 +34,39 @@ pub struct CheckItem<'a> { pub fp: BuiltinCmdFp, pub perm: PermissionType, } + +// Builtin module parameter check handler type. +// +// BuiltinModuleParamFp represents a predicate function to determine if a +// kernel parameter _value_ is as expected. If not, the returned Error will +// explain what is wrong. +// +// Parameters: +// +// - module: name of kernel module. +// - param: name of parameter for the kernel module. +// - value: value of the kernel parameter. +pub type BuiltinModuleParamFp = fn(module: &str, param: &str, value: &str) -> Result<()>; + +// KernelParamType encodes the value and a handler +// function for kernel module parameters +#[allow(dead_code)] +#[derive(Clone)] +pub enum KernelParamType<'a> { + Simple(&'a str), + Predicate(BuiltinModuleParamFp), +} + +// Parameters is used to encode the module parameters +#[derive(Clone)] +pub struct KernelParam<'a> { + pub name: &'a str, + pub value: KernelParamType<'a>, +} + +// KernelModule is used to describe a kernel module along with its required parameters. +#[allow(dead_code)] +pub struct KernelModule<'a> { + pub name: &'a str, + pub parameter: KernelParam<'a>, +}