From fd55bffddb9e3511ba57b7e6c2af3dfe1382e903 Mon Sep 17 00:00:00 2001 From: Johannes Leupolz Date: Tue, 20 Jan 2026 21:26:18 +0000 Subject: [PATCH] Implementation of the remove-device-step for --placement on-host. Not tested, yet --- docs/TROUBLESHOOTING.md | 2 + vuinputd/src/actions/handle_action.rs | 8 ++- vuinputd/src/cuse_device/state.rs | 1 + vuinputd/src/cuse_device/vuinput_ioctl.rs | 3 +- vuinputd/src/cuse_device/vuinput_release.rs | 4 +- vuinputd/src/global_config.rs | 10 +-- vuinputd/src/input_realizer/host_fs.rs | 80 +++++++++++++++++++++ vuinputd/src/input_realizer/mod.rs | 1 + vuinputd/src/input_realizer/runtime_data.rs | 6 +- vuinputd/src/jobs/emit_udev_event_job.rs | 4 +- vuinputd/src/jobs/mknod_device_job.rs | 9 +-- vuinputd/src/jobs/remove_device_job.rs | 24 ++++--- vuinputd/src/main.rs | 6 +- 13 files changed, 125 insertions(+), 33 deletions(-) create mode 100644 vuinputd/src/input_realizer/host_fs.rs diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 77d7036..5ba8194 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -24,6 +24,8 @@ Error messages may change over time; error codes do not. |------|------|--------| | VUI-UDEV-001 | udev | udev control socket not reachable | | VUI-UDEV-002 | udev | could not write into /run/vuinputd/... | +| VUI-UDEV-003 | udev | could not remove udev data from ... | +| VUI-DEV-001 | ddev | could not remove device node ... | --- diff --git a/vuinputd/src/actions/handle_action.rs b/vuinputd/src/actions/handle_action.rs index 6b95bc9..3e12026 100644 --- a/vuinputd/src/actions/handle_action.rs +++ b/vuinputd/src/actions/handle_action.rs @@ -26,10 +26,12 @@ fn handle_action(action: Action) -> anyhow::Result<()> { major, minor, } => { - runtime_data::ensure_udev_structure("/run",true)?; + runtime_data::ensure_udev_structure()?; match runtime_data { - Some(data) => runtime_data::write_udev_data("/run",&data, major.into(), minor.into())?, - None => runtime_data::delete_udev_data("/run",major.into(), minor.into())?, + Some(data) => { + runtime_data::write_udev_data("/run", &data, major.into(), minor.into())? + } + None => runtime_data::delete_udev_data("/run", major.into(), minor.into())?, } Ok(()) } diff --git a/vuinputd/src/cuse_device/state.rs b/vuinputd/src/cuse_device/state.rs index 713c771..36a1410 100644 --- a/vuinputd/src/cuse_device/state.rs +++ b/vuinputd/src/cuse_device/state.rs @@ -15,6 +15,7 @@ pub struct VuInputDevice { pub major: u64, pub minor: u64, pub syspath: String, + pub devname: String, pub devnode: String, } diff --git a/vuinputd/src/cuse_device/vuinput_ioctl.rs b/vuinputd/src/cuse_device/vuinput_ioctl.rs index b4ae890..93a3961 100644 --- a/vuinputd/src/cuse_device/vuinput_ioctl.rs +++ b/vuinputd/src/cuse_device/vuinput_ioctl.rs @@ -172,6 +172,7 @@ pub unsafe extern "C" fn vuinput_ioctl( major: major, minor: minor, syspath: sysname.clone(), + devname: devname.clone(), devnode: devnode.clone(), }); @@ -231,7 +232,7 @@ pub unsafe extern "C" fn vuinput_ioctl( let input_device = input_device.unwrap(); let remove_job = RemoveDeviceJob::new( vuinput_state.requesting_process.clone(), - input_device.devnode.clone(), + input_device.devname.clone(), input_device.syspath.clone(), input_device.major, input_device.minor, diff --git a/vuinputd/src/cuse_device/vuinput_release.rs b/vuinputd/src/cuse_device/vuinput_release.rs index 51e5906..2857064 100644 --- a/vuinputd/src/cuse_device/vuinput_release.rs +++ b/vuinputd/src/cuse_device/vuinput_release.rs @@ -3,7 +3,7 @@ // Author: Johannes Leupolz use crate::job_engine::JOB_DISPATCHER; -use crate::jobs::remove_device_job::{self, RemoveDeviceJob}; +use crate::jobs::remove_device_job::RemoveDeviceJob; use crate::process_tools::SELF_NAMESPACES; use crate::{cuse_device::*, jobs}; use ::cuse_lowlevel::*; @@ -34,7 +34,7 @@ pub unsafe extern "C" fn vuinput_release( let input_device = input_device.unwrap(); let remove_job = RemoveDeviceJob::new( vuinput_state.requesting_process.clone(), - input_device.devnode.clone(), + input_device.devname.clone(), input_device.syspath.clone(), input_device.major, input_device.minor, diff --git a/vuinputd/src/global_config.rs b/vuinputd/src/global_config.rs index 8305284..d91de99 100644 --- a/vuinputd/src/global_config.rs +++ b/vuinputd/src/global_config.rs @@ -2,14 +2,14 @@ // // Author: Johannes Leupolz -use clap::{Parser, ValueEnum}; +use clap::ValueEnum; use std::sync::OnceLock; #[derive(Debug)] pub struct GlobalConfig { pub policy: DevicePolicy, pub placement: Placement, - pub devname: String, + pub vudevname: String, } // The actual static variable. It starts empty and is set once in main(). @@ -50,7 +50,7 @@ pub fn initialize_global_config( .set(GlobalConfig { policy: device_policy.clone(), placement: placement.clone(), - devname: devname.clone().unwrap_or("vuinput".to_string()), + vudevname: devname.clone().unwrap_or("vuinput".to_string()), }) .is_err() { @@ -67,6 +67,6 @@ pub fn get_placement<'a>() -> &'a Placement { &CONFIG.get().unwrap().placement } -pub fn get_devname<'a>() -> &'a String { - &CONFIG.get().unwrap().devname +pub fn get_vudevname<'a>() -> &'a String { + &CONFIG.get().unwrap().vudevname } diff --git a/vuinputd/src/input_realizer/host_fs.rs b/vuinputd/src/input_realizer/host_fs.rs new file mode 100644 index 0000000..06df7d1 --- /dev/null +++ b/vuinputd/src/input_realizer/host_fs.rs @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use std::io::{self, BufRead}; +use std::{ + fs::{self, File}, + path::Path, +}; + +/// Ensure required dev-input, udev directories and files exist +pub fn ensure_host_fs_structure(path_prefix: &str) -> io::Result<()> { + let _ = check_if_path_allows_char_devs(&path_prefix); + let dev_input_dir = format!("{}/dev-input", path_prefix); + let dev_input_dir = Path::new(&dev_input_dir); + // Create directory like `mkdir -p` + if !dev_input_dir.exists() { + fs::create_dir_all(dev_input_dir)?; + } + + // Note that this structure _must_ exist, before a service using libinput is run. + let data_dir = format!("{}/udev/data", path_prefix); + let data_dir = Path::new(&data_dir); + // Create directory like `mkdir -p` + if !data_dir.exists() { + fs::create_dir_all(data_dir)?; + } + + let control_file = format!("{}/udev/control", path_prefix); + let control_file = Path::new(&control_file); + // Ensure /run/udev/control exists, create empty if not + if !control_file.exists() { + File::create(control_file)?; + } + + Ok(()) +} + +/// simple heuristic that checks whether path_prefix allows the hosting of character devices +/// This heuristic is not 100%, but a simple indicator +pub fn check_if_path_allows_char_devs(path: &str) -> io::Result<()> { + let file = File::open("/proc/self/mountinfo")?; + let reader = io::BufReader::new(file); + + for line in reader.lines() { + let line = line?; + + let (left, _) = match line.split_once(" - ") { + Some(v) => v, + None => continue, + }; + + let fields: Vec<&str> = left.split_whitespace().collect(); + + // mount point is field 5 + let mount_point = fields.get(4).copied().unwrap_or(""); + // mount options are field 6 + let options = fields.get(5).copied().unwrap_or(""); + + if mount_point.contains(path) { + if options.split(',').any(|o| o == "nodev") { + log::warn!( + "mount {} is present but mounted with nodev; device nodes will not work", + path + ); + } else { + log::info!("mount {} is present and allows device nodes", path); + } + + return Ok(()); + } + } + + log::warn!( + "expected mount {} not found; user likely forgot to mount tmpfs with dev-option on it", + path + ); + + Ok(()) +} diff --git a/vuinputd/src/input_realizer/mod.rs b/vuinputd/src/input_realizer/mod.rs index 0be3f87..97565e8 100644 --- a/vuinputd/src/input_realizer/mod.rs +++ b/vuinputd/src/input_realizer/mod.rs @@ -2,6 +2,7 @@ // // Author: Johannes Leupolz +pub mod host_fs; pub mod input_device; pub mod netlink_message; pub mod runtime_data; diff --git a/vuinputd/src/input_realizer/runtime_data.rs b/vuinputd/src/input_realizer/runtime_data.rs index 2ad44db..b0be8cc 100644 --- a/vuinputd/src/input_realizer/runtime_data.rs +++ b/vuinputd/src/input_realizer/runtime_data.rs @@ -9,12 +9,12 @@ use std::path::Path; use log::{info, warn}; /// Ensure required udev directories and files exist -pub fn ensure_udev_structure(path_prefix: &str, warn_if_nonexistent: bool) -> io::Result<()> { +pub fn ensure_udev_structure() -> io::Result<()> { // Note that this structure _must_ exist, before a service using libinput is run. The time of device creation might be too late. - let data_dir = format!("{}/udev/data", path_prefix); + let data_dir = format!("/run/udev/data"); let data_dir = Path::new(&data_dir); - let control_file = format!("{}/udev/control", path_prefix); + let control_file = format!("/run/udev/control"); let control_file = Path::new(&control_file); // Create directory like `mkdir -p` diff --git a/vuinputd/src/jobs/emit_udev_event_job.rs b/vuinputd/src/jobs/emit_udev_event_job.rs index 25034b9..a441d81 100644 --- a/vuinputd/src/jobs/emit_udev_event_job.rs +++ b/vuinputd/src/jobs/emit_udev_event_job.rs @@ -14,7 +14,7 @@ use async_io::Timer; use log::debug; use crate::{ - actions::{self, action::Action}, + actions::action::Action, global_config::{self, get_placement, Placement}, input_realizer::runtime_data, job_engine::job::{Job, JobTarget}, @@ -161,7 +161,7 @@ impl EmitUdevEventJob { let _exit_info = await_process(Pid::Pid(child_pid)).await.unwrap(); } Placement::OnHost => { - let path_prefix = format!("/run/vuinputd/{}", global_config::get_devname()); + let path_prefix = format!("/run/vuinputd/{}", global_config::get_vudevname()); runtime_data::write_udev_data( &path_prefix, &runtime_data, diff --git a/vuinputd/src/jobs/mknod_device_job.rs b/vuinputd/src/jobs/mknod_device_job.rs index bc443c1..1146a36 100644 --- a/vuinputd/src/jobs/mknod_device_job.rs +++ b/vuinputd/src/jobs/mknod_device_job.rs @@ -16,8 +16,6 @@ use crate::{ process_tools::{self, await_process, Pid, RequestingProcess}, }; -use crate::input_realizer::runtime_data::write_udev_data; - #[derive(Clone, Debug, Copy, PartialOrd, PartialEq)] pub enum State { Initialized, @@ -112,11 +110,8 @@ impl MknodDeviceJob { let _exit_info = await_process(Pid::Pid(child_pid)).await.unwrap(); } Placement::OnHost => { - let path = format!( - "/run/vuinputd/{}/dev-input/{}", - global_config::get_devname(), - self.devname - ); + let path_prefix = format!("/run/vuinputd/{}", global_config::get_vudevname()); + let path = format!("{}/dev-input/{}", path_prefix, self.devname); input_device::ensure_input_device(path.clone(), self.major, self.minor) .expect(&format!("VUI-DEV-001: could not create {}", &path)); //TODO: somewhat costly diff --git a/vuinputd/src/jobs/remove_device_job.rs b/vuinputd/src/jobs/remove_device_job.rs index e3c7281..4e76260 100644 --- a/vuinputd/src/jobs/remove_device_job.rs +++ b/vuinputd/src/jobs/remove_device_job.rs @@ -30,7 +30,7 @@ pub enum State { pub struct RemoveDeviceJob { requesting_process: RequestingProcess, target: JobTarget, - dev_path: String, + dev_name: String, sys_path: String, major: u64, minor: u64, @@ -40,7 +40,7 @@ pub struct RemoveDeviceJob { impl RemoveDeviceJob { pub fn new( requesting_process: RequestingProcess, - dev_path: String, + dev_name: String, sys_path: String, major: u64, minor: u64, @@ -48,7 +48,7 @@ impl RemoveDeviceJob { Self { requesting_process: requesting_process.clone(), target: JobTarget::Container(requesting_process), - dev_path: dev_path, + dev_name: dev_name, sys_path: sys_path, major: major, minor: minor, @@ -122,14 +122,14 @@ impl RemoveDeviceJob { let netlink_data = netlink_event.add_data; let mut netlink_data = netlink_data.unwrap().clone(); - let dev_path = self.dev_path.clone(); let _ = netlink_data.insert("ACTION".to_string(), "remove".to_string()); match global_config::get_placement() { Placement::InContainer => { + let dev_path = format!("/dev/input/{}", &self.dev_name); let remove_device_action = Action::RemoveDevice { - path: dev_path.clone(), + path: dev_path, major: self.major, minor: self.minor, }; @@ -154,9 +154,17 @@ impl RemoveDeviceJob { let _exit_info = await_process(Pid::Pid(child_pid_2)).await; } Placement::OnHost => { - todo!(); - //input_device::remove_input_device(path, major.into(), minor.into())?; - //runtime_data::delete_udev_data("/run",major.into(), minor.into())?; + let path_prefix = format!("/run/vuinputd/{}", global_config::get_vudevname()); + let devnode = format!("{}/dev-input/{}", path_prefix, self.dev_name); + input_device::remove_input_device(devnode.clone(), self.major, self.minor).expect( + &format!("VUI-DEV-003: could not remove device node {}", &devnode), + ); + runtime_data::delete_udev_data(&path_prefix, self.major, self.minor).expect( + &format!( + "VUI-UDEV-003: could not remove udev data from {}", + &path_prefix + ), + ); } Placement::None => {} } diff --git a/vuinputd/src/main.rs b/vuinputd/src/main.rs index 43c2aa5..a6e69cc 100644 --- a/vuinputd/src/main.rs +++ b/vuinputd/src/main.rs @@ -33,6 +33,7 @@ use crate::cuse_device::state::{initialize_dedup_last_error, initialize_vuinput_ use crate::cuse_device::vuinput_make_cuse_ops; use crate::cuse_device::vuinput_open::VUINPUT_COUNTER; use crate::global_config::{DevicePolicy, Placement}; +use crate::input_realizer::host_fs; use crate::jobs::monitor_udev_job::MonitorBackgroundLoop; pub mod process_tools; @@ -219,8 +220,9 @@ fn main() -> std::io::Result<()> { None => "vuinput", Some(devname) => devname, }; - if args.placement==Placement::OnHost { - todo!("ensure structure and writablity of dev-input") + if args.placement == Placement::OnHost { + let path_prefix = format!("/run/vuinputd/{}", global_config::get_vudevname()); + let _ = host_fs::ensure_host_fs_structure(&path_prefix); } let vuinput_devicename = CString::new(format!("DEVNAME={}", vuinput_devicename)).unwrap();