diff --git a/docs/TESTS.md b/docs/TESTS.md index f94670b..c279e08 100644 --- a/docs/TESTS.md +++ b/docs/TESTS.md @@ -29,7 +29,7 @@ cargo build -p vuinputd-tests podman build --dns 1.1.1.1 -t vuinputd-tests -f vuinputd-tests/podman/Containerfile . ``` -Run with `cargo test -p vuinputd-tests --features "requires-privileges requires-uinput requires-podman -- --test-threads=1"`. +Run with `cargo test -p vuinputd-tests --features "requires-privileges requires-uinput requires-podman" -- --test-threads=1`. ## Performance tests diff --git a/vuinputd-tests/src/podman.rs b/vuinputd-tests/src/podman.rs index fc3821d..839ba3c 100644 --- a/vuinputd-tests/src/podman.rs +++ b/vuinputd-tests/src/podman.rs @@ -107,6 +107,18 @@ impl PodmanBuilder { self } + pub fn userns(mut self, mode: &str) -> Self { + self.args.push("--userns".into()); + self.args.push(mode.into()); + self + } + + pub fn uidmap(mut self, uidmap: &str) -> Self { + self.args.push("--uidmap".into()); + self.args.push(uidmap.into()); + self + } + /// Enable bidirectional IPC using a Unix seqpacket socketpair. pub fn with_ipc(mut self) -> io::Result<(Self, SandboxIpc)> { let (parent, child) = nix::sys::socket::socketpair( diff --git a/vuinputd-tests/src/run_vuinputd.rs b/vuinputd-tests/src/run_vuinputd.rs index 08397bb..8cb8b78 100644 --- a/vuinputd-tests/src/run_vuinputd.rs +++ b/vuinputd-tests/src/run_vuinputd.rs @@ -71,6 +71,7 @@ impl Drop for VuinputdGuard { // Wait a bit for _ in 0..10 { if let Ok(Some(_)) = self.child.try_wait() { + println!("vuinputd for tests shutdown gracefully"); return; } thread::sleep(Duration::from_millis(100)); @@ -79,5 +80,6 @@ impl Drop for VuinputdGuard { // Still alive → SIGKILL let _ = signal::kill(pid, Signal::SIGKILL); let _ = self.child.wait(); + println!("vuinputd for tests killed"); } } diff --git a/vuinputd/src/global_config.rs b/vuinputd/src/global_config.rs index d91de99..c851c6e 100644 --- a/vuinputd/src/global_config.rs +++ b/vuinputd/src/global_config.rs @@ -10,6 +10,7 @@ pub struct GlobalConfig { pub policy: DevicePolicy, pub placement: Placement, pub vudevname: String, + pub device_owner: DeviceOwner, } // The actual static variable. It starts empty and is set once in main(). @@ -41,16 +42,40 @@ pub enum Placement { None, } +/// Device owner of the created devices +#[derive(Debug, Clone, ValueEnum, Default, PartialEq, Eq)] +pub enum DeviceOwner { + #[default] + /// Automatically derive useful settings (how might change in the future) + Auto, + /// Use the uid and gid of vuinputd + Vuinputd, + /// Same as dev folder in container + ContainerDevFolder, +} + +impl DeviceOwner { + pub fn to_string_rep(&self) -> String { + match self { + DeviceOwner::Auto => "auto".to_string(), + DeviceOwner::Vuinputd => "vuinputd".to_string(), + DeviceOwner::ContainerDevFolder => "container-dev-folder".to_string(), + } + } +} + pub fn initialize_global_config( device_policy: &DevicePolicy, placement: &Placement, devname: &Option, + device_owner: &DeviceOwner, ) { if CONFIG .set(GlobalConfig { policy: device_policy.clone(), placement: placement.clone(), vudevname: devname.clone().unwrap_or("vuinput".to_string()), + device_owner: device_owner.clone(), }) .is_err() { @@ -70,3 +95,7 @@ pub fn get_placement<'a>() -> &'a Placement { pub fn get_vudevname<'a>() -> &'a String { &CONFIG.get().unwrap().vudevname } + +pub fn get_device_owner<'a>() -> &'a DeviceOwner { + &CONFIG.get().unwrap().device_owner +} diff --git a/vuinputd/src/main.rs b/vuinputd/src/main.rs index 199bd79..1b3c519 100644 --- a/vuinputd/src/main.rs +++ b/vuinputd/src/main.rs @@ -35,7 +35,7 @@ use crate::cuse_device::evdev_write_watcher::{ use crate::cuse_device::state::{initialize_dedup_last_error, initialize_vuinput_state}; use crate::cuse_device::vuinput_make_cuse_ops; use crate::cuse_device::vuinput_open::VUINPUT_COUNTER; -use crate::global_config::{DevicePolicy, Placement}; +use crate::global_config::{DeviceOwner, DevicePolicy, Placement}; use crate::input_realizer::host_fs; use crate::jobs::monitor_udev_job::MonitorBackgroundLoop; @@ -80,13 +80,13 @@ struct Args { #[arg(long = "action-base64", value_name = "BASE64")] pub action_base64: Option, - /// Path to the target process's /proc//ns directory used as namespace source. + /// Process id that is used as the namespace source (e.g. 1234 is used to read the namespaces from /proc/1234/ns). #[arg( - long = "target-namespace", - value_name = "NS_PATH", - help = "Path to /proc//ns used as the namespace source (e.g. /proc/1234/ns or /proc/self/ns)" + long = "target-pid", + value_name = "PID", + help = "Process id that is used as the namespace source (e.g. 1234 is used to read the namespaces from /proc/1234/ns)." )] - pub target_namespace: Option, + pub target_pid: Option, #[arg( long = "vt-guard", @@ -104,6 +104,10 @@ struct Args { /// Placement of device nodes and udev data #[arg(long, value_enum, default_value_t)] pub placement: Placement, + + /// Owner of the created devices + #[arg(long = "device-owner", value_enum, default_value_t)] + pub device_owner: DeviceOwner, } fn validate_args(args: &Args) -> Result<(), String> { @@ -116,18 +120,18 @@ fn validate_args(args: &Args) -> Result<(), String> { } }; - // action might only occur with target-namespace + // action might only occur with target-pid match ( &args.major, &args.minor, &args.devname, action, - &args.target_namespace, + &args.target_pid, ) { (None, None, None, Some(_), _) => {} (_, _, _, None, None) => {} _ => { - return Err("--action or --action-base64 must not be used in combination with any other argument other than target-namespace".into()); + return Err("--action or --action-base64 must not be used in combination with any other argument other than target-pid".into()); } } @@ -181,8 +185,8 @@ fn main() -> std::io::Result<()> { }; if action.is_some() { - if let Some(target_namespace) = args.target_namespace { - process_tools::run_in_net_and_mnt_namespace(target_namespace.as_str()).unwrap(); + if let Some(target_pid) = args.target_pid { + process_tools::run_in_net_and_mnt_namespace(target_pid.as_str(),&args.device_owner).unwrap(); } let error_code = actions::handle_action::handle_cli_action(action.unwrap()); std::process::exit(error_code); @@ -196,7 +200,12 @@ fn main() -> std::io::Result<()> { check_permissions().expect("failed to read the capabilities of the vuinputd process"); vt_tools::check_vt_status(); - global_config::initialize_global_config(&args.device_policy, &args.placement, &args.devname); + global_config::initialize_global_config( + &args.device_policy, + &args.placement, + &args.devname, + &args.device_owner, + ); initialize_evdev_write_watcher().expect( "failed to initialize the watcher that watches for writes on the created evdev devices", ); @@ -208,7 +217,7 @@ fn main() -> std::io::Result<()> { .set(Mutex::new(Dispatcher::new())) .expect("failed to initialize the job dispatcher"); SELF_NAMESPACES - .set(get_namespace(Pid::SelfPid)) + .set(get_self_namespace()) .expect("failed to retrieve the namespaces of the vuinputd process"); initialize_dedup_last_error(); JOB_DISPATCHER diff --git a/vuinputd/src/process_tools/mod.rs b/vuinputd/src/process_tools/mod.rs index e54119e..ad78125 100644 --- a/vuinputd/src/process_tools/mod.rs +++ b/vuinputd/src/process_tools/mod.rs @@ -11,7 +11,7 @@ use std::{ io::Read, os::{ fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}, - unix::process::CommandExt, + unix::{fs::MetadataExt, process::CommandExt}, }, path::Path, process::Command, @@ -21,23 +21,34 @@ use std::{ use anyhow::anyhow; use std::io; -use crate::actions::action::Action; +use crate::{ + actions::action::Action, + global_config::{get_device_owner, DeviceOwner}, +}; + +pub mod ns_fscreds; pub static SELF_NAMESPACES: OnceLock = OnceLock::new(); #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] pub enum Pid { - SelfPid, Pid(u32), } impl Pid { pub fn path(&self) -> String { match self { - Pid::SelfPid => "/proc/self".to_string(), Pid::Pid(pid_no) => format!("/proc/{}", pid_no), } } + pub fn to_string_rep(&self) -> String { + let Pid::Pid(val) = self; + val.to_string() + } +} +enum PidOrSelf { + Pid(u32), + SelfPid, } #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] @@ -80,15 +91,13 @@ pub fn is_compat_process(pid: Pid) -> Option { Err(_) => None, } } - Pid::SelfPid => unreachable!(), } } -// TODO: Rename to capture all relevant process information -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct RequestingProcess { - pub nspath: String, - pub nsroot: String, + pub pid_requestor: Pid, + pub pid_requestor_root: Pid, pub namespaces: Namespaces, pub is_compat: bool, } @@ -134,10 +143,19 @@ impl std::fmt::Display for RequestingProcess { } } +pub fn get_self_namespace() -> Namespaces { + get_namespace_of_pid_or_self(PidOrSelf::SelfPid) +} + pub fn get_namespace(pid: Pid) -> Namespaces { - let pid: String = match pid { - Pid::Pid(pid) => pid.to_string(), - Pid::SelfPid => "self".to_string(), + let Pid::Pid(pid) = pid; + get_namespace_of_pid_or_self(PidOrSelf::Pid(pid)) +} + +fn get_namespace_of_pid_or_self(pid_or_self: PidOrSelf) -> Namespaces { + let pid: String = match pid_or_self { + PidOrSelf::Pid(pid) => pid.to_string(), + PidOrSelf::SelfPid => "self".to_string(), }; let nspath = format!("/proc/{}/ns", pid); @@ -181,7 +199,6 @@ pub fn get_namespace(pid: Pid) -> Namespaces { fn get_ppid(pid: Pid) -> Option { let content = match pid { - Pid::SelfPid => fs::read_to_string(format!("/proc/self/status")).ok()?, Pid::Pid(pid) => fs::read_to_string(format!("/proc/{}/status", pid)).ok()?, }; let ppid = content @@ -239,29 +256,26 @@ pub fn get_requesting_process(pid: Pid) -> RequestingProcess { pid.path() ); - let nspath = format!("{}/ns", pid.path()); - let nsroot = format!("{}/ns", ppid.path()); RequestingProcess { - nspath: nspath, - nsroot: nsroot, + pid_requestor: pid, + pid_requestor_root: ppid, namespaces: nsinodes, is_compat: is_compat, } } - Pid::SelfPid => { - unreachable!(); - } } } fn print_debug_string(action: &str, ns: &RequestingProcess) { - let action_base64 = (BASE64_STANDARD.encode(action)); + let action_base64 = BASE64_STANDARD.encode(action); let mut debugstring = String::new(); debugstring.push_str("In case you need to debug the system calls, call `strace vuinputd"); - debugstring.push_str(" --target-namespace "); - debugstring.push_str(ns.nsroot.as_str()); + debugstring.push_str(" --target-pid "); + debugstring.push_str(&ns.pid_requestor_root.to_string_rep()); debugstring.push_str(" --action-base64 "); debugstring.push_str(action_base64.as_str()); + debugstring.push_str(" --device-owner "); + debugstring.push_str(get_device_owner().to_string_rep().as_str()); debugstring.push_str("`"); debug!("{}", debugstring); } @@ -272,13 +286,17 @@ pub fn start_action(action: Action, ns: &RequestingProcess) -> anyhow::Result anyhow::Result anyhow::Result<()> { +pub fn run_in_net_and_mnt_namespace(target_pid: &str, device_owner: &DeviceOwner) -> anyhow::Result<()> { debug!( "Entering namespaces of process {}. We assume this is the root process of the container.", - target_namespace + target_pid ); - let path: &Path = Path::new(target_namespace); + let fs_uid_gid = if *device_owner == DeviceOwner::ContainerDevFolder { + let pid:u32 = target_pid.trim().parse()?; + let pid = Pid::Pid(pid); + let fs_uid=ns_fscreds::get_uid_in_container(pid, 0)?; + let fs_gid=ns_fscreds::get_gid_in_container(pid, 0)?; + Some((fs_uid,fs_gid)) + } else { + None + }; + + let nspath = format!("/proc/{}/ns", target_pid); + let path: &Path = Path::new(&nspath); if !fs::exists(path).unwrap() { return Err(anyhow!("the root process of the container whose namespaces we want to enter does not exist anymore")); } - let net = File::open(target_namespace.to_string() + "/net")?; - let mnt = File::open(target_namespace.to_string() + "/mnt")?; + let net = File::open(nspath.to_string() + "/net")?; + let mnt = File::open(nspath.to_string() + "/mnt")?; unsafe { // enter namespaces libc::setns(net.as_raw_fd(), libc::CLONE_NEWNET); libc::setns(mnt.as_raw_fd(), libc::CLONE_NEWNS); }; + + if let Some((fs_uid,fs_gid)) = fs_uid_gid { + ns_fscreds::acquire_uid_and_gid(fs_uid, fs_gid)?; + } + anyhow::Ok(()) } @@ -338,9 +372,6 @@ pub async fn await_process(pid: Pid) -> io::Result { Ok(si.si_status()) } } - Pid::SelfPid => { - unreachable!(); - } } } diff --git a/vuinputd/src/process_tools/ns_fscreds.rs b/vuinputd/src/process_tools/ns_fscreds.rs new file mode 100644 index 0000000..7cf3268 --- /dev/null +++ b/vuinputd/src/process_tools/ns_fscreds.rs @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use std::fs; +use std::io::{self, BufRead}; +use std::path::Path; + +use crate::process_tools::Pid; + +#[derive(Debug, Clone, PartialEq)] +struct IdMapEntry { + pub inside_start: u64, + pub outside_start: u64, + pub length: u64, +} + +fn parse_id_map(pid: u32, map_type: &str) -> io::Result> { + let path = format!("/proc/{}/{}", pid, map_type); + let file = fs::File::open(&path)?; + let reader = io::BufReader::new(file); + + Ok(reader + .lines() + .filter_map(|line| { + let line = line.ok()?; + let mut parts = line.split_whitespace(); + let inside_start = parts.next()?.parse().ok()?; + let outside_start = parts.next()?.parse().ok()?; + let length = parts.next()?.parse().ok()?; + Some(IdMapEntry { + inside_start, + outside_start, + length, + }) + }) + .collect()) +} + +fn to_host_id(entries: &[IdMapEntry], inside_id: u64) -> Option { + entries.iter().find_map(|e| { + if inside_id >= e.inside_start && inside_id < e.inside_start + e.length { + Some(e.outside_start + (inside_id - e.inside_start)) + } else { + None + } + }) +} + +/// Returns the host UID that corresponds to `ns_uid` (e.g. 0) inside the container. +pub fn get_uid_in_container(pid: Pid, ns_uid: u64) -> anyhow::Result { + let Pid::Pid(pid) = pid; + let entries = parse_id_map(pid, "uid_map")?; + to_host_id(&entries, ns_uid) + .map(|id| id as u32) + .ok_or_else(|| anyhow::anyhow!("uid {} is not mapped in /proc/{}/uid_map", ns_uid, pid)) +} + +/// Returns the host GID that corresponds to `ns_gid` (e.g. 0) inside the container. +pub fn get_gid_in_container(pid: Pid, ns_gid: u64) -> anyhow::Result { + let Pid::Pid(pid) = pid; + let entries = parse_id_map(pid, "gid_map")?; + to_host_id(&entries, ns_gid) + .map(|id| id as u32) + .ok_or_else(|| anyhow::anyhow!("gid {} is not mapped in /proc/{}/gid_map", ns_gid, pid)) +} + +/// Switch filesystem UID/GID to the given host IDs. +/// GID must be set before UID — dropping UID=0 removes the ability to change GID. +pub fn acquire_uid_and_gid(target_uid: u32, target_gid: u32) -> anyhow::Result<()> { + unsafe { + libc::setfsgid(target_gid as libc::gid_t); + libc::setfsuid(target_uid as libc::uid_t); + } + Ok(()) +} + +/// Switch filesystem UID/GID to match whatever owner the given path has on the host. +pub fn acquire_uid_and_gid_of_path(path: &str) -> anyhow::Result<()> { + use std::os::unix::fs::MetadataExt; + + let metadata = fs::metadata(Path::new(path))?; + let target_uid = metadata.uid(); + let target_gid = metadata.gid(); + acquire_uid_and_gid(target_uid, target_gid) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse_str(s: &str) -> Vec { + s.lines() + .filter_map(|line| { + let mut parts = line.split_whitespace(); + let inside_start = parts.next()?.parse().ok()?; + let outside_start = parts.next()?.parse().ok()?; + let length = parts.next()?.parse().ok()?; + Some(IdMapEntry { + inside_start, + outside_start, + length, + }) + }) + .collect() + } + + #[test] + fn uid0_in_rootless_container_maps_to_host_uid() { + // Typical rootless setup: container root (0) → host uid 100000 + let map = parse_str("0 100000 65536"); + assert_eq!(to_host_id(&map, 0), Some(100000)); + assert_eq!(to_host_id(&map, 1), Some(100001)); + } + + #[test] + fn uid_outside_range_returns_none() { + let map = parse_str("0 100000 65536"); + assert_eq!(to_host_id(&map, 65536), None); + } + + #[test] + fn identity_map_returns_same_id() { + // Process not in a user namespace: 0 0 4294967295 + let map = parse_str("0 0 4294967295"); + assert_eq!(to_host_id(&map, 0), Some(0)); + assert_eq!(to_host_id(&map, 1000), Some(1000)); + } + + #[test] + fn proc_self_uid_is_parseable() { + let uid = unsafe { libc::getuid() } as u64; + let entries = + parse_id_map(std::process::id(), "uid_map").expect("failed to read /proc/self/uid_map"); + assert!( + to_host_id(&entries, uid).is_some(), + "current uid {} not found in uid_map", + uid + ); + } +}