Skip to content

Commit 7d3389f

Browse files
committed
fixup
1 parent a4a5643 commit 7d3389f

8 files changed

Lines changed: 326 additions & 288 deletions

File tree

hack/debug/local.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ docker run --privileged --pid="host" \
1111
-e RUST_LOG="debug" \
1212
-e EDERA_PREFLIGHT_VERBOSE=true \
1313
-e EDERA_PREFLIGHT_TARGET_DIR='/host' \
14-
-e EDERA_PREFLIGHT_SKIP_GROUPS='ScriptedChecks' \
14+
-e EDERA_PREFLIGHT_SKIP_GROUPS='' \
1515
-e EDERA_PREFLIGHT_SCRIPTS_DIR=/scripts \
1616
"${REGISTRY}/${USERIMG}-preflight-debug:${VER}"

src/checkers/kernel.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::{fs, path::PathBuf, process::Command};
1414
const GROUP_IDENTIFIER: &str = "KernelChecks";
1515
const NAME: &str = "Kernel Checks";
1616
// TODO (bml) assemble actual list
17-
const REQUIRED_MODULES: &[&str] = &["nf_tables"];
17+
const REQUIRED_MODULES: &[&str] = &["nf_tables", "msr"];
1818

1919
pub struct KernelChecks {
2020
host_executor: HostNamespaceExecutor,
@@ -104,8 +104,8 @@ impl KernelChecks {
104104
/// Looks at builtins for kernel_version and compares that to the list of
105105
/// required modules.
106106
/// Returns a vec of everything from required_modules that WAS NOT found in builtins.
107-
async fn find_builtins(&self, required_modules: &Vec<String>) -> Result<Vec<String>> {
108-
let mut modules_to_find: Vec<String> = required_modules.clone();
107+
async fn find_builtins(&self, required_modules: &[String]) -> Result<Vec<String>> {
108+
let mut modules_to_find: Vec<String> = required_modules.to_owned();
109109

110110
// read host builtins
111111
let builtins = self
@@ -141,8 +141,8 @@ impl KernelChecks {
141141
/// Looks at loaded modules for the current host kernel and compares that to the list of
142142
/// required modules.
143143
/// Returns a vec of everything from required_modules that WAS NOT loaded.
144-
async fn find_loaded(&self, required_modules: &Vec<String>) -> Result<Vec<String>> {
145-
let mut modules_to_find: Vec<String> = required_modules.clone();
144+
async fn find_loaded(&self, required_modules: &[String]) -> Result<Vec<String>> {
145+
let mut modules_to_find: Vec<String> = required_modules.to_owned();
146146

147147
let modules = self
148148
.host_executor

src/checkers/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
pub mod kernel;
2-
pub mod script;
2+
pub mod pvh;
33
pub mod system;

src/checkers/pvh.rs

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
use crate::helpers::{
2+
CheckGroup, CheckGroupResult, CheckResult,
3+
CheckResultValue::{Errored, Failed, Passed, Skipped},
4+
host_executor::HostNamespaceExecutor,
5+
};
6+
use anyhow::{Result, bail};
7+
use async_trait::async_trait;
8+
use futures::{FutureExt, future::join_all};
9+
use log::{debug, warn};
10+
use std::{
11+
fs,
12+
fs::File,
13+
io::{Read, Seek, SeekFrom},
14+
path::Path,
15+
process::Command,
16+
};
17+
18+
const GROUP_IDENTIFIER: &str = "PVHChecks";
19+
const NAME: &str = "PVH Checks";
20+
21+
#[derive(Debug, PartialEq)]
22+
enum VirtStatus {
23+
Enabled, // Currently active
24+
CanBeEnabled, // Available but not active
25+
Disabled, // Not available or BIOS disabled
26+
}
27+
28+
pub struct PVHChecks {
29+
host_executor: HostNamespaceExecutor,
30+
}
31+
32+
#[cfg(target_arch = "x86_64")]
33+
impl PVHChecks {
34+
pub fn new(host_executor: HostNamespaceExecutor) -> Self {
35+
PVHChecks { host_executor }
36+
}
37+
38+
/// Run all the checkers asynchronously, then
39+
/// join and collect the results.
40+
pub async fn run_all(&self) -> CheckGroupResult {
41+
let results = join_all([self.check_virtualization().boxed()]).await;
42+
43+
let mut group_result = Skipped;
44+
for res in results.iter() {
45+
// Set group result to Failed if we failed and aren't already in an Errored state
46+
if !matches!(group_result, Errored(_)) && matches!(res.result, Failed(_)) {
47+
group_result = Failed(String::from("group failed"));
48+
}
49+
50+
if matches!(res.result, Errored(_)) {
51+
group_result = Errored(String::from("group errored"));
52+
}
53+
}
54+
55+
CheckGroupResult {
56+
name: NAME.to_string(),
57+
result: group_result,
58+
results,
59+
}
60+
}
61+
62+
async fn ensure_msr_modprobe(&self) {
63+
let _ = self
64+
.host_executor
65+
.spawn_in_host_ns(async {
66+
// Load msr kernel module (ignore errors)
67+
Command::new("modprobe").arg("msr").output()
68+
})
69+
.await;
70+
}
71+
72+
async fn check_virtualization(&self) -> CheckResult {
73+
let name = String::from("PVH Support");
74+
75+
self.ensure_msr_modprobe().await;
76+
77+
match self.discover_cpu_virtualization().await {
78+
Ok(VirtStatus::Enabled) | Ok(VirtStatus::CanBeEnabled) => {
79+
debug!("Virtualization is enabled or can be enabled");
80+
CheckResult::new(&name, Passed)
81+
}
82+
Ok(VirtStatus::Disabled) => {
83+
debug!("Virtualization disabled");
84+
CheckResult::new(
85+
&name,
86+
Failed(String::from("PVH Not Supported, Virtualization Disabled")),
87+
)
88+
}
89+
Err(e) => {
90+
eprintln!("Error: {}", e);
91+
CheckResult::new(&name, Errored(e.to_string()))
92+
}
93+
}
94+
}
95+
96+
async fn discover_cpu_virtualization(&self) -> Result<VirtStatus> {
97+
let cpuinfo = self
98+
.host_executor
99+
.spawn_in_host_ns(async { fs::read_to_string("/proc/cpuinfo") })
100+
.await??;
101+
102+
let cpu_vendor = extract_cpu_vendor(&cpuinfo);
103+
let flags = extract_flags(&cpuinfo);
104+
105+
match cpu_vendor.as_str() {
106+
"GenuineIntel" => self.check_intel(&flags).await,
107+
"AuthenticAMD" => self.check_amd(&flags).await,
108+
_ => {
109+
eprintln!("Unknown CPU vendor: {}", cpu_vendor);
110+
Ok(VirtStatus::Disabled)
111+
}
112+
}
113+
}
114+
115+
async fn check_intel(&self, flags: &str) -> Result<VirtStatus> {
116+
let has_vmx = flags.split_whitespace().any(|f| f == "vmx");
117+
let cpuid = 0; // TODO(bml) check all CPUs
118+
let under_hypervisor = flags.split_whitespace().any(|f| f == "hypervisor");
119+
120+
// Always try to read MSRs to report BIOS settings, even if CPU lacks support
121+
match self.read_msr(0x3a, cpuid).await {
122+
Ok(val) => {
123+
let lock = val & 1;
124+
let vmx_outside_smx = (val >> 2) & 1;
125+
let hw_supports = lock == 1 && vmx_outside_smx == 1;
126+
debug!(
127+
"IA32_FEATURE_CONTROL=0x{:x} (lock={}, vmx_outside_smx={})",
128+
val, lock, vmx_outside_smx
129+
);
130+
131+
if !hw_supports {
132+
debug!("Intel VT-x disabled in BIOS or not supported by hardware");
133+
Ok(VirtStatus::Disabled)
134+
} else if has_vmx {
135+
debug!("Intel VT-x supported and available (vmx flag present)");
136+
Ok(VirtStatus::Enabled)
137+
} else if under_hypervisor {
138+
debug!("Intel VT-x supported but unavailable under hypervisor");
139+
Ok(VirtStatus::CanBeEnabled)
140+
} else {
141+
debug!("Intel VT-x supported by system but not current CPU");
142+
Ok(VirtStatus::Disabled)
143+
}
144+
}
145+
Err(e) => {
146+
debug!("error reading MSR registers: {e}");
147+
warn!("could not read MSR registers, falling back to cpuinfo detection");
148+
if !has_vmx {
149+
debug!("CPU does not support Intel VT-x");
150+
Ok(VirtStatus::Disabled)
151+
} else {
152+
debug!("vmx flag present, assuming hardware supports it");
153+
Ok(VirtStatus::CanBeEnabled)
154+
}
155+
}
156+
}
157+
}
158+
159+
async fn check_amd(&self, flags: &str) -> Result<VirtStatus> {
160+
let has_svm = flags.split_whitespace().any(|f| f == "svm");
161+
let cpuid = 0; // TODO(bml) check all CPUs
162+
let under_hypervisor = flags.split_whitespace().any(|f| f == "hypervisor");
163+
164+
// Always try to read MSRs to report BIOS settings, even if CPU lacks support
165+
match self.read_msr(0xC0010114, cpuid).await {
166+
Ok(vmcr) => {
167+
let svmdis = (vmcr >> 4) & 1;
168+
let hw_supports = svmdis == 0;
169+
debug!("VM_CR=0x{:x} (svmdis={})", vmcr, svmdis);
170+
171+
if !hw_supports {
172+
debug!("AMD-V disabled in BIOS or not supported by hardware");
173+
Ok(VirtStatus::Disabled)
174+
} else {
175+
// Hardware supports AMD-V - check current state
176+
match self.read_msr(0xC0000080, cpuid).await {
177+
Ok(efer) => {
178+
let svme = (efer >> 12) & 1;
179+
debug!("EFER=0x{:x} (SVME={})", efer, svme);
180+
if has_svm {
181+
if svme == 1 {
182+
debug!("AMD-V currently enabled and active");
183+
Ok(VirtStatus::Enabled)
184+
} else {
185+
debug!("AMD-V supported and available (can be enabled)");
186+
Ok(VirtStatus::CanBeEnabled)
187+
}
188+
} else if under_hypervisor && (svme == 0 || svme == 1) {
189+
debug!("AMD-V supported but unavailable under hypervisor");
190+
// Hardware supports but flag absent - likely masked by hypervisor
191+
Ok(VirtStatus::CanBeEnabled)
192+
} else {
193+
debug!("AMD-V supported by system but not current CPU");
194+
Ok(VirtStatus::Disabled)
195+
}
196+
}
197+
Err(e) => {
198+
eprintln!("WARN: Cannot read EFER: {}", e);
199+
if has_svm {
200+
eprintln!(
201+
" Hardware supports AMD-V, assuming it can be enabled"
202+
);
203+
Ok(VirtStatus::CanBeEnabled)
204+
} else {
205+
eprintln!(" Cannot determine if AMD-V can be used");
206+
Ok(VirtStatus::Disabled)
207+
}
208+
}
209+
}
210+
}
211+
}
212+
Err(e) => {
213+
debug!("error reading MSR registers: {e}");
214+
warn!("could not read MSR registers, falling back to cpuinfo detection");
215+
if !has_svm {
216+
debug!("CPU does not support AMD-V");
217+
Ok(VirtStatus::Disabled)
218+
} else {
219+
debug!("svm flag present, assuming hardware supports it");
220+
Ok(VirtStatus::CanBeEnabled)
221+
}
222+
}
223+
}
224+
}
225+
226+
async fn read_msr(&self, msr: u32, cpuid: u32) -> Result<u64> {
227+
let result = self
228+
.host_executor
229+
.spawn_in_host_ns(async move {
230+
let msr_path = format!("/dev/cpu/{}/msr", cpuid);
231+
232+
// Check if MSR device exists
233+
if !Path::new(&msr_path).exists() {
234+
bail!(format!(
235+
"Failed to read MSR 0x{:x}: /dev/cpu/0/msr doesn't exist, load 'msr' kernel module",
236+
msr
237+
));
238+
}
239+
240+
// Open and read from the MSR device file
241+
let mut file = File::open(msr_path)?;
242+
file.seek(SeekFrom::Start(msr as u64))?;
243+
244+
let mut buffer = [0u8; 8];
245+
file.read_exact(&mut buffer)?;
246+
247+
// Convert little-endian bytes to u64
248+
Ok(u64::from_le_bytes(buffer))
249+
}).await??;
250+
251+
Ok(result)
252+
}
253+
}
254+
255+
// No-op for other archs
256+
// TODO(bml) arm64 PVH??
257+
#[cfg(not(target_arch = "x86_64"))]
258+
impl PVHChecks {
259+
pub fn new(host_executor: HostNamespaceExecutor) -> Self {
260+
PVHChecks { host_executor }
261+
}
262+
263+
pub async fn run_all(&self) -> CheckGroupResult {
264+
CheckGroupResult::new(NAME)
265+
}
266+
}
267+
268+
#[async_trait]
269+
impl CheckGroup for PVHChecks {
270+
fn id(&self) -> &str {
271+
GROUP_IDENTIFIER
272+
}
273+
274+
fn name(&self) -> &str {
275+
NAME
276+
}
277+
278+
fn description(&self) -> &str {
279+
"PVH capability checks"
280+
}
281+
282+
async fn run(&self) -> CheckGroupResult {
283+
self.run_all().await
284+
}
285+
}
286+
287+
fn extract_cpu_vendor(cpuinfo: &str) -> String {
288+
for line in cpuinfo.lines() {
289+
if line.starts_with("vendor_id")
290+
&& let Some(value) = line.split(':').nth(1)
291+
{
292+
return value.trim().to_string();
293+
}
294+
}
295+
String::from("Unknown")
296+
}
297+
298+
fn extract_flags(cpuinfo: &str) -> String {
299+
for line in cpuinfo.lines() {
300+
if line.starts_with("flags")
301+
&& let Some(value) = line.split(':').nth(1)
302+
{
303+
return value.trim().to_string();
304+
}
305+
}
306+
String::new()
307+
}

0 commit comments

Comments
 (0)