Skip to content

Commit 0115bd1

Browse files
committed
Xmuvm: add container mode and passt sandbox policy controls
- add --no-hid and --container-mode CLI options - add --passt-sandbox-policy=warn|strict|silent - default to silent passt policy in container-mode - add strict passt userns validation and fail fast Signed-off-by: Zewei Yang <[email protected]>
1 parent ea73c7c commit 0115bd1

3 files changed

Lines changed: 119 additions & 21 deletions

File tree

crates/muvm/src/bin/muvm.rs

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use krun_sys::{
1515
VIRGLRENDERER_USE_ASYNC_FENCE_CB, VIRGLRENDERER_USE_EGL, VIRGLRENDERER_VENUS,
1616
};
1717
use log::debug;
18-
use muvm::cli_options::options;
18+
use muvm::cli_options::{options, PasstSandboxPolicy};
1919
use muvm::config::Configuration;
2020
use muvm::cpu::{get_fallback_cores, get_performance_cores};
2121
use muvm::env::{find_muvm_exec, prepare_env_vars};
@@ -75,6 +75,14 @@ fn main() -> Result<ExitCode> {
7575
}
7676

7777
let options = options().fallback_to_usage().run();
78+
let disable_hid = options.no_hid || options.container_mode;
79+
let passt_policy = options
80+
.passt_sandbox_policy
81+
.unwrap_or(if options.container_mode {
82+
PasstSandboxPolicy::Silent
83+
} else {
84+
PasstSandboxPolicy::Warn
85+
});
7886
let config = Configuration::parse_config_file()?;
7987

8088
let mut init_commands = options.init_commands;
@@ -278,7 +286,7 @@ fn main() -> Result<ExitCode> {
278286
.context("Failed to connect to `passt`")?
279287
.into()
280288
} else {
281-
start_passt(&options.publish_ports)
289+
start_passt(&options.publish_ports, passt_policy)
282290
.context("Failed to start `passt`")?
283291
.into()
284292
};
@@ -308,20 +316,22 @@ fn main() -> Result<ExitCode> {
308316
}
309317
}
310318

