Small refactorings and note in SECURITY.md

This commit is contained in:
Johannes Leupolz 2025-12-10 22:46:17 +00:00
parent 2fe9a70006
commit b3d0a0225d
7 changed files with 174 additions and 147 deletions

View file

@ -8,4 +8,15 @@
## Unsafe code
Known problem: A lot of unsafe code. Open are ideas to mitigate this issue.
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.

View file

@ -2,153 +2,21 @@
//
// Author: Johannes Leupolz <dev@leupolz.eu>
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<String>,
netlink_data: Option<String>
}
#[derive(Debug)]
pub struct VuInputState {
file: File,
requesting_process: RequestingProcess,
input_device: Option<VuInputDevice>
}
#[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<RwLock<HashMap<VuFileHandle, Arc<Mutex<VuInputState>>>>> = OnceLock::new();
// For log limiting. Idea: Move to log_limit crate
pub static DEDUP_LAST_ERROR: OnceLock<Mutex<Option<(u64,VuError)>>> = 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<Arc<Mutex<VuInputState>>, 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<Arc<Mutex<VuInputState>>, 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<String> {
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".

View file

@ -0,0 +1,112 @@
// SPDX-License-Identifier: MIT
//
// Author: Johannes Leupolz <dev@leupolz.eu>
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<VuInputDevice>
}
#[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<Arc<Mutex<VuInputState>>, 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<Arc<Mutex<VuInputState>>, 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<RwLock<HashMap<VuFileHandle, Arc<Mutex<VuInputState>>>>> = OnceLock::new();
// For log limiting. Idea: Move to log_limit crate
pub static DEDUP_LAST_ERROR: OnceLock<Mutex<Option<(u64,VuError)>>> = OnceLock::new();

View file

@ -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);
}
}
}
}
pub fn fetch_device_node(path: &str) -> io::Result<String> {
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))
}

View file

@ -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,

View file

@ -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);

View file

@ -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<String> = 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");