Skip to content

Commit 391573d

Browse files
authored
fix(virtualized): detect gVisor more reliably, and introduce a maybe present state (#34)
Inherently, detecting running under virtualization is hard on some virtualization runtimes. This change attempts to improve that situation by detecting container runtime env vars, and detecting gVisor using /proc/cmdline. Kata Containers cannot be reliably determined using this version of Am I Isolated, but the fallback uptime check test might present a maybe present result, to indicate that signs were detected.
1 parent 2587606 commit 391573d

File tree

3 files changed

+111
-35
lines changed

3 files changed

+111
-35
lines changed

src/cap.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ impl Test for CapTest {
4141
let mut result = CapResult { flags: 0 };
4242

4343
if let Ok(stat) = read_file_as_tuples("/proc/self/status") {
44-
if let Ok(flags) = u64::from_str_radix(stat["CapAmb"].as_str(), 16) {
45-
result.flags |= flags;
44+
if stat.contains_key("CapAmb") {
45+
if let Ok(flags) = u64::from_str_radix(stat["CapAmb"].as_str(), 16) {
46+
result.flags |= flags;
47+
}
4648
}
4749

4850
if let Ok(flags) = u64::from_str_radix(stat["CapEff"].as_str(), 16) {

src/util.rs

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use std::collections::HashMap;
21
use std::fs::read_to_string;
32
use std::path::Path;
43
use std::ptr::addr_of_mut;
4+
use std::{collections::HashMap, fs};
55

66
use libc::uname;
77

@@ -68,3 +68,21 @@ pub fn kernel_release_info() -> (String, (u32, u32, u32)) {
6868

6969
(release, (parts[0], parts[1], parts[2]))
7070
}
71+
72+
pub fn kernel_cmdline() -> Vec<String> {
73+
fs::read_to_string("/proc/cmdline")
74+
.ok()
75+
.unwrap_or_default()
76+
.split(" ")
77+
.map(|part| part.to_string())
78+
.collect()
79+
}
80+
81+
pub fn is_running_gvisor() -> bool {
82+
let cmdline = kernel_cmdline();
83+
if let Some(first) = cmdline.first() {
84+
first.starts_with("BOOT_IMAGE=/vmlinuz-") && first.ends_with("-gvisor")
85+
} else {
86+
false
87+
}
88+
}

src/virtualized.rs

+88-32
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,80 @@
1+
/// Container Virtualization Test
2+
///
3+
/// It is very difficult to detect from within a container whether we are running
4+
/// under virtualization. Am I Isolated relies on heuristics to detect virtualization.
5+
/// This is an incremental process, we expect this test to sometimes fail, particularly
6+
/// in systems like Kata Containers which have no easy way to detect.
7+
///
8+
/// There is a simple fallback mechanism that can give a "maybe" result (represented as a failure.)
9+
/// We read /proc/uptime to attempt to get the system uptime. If the uptime is less than 60 seconds,
10+
/// we assume that the container might be running under virtualization. This makes the assumption
11+
/// that container lifetime is closely related to VM lifetime. In some VM systems, this might not
12+
/// be true. In microVMs, like Edera Protect or firecracker, this should be true if the container
13+
/// is running inside a single Linux kernel boot.
114
use anyhow::Result;
215

3-
use crate::{util::read_file_as_space_separated_lines, Test, TestCategory, TestResult};
16+
use crate::{
17+
util::{is_running_gvisor, read_file_as_space_separated_lines},
18+
Test, TestCategory, TestResult,
19+
};
420

5-
pub struct VirtualizedTest {}
21+
const KNOWN_VIRT_RUNTIMES: &[&'static str] = &["edera"];
22+
23+
#[derive(Default, Debug, PartialEq, Eq)]
24+
pub enum VirtualizationEnabled {
25+
DefinitelyPresent(String),
26+
MaybePresent,
27+
#[default]
28+
NotPresent,
29+
}
630

731
#[derive(Default)]
832
pub struct VirtualizedResult {
9-
pub visible: bool,
10-
pub uptime: u64,
33+
pub enabled: VirtualizationEnabled,
34+
}
35+
36+
pub struct VirtualizedTest;
37+
38+
impl VirtualizedTest {
39+
pub fn check_definite_runtime_env(&self) -> Option<String> {
40+
let container_runtime = std::env::var("container").unwrap_or_default();
41+
KNOWN_VIRT_RUNTIMES
42+
.iter()
43+
.find(|runtime| **runtime == container_runtime.as_str())
44+
.map(|runtime| runtime.to_string())
45+
}
46+
47+
pub fn check_definite_gvisor(&self) -> Option<String> {
48+
if is_running_gvisor() {
49+
Some("gvisor".to_string())
50+
} else {
51+
None
52+
}
53+
}
54+
55+
pub fn check_maybe_present(&self) -> bool {
56+
let Ok(lines) = read_file_as_space_separated_lines("/proc/uptime") else {
57+
return false;
58+
};
59+
60+
if lines.is_empty() {
61+
return false;
62+
}
63+
64+
let line = &lines[0];
65+
if line.len() != 2 {
66+
return false;
67+
}
68+
69+
let Ok(uptime) = line[0].parse::<f64>() else {
70+
return false;
71+
};
72+
73+
if uptime < 60.0 {
74+
return true;
75+
}
76+
false
77+
}
1178
}
1279

1380
impl Test for VirtualizedTest {
@@ -16,22 +83,18 @@ impl Test for VirtualizedTest {
1683
}
1784

1885
fn run(&self) -> Result<Box<dyn TestResult>, ()> {
19-
let mut result = VirtualizedResult {
20-
visible: false,
21-
uptime: 0,
86+
let enabled = if let Some(definite_runtime) = self
87+
.check_definite_runtime_env()
88+
.or(self.check_definite_gvisor())
89+
{
90+
VirtualizationEnabled::DefinitelyPresent(definite_runtime)
91+
} else if self.check_maybe_present() {
92+
VirtualizationEnabled::MaybePresent
93+
} else {
94+
VirtualizationEnabled::NotPresent
2295
};
23-
if let Ok(lines) = read_file_as_space_separated_lines("/proc/uptime") {
24-
if !lines.is_empty() {
25-
let line = &lines[0];
26-
if line.len() == 2 {
27-
if let Ok(uptime) = line[0].parse::<f64>() {
28-
result.visible = true;
29-
result.uptime = uptime as u64;
30-
}
31-
}
32-
}
33-
}
34-
Ok(Box::new(result))
96+
97+
Ok(Box::new(VirtualizedResult { enabled }))
3598
}
3699

37100
fn category(&self) -> crate::TestCategory {
@@ -41,29 +104,22 @@ impl Test for VirtualizedTest {
41104

42105
impl TestResult for VirtualizedResult {
43106
fn success(&self) -> bool {
44-
!self.visible || self.uptime <= 10
107+
matches!(self.enabled, VirtualizationEnabled::DefinitelyPresent(_))
45108
}
46109

47110
fn explain(&self) -> String {
48-
if self.success() {
49-
return "separate kernel used for each container".to_string();
111+
match &self.enabled {
112+
VirtualizationEnabled::DefinitelyPresent(runtime) => format!("virtualization runtime '{}' found", runtime),
113+
VirtualizationEnabled::MaybePresent => "signs of virtualization detected, but couldn't definitively determine a virtualization method".to_string(),
114+
VirtualizationEnabled::NotPresent => "virtualization not detected".to_string(),
50115
}
51-
"without virtualization, the kernel state is shared, opening escape attacks".to_string()
52116
}
53117

54118
fn fault_code(&self) -> String {
55119
"AII2280".to_string()
56120
}
57121

58122
fn as_string(&self) -> String {
59-
if !self.visible {
60-
return "result not reliable".to_string();
61-
}
62-
63-
if self.uptime <= 10 {
64-
return "virtualization in use".to_string();
65-
}
66-
67-
"virtualization not in use".to_string()
123+
self.explain()
68124
}
69125
}

0 commit comments

Comments
 (0)