311-
let hidpipe_path = Path::new(&run_path).join("hidpipe");
312-
spawn_hidpipe_server(hidpipe_path.clone()).context("Failed to spawn hidpipe thread")?;
313-
let hidpipe_path = CString::new(
314-
hidpipe_path
315-
.to_str()
316-
.expect("hidpipe_path should not contain invalid UTF-8"),
317-
)
318-
.context("Failed to process `hidpipe` path as it contains NUL character")?;
319+
if !disable_hid {
320+
let hidpipe_path = Path::new(&run_path).join("hidpipe");
321+
spawn_hidpipe_server(hidpipe_path.clone()).context("Failed to spawn hidpipe thread")?;
322+
let hidpipe_path = CString::new(
323+
hidpipe_path
324+
.to_str()
325+
.expect("hidpipe_path should not contain invalid UTF-8"),
326+
)
327+
.context("Failed to process `hidpipe` path as it contains NUL character")?;
319328

320-
// SAFETY: `hidpipe_path` is a pointer to a `CString` with long enough lifetime.
321-
let err = unsafe { krun_add_vsock_port(ctx_id, HIDPIPE_SOCKET, hidpipe_path.as_ptr()) };
322-
if err < 0 {
323-
let err = Errno::from_raw_os_error(-err);
324-
return Err(err).context("Failed to configure vsock for hidpipe socket");
329+
// SAFETY: `hidpipe_path` is a pointer to a `CString` with long enough lifetime.
330+
let err = unsafe { krun_add_vsock_port(ctx_id, HIDPIPE_SOCKET, hidpipe_path.as_ptr()) };
331+
if err < 0 {
332+
let err = Errno::from_raw_os_error(-err);
333+
return Err(err).context("Failed to configure vsock for hidpipe socket");
334+
}
325335
}
326336

327337
let socket_dir = Path::new(&run_path).join("krun/socket");

crates/muvm/src/cli_options.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ use bpaf::{any, construct, long, positional, OptionParser, Parser};
77
use crate::types::MiB;
88
use crate::utils::launch::Emulator;
99

10+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11+
pub enum PasstSandboxPolicy {
12+
Warn,
13+
Strict,
14+
Silent,
15+
}
16+
17+
impl std::str::FromStr for PasstSandboxPolicy {
18+
type Err = String;
19+
fn from_str(s: &str) -> Result<Self, String> {
20+
match s {
21+
"warn" => Ok(Self::Warn),
22+
"strict" => Ok(Self::Strict),
23+
"silent" => Ok(Self::Silent),
24+
x => Err(format!("Expected warn|strict|silent, got '{x}'")),
25+
}
26+
}
27+
}
28+
1029
#[derive(Debug, Clone, Copy, Default, PartialEq)]
1130
pub enum GpuMode {
1231
#[default]
@@ -42,6 +61,9 @@ pub struct Options {
4261
pub interactive: bool,
4362
pub tty: bool,
4463
pub privileged: bool,
64+
pub no_hid: bool,
65+
pub container_mode: bool,
66+
pub passt_sandbox_policy: Option<PasstSandboxPolicy>,
4567
pub gpu_mode: Option<GpuMode>,
4668
pub publish_ports: Vec<String>,
4769
pub emulator: Option<Emulator>,
@@ -149,6 +171,23 @@ pub fn options() -> OptionParser<Options> {
149171
This notably does not allow root access to the host fs.",
150172
)
151173
.switch();
174+
let no_hid = long("no-hid").help("Disable HID device emulation").switch();
175+
let container_mode = long("container-mode")
176+
.help(
177+
"Container-friendly defaults.
178+
Implies --no-hid.
179+
Also defaults --passt-sandbox-policy to 'silent' unless explicitly set.",
180+
)
181+
.switch();
182+
let passt_sandbox_policy = long("passt-sandbox-policy")
183+
.help(
184+
"How to handle passt sandbox downgrade when starting passt internally:
185+
warn (default): print passt warnings to stderr and continue
186+
strict: fail if passt doesn't enter a separate user namespace
187+
silent: suppress passt warnings and continue",
188+
)
189+
.argument::<PasstSandboxPolicy>("warn|strict|silent")
190+
.optional();
152191
let gpu_mode = long("gpu-mode")
153192
.help("Use the given GPU virtualization method")
154193
.argument::<GpuMode>("drm|venus|software")
@@ -197,6 +236,9 @@ pub fn options() -> OptionParser<Options> {
197236
interactive,
198237
tty,
199238
privileged,
239+
no_hid,
240+
container_mode,
241+
passt_sandbox_policy,
200242
gpu_mode,
201243
publish_ports,
202244
emulator,

crates/muvm/src/net.rs

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,57 @@
11
use std::os::fd::{AsRawFd, IntoRawFd};
22
use std::os::unix::net::UnixStream;
33
use std::path::Path;
4-
use std::process::Command;
4+
use std::process::{Child, Command, Stdio};
5+
use std::thread::sleep;
6+
use std::time::{Duration, Instant};
57

6-
use anyhow::{Context, Result};
8+
use anyhow::{anyhow, Context, Result};
79
use log::debug;
810
use rustix::io::dup;
911

12+
use crate::cli_options::PasstSandboxPolicy;
13+
1014
struct PublishSpec<'a> {
1115
udp: bool,
1216
guest_range: (u32, u32),
1317
host_range: (u32, u32),
1418
ip: &'a str,
1519
}
1620

21+
fn ensure_passt_userns(child: &mut Child) -> Result<()> {
22+
let host_userns =
23+
std::fs::read_link("/proc/self/ns/user").context("Failed to read /proc/self/ns/user")?;
24+
let pid = child.id();
25+
let child_userns_path = format!("/proc/{pid}/ns/user");
26+
let deadline = Instant::now() + Duration::from_secs(1);
27+
28+
while Instant::now() < deadline {
29+
if let Some(status) = child
30+
.try_wait()
31+
.context("Failed to query `passt` process state")?
32+
{
33+
return Err(anyhow!(
34+
"`passt` exited before sandbox check completed with status {status}"
35+
));
36+
}
37+
38+
match std::fs::read_link(&child_userns_path) {
39+
Ok(ns) if ns != host_userns => return Ok(()),
40+
Ok(_) => {},
41+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {},
42+
Err(e) => return Err(e).context("Failed to read child user namespace"),
43+
}
44+
45+
sleep(Duration::from_millis(25));
46+
}
47+
48+
let _ = child.kill();
49+
let _ = child.wait();
50+
Err(anyhow!(
51+
"`passt` failed strict sandbox policy: no separate user namespace was established"
52+
))
53+
}
54+
1755
fn parse_range(r: &str) -> Result<(u32, u32)> {
1856
Ok(if let Some(pos) = r.find('-') {
1957
(r[..pos].parse()?, r[pos + 1..].parse()?)
@@ -82,7 +120,10 @@ where
82120
Ok(UnixStream::connect(passt_socket_path)?)
83121
}
84122

85-
pub fn start_passt(publish_ports: &[String]) -> Result<UnixStream> {
123+
pub fn start_passt(
124+
publish_ports: &[String],
125+
passt_sandbox_policy: PasstSandboxPolicy,
126+
) -> Result<UnixStream> {
86127
// SAFETY: The child process should not inherit the file descriptor of
87128
// `parent_socket`. There is no documented guarantee of this, but the
88129
// implementation as of writing atomically sets `SOCK_CLOEXEC`.
@@ -104,6 +145,9 @@ pub fn start_passt(publish_ports: &[String]) -> Result<UnixStream> {
104145
debug!(fd = child_fd.as_raw_fd(); "passing fd to passt");
105146

106147
let mut cmd = Command::new("passt");
148+
if passt_sandbox_policy != PasstSandboxPolicy::Warn {
149+
cmd.stderr(Stdio::null());
150+
}
107151
// SAFETY: `child_fd` is an `OwnedFd` and consumed to prevent closing on drop,
108152
// as it will now be owned by the child process.
109153
// See https://doc.rust-lang.org/std/io/index.html#io-safety
@@ -112,9 +156,11 @@ pub fn start_passt(publish_ports: &[String]) -> Result<UnixStream> {
112156
for spec in publish_ports {
113157
cmd.args(PublishSpec::parse(spec)?.to_args());
114158
}
115-
let child = cmd.spawn();
116-
if let Err(err) = child {
117-
return Err(err).context("Failed to execute `passt` as child process");
159+
let mut child = cmd
160+
.spawn()
161+
.context("Failed to execute `passt` as child process")?;
162+
if passt_sandbox_policy == PasstSandboxPolicy::Strict {
163+
ensure_passt_userns(&mut child).context("`passt` sandbox policy check failed")?;
118164
}
119165

120166
Ok(parent_socket)

0 commit comments

Comments
 (0)