Started work on device policy #2 #4

This commit is contained in:
Johannes Leupolz 2026-01-06 22:49:15 +00:00
parent f8bdb04d7e
commit 5277a13904
7 changed files with 93 additions and 5 deletions

View file

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

View file

@ -0,0 +1,3 @@
// SPDX-License-Identifier: MIT
//
// Author: Johannes Leupolz <dev@leupolz.eu>

View file

@ -2,6 +2,7 @@
//
// Author: Johannes Leupolz <dev@leupolz.eu>
pub mod device_policy;
pub mod state;
pub mod vuinput_ioctl;
pub mod vuinput_open;

View file

@ -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<VuInputDevice>,
pub keytracker: KeyTracker,
}
#[derive(Debug, Eq, Hash, PartialEq, Clone)]

View file

@ -54,6 +54,7 @@ pub unsafe extern "C" fn vuinput_open(
file: v,
requesting_process,
input_device: None,
keytracker: KeyTracker::new()
},
)
.unwrap();

View file

@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
//
// Author: Johannes Leupolz <dev@leupolz.eu>
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<GlobalConfig> = 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
}

View file

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