Skip to content

Commit ab8946a

Browse files
mtjhrcslp
authored andcommitted
tests: add virtiofs-root-ro test
Verify that krun_add_virtiofs3 with KRUN_FS_ROOT_TAG and read_only=true correctly exposes the root filesystem as read-only to the guest. Signed-off-by: Matej Hrica <[email protected]>
1 parent 8666bab commit ab8946a

3 files changed

Lines changed: 222 additions & 1 deletion

File tree

tests/test_cases/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ name = "test_cases"
1212
[dependencies]
1313
krun-sys = { path = "../../krun-sys", optional = true }
1414
macros = { path = "../macros" }
15-
nix = { version = "0.29.0", features = ["socket"] }
15+
nix = { version = "0.29.0", features = ["fs", "socket"] }
1616
anyhow = "1.0.95"
1717
tempdir = "0.3.7"

tests/test_cases/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ use test_tsi_tcp_guest_listen::TestTsiTcpGuestListen;
1313
mod test_multiport_console;
1414
use test_multiport_console::TestMultiportConsole;
1515

16+
mod test_virtiofs_root_ro;
17+
use test_virtiofs_root_ro::TestVirtiofsRootRo;
18+
1619
pub enum ShouldRun {
1720
Yes,
1821
No(&'static str),
@@ -56,6 +59,7 @@ pub fn test_cases() -> Vec<TestCase> {
5659
Box::new(TestTsiTcpGuestListen::new()),
5760
),
5861
TestCase::new("multiport-console", Box::new(TestMultiportConsole)),
62+
TestCase::new("virtiofs-root-ro", Box::new(TestVirtiofsRootRo)),
5963
]
6064
}
6165

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// NOTE: This is a smoke test that asserts basic mutation operations fail on a read-only
2+
// virtiofs root. It is not exhaustive.For a security sensitive test it would also be better
3+
// to bypass the guest kernel and execute the virtiofs commands directly.
4+
5+
use macros::{guest, host};
6+
7+
pub struct TestVirtiofsRootRo;
8+
9+
const TEST_FILE: &str = "test-file";
10+
const TEST_CONTENT: &[u8] = b"original content";
11+
const EMPTY_DIR: &str = "empty-dir";
12+
13+
#[host]
14+
mod host {
15+
use super::*;
16+
17+
use crate::common::setup_rootfs;
18+
use crate::{krun_call, krun_call_u32};
19+
use crate::{Test, TestSetup};
20+
use krun_sys::*;
21+
use std::ffi::CString;
22+
use std::fs;
23+
use std::os::unix::ffi::OsStrExt;
24+
use std::ptr::null;
25+
26+
impl Test for TestVirtiofsRootRo {
27+
fn start_vm(self: Box<Self>, test_setup: TestSetup) -> anyhow::Result<()> {
28+
let root_dir = setup_rootfs(&test_setup)?;
29+
30+
// The guest init needs /dev, /proc, /sys as mount points. With a read-only
31+
// root these must already exist in the host directory.
32+
for dir in ["dev", "proc", "sys"] {
33+
fs::create_dir(root_dir.join(dir))?;
34+
}
35+
fs::create_dir(root_dir.join(EMPTY_DIR))?;
36+
fs::write(root_dir.join(TEST_FILE), TEST_CONTENT)?;
37+
let root_path = CString::new(root_dir.as_os_str().as_bytes())?;
38+
let test_case = CString::new(test_setup.test_case)?;
39+
let argv = [test_case.as_ptr(), null()];
40+
let envp = [null()];
41+
42+
unsafe {
43+
krun_call!(krun_set_log_level(KRUN_LOG_LEVEL_TRACE))?;
44+
let ctx = krun_call_u32!(krun_create_ctx())?;
45+
krun_call!(krun_set_vm_config(ctx, 1, 512))?;
46+
47+
// Use "/dev/root" tag (KRUN_FS_ROOT_TAG) with read_only=true
48+
krun_call!(krun_add_virtiofs3(
49+
ctx,
50+
c"/dev/root".as_ptr(),
51+
root_path.as_ptr(),
52+
0,
53+
true,
54+
))?;
55+
56+
krun_call!(krun_set_workdir(ctx, c"/".as_ptr()))?;
57+
krun_call!(krun_set_exec(
58+
ctx,
59+
c"/guest-agent".as_ptr(),
60+
argv.as_ptr(),
61+
envp.as_ptr(),
62+
))?;
63+
krun_call!(krun_start_enter(ctx))?;
64+
}
65+
Ok(())
66+
}
67+
}
68+
}
69+
70+
#[guest]
71+
mod guest {
72+
use super::*;
73+
use crate::Test;
74+
use nix::errno::Errno;
75+
use nix::libc;
76+
use nix::sys::stat::{mknod, stat, Mode, SFlag};
77+
use nix::unistd::{mkfifo, truncate};
78+
use std::fs;
79+
use std::fs::Permissions;
80+
use std::io::ErrorKind;
81+
use std::os::unix::fs::{chown, symlink, PermissionsExt};
82+
use std::os::unix::net::UnixListener;
83+
use std::path::Path;
84+
85+
fn setxattr(path: &Path, name: &str, value: &[u8]) -> nix::Result<()> {
86+
use std::ffi::CString;
87+
use std::os::unix::ffi::OsStrExt;
88+
let c_path = CString::new(path.as_os_str().as_bytes()).unwrap();
89+
let c_name = CString::new(name).unwrap();
90+
let ret = unsafe {
91+
libc::setxattr(
92+
c_path.as_ptr(),
93+
c_name.as_ptr(),
94+
value.as_ptr() as *const libc::c_void,
95+
value.len(),
96+
0,
97+
)
98+
};
99+
Errno::result(ret).map(drop)
100+
}
101+
102+
/// Run `op` with `path`, assert it fails with EROFS, then verify `path` is unchanged.
103+
fn assert_unchanged_after<T, E>(
104+
description: &str,
105+
path: &Path,
106+
snapshot: &nix::sys::stat::FileStat,
107+
op: impl FnOnce(&Path) -> Result<T, E>,
108+
) where
109+
T: std::fmt::Debug,
110+
E: Into<std::io::Error>,
111+
{
112+
match op(path) {
113+
Err(e) => {
114+
let err: std::io::Error = e.into();
115+
assert_eq!(
116+
err.kind(),
117+
ErrorKind::ReadOnlyFilesystem,
118+
"Expected ReadOnlyFilesystem for {description}, got: {err}",
119+
);
120+
}
121+
Ok(val) => panic!("Expected ReadOnlyFilesystem for {description}, got: Ok({val:?})"),
122+
}
123+
124+
let after = stat(path).unwrap_or_else(|e| {
125+
panic!("stat {} after {description}: {e}", path.display());
126+
});
127+
assert_eq!(
128+
snapshot.st_size, after.st_size,
129+
"{description}: size changed"
130+
);
131+
assert_eq!(
132+
snapshot.st_mode, after.st_mode,
133+
"{description}: mode changed"
134+
);
135+
assert_eq!(snapshot.st_uid, after.st_uid, "{description}: uid changed");
136+
assert_eq!(snapshot.st_gid, after.st_gid, "{description}: gid changed");
137+
assert_eq!(
138+
snapshot.st_mtime, after.st_mtime,
139+
"{description}: mtime changed"
140+
);
141+
assert_eq!(
142+
snapshot.st_mtime_nsec, after.st_mtime_nsec,
143+
"{description}: mtime_nsec changed",
144+
);
145+
assert_eq!(
146+
snapshot.st_ctime, after.st_ctime,
147+
"{description}: ctime changed"
148+
);
149+
assert_eq!(
150+
snapshot.st_ctime_nsec, after.st_ctime_nsec,
151+
"{description}: ctime_nsec changed",
152+
);
153+
if SFlag::from_bits_truncate(after.st_mode).contains(SFlag::S_IFREG) {
154+
assert_eq!(
155+
fs::read(path).unwrap_or_else(|_| panic!("read {}", path.display())),
156+
TEST_CONTENT,
157+
"{description}: content changed",
158+
);
159+
}
160+
}
161+
162+
impl Test for TestVirtiofsRootRo {
163+
fn in_guest(self: Box<Self>) {
164+
let test_file = Path::new("/").join(TEST_FILE);
165+
let empty_dir = Path::new("/").join(EMPTY_DIR);
166+
let snap = stat(test_file.as_path()).expect("stat test-file");
167+
let dir_snap = stat(empty_dir.as_path()).expect("stat empty-dir");
168+
169+
// -- Operations that try to create new entries --
170+
assert_unchanged_after("write new file", &test_file, &snap, |_| {
171+
fs::write("/new-file", b"hello")
172+
});
173+
assert_unchanged_after("create dir", &test_file, &snap, |_| {
174+
fs::create_dir("/new-dir")
175+
});
176+
assert_unchanged_after("create symlink", &test_file, &snap, |_| {
177+
symlink(TEST_FILE, "/new-symlink")
178+
});
179+
assert_unchanged_after("create hard link", &test_file, &snap, |_| {
180+
fs::hard_link(TEST_FILE, "/new-hardlink")
181+
});
182+
assert_unchanged_after("create unix socket", &test_file, &snap, |_| {
183+
UnixListener::bind("/new-socket").map(|_| ())
184+
});
185+
assert_unchanged_after("mkfifo", &test_file, &snap, |_| {
186+
mkfifo("/new-fifo", Mode::S_IRUSR)
187+
});
188+
assert_unchanged_after("mknod", &test_file, &snap, |_| {
189+
mknod("/new-node", SFlag::S_IFREG, Mode::S_IRUSR, 0)
190+
});
191+
192+
// -- Operations that try to mutate the existing test file --
193+
assert_unchanged_after("write existing file", &test_file, &snap, |p| {
194+
fs::write(p, b"overwritten")
195+
});
196+
assert_unchanged_after("truncate", &test_file, &snap, |p| truncate(p, 0));
197+
assert_unchanged_after("chmod", &test_file, &snap, |p| {
198+
fs::set_permissions(p, Permissions::from_mode(0o777))
199+
});
200+
assert_unchanged_after("chown", &test_file, &snap, |p| {
201+
chown(p, Some(12345), Some(12345))
202+
});
203+
assert_unchanged_after("rename", &test_file, &snap, |p| {
204+
fs::rename(p, "/test-file-renamed")
205+
});
206+
assert_unchanged_after("setxattr", &test_file, &snap, |p| {
207+
setxattr(p, "user.test", b"value")
208+
});
209+
210+
// -- Operations that try to remove existing entries --
211+
assert_unchanged_after("remove file", &test_file, &snap, |p| fs::remove_file(p));
212+
assert_unchanged_after("remove dir", &empty_dir, &dir_snap, |p| fs::remove_dir(p));
213+
214+
println!("OK");
215+
}
216+
}
217+
}

0 commit comments

Comments
 (0)