diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 5f58dac..503d468 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -503,8 +503,6 @@ the compositor, via `systemd-logind`, owns a VT, switches it to `KD_GRAPHICS`, a The missing piece is a **well-defined fallback** for the “no graphical session” case. ---- - ### **Decision** `fallbackdm` is implemented as a **logind-managed fallback graphical session**. @@ -518,7 +516,6 @@ It runs only when **no other graphical session is active** on the seat and exist `fallbackdm` itself does **not** manipulate VTs, perform `KDSETMODE` ioctls, or access `/dev/tty` directly. All VT and seat handling is delegated to `systemd-logind` **via a standard PAM session**. ---- ### **Behavior and Lifecycle** @@ -535,7 +532,6 @@ All VT and seat handling is delegated to `systemd-logind` **via a standard PAM s `fallbackdm` is non-interactive by design but may display **minimal status information** in the future. ---- ### **Rationale** @@ -551,7 +547,6 @@ This design: Conceptually, `fallbackdm` acts as a **headless placeholder graphical session** that ensures consistent system behavior even when no real graphical environment is running. ---- ### **Alternatives Considered** @@ -566,6 +561,22 @@ Conceptually, `fallbackdm` acts as a **headless placeholder graphical session** --- +### 3.12 Device Policies & Input Sanitization + +Exposing raw access to `/dev/uinput` inside a container introduces significant security risks. A malicious process could theoretically emulate a keyboard to execute "BadUSB"-style attacks, trigger kernel-level commands (Magic SysRq), or switch Virtual Terminals (VT) to escape the graphical session. + +To mitigate this, `vuinputd` implements an **Active Filtering Layer** (CUSE middleware) that enforces strict device policies before requests reach the host kernel. This is controlled via the `--device-policy` flag. + +The filtering operates on two levels (Defense in Depth): + +1. **Capability Filtering (`ioctl`):** During device creation, `vuinputd` inspects `UI_SET_KEYBIT`, `UI_SET_RELBIT`, etc. If a container requests capabilities forbidden by the active policy (e.g., a gamepad trying to claim it has a SysRq key), the request is silently ignored or rejected. The resulting device on the host simply lacks those hardware capabilities. +2. **Event Filtering (`write`):** At runtime, `vuinputd` inspects the stream of input events. It maintains internal state (tracking modifiers like `Alt` or `Ctrl`) to detect and drop dangerous sequences (e.g., `Alt` + `F1-F12` for VT switching) that the capability filter alone cannot block. + +**Supported Policies:** + +* **`strict-gamepad` (Whitelist):** Designed for console-like isolation. It strictly permits only Gamepad/Joystick events (`EV_KEY` buttons, `EV_ABS` axes). It proactively blocks `EV_REL` (mouse movement) and `ABS_MT` (multitouch), effectively "neutering" complex controllers (like DualSense or Wiimotes) so they cannot be used to hijack the host mouse cursor. +* **`sanitized` (Blacklist):** Designed for desktop gaming. It allows standard Keyboard and Mouse input but strictly filters dangerous keys (`KEY_SYSRQ`, `KEY_POWER`) and host-management shortcuts (VT switching, CAD), providing a safe "sandboxed keyboard." + ## 4. Security Considerations `vuinputd` must currently run with **root privileges** to: diff --git a/vuinputd/src/cuse_device/device_policy.rs b/vuinputd/src/cuse_device/device_policy.rs new file mode 100644 index 0000000..9e775cc --- /dev/null +++ b/vuinputd/src/cuse_device/device_policy.rs @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz \ No newline at end of file diff --git a/vuinputd/src/cuse_device/mod.rs b/vuinputd/src/cuse_device/mod.rs index 5c5852e..bb71a3f 100644 --- a/vuinputd/src/cuse_device/mod.rs +++ b/vuinputd/src/cuse_device/mod.rs @@ -2,6 +2,7 @@ // // Author: Johannes Leupolz +pub mod device_policy; pub mod state; pub mod vuinput_ioctl; pub mod vuinput_open; diff --git a/vuinputd/src/cuse_device/state.rs b/vuinputd/src/cuse_device/state.rs index d296a9d..713c771 100644 --- a/vuinputd/src/cuse_device/state.rs +++ b/vuinputd/src/cuse_device/state.rs @@ -18,11 +18,31 @@ pub struct VuInputDevice { pub devnode: String, } +#[derive(Debug)] +pub struct KeyTracker { + pub left_alt_down: bool, + pub right_alt_down: bool, + pub left_ctrl_down: bool, + pub right_ctrl_down: bool, +} + +impl KeyTracker { + pub fn new() -> Self { + Self { + left_alt_down: false, + right_alt_down: false, + left_ctrl_down: false, + right_ctrl_down: false, + } + } +} + #[derive(Debug)] pub struct VuInputState { pub file: File, pub requesting_process: RequestingProcess, pub input_device: Option, + pub keytracker: KeyTracker, } #[derive(Debug, Eq, Hash, PartialEq, Clone)] diff --git a/vuinputd/src/cuse_device/vuinput_open.rs b/vuinputd/src/cuse_device/vuinput_open.rs index 6603000..666de32 100644 --- a/vuinputd/src/cuse_device/vuinput_open.rs +++ b/vuinputd/src/cuse_device/vuinput_open.rs @@ -54,6 +54,7 @@ pub unsafe extern "C" fn vuinput_open( file: v, requesting_process, input_device: None, + keytracker: KeyTracker::new() }, ) .unwrap(); diff --git a/vuinputd/src/global_config.rs b/vuinputd/src/global_config.rs new file mode 100644 index 0000000..c5fd5ad --- /dev/null +++ b/vuinputd/src/global_config.rs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use clap::{Parser, ValueEnum}; +use std::sync::OnceLock; + +// --- 1. Define the Global State Container --- +// This struct is extensible. You can add more global settings here later. +#[derive(Debug)] +pub struct GlobalConfig { + pub policy: DevicePolicy, +} + +// The actual static variable. It starts empty and is set once in main(). +pub static CONFIG: OnceLock = OnceLock::new(); + +// --- 2. Define the Policy Enum --- +#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum, Default)] +#[clap(rename_all = "kebab-case")] // This ensures StrictGamepad becomes "strict-gamepad" +pub enum DevicePolicy { + /// Allow all device capabilities + None, + /// Default: Allow keyboards/mice but block dangerous keys (SysRq, VT switching) + #[default] + Sanitized, + /// Only allow Gamepad-like devices. Block mice and keyboards. + StrictGamepad, +} + +pub fn initialize_global_config(device_policy: &DevicePolicy) { + if CONFIG + .set(GlobalConfig { + policy: device_policy.clone(), + }) + .is_err() + { + eprintln!("Failed to initialize global config"); + std::process::exit(1); + } +} + +pub fn get_device_policy<'a>() -> &'a DevicePolicy { + &CONFIG.get().unwrap().policy +} diff --git a/vuinputd/src/main.rs b/vuinputd/src/main.rs index 822c501..6c3559f 100644 --- a/vuinputd/src/main.rs +++ b/vuinputd/src/main.rs @@ -32,6 +32,7 @@ pub mod cuse_device; use crate::cuse_device::state::{initialize_dedup_last_error, initialize_vuinput_state}; use crate::cuse_device::vuinput_make_cuse_ops; use crate::cuse_device::vuinput_open::VUINPUT_COUNTER; +use crate::global_config::DevicePolicy; use crate::jobs::monitor_udev_job::MonitorBackgroundLoop; pub mod process_tools; @@ -42,6 +43,7 @@ use crate::process_tools::*; pub mod actions; +pub mod global_config; pub mod jobs; pub mod vt_tools; @@ -89,6 +91,10 @@ struct Args { Loss of local access may require recovery via SSH or a rescue boot." )] pub vt_guard: bool, + + /// Enforce a device policy on created devices + #[arg(long, value_enum, default_value_t)] + device_policy: DevicePolicy, } fn validate_args(args: &Args) -> Result<(), String> { @@ -181,6 +187,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); initialize_vuinput_state(); VUINPUT_COUNTER.set(AtomicU64::new(3)).expect( "failed to initialize the counter that provides the values of the CUSE file handles",