Merge pull request #6295 from dborquez/add_kernel_module_checks_kvm

kata-ctl: checks for kvm, kvm_intel modules loaded
This commit is contained in:
David Esparza 2023-04-24 13:33:18 -06:00 committed by GitHub
commit 7fdaab49bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 348 additions and 17 deletions

View File

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

View File

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

View File

@ -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<bool> {
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,
&param_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")]

View File

@ -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<String> {
let contents = std::fs::read_to_string(cpu_info_file)?;
fn read_file_contents(file_path: &str) -> Result<String> {
let contents = std::fs::read_to_string(file_path)?;
Ok(contents)
}
@ -45,7 +56,7 @@ fn get_cpu_info(cpu_info_file: &str) -> Result<String> {
// 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<String> {
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<Strin
.ok_or("error splitting contents of cpuinfo")
.map_err(|e| anyhow!(e))?
.to_string();
Ok(result)
}
@ -70,7 +80,7 @@ pub fn get_cpu_flags(cpu_info: &str, cpu_flags_tag: &str) -> Result<String> {
}
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<String, String> {
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<String>,
}
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);
}
}
}

View File

@ -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 => {

View File

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