Only write to uinput when we have a complete message

This commit is contained in:
Johannes Leupolz 2025-11-09 20:37:43 +00:00
parent b645f73dc0
commit e66347135d
2 changed files with 85 additions and 53 deletions

View file

@ -96,7 +96,7 @@ enum VuError {
static VUINPUT_COUNTER: OnceLock<AtomicU64> = OnceLock::new();
static VUINPUT_STATE: OnceLock<RwLock<HashMap<VuFileHandle, Arc<Mutex<VuInputState>>>>> = OnceLock::new();
static JOB_DISPATCHER: OnceLock<Mutex<Dispatcher>>= OnceLock::new();
static SELF_REQUESTING_PROCESSES: OnceLock<RequestingProcess>= OnceLock::new();
static VUINPUTD_NAMESPACES: OnceLock<Namespaces>= OnceLock::new();
// For log limiting. Idea: Move to log_limit crate
static DEDUP_LAST_ERROR: OnceLock<Mutex<Option<(u64,VuError)>>> = OnceLock::new();
@ -193,8 +193,8 @@ unsafe extern "C" fn vuinput_open(
let fh = get_fresh_filehandle();
let ctx = fuse_lowlevel::fuse_req_ctx(_req);
debug!("fh {}: opened by process id {} (host view)", fh, (*ctx).pid);
let namespaces = get_namespaces(Pid::Pid((*ctx).pid));
debug!("fh {}: namespaces {}", fh, namespaces);
let requesting_process = get_requesting_process(Pid::Pid((*ctx).pid));
debug!("fh {}: namespaces {}", fh, requesting_process);
// namespaces net:4026531840, uts:4026531838, ipc:4026531839, pid:4026531836, pid_for_children:4026531836, user:4026531837, mnt:4026531841, cgroup:4026531835, time:4026531834, time_for_children:4026531834
(*_fi).fh = fh;
// Open the path in read-only mode, returns `io::Result<File>`
@ -210,7 +210,7 @@ unsafe extern "C" fn vuinput_open(
&VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap()),
VuInputState {
file: v,
requesting_process: namespaces,
requesting_process,
input_device: None
},
)
@ -238,6 +238,7 @@ unsafe extern "C" fn vuinput_write(
"vuinput_write: offset needs to be 0 but is {}",
_off
);
let fh = &(*_fi).fh;
let slice = std::slice::from_raw_parts(_buf as *const u8, _size);
let vuinput_state_mutex = get_vuinput_state(&VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap())).unwrap();
@ -281,12 +282,20 @@ unsafe extern "C" fn vuinput_write(
return;
}
let result = vuinput_state.file.write_all(slice);
let chunksize = std::mem::size_of::<libc::input_event>();
let mut bytes = 0;
let mut result = Result::Ok(());
while bytes + chunksize <= _size && result.is_ok() {
result = vuinput_state.file.write_all(&slice[bytes..bytes + chunksize]);
bytes += chunksize;
}
match result {
Ok(_) => {
trace!("wrote {} bytes", _size);
fuse_lowlevel::fuse_reply_write(_req, _size);
debug!("wrote {} bytes", bytes);
fuse_lowlevel::fuse_reply_write(_req, bytes);
}
Err(e) => {
let mut last_error = DEDUP_LAST_ERROR.get().unwrap().lock().unwrap();
@ -317,7 +326,7 @@ unsafe extern "C" fn vuinput_release(
// Only do this in case it has not already been done by the ioctl UI_DEV_DESTROY
// this here is relevant if the process was killed and didn't have the chance to send the
// ioctl UI_DEV_DESTROY.
if input_device.is_some() && ! SELF_REQUESTING_PROCESSES.get().unwrap().equal_mnt_and_net(&vuinput_state.requesting_process) {
if input_device.is_some() && ! VUINPUTD_NAMESPACES.get().unwrap().equal_mnt_and_net(&vuinput_state.requesting_process.namespaces) {
let input_device = input_device.unwrap();
let remove_job=RemoveFromContainerJob::new(vuinput_state.requesting_process.clone(),input_device.devnode.clone(),input_device.syspath.clone(),input_device.major,input_device.minor);
JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(remove_job));
@ -485,7 +494,7 @@ unsafe extern "C" fn vuinput_ioctl(
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 });
// Create device in container, if the request was really from another namespace
if ! SELF_REQUESTING_PROCESSES.get().unwrap().equal_mnt_and_net(&vuinput_state.requesting_process) {
if ! VUINPUTD_NAMESPACES.get().unwrap().equal_mnt_and_net(&vuinput_state.requesting_process.namespaces) {
let inject_job=InjectInContainerJob::new(vuinput_state.requesting_process.clone(),devnode.clone(),sysname.clone(),major,minor);
JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(inject_job));
}
@ -500,7 +509,7 @@ unsafe extern "C" fn vuinput_ioctl(
let input_device = vuinput_state.input_device.take();
// Remove device in container, if the request was really from another namespace
if input_device.is_some() && ! SELF_REQUESTING_PROCESSES.get().unwrap().equal_mnt_and_net(&vuinput_state.requesting_process) {
if input_device.is_some() && ! VUINPUTD_NAMESPACES.get().unwrap().equal_mnt_and_net(&vuinput_state.requesting_process.namespaces) {
let input_device = input_device.unwrap();
let remove_job=RemoveFromContainerJob::new(vuinput_state.requesting_process.clone(),input_device.devnode.clone(),input_device.syspath.clone(),input_device.major,input_device.minor);
JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(remove_job));
@ -724,7 +733,7 @@ fn main() -> std::io::Result<()> {
VUINPUT_STATE.set(RwLock::new(HashMap::new())).unwrap();
VUINPUT_COUNTER.set(AtomicU64::new(3)).unwrap();
JOB_DISPATCHER.set(Mutex::new(Dispatcher::new())).unwrap();
SELF_REQUESTING_PROCESSES.set(get_namespaces(Pid::SelfPid)).unwrap();
VUINPUTD_NAMESPACES.set(get_namespace(Pid::SelfPid)).unwrap();
DEDUP_LAST_ERROR.set(Mutex::new(None)).unwrap();
JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(MonitorBackgroundLoop::new()));

View file

@ -31,7 +31,7 @@ impl Pid {
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
struct Namespaces {
pub struct Namespaces {
pub net: Option<u64>,
pub uts: Option<u64>,
pub ipc: Option<u64>,
@ -46,27 +46,34 @@ struct Namespaces {
}
/// Returns true if the process with `pid` is a 32-bit (compat) process. None, if unsure.
pub fn is_compat_process(pid: i32) -> Option<bool> {
const EI_CLASS: usize = 4;
const ELFCLASS32: u8 = 1;
const ELFCLASS64: u8 = 2;
pub fn is_compat_process(pid: Pid) -> Option<bool> {
let exe_path = format!("/proc/{}/exe", pid);
let mut buf = [0u8; 5];
match pid {
Pid::Pid(pid) => {
const EI_CLASS: usize = 4;
const ELFCLASS32: u8 = 1;
const ELFCLASS64: u8 = 2;
match File::open(&exe_path).and_then(|mut f| f.read_exact(&mut buf)) {
Ok(()) => {
// ELF magic check
if &buf[0..4] != b"\x7FELF" {
return None;
}
match buf[EI_CLASS] {
ELFCLASS32 => Some(true),
ELFCLASS64 => Some(false),
_ => None,
let exe_path = format!("/proc/{}/exe", pid);
let mut buf = [0u8; 5];
match File::open(&exe_path).and_then(|mut f| f.read_exact(&mut buf)) {
Ok(()) => {
// ELF magic check
if &buf[0..4] != b"\x7FELF" {
return None;
}
match buf[EI_CLASS] {
ELFCLASS32 => Some(true),
ELFCLASS64 => Some(false),
_ => None,
}
}
Err(_) => None,
}
}
Err(_) => None,
Pid::SelfPid =>
unreachable!()
}
}
@ -75,7 +82,8 @@ pub fn is_compat_process(pid: i32) -> Option<bool> {
pub struct RequestingProcess {
pub nspath: String,
pub nsroot: String,
nsinodes: Namespaces,
pub namespaces: Namespaces,
pub is_compat: bool,
}
impl Namespaces {
@ -86,28 +94,32 @@ impl Namespaces {
impl RequestingProcess {
pub fn equal_mnt_and_net(&self, other: &RequestingProcess) -> bool {
self.nsinodes.equal_mnt_and_net(&other.nsinodes)
self.namespaces.equal_mnt_and_net(&other.namespaces)
}
pub fn equal_mnt_and_net_ns(&self, other: &Namespaces) -> bool {
self.namespaces.equal_mnt_and_net(&other)
}
}
impl std::fmt::Display for RequestingProcess {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Namespaces:")?;
writeln!(f, " net: {:?}", self.nsinodes.net)?;
writeln!(f, " uts: {:?}", self.nsinodes.uts)?;
writeln!(f, " ipc: {:?}", self.nsinodes.ipc)?;
writeln!(f, " pid: {:?}", self.nsinodes.pid)?;
writeln!(f, " pid_for_children: {:?}", self.nsinodes.pid_for_children)?;
writeln!(f, " user: {:?}", self.nsinodes.user)?;
writeln!(f, " mnt: {:?}", self.nsinodes.mnt)?;
writeln!(f, " cgroup: {:?}", self.nsinodes.cgroup)?;
writeln!(f, " time: {:?}", self.nsinodes.time)?;
writeln!(f, " time_for_children: {:?}", self.nsinodes.time_for_children)?;
writeln!(f, " net: {:?}", self.namespaces.net)?;
writeln!(f, " uts: {:?}", self.namespaces.uts)?;
writeln!(f, " ipc: {:?}", self.namespaces.ipc)?;
writeln!(f, " pid: {:?}", self.namespaces.pid)?;
writeln!(f, " pid_for_children: {:?}", self.namespaces.pid_for_children)?;
writeln!(f, " user: {:?}", self.namespaces.user)?;
writeln!(f, " mnt: {:?}", self.namespaces.mnt)?;
writeln!(f, " cgroup: {:?}", self.namespaces.cgroup)?;
writeln!(f, " time: {:?}", self.namespaces.time)?;
writeln!(f, " time_for_children: {:?}", self.namespaces.time_for_children)?;
Ok(())
}
}
fn get_namespace_inodes(pid: Pid) -> Namespaces {
pub fn get_namespace(pid: Pid) -> Namespaces {
let pid: String = match pid {
Pid::Pid(pid) => pid.to_string(),
Pid::SelfPid => "self".to_string(),
@ -171,21 +183,37 @@ fn get_ppid(pid: Pid) -> Option<Pid> {
pub fn get_namespaces(pid: Pid) -> RequestingProcess {
pub fn get_requesting_process(pid: Pid) -> RequestingProcess {
match pid {
Pid::Pid(_) =>
{
let is_compat = match is_compat_process(pid) {
Some(true) => {
debug!("identified process {} as 64 bit process",pid.path());
false
},
Some(false) => {
debug!("identified process {} as 32 bit process",pid.path());
true
},
None => {
debug!("could not identify bitness of process {}. Assume 64 bit process",pid.path());
false
},
};
// go up the parent hierarchy until we find a parent with different namespaces
let mut ppid = pid;
let nsinodes = get_namespace_inodes(pid);
let nsinodes = get_namespace(pid);
loop {
let candidate_ppid = get_ppid(ppid);
match candidate_ppid {
None => break,
Some(candidate_ppid) =>
{
let ppid_nsinodes = get_namespace_inodes(candidate_ppid);
let ppid_nsinodes = get_namespace(candidate_ppid);
if nsinodes.equal_mnt_and_net(&ppid_nsinodes) {
ppid=candidate_ppid;
} else {
@ -202,18 +230,13 @@ pub fn get_namespaces(pid: Pid) -> RequestingProcess {
RequestingProcess {
nspath: nspath,
nsroot: nsroot,
nsinodes: nsinodes,
namespaces: nsinodes,
is_compat: is_compat
}
},
Pid::SelfPid =>
{
let nsinodes = get_namespace_inodes(pid);
let nspath = format!("{}/ns", pid.path());
RequestingProcess {
nspath: nspath.clone(),
nsroot: nspath,
nsinodes: nsinodes,
}
unreachable!();
},
}
}