Start implementation of --placement on-host. Not complete and no automated tests, yet. #1

This commit is contained in:
Johannes Leupolz 2026-01-19 22:50:05 +00:00
parent b4c6c32431
commit 6bde733b09
13 changed files with 193 additions and 99 deletions

View file

@ -126,8 +126,9 @@ on your container setup and isolation model.
#### `--placement on-host`
* Device nodes and udev runtime data are created **on the host** under:
* `/run/vuinputd/{devname}/dev`
* `/run/vuinputd/{devname}/udev/data`
* `/run/vuinputd/{devname}/dev-input`
* `/run/vuinputd/{devname}/udev`
* `/run/vuinputd/{devname}/dev-input` **must** have the mount option `dev`
* The user is expected to **bind-mount these directories** into the container
* Suitable for:
* read-only containers

View file

@ -16,14 +16,18 @@ pub enum Action {
minor: u64,
},
#[serde(rename = "emit-udev-event")]
EmitUdevEvent {
netlink_message: HashMap<String, String>,
#[serde(rename = "write-udev-runtime-data")]
WriteUdevRuntimeData {
runtime_data: Option<String>,
major: u64,
minor: u64,
},
#[serde(rename = "emit-netlink-message")]
EmitNetlinkMessage {
netlink_message: HashMap<String, String>,
},
#[serde(rename = "remove-device")]
RemoveDevice {
path: String,

View file

@ -21,20 +21,22 @@ fn handle_action(action: Action) -> anyhow::Result<()> {
input_device::ensure_input_device(path, major.into(), minor.into())?;
Ok(())
}
Action::EmitUdevEvent {
netlink_message,
Action::WriteUdevRuntimeData {
runtime_data,
major,
minor,
} => {
netlink_message::send_udev_monitor_message_with_properties(netlink_message);
runtime_data::ensure_udev_structure()?;
runtime_data::ensure_udev_structure("/run",true)?;
match runtime_data {
Some(data) => runtime_data::write_udev_data(&data, major.into(), minor.into())?,
None => runtime_data::delete_udev_data(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(())
}
Action::EmitNetlinkMessage { netlink_message } => {
netlink_message::send_udev_monitor_message_with_properties(netlink_message);
Ok(())
}
Action::RemoveDevice { path, major, minor } => {
input_device::remove_input_device(path, major.into(), minor.into())?;
Ok(())

View file

@ -9,11 +9,13 @@ use std::path::Path;
use log::{info, warn};
/// Ensure required udev directories and files exist
pub fn ensure_udev_structure() -> io::Result<()> {
pub fn ensure_udev_structure(path_prefix: &str, warn_if_nonexistent: bool) -> 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 = Path::new("/run/udev/data");
let control_file = Path::new("/run/udev/control");
let data_dir = format!("{}/udev/data", path_prefix);
let data_dir = Path::new(&data_dir);
let control_file = format!("{}/udev/control", path_prefix);
let control_file = Path::new(&control_file);
// Create directory like `mkdir -p`
if !data_dir.exists() {
@ -42,7 +44,7 @@ pub fn ensure_udev_structure() -> io::Result<()> {
/// - remove all lines containing `seat_` references (G:, Q: lines)
/// - replace ID_VUINPUT_* with ID_INPUT_*
/// - write updated content to `/run/udev/data/c<major>:<minor>`
pub fn write_udev_data(content: &str, major: u64, minor: u64) -> io::Result<()> {
pub fn write_udev_data(path_prefix: &str, content: &str, major: u64, minor: u64) -> io::Result<()> {
let mut cleaned = String::new();
for line in content.lines() {
@ -60,7 +62,7 @@ pub fn write_udev_data(content: &str, major: u64, minor: u64) -> io::Result<()>
cleaned.push('\n');
}
let path = format!("/run/udev/data/c{}:{}", major, minor);
let path = format!("{}/udev/data/c{}:{}", path_prefix, major, minor);
let mut file = File::create(&path)?;
file.write_all(cleaned.as_bytes())?;
@ -69,8 +71,8 @@ pub fn write_udev_data(content: &str, major: u64, minor: u64) -> io::Result<()>
/// Delete udev data for a given major/minor number
/// - `major`, `minor` = device numbers
pub fn delete_udev_data(major: u64, minor: u64) -> io::Result<()> {
let path = format!("/run/udev/data/c{}:{}", major, minor);
pub fn delete_udev_data(path_prefix: &str, major: u64, minor: u64) -> io::Result<()> {
let path = format!("{}/udev/data/c{}:{}", path_prefix, major, minor);
fs::remove_file(&path)?;
Ok(())
}

View file

@ -61,10 +61,10 @@ pub fn is_allowed(keytracker: &mut KeyTracker, policy: &DevicePolicy, event: &in
}
}
fn is_allowed_in_mute_sysrq(keytracker: &mut KeyTracker, event: &input_event) -> bool {
fn is_allowed_in_mute_sysrq(_keytracker: &mut KeyTracker, event: &input_event) -> bool {
if event.type_ == EV_KEY && event.code == KEY_SYSRQ {
return false;
}
}
true
}

View file

@ -13,9 +13,9 @@ use uinput_ioctls::*;
use crate::cuse_device::{get_vuinput_state, VuFileHandle};
use crate::job_engine::JOB_DISPATCHER;
use crate::jobs::emit_udev_event_in_container_job::EmitUdevEventInContainerJob;
use crate::jobs::mknod_device_in_container_job::MknodDeviceInContainerJob;
use crate::jobs::remove_from_container_job::RemoveFromContainerJob;
use crate::jobs::emit_udev_event_job::EmitUdevEventJob;
use crate::jobs::mknod_device_job::MknodDeviceJob;
use crate::jobs::remove_device_job::RemoveDeviceJob;
use crate::process_tools::SELF_NAMESPACES;
use crate::{cuse_device::*, jobs};
@ -164,7 +164,7 @@ pub unsafe extern "C" fn vuinput_ioctl(
CStr::from_ptr(resultbuf.as_ptr()).to_string_lossy()
);
debug!("fh {}: syspath: {}", fh, sysname);
let devnode = fetch_device_node(&sysname).unwrap();
let (devname, devnode) = fetch_device_node(&sysname).unwrap();
debug!("fh {}: devnode: {}", fh, devnode);
let (major, minor) = fetch_major_minor(&devnode).unwrap();
debug!("fh {}: major: {} minor: {} ", fh, major, minor);
@ -181,9 +181,9 @@ pub unsafe extern "C" fn vuinput_ioctl(
.unwrap()
.equal_mnt_and_net(&vuinput_state.requesting_process.namespaces)
{
let mknod_job = MknodDeviceInContainerJob::new(
let mknod_job = MknodDeviceJob::new(
vuinput_state.requesting_process.clone(),
devnode.clone(),
devname.clone(),
sysname.clone(),
major,
minor,
@ -195,12 +195,12 @@ pub unsafe extern "C" fn vuinput_ioctl(
.lock()
.unwrap()
.dispatch(Box::new(mknod_job));
awaiter(&jobs::mknod_device_in_container_job::State::Finished);
awaiter(&jobs::mknod_device_job::State::Finished);
debug!("fh {}: mknod_device in container has been finished ", fh);
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
// we do not wait for the udev stuff
let emit_udev_event_job = EmitUdevEventInContainerJob::new(
let emit_udev_event_job = EmitUdevEventJob::new(
vuinput_state.requesting_process.clone(),
devnode.clone(),
sysname.clone(),
@ -229,7 +229,7 @@ pub unsafe extern "C" fn vuinput_ioctl(
.equal_mnt_and_net(&vuinput_state.requesting_process.namespaces)
{
let input_device = input_device.unwrap();
let remove_job = RemoveFromContainerJob::new(
let remove_job = RemoveDeviceJob::new(
vuinput_state.requesting_process.clone(),
input_device.devnode.clone(),
input_device.syspath.clone(),
@ -243,7 +243,7 @@ pub unsafe extern "C" fn vuinput_ioctl(
.lock()
.unwrap()
.dispatch(Box::new(remove_job));
awaiter(&jobs::remove_from_container_job::State::Finished);
awaiter(&jobs::remove_device_job::State::Finished);
debug!(
"fh {}: removing dev-nodes from container has been finished ",
fh
@ -415,12 +415,12 @@ pub unsafe extern "C" fn vuinput_ioctl(
}
}
pub fn fetch_device_node(path: &str) -> io::Result<String> {
pub fn fetch_device_node(path: &str) -> io::Result<(String, 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));
return Ok((name.to_string(), format!("/dev/input/{}", name)));
}
}
}

View file

@ -3,7 +3,7 @@
// Author: Johannes Leupolz <dev@leupolz.eu>
use crate::job_engine::JOB_DISPATCHER;
use crate::jobs::remove_from_container_job::RemoveFromContainerJob;
use crate::jobs::remove_device_job::{self, RemoveDeviceJob};
use crate::process_tools::SELF_NAMESPACES;
use crate::{cuse_device::*, jobs};
use ::cuse_lowlevel::*;
@ -32,7 +32,7 @@ pub unsafe extern "C" fn vuinput_release(
.equal_mnt_and_net(&vuinput_state.requesting_process.namespaces)
{
let input_device = input_device.unwrap();
let remove_job = RemoveFromContainerJob::new(
let remove_job = RemoveDeviceJob::new(
vuinput_state.requesting_process.clone(),
input_device.devnode.clone(),
input_device.syspath.clone(),
@ -46,7 +46,7 @@ pub unsafe extern "C" fn vuinput_release(
.lock()
.unwrap()
.dispatch(Box::new(remove_job));
awaiter(&jobs::remove_from_container_job::State::Finished);
awaiter(&jobs::remove_device_job::State::Finished);
}
drop(vuinput_state);

View file

@ -9,6 +9,7 @@ use std::sync::OnceLock;
pub struct GlobalConfig {
pub policy: DevicePolicy,
pub placement: Placement,
pub devname: String,
}
// The actual static variable. It starts empty and is set once in main().
@ -33,18 +34,23 @@ pub enum DevicePolicy {
pub enum Placement {
#[default]
/// Create inside the container
Inject,
InContainer,
/// Create on the host (user is expected to bind-mount)
Host,
/// Do not create any artifacts
OnHost,
/// Do not create any artifacts (netlink message in container is unaffected)
None,
}
pub fn initialize_global_config(device_policy: &DevicePolicy, placement: &Placement) {
pub fn initialize_global_config(
device_policy: &DevicePolicy,
placement: &Placement,
devname: &Option<String>,
) {
if CONFIG
.set(GlobalConfig {
policy: device_policy.clone(),
placement: placement.clone(),
devname: devname.clone().unwrap_or("vuinput".to_string()),
})
.is_err()
{
@ -60,3 +66,7 @@ pub fn get_device_policy<'a>() -> &'a DevicePolicy {
pub fn get_placement<'a>() -> &'a Placement {
&CONFIG.get().unwrap().placement
}
pub fn get_devname<'a>() -> &'a String {
&CONFIG.get().unwrap().devname
}

View file

@ -14,7 +14,12 @@ use async_io::Timer;
use log::debug;
use crate::{
actions::{action::Action, runtime_data::read_udev_data},
actions::{
self,
action::Action,
runtime_data::{self, read_udev_data},
},
global_config::{self, get_placement, Placement},
job_engine::job::{Job, JobTarget},
jobs::monitor_udev_job::EVENT_STORE,
process_tools::{self, await_process, Pid, RequestingProcess},
@ -28,7 +33,7 @@ pub enum State {
}
#[derive(Clone, Debug)]
pub struct EmitUdevEventInContainerJob {
pub struct EmitUdevEventJob {
requesting_process: RequestingProcess,
target: JobTarget,
dev_path: String,
@ -38,7 +43,7 @@ pub struct EmitUdevEventInContainerJob {
sync_state: Arc<(Mutex<State>, Condvar)>,
}
impl EmitUdevEventInContainerJob {
impl EmitUdevEventJob {
pub fn new(
requesting_process: RequestingProcess,
dev_path: String,
@ -79,7 +84,7 @@ impl EmitUdevEventInContainerJob {
}
}
impl Job for EmitUdevEventInContainerJob {
impl Job for EmitUdevEventJob {
fn desc(&self) -> &str {
"emit udev event into container"
}
@ -88,8 +93,8 @@ impl Job for EmitUdevEventInContainerJob {
false
}
fn create_task(self: &EmitUdevEventInContainerJob) -> Pin<Box<dyn Future<Output = ()>>> {
Box::pin(self.clone().inject_in_container())
fn create_task(self: &EmitUdevEventJob) -> Pin<Box<dyn Future<Output = ()>>> {
Box::pin(self.clone().emit_udev_event())
}
fn job_target(&self) -> JobTarget {
@ -97,8 +102,8 @@ impl Job for EmitUdevEventInContainerJob {
}
}
impl EmitUdevEventInContainerJob {
async fn inject_in_container(self) {
impl EmitUdevEventJob {
async fn emit_udev_event(self) {
// temporary hack that needs to be replaced. We try 50 times
// Should be: Wait for the device to be created, the runtime data to be written and the
// netlink message to be sent
@ -144,18 +149,46 @@ impl EmitUdevEventInContainerJob {
let runtime_data = runtime_data.unwrap();
let netlink_data = netlink_data.unwrap();
let emit_udev_event_action = Action::EmitUdevEvent {
match get_placement() {
Placement::InContainer => {
let write_udev_runtime_data = Action::WriteUdevRuntimeData {
runtime_data: Some(runtime_data),
major: self.major,
minor: self.minor,
};
let child_pid =
process_tools::start_action(write_udev_runtime_data, &self.requesting_process)
.expect("subprocess should work");
let _exit_info = await_process(Pid::Pid(child_pid)).await.unwrap();
}
Placement::OnHost => {
let path_prefix = format!("/run/vuinputd/{}", global_config::get_devname());
runtime_data::write_udev_data(
&path_prefix,
&runtime_data,
self.major.into(),
self.minor.into(),
)
.expect(&format!(
"VUI-UDEV-002: could not write into {}",
&path_prefix
)); //TODO: somewhat costly
}
Placement::None => {}
}
// this is always in the container
let emit_netlink_message = Action::EmitNetlinkMessage {
netlink_message: netlink_data.clone(),
runtime_data: Some(runtime_data),
major: self.major,
minor: self.minor,
};
let child_pid =
process_tools::start_action(emit_udev_event_action, &self.requesting_process)
.expect("subprocess should work");
let child_pid = process_tools::start_action(emit_netlink_message, &self.requesting_process)
.expect("subprocess should work");
let _exit_info = await_process(Pid::Pid(child_pid)).await.unwrap();
self.set_state(&State::Finished);
}
}

View file

@ -9,11 +9,14 @@ use std::{
};
use crate::{
actions::action::Action,
actions::{action::Action, input_device},
global_config::{self, Placement},
job_engine::job::{Job, JobTarget},
process_tools::{self, await_process, Pid, RequestingProcess},
};
use crate::actions::runtime_data::write_udev_data;
#[derive(Clone, Debug, Copy, PartialOrd, PartialEq)]
pub enum State {
Initialized,
@ -22,20 +25,20 @@ pub enum State {
}
#[derive(Clone, Debug)]
pub struct MknodDeviceInContainerJob {
pub struct MknodDeviceJob {
requesting_process: RequestingProcess,
target: JobTarget,
dev_path: String,
devname: String,
sys_path: String,
major: u64,
minor: u64,
sync_state: Arc<(Mutex<State>, Condvar)>,
}
impl MknodDeviceInContainerJob {
impl MknodDeviceJob {
pub fn new(
requesting_process: RequestingProcess,
dev_path: String,
devname: String,
sys_path: String,
major: u64,
minor: u64,
@ -43,7 +46,7 @@ impl MknodDeviceInContainerJob {
Self {
requesting_process: requesting_process.clone(),
target: JobTarget::Container(requesting_process),
dev_path: dev_path,
devname: devname,
sys_path: sys_path,
major: major,
minor: minor,
@ -73,7 +76,7 @@ impl MknodDeviceInContainerJob {
}
}
impl Job for MknodDeviceInContainerJob {
impl Job for MknodDeviceJob {
fn desc(&self) -> &str {
"mknod input device in container"
}
@ -82,8 +85,8 @@ impl Job for MknodDeviceInContainerJob {
false
}
fn create_task(self: &MknodDeviceInContainerJob) -> Pin<Box<dyn Future<Output = ()>>> {
Box::pin(self.clone().inject_in_container())
fn create_task(self: &MknodDeviceJob) -> Pin<Box<dyn Future<Output = ()>>> {
Box::pin(self.clone().mknod_device())
}
fn job_target(&self) -> JobTarget {
@ -91,18 +94,35 @@ impl Job for MknodDeviceInContainerJob {
}
}
impl MknodDeviceInContainerJob {
async fn inject_in_container(self) {
let mknod_device_action = Action::MknodDevice {
path: self.dev_path.clone(),
major: self.major,
minor: self.minor,
};
impl MknodDeviceJob {
async fn mknod_device(self) {
match global_config::get_placement() {
Placement::InContainer => {
let mknod_device_action = Action::MknodDevice {
path: format!("/dev/input/{}", &self.devname),
major: self.major,
minor: self.minor,
};
let child_pid = process_tools::start_action(mknod_device_action, &self.requesting_process)
.expect("subprocess should work");
let child_pid =
process_tools::start_action(mknod_device_action, &self.requesting_process)
.expect("subprocess should work");
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
);
input_device::ensure_input_device(path.clone(), self.major, self.minor)
.expect(&format!("VUI-DEV-001: could not create {}", &path));
//TODO: somewhat costly
}
Placement::None => {}
}
let _exit_info = await_process(Pid::Pid(child_pid)).await.unwrap();
self.set_state(&State::Finished);
}
}

View file

@ -2,7 +2,7 @@
//
// Author: Johannes Leupolz <dev@leupolz.eu>
pub mod emit_udev_event_in_container_job;
pub mod mknod_device_in_container_job;
pub mod emit_udev_event_job;
pub mod mknod_device_job;
pub mod monitor_udev_job;
pub mod remove_from_container_job;
pub mod remove_device_job;

View file

@ -12,6 +12,7 @@ use log::debug;
use crate::{
actions::action::Action,
global_config::{self, Placement},
job_engine::job::{Job, JobTarget},
jobs::monitor_udev_job::EVENT_STORE,
process_tools::{self, await_process, Pid, RequestingProcess},
@ -25,7 +26,7 @@ pub enum State {
}
#[derive(Clone, Debug)]
pub struct RemoveFromContainerJob {
pub struct RemoveDeviceJob {
requesting_process: RequestingProcess,
target: JobTarget,
dev_path: String,
@ -35,7 +36,7 @@ pub struct RemoveFromContainerJob {
sync_state: Arc<(Mutex<State>, Condvar)>,
}
impl RemoveFromContainerJob {
impl RemoveDeviceJob {
pub fn new(
requesting_process: RequestingProcess,
dev_path: String,
@ -75,7 +76,7 @@ impl RemoveFromContainerJob {
}
}
impl Job for RemoveFromContainerJob {
impl Job for RemoveDeviceJob {
fn desc(&self) -> &str {
"Remove input device from container"
}
@ -84,8 +85,8 @@ impl Job for RemoveFromContainerJob {
false
}
fn create_task(self: &RemoveFromContainerJob) -> Pin<Box<dyn Future<Output = ()>>> {
Box::pin(self.clone().remove_from_container())
fn create_task(self: &RemoveDeviceJob) -> Pin<Box<dyn Future<Output = ()>>> {
Box::pin(self.clone().remove_device())
}
fn job_target(&self) -> JobTarget {
@ -93,8 +94,8 @@ impl Job for RemoveFromContainerJob {
}
}
impl RemoveFromContainerJob {
async fn remove_from_container(self) {
impl RemoveDeviceJob {
async fn remove_device(self) {
self.set_state(&State::Started);
let netlink_event = match EVENT_STORE
@ -124,29 +125,50 @@ impl RemoveFromContainerJob {
let _ = netlink_data.insert("ACTION".to_string(), "remove".to_string());
let remove_device_action = Action::RemoveDevice {
path: dev_path.clone(),
major: self.major,
minor: self.minor,
};
match global_config::get_placement() {
Placement::InContainer => {
let remove_device_action = Action::RemoveDevice {
path: dev_path.clone(),
major: self.major,
minor: self.minor,
};
let child_pid_1 =
process_tools::start_action(remove_device_action, &self.requesting_process)
let child_pid_1 =
process_tools::start_action(remove_device_action, &self.requesting_process)
.expect("subprocess should work");
let write_udev_runtime_data_action = Action::WriteUdevRuntimeData {
runtime_data: None,
major: self.major,
minor: self.minor,
};
let child_pid_2 = process_tools::start_action(
write_udev_runtime_data_action,
&self.requesting_process,
)
.expect("subprocess should work");
let emit_udev_event_action = Action::EmitUdevEvent {
let _exit_info = await_process(Pid::Pid(child_pid_1)).await;
let _exit_info = await_process(Pid::Pid(child_pid_2)).await;
}
Placement::OnHost => {
todo!();
}
Placement::None => {}
}
// this is always in the container
let emit_netlink_message = Action::EmitNetlinkMessage {
netlink_message: netlink_data.clone(),
runtime_data: None,
major: self.major,
minor: self.minor,
};
let child_pid_2 =
process_tools::start_action(emit_udev_event_action, &self.requesting_process)
let child_pid_netlink =
process_tools::start_action(emit_netlink_message, &self.requesting_process)
.expect("subprocess should work");
let _exit_info = await_process(Pid::Pid(child_pid_1)).await;
let _exit_info = await_process(Pid::Pid(child_pid_2)).await;
let _exit_info = await_process(Pid::Pid(child_pid_netlink)).await;
self.set_state(&State::Finished);
}
}

View file

@ -191,7 +191,7 @@ 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);
global_config::initialize_global_config(&args.device_policy, &args.placement, &args.devname);
initialize_vuinput_state();
VUINPUT_COUNTER.set(AtomicU64::new(3)).expect(
"failed to initialize the counter that provides the values of the CUSE file handles",