From b3d0a0225d89855bd5f990badc307342aa8e4bfa Mon Sep 17 00:00:00 2001 From: Johannes Leupolz Date: Wed, 10 Dec 2025 22:46:17 +0000 Subject: [PATCH] Small refactorings and note in SECURITY.md --- docs/SECURITY.md | 13 +- vuinputd/src/cuse_device/mod.rs | 138 +--------------------- vuinputd/src/cuse_device/state.rs | 112 ++++++++++++++++++ vuinputd/src/cuse_device/vuinput_ioctl.rs | 43 ++++++- vuinputd/src/cuse_device/vuinput_write.rs | 1 - vuinputd/src/jobs/mknod_input_device.rs | 2 +- vuinputd/src/main.rs | 12 +- 7 files changed, 174 insertions(+), 147 deletions(-) create mode 100644 vuinputd/src/cuse_device/state.rs diff --git a/docs/SECURITY.md b/docs/SECURITY.md index fbe94ac..64de57a 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -8,4 +8,15 @@ ## Unsafe code -Known problem: A lot of unsafe code. Open are ideas to mitigate this issue. \ No newline at end of file +Known problem: A lot of unsafe code. Open are ideas to mitigate this issue. + +## seccomp, AppArmor, SELinux, cgroups mounts, /sys read-write + +This is a big TODO. Which permissions can be reduced. Now we assume we are quite privileagued: +- We have all Linux kernel capabilities, +- The default seccomp profile is disabled, +- The default AppArmor profile is disabled, +- The default SELinux process label is disabled, +- all host devices are accessible, +- /sys is read-write, +- cgroups mount is read-write. \ No newline at end of file diff --git a/vuinputd/src/cuse_device/mod.rs b/vuinputd/src/cuse_device/mod.rs index 5c928a7..b4e4d8e 100644 --- a/vuinputd/src/cuse_device/mod.rs +++ b/vuinputd/src/cuse_device/mod.rs @@ -2,153 +2,21 @@ // // Author: Johannes Leupolz +pub mod state; pub mod vuinput_ioctl; pub mod vuinput_write; pub mod vuinput_release; pub mod vuinput_open; -use std::collections::HashMap; -use std::fs::{self, File}; -use std::io; +use std::{fs, io}; use std::os::unix::fs::{FileTypeExt, MetadataExt}; -use std::sync::{Arc, Mutex, OnceLock, RwLock}; use std::io::{ErrorKind}; use ::cuse_lowlevel::*; +use state::*; -use crate::process_tools::RequestingProcess; - -#[derive(Debug)] -struct VuInputDevice { - cuse_fh : u64, - major : u64, - minor : u64, - syspath: String, - devnode: String, - runtime_data: Option, - netlink_data: Option -} - -#[derive(Debug)] -pub struct VuInputState { - file: File, - requesting_process: RequestingProcess, - input_device: Option -} - -#[derive(Debug,Eq, Hash, PartialEq, Clone)] -pub enum VuFileHandle { - Fh(u64) -} - -impl VuFileHandle { - fn from_fuse_file_info(fi: &fuse_lowlevel::fuse_file_info) -> VuFileHandle { - VuFileHandle::Fh(fi.fh) - } -} - -impl std::fmt::Display for VuFileHandle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - VuFileHandle::Fh(fh) => writeln!(f, "VuFileHandle({:?})",fh)?, - } - Ok(()) - } -} - -#[derive(Debug)] -pub enum VuError { - WriteError -} - -pub static VUINPUT_STATE: OnceLock>>>> = OnceLock::new(); - -// For log limiting. Idea: Move to log_limit crate -pub static DEDUP_LAST_ERROR: OnceLock>> = OnceLock::new(); - - -pub const SYS_INPUT_DIR: &str = "/sys/devices/virtual/input/"; pub const BUS_USB: u16 = 0x03; -pub fn get_vuinput_state( - fh:&VuFileHandle, -) -> Result>, String> { - let map = VUINPUT_STATE - .get() - .ok_or("global not initialized".to_string())?; - let guard = map.read().map_err(|e| e.to_string())?; - guard - .get(&fh) - .cloned() - .ok_or("handle not opened".to_string()) -} - - -pub fn insert_vuinput_state( - fh:&VuFileHandle, - state: VuInputState, -) -> Result<(), String> { - let map = VUINPUT_STATE - .get() - .ok_or("global not initialized".to_string())?; - let mut guard = map.write().map_err(|e| e.to_string())?; - - if guard.contains_key(&fh) { - return Err(format!( - "file handle {} already exists. file handles must not be reused!", - &fh - )); - } - - let _ = guard.insert(fh.clone(), Arc::new(Mutex::new(state))); - Ok(()) -} - -pub fn remove_vuinput_state( - fh:&VuFileHandle, -) -> Result>, String> { - let map = VUINPUT_STATE - .get() - .ok_or("global not initialized".to_string())?; - let mut guard = map.write().map_err(|e| e.to_string())?; - let old_value = guard.remove(&fh).ok_or("fh unknown")?; - Ok(old_value) -} - -pub fn fetch_device_node(path: &str) -> io::Result { - for entry in fs::read_dir(path)? { - let entry = entry?; // propagate per-entry errors - if let Some(name) = entry.file_name().to_str() { - if name.starts_with("event") { - return Ok(format!("/dev/input/{}", name)); - } - } - } - // If no device is found, return an error - Err(io::Error::new(ErrorKind::NotFound, "no device found")) -} - -/// Returns (major, minor) numbers of a device node at `path` -pub fn fetch_major_minor(path: &str) -> io::Result<(u64, u64)> { - let metadata = fs::metadata(path)?; - - // Ensure it's a character device - if !metadata.file_type().is_char_device() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Not a character device", - )); - } - - let rdev = metadata.rdev(); - let major = ((rdev >> 8) & 0xfff) as u64; - let minor = ((rdev & 0xff) | ((rdev >> 12) & 0xfff00)) as u64; - - Ok((major, minor)) -} - - - // Instance of cuse_lowlevel_ops with all stubs assigned. // Setting to None leads to e.g. "write error: Function not implemented". diff --git a/vuinputd/src/cuse_device/state.rs b/vuinputd/src/cuse_device/state.rs new file mode 100644 index 0000000..7e8b503 --- /dev/null +++ b/vuinputd/src/cuse_device/state.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use std::collections::HashMap; +use std::fs::{File}; +use std::sync::{Arc, Mutex, OnceLock, RwLock}; + +use ::cuse_lowlevel::*; + +use crate::process_tools::RequestingProcess; + +#[derive(Debug)] +pub struct VuInputDevice { + pub major : u64, + pub minor : u64, + pub syspath: String, + pub devnode: String, +} + +#[derive(Debug)] +pub struct VuInputState { + pub file: File, + pub requesting_process: RequestingProcess, + pub input_device: Option +} + +#[derive(Debug,Eq, Hash, PartialEq, Clone)] +pub enum VuFileHandle { + Fh(u64) +} + +impl VuFileHandle { + pub fn from_fuse_file_info(fi: &fuse_lowlevel::fuse_file_info) -> VuFileHandle { + VuFileHandle::Fh(fi.fh) + } +} + +impl std::fmt::Display for VuFileHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VuFileHandle::Fh(fh) => writeln!(f, "VuFileHandle({:?})",fh)?, + } + Ok(()) + } +} + +pub fn get_vuinput_state( + fh:&VuFileHandle, +) -> Result>, String> { + let map = VUINPUT_STATE + .get() + .ok_or("global not initialized".to_string())?; + let guard = map.read().map_err(|e| e.to_string())?; + guard + .get(&fh) + .cloned() + .ok_or("handle not opened".to_string()) +} + + +pub fn insert_vuinput_state( + fh:&VuFileHandle, + state: VuInputState, +) -> Result<(), String> { + let map = VUINPUT_STATE + .get() + .ok_or("global not initialized".to_string())?; + let mut guard = map.write().map_err(|e| e.to_string())?; + + if guard.contains_key(&fh) { + return Err(format!( + "file handle {} already exists. file handles must not be reused!", + &fh + )); + } + + let _ = guard.insert(fh.clone(), Arc::new(Mutex::new(state))); + Ok(()) +} + +pub fn remove_vuinput_state( + fh:&VuFileHandle, +) -> Result>, String> { + let map = VUINPUT_STATE + .get() + .ok_or("global not initialized".to_string())?; + let mut guard = map.write().map_err(|e| e.to_string())?; + let old_value = guard.remove(&fh).ok_or("fh unknown")?; + Ok(old_value) +} + +pub fn initialize_vuinput_state() { + VUINPUT_STATE.set(RwLock::new(HashMap::new())).expect("failed to initialize global state"); +} + +pub fn initialize_dedup_last_error() { + DEDUP_LAST_ERROR.set(Mutex::new(None)).expect("failed to initialize the log deduplication state"); +} + + +#[derive(Debug)] +pub enum VuError { + WriteError +} + + +pub static VUINPUT_STATE: OnceLock>>>> = OnceLock::new(); + +// For log limiting. Idea: Move to log_limit crate +pub static DEDUP_LAST_ERROR: OnceLock>> = OnceLock::new(); + diff --git a/vuinputd/src/cuse_device/vuinput_ioctl.rs b/vuinputd/src/cuse_device/vuinput_ioctl.rs index d157471..9483c70 100644 --- a/vuinputd/src/cuse_device/vuinput_ioctl.rs +++ b/vuinputd/src/cuse_device/vuinput_ioctl.rs @@ -12,13 +12,15 @@ use std::os::fd::AsRawFd; use std::os::raw::{c_char, c_int, c_uint, c_void}; use uinput_ioctls::*; -use crate::cuse_device::{SYS_INPUT_DIR, VuFileHandle, VuInputDevice, fetch_device_node, get_vuinput_state}; +use crate::cuse_device::{VuFileHandle, get_vuinput_state}; use crate::job_engine::JOB_DISPATCHER; use crate::jobs::inject_in_container_job::InjectInContainerJob; use crate::jobs::remove_from_container_job::RemoveFromContainerJob; use crate::process_tools::SELF_NAMESPACES; use crate::{cuse_device::*, jobs}; +pub const SYS_INPUT_DIR: &str = "/sys/devices/virtual/input/"; + pub unsafe extern "C" fn vuinput_ioctl( _req: fuse_lowlevel::fuse_req_t, _cmd: c_int, @@ -166,7 +168,7 @@ pub unsafe extern "C" fn vuinput_ioctl( debug!("fh {}: devnode: {}", fh, devnode); let (major,minor) = fetch_major_minor(&devnode).unwrap(); debug!("fh {}: major: {} minor: {} ", fh, major,minor); - vuinput_state.input_device = Some(VuInputDevice {cuse_fh:*fh, major: major, minor: minor, syspath: sysname.clone(), devnode: devnode.clone(), runtime_data: None, netlink_data: None }); + vuinput_state.input_device = Some(VuInputDevice {major: major, minor: minor, syspath: sysname.clone(), devnode: devnode.clone() }); // Create device in container, if the request was really from another namespace if ! SELF_NAMESPACES.get().unwrap().equal_mnt_and_net(&vuinput_state.requesting_process.namespaces) { @@ -211,6 +213,7 @@ pub unsafe extern "C" fn vuinput_ioctl( ); // replace vendor and product id to the values from sunshine (see inputtino_common.h of sunshine) // The pid is registered for vuinputd, see https://pid.codes/1209/5020/ + (*setup_ptr).id.bustype = BUS_USB; (*setup_ptr).id.product = 0x5020; (*setup_ptr).id.vendor = 0x1209; ui_dev_setup(fd, setup_ptr).unwrap(); @@ -359,4 +362,38 @@ pub unsafe extern "C" fn vuinput_ioctl( fuse_lowlevel::fuse_reply_err(_req, EBADRQC); } } -} \ No newline at end of file +} + + +pub fn fetch_device_node(path: &str) -> io::Result { + for entry in fs::read_dir(path)? { + let entry = entry?; // propagate per-entry errors + if let Some(name) = entry.file_name().to_str() { + if name.starts_with("event") { + return Ok(format!("/dev/input/{}", name)); + } + } + } + // If no device is found, return an error + Err(io::Error::new(ErrorKind::NotFound, "no device found")) +} + +/// Returns (major, minor) numbers of a device node at `path` +pub fn fetch_major_minor(path: &str) -> io::Result<(u64, u64)> { + let metadata = fs::metadata(path)?; + + // Ensure it's a character device + if !metadata.file_type().is_char_device() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Not a character device", + )); + } + + let rdev = metadata.rdev(); + let major = ((rdev >> 8) & 0xfff) as u64; + let minor = ((rdev & 0xff) | ((rdev >> 12) & 0xfff00)) as u64; + + Ok((major, minor)) +} + diff --git a/vuinputd/src/cuse_device/vuinput_write.rs b/vuinputd/src/cuse_device/vuinput_write.rs index b788dcb..6665b24 100644 --- a/vuinputd/src/cuse_device/vuinput_write.rs +++ b/vuinputd/src/cuse_device/vuinput_write.rs @@ -14,7 +14,6 @@ use libc::{__s32, __u16, input_event}; use crate::cuse_device::*; - // TODO: compat-mode+ ensure sizeof(struct input_event) pub unsafe extern "C" fn vuinput_write( _req: fuse_lowlevel::fuse_req_t, diff --git a/vuinputd/src/jobs/mknod_input_device.rs b/vuinputd/src/jobs/mknod_input_device.rs index 4f2252b..f2ae97b 100644 --- a/vuinputd/src/jobs/mknod_input_device.rs +++ b/vuinputd/src/jobs/mknod_input_device.rs @@ -87,7 +87,7 @@ pub fn remove_input_device(dev_path: String, major: u64, minor: u64) -> Result<( return Err("Device that should be deleted has wrong major and minor".into()) } } - Err(x) => return Err("Could not execute stat on device file".into()) + Err(_x) => return Err("Could not execute stat on device file".into()) } let _ = fs::remove_file(path); diff --git a/vuinputd/src/main.rs b/vuinputd/src/main.rs index 52f2410..42045a3 100644 --- a/vuinputd/src/main.rs +++ b/vuinputd/src/main.rs @@ -19,16 +19,16 @@ use ::cuse_lowlevel::*; use log::info; -use std::collections::HashMap; use std::ffi::CString; use std::os::raw::{c_char}; use std::sync::atomic::{AtomicU64}; -use std::sync::{Mutex, RwLock}; +use std::sync::{Mutex}; pub mod cuse_device; +use crate::cuse_device::state::{initialize_dedup_last_error, initialize_vuinput_state}; use crate::cuse_device::vuinput_open::VUINPUT_COUNTER; -use crate::cuse_device::{DEDUP_LAST_ERROR, VUINPUT_STATE, vuinput_make_cuse_ops}; +use crate::cuse_device::{vuinput_make_cuse_ops}; use crate::jobs::monitor_udev_job::MonitorBackgroundLoop; pub mod process_tools; @@ -43,15 +43,15 @@ pub mod jobs; fn main() -> std::io::Result<()> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init(); - check_permissions().expect("failed to read the capabilities of the vuinputd process");; + check_permissions().expect("failed to read the capabilities of the vuinputd process"); let args: Vec = std::env::args().collect(); - VUINPUT_STATE.set(RwLock::new(HashMap::new())).expect("failed to initialize global state"); + initialize_vuinput_state(); VUINPUT_COUNTER.set(AtomicU64::new(3)).expect("failed to initialize the counter that provides the values of the CUSE file handles"); // 3, because 1 and 2 are usually STDOUT and STDERR JOB_DISPATCHER.set(Mutex::new(Dispatcher::new())).expect("failed to initialize the job dispatcher"); SELF_NAMESPACES.set(get_namespace(Pid::SelfPid)).expect("failed to retrieve the namespaces of the vuinputd process"); - DEDUP_LAST_ERROR.set(Mutex::new(None)).expect("failed to initialize the log deduplication state"); + initialize_dedup_last_error(); JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(MonitorBackgroundLoop::new())); info!("Starting vuinputd");