Skip to content

Commit 63d6e65

Browse files
committed
Protect muvm-server with a cookie
While our threat model targets single user systems, let's at least protect muvm-server with an authentication cookie, so only the user creating the microVM (and root) can access it. The cookie is stored in the lockfile, instead of the server port (this was useless in practice and generated confusion as could override the command line). The lockfile is located in XDG_RUNTIME_DIR, which in properly configured systems is only accessible to its owner. Signed-off-by: Sergio Lopez <[email protected]>
1 parent e5da0be commit 63d6e65

7 files changed

Lines changed: 87 additions & 52 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/muvm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ serde_json = { version = "1.0.117", default-features = false, features = ["std"]
2222
tempfile = { version = "3.10.1", default-features = false, features = [] }
2323
tokio = { version = "1.38.0", default-features = false, features = ["io-util", "macros", "net", "process", "rt-multi-thread", "sync"] }
2424
tokio-stream = { version = "0.1.15", default-features = false, features = ["net", "sync"] }
25-
uuid = { version = "1.10.0", default-features = false, features = ["std", "v7"] }
25+
uuid = { version = "1.10.0", default-features = false, features = ["serde", "std", "v7"] }
2626

2727
[features]
2828
default = []

crates/muvm/src/bin/muvm.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ fn main() -> Result<()> {
6767

6868
let options = options().fallback_to_usage().run();
6969

70-
let (_lock_file, command, command_args, env) = match launch_or_lock(
70+
let (cookie, _lock_file, command, command_args, env) = match launch_or_lock(
7171
options.server_port,
7272
options.command,
7373
options.command_args,
@@ -79,11 +79,12 @@ fn main() -> Result<()> {
7979
return Ok(());
8080
},
8181
LaunchResult::LockAcquired {
82+
cookie,
8283
lock_file,
8384
command,
8485
command_args,
8586
env,
86-
} => (lock_file, command, command_args, env),
87+
} => (cookie, lock_file, command, command_args, env),
8788
};
8889

8990
{
@@ -374,6 +375,7 @@ fn main() -> Result<()> {
374375
"MUVM_SERVER_PORT".to_owned(),
375376
options.server_port.to_string(),
376377
);
378+
env.insert("MUVM_SERVER_COOKIE".to_owned(), cookie.to_string());
377379

378380
if options.direct_x11 {
379381
let display =

crates/muvm/src/launch.rs

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@ use std::collections::HashMap;
22
use std::env;
33
use std::error::Error;
44
use std::fmt::{Display, Formatter};
5-
use std::fs::File;
5+
use std::fs::{self, File};
66
use std::io::{BufRead, BufReader, Read, Write};
77
use std::net::TcpStream;
88
use std::path::{Path, PathBuf};
99

1010
use anyhow::{anyhow, Context, Result};
1111
use rustix::fs::{flock, FlockOperation};
12-
use rustix::path::Arg;
12+
use uuid::Uuid;
1313

1414
use crate::env::prepare_env_vars;
1515
use crate::utils::launch::Launch;
1616

1717
pub enum LaunchResult {
1818
LaunchRequested,
1919
LockAcquired {
20+
cookie: Uuid,
2021
lock_file: File,
2122
command: PathBuf,
2223
command_args: Vec<String>,
@@ -59,53 +60,63 @@ pub fn launch_or_lock(
5960
if let Some(port) = running_server_port {
6061
let port: u32 = port.parse()?;
6162
let env = prepare_env_vars(env)?;
62-
if let Err(err) = request_launch(port, command, command_args, env) {
63+
let cookie = read_cookie()?;
64+
if let Err(err) = request_launch(port, cookie, command, command_args, env) {
6365
return Err(anyhow!("could not request launch to server: {err}"));
6466
}
6567
return Ok(LaunchResult::LaunchRequested);
6668
}
6769

68-
let (lock_file, running_server_port) = lock_file(server_port)?;
70+
let (lock_file, cookie) = lock_file()?;
6971
match lock_file {
7072
Some(lock_file) => Ok(LaunchResult::LockAcquired {
73+
cookie,
7174
lock_file,
7275
command,
7376
command_args,
7477
env,
7578
}),
7679
None => {
77-
if let Some(port) = running_server_port {
78-
let env = prepare_env_vars(env)?;
79-
let mut tries = 0;
80-
loop {
81-
match request_launch(port, command.clone(), command_args.clone(), env.clone()) {
82-
Err(err) => match err.downcast_ref::<LaunchError>() {
83-
Some(&LaunchError::Connection(_)) => {
84-
if tries == 3 {
85-
return Err(anyhow!(
86-
"could not request launch to server: {err}"
87-
));
88-
} else {
89-
tries += 1;
90-
}
91-
},
92-
_ => {
80+
let env = prepare_env_vars(env)?;
81+
let mut tries = 0;
82+
loop {
83+
match request_launch(
84+
server_port,
85+
cookie,
86+
command.clone(),
87+
command_args.clone(),
88+
env.clone(),
89+
) {
90+
Err(err) => match err.downcast_ref::<LaunchError>() {
91+
Some(&LaunchError::Connection(_)) => {
92+
if tries == 3 {
9393
return Err(anyhow!("could not request launch to server: {err}"));
94-
},
94+
} else {
95+
tries += 1;
96+
}
9597
},
96-
Ok(_) => return Ok(LaunchResult::LaunchRequested),
97-
}
98+
_ => {
99+
return Err(anyhow!("could not request launch to server: {err}"));
100+
},
101+
},
102+
Ok(_) => return Ok(LaunchResult::LaunchRequested),
98103
}
99-
} else {
100-
Err(anyhow!(
101-
"muvm is already running but couldn't find its server port, bailing out"
102-
))
103104
}
104105
},
105106
}
106107
}
107108

108-
fn lock_file(server_port: u32) -> Result<(Option<File>, Option<u32>)> {
109+
fn read_cookie() -> Result<Uuid> {
110+
let run_path = env::var("XDG_RUNTIME_DIR")
111+
.context("Failed to read XDG_RUNTIME_DIR environment variable")?;
112+
let lock_path = Path::new(&run_path).join("muvm.lock");
113+
let data: Vec<u8> = fs::read(lock_path).context("Failed to read lock file")?;
114+
assert!(data.len() == 16);
115+
116+
Uuid::from_slice(&data).context("Failed to read cookie from lock file")
117+
}
118+
119+
fn lock_file() -> Result<(Option<File>, Uuid)> {
109120
let run_path = env::var("XDG_RUNTIME_DIR")
110121
.context("Failed to read XDG_RUNTIME_DIR environment variable")?;
111122
let lock_path = Path::new(&run_path).join("muvm.lock");
@@ -123,30 +134,23 @@ fn lock_file(server_port: u32) -> Result<(Option<File>, Option<u32>)> {
123134
.context("Failed to create lock file")?;
124135
let ret = flock(&lock_file, FlockOperation::NonBlockingLockExclusive);
125136
if ret.is_err() {
126-
let mut data: Vec<u8> = Vec::with_capacity(5);
137+
let mut data: Vec<u8> = Vec::with_capacity(16);
127138
lock_file.read_to_end(&mut data)?;
128-
let port = match data.to_string_lossy().parse::<u32>() {
129-
Ok(port) => {
130-
if port > 1024 {
131-
Some(port)
132-
} else {
133-
None
134-
}
135-
},
136-
Err(_) => None,
137-
};
138-
return Ok((None, port));
139+
let cookie = Uuid::from_slice(&data).context("Failed to read cookie from lock file")?;
140+
return Ok((None, cookie));
139141
}
140142
lock_file
141143
};
142144

145+
let cookie = Uuid::now_v7();
143146
lock_file.set_len(0)?;
144-
lock_file.write_all(format!("{server_port}").as_bytes())?;
145-
Ok((Some(lock_file), None))
147+
lock_file.write_all(cookie.as_bytes())?;
148+
Ok((Some(lock_file), cookie))
146149
}
147150

148151
fn request_launch(
149152
server_port: u32,
153+
cookie: Uuid,
150154
command: PathBuf,
151155
command_args: Vec<String>,
152156
env: HashMap<String, String>,
@@ -155,6 +159,7 @@ fn request_launch(
155159
TcpStream::connect(format!("127.0.0.1:{server_port}")).map_err(LaunchError::Connection)?;
156160

157161
let launch = Launch {
162+
cookie,
158163
command,
159164
command_args,
160165
env,

crates/muvm/src/server/bin/muvm-server.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
use std::env;
12
use std::os::unix::process::ExitStatusExt as _;
23

3-
use anyhow::Result;
4+
use anyhow::{Context, Result};
45
use log::error;
56
use muvm::server::cli_options::options;
67
use muvm::server::worker::{State, Worker};
@@ -9,18 +10,27 @@ use tokio::process::Command;
910
use tokio::sync::watch;
1011
use tokio_stream::wrappers::WatchStream;
1112
use tokio_stream::StreamExt as _;
13+
use uuid::Uuid;
1214

13-
#[tokio::main]
14-
async fn main() -> Result<()> {
15+
fn main() -> Result<()> {
16+
let cookie = env::var("MUVM_SERVER_COOKIE")
17+
.with_context(|| "Could find server cookie as an environment variable")?;
18+
19+
let rt = tokio::runtime::Runtime::new().unwrap();
20+
rt.block_on(async { tokio_main(cookie).await })
21+
}
22+
23+
async fn tokio_main(cookie: String) -> Result<()> {
1524
env_logger::init();
1625

1726
let options = options().run();
27+
let cookie = Uuid::try_parse(&cookie).context("Couldn't parse cookie as UUID v7")?;
1828

1929
let listener = TcpListener::bind(format!("0.0.0.0:{}", options.server_port)).await?;
2030
let (state_tx, state_rx) = watch::channel(State::new());
2131

2232
let mut worker_handle = tokio::spawn(async move {
23-
let mut worker = Worker::new(listener, state_tx);
33+
let mut worker = Worker::new(cookie, listener, state_tx);
2434
worker.run().await;
2535
});
2636
let command_status = Command::new(&options.command)

crates/muvm/src/server/worker.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ use tokio::sync::watch;
1313
use tokio::task::{JoinError, JoinSet};
1414
use tokio_stream::wrappers::TcpListenerStream;
1515
use tokio_stream::StreamExt as _;
16+
use uuid::Uuid;
1617

1718
use crate::utils::launch::Launch;
1819
use crate::utils::stdio::make_stdout_stderr;
1920

2021
#[derive(Debug)]
2122
pub struct Worker {
23+
cookie: Uuid,
2224
listener_stream: TcpListenerStream,
2325
state_tx: watch::Sender<State>,
2426
child_set: JoinSet<(PathBuf, ChildResult)>,
@@ -33,8 +35,9 @@ pub struct State {
3335
type ChildResult = Result<ExitStatus, io::Error>;
3436

3537
impl Worker {
36-
pub fn new(listener: TcpListener, state_tx: watch::Sender<State>) -> Self {
38+
pub fn new(cookie: Uuid, listener: TcpListener, state_tx: watch::Sender<State>) -> Self {
3739
Worker {
40+
cookie,
3841
listener_stream: TcpListenerStream::new(listener),
3942
state_tx,
4043
child_set: JoinSet::new(),
@@ -56,7 +59,7 @@ impl Worker {
5659
};
5760
let stream = BufStream::new(stream);
5861

59-
match handle_connection(stream).await {
62+
match handle_connection(self.cookie, stream).await {
6063
Ok((command, mut child)) => {
6164
self.child_set.spawn(async move { (command, child.wait().await) });
6265
self.set_child_processes(self.child_set.len());
@@ -158,15 +161,27 @@ async fn read_request(stream: &mut BufStream<TcpStream>) -> Result<Launch> {
158161
}
159162
}
160163

161-
async fn handle_connection(mut stream: BufStream<TcpStream>) -> Result<(PathBuf, Child)> {
164+
async fn handle_connection(
165+
server_cookie: Uuid,
166+
mut stream: BufStream<TcpStream>,
167+
) -> Result<(PathBuf, Child)> {
162168
let mut envs: HashMap<String, String> = env::vars().collect();
163169

164170
let Launch {
171+
cookie,
165172
command,
166173
command_args,
167174
env,
168175
} = read_request(&mut stream).await?;
169176
debug!(command:?, command_args:?, env:?; "received launch request");
177+
if cookie != server_cookie {
178+
debug!("invalid cookie in launch request");
179+
let msg = "Invalid cookie";
180+
stream.write_all(msg.as_bytes()).await.ok();
181+
stream.flush().await.ok();
182+
return Err(anyhow!(msg));
183+
}
184+
170185
envs.extend(env);
171186

172187
let (stdout, stderr) = make_stdout_stderr(&command, &envs)?;

crates/muvm/src/utils/launch.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ use std::collections::HashMap;
22
use std::path::PathBuf;
33

44
use serde::{Deserialize, Serialize};
5+
use uuid::Uuid;
56

67
#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
78
pub struct Launch {
9+
pub cookie: Uuid,
810
pub command: PathBuf,
911
pub command_args: Vec<String>,
1012
pub env: HashMap<String, String>,

0 commit comments

Comments
 (0)