Skip to main content

pixelflux/
uinput.rs

1//! Kernel input devices for host-capture injection, over `/dev/uinput`.
2//!
3//! A host session takes its input from the kernel, so a uinput keyboard and
4//! pointer reach it whatever display server it runs: no compositor protocol to
5//! advertise, no portal on the bus, and no consent dialog to answer. That puts
6//! it on the host-capture input ladder below libei and the virtual keyboard and
7//! pointer protocols, which a compositor may not offer, and above the portal's
8//! own methods (a socket write to the compositor lands in about 4 us, a uinput
9//! write in about 12 us, a portal call in about 2 ms); it is taken only where
10//! `/dev/uinput` is writable — a bare-metal host rather than an unprivileged
11//! container.
12//!
13//! The pointer carries a normalized absolute axis rather than the layout's own
14//! pixels: a uinput device fixes its axis range when the kernel creates it,
15//! while the union layout changes whenever a display is added or resized.
16
17use std::ffi::c_ulong;
18use std::fs::OpenOptions;
19use std::os::fd::{AsRawFd, OwnedFd};
20
21const IOC_WRITE: c_ulong = 1;
22const UINPUT_NAME_LEN: usize = 80;
23
24/// `_IOW('U', nr, size)`, the encoding `<linux/uinput.h>` gives its setup ioctls.
25const fn iow(nr: c_ulong, size: c_ulong) -> c_ulong {
26    (IOC_WRITE << 30) | (size << 16) | (b'U' as c_ulong) << 8 | nr
27}
28/// `_IO('U', nr)`, for the ioctls that carry no argument.
29const fn io(nr: c_ulong) -> c_ulong {
30    ((b'U' as c_ulong) << 8) | nr
31}
32
33const UI_DEV_CREATE: c_ulong = io(1);
34const UI_DEV_DESTROY: c_ulong = io(2);
35const UI_DEV_SETUP: c_ulong = iow(3, std::mem::size_of::<UinputSetup>() as c_ulong);
36const UI_ABS_SETUP: c_ulong = iow(4, std::mem::size_of::<UinputAbsSetup>() as c_ulong);
37const UI_SET_EVBIT: c_ulong = iow(100, 4);
38const UI_SET_KEYBIT: c_ulong = iow(101, 4);
39const UI_SET_RELBIT: c_ulong = iow(102, 4);
40const UI_SET_ABSBIT: c_ulong = iow(103, 4);
41
42pub const EV_SYN: u16 = 0x00;
43pub const EV_KEY: u16 = 0x01;
44pub const EV_REL: u16 = 0x02;
45pub const EV_ABS: u16 = 0x03;
46const SYN_REPORT: u16 = 0;
47const BTN_MISC: u16 = 0x100;
48const BTN_LEFT: u16 = 0x110;
49const BTN_RIGHT: u16 = 0x111;
50const BTN_MIDDLE: u16 = 0x112;
51pub const REL_X: u16 = 0x00;
52pub const REL_Y: u16 = 0x01;
53pub const REL_HWHEEL: u16 = 0x06;
54pub const REL_WHEEL: u16 = 0x08;
55pub const ABS_X: u16 = 0x00;
56pub const ABS_Y: u16 = 0x01;
57const BUS_VIRTUAL: u16 = 0x06;
58
59/// The span an absolute axis is declared over; positions are scaled onto it.
60pub const ABS_RANGE: i32 = 65535;
61
62#[repr(C)]
63#[derive(Default)]
64struct InputId {
65    bustype: u16,
66    vendor: u16,
67    product: u16,
68    version: u16,
69}
70
71#[repr(C)]
72struct UinputSetup {
73    id: InputId,
74    name: [u8; UINPUT_NAME_LEN],
75    ff_effects_max: u32,
76}
77
78#[repr(C)]
79#[derive(Default)]
80struct AbsInfo {
81    value: i32,
82    minimum: i32,
83    maximum: i32,
84    fuzz: i32,
85    flat: i32,
86    resolution: i32,
87}
88
89#[repr(C)]
90struct UinputAbsSetup {
91    code: u16,
92    absinfo: AbsInfo,
93}
94
95#[repr(C)]
96struct InputEvent {
97    sec: i64,
98    usec: i64,
99    kind: u16,
100    code: u16,
101    value: i32,
102}
103
104/// Whether this process can create kernel input devices.
105pub fn available() -> bool {
106    OpenOptions::new().write(true).open("/dev/uinput").is_ok()
107}
108
109unsafe fn set(fd: i32, request: c_ulong, value: c_ulong) -> Result<(), String> {
110    if unsafe { libc::ioctl(fd, request as _, value) } < 0 {
111        return Err(format!(
112            "uinput ioctl {request:#x}: {}",
113            std::io::Error::last_os_error()
114        ));
115    }
116    Ok(())
117}
118
119/// One kernel device: the fd that created it carries its events until it drops.
120pub struct Device {
121    fd: OwnedFd,
122}
123
124impl Device {
125    fn open(
126        name: &str,
127        product: u16,
128        build: impl Fn(i32) -> Result<(), String>,
129    ) -> Result<Self, String> {
130        let file = OpenOptions::new()
131            .write(true)
132            .open("/dev/uinput")
133            .map_err(|e| format!("open /dev/uinput: {e}"))?;
134        let fd: OwnedFd = file.into();
135        let raw = fd.as_raw_fd();
136        unsafe { set(raw, UI_SET_EVBIT, EV_SYN as c_ulong)? };
137        build(raw)?;
138        let mut setup = UinputSetup {
139            id: InputId {
140                bustype: BUS_VIRTUAL,
141                vendor: 0x1d6b,
142                product,
143                version: 1,
144            },
145            name: [0; UINPUT_NAME_LEN],
146            ff_effects_max: 0,
147        };
148        let bytes = name.as_bytes();
149        let n = bytes.len().min(UINPUT_NAME_LEN - 1);
150        setup.name[..n].copy_from_slice(&bytes[..n]);
151        if unsafe { libc::ioctl(raw, UI_DEV_SETUP as _, &setup as *const _) } < 0 {
152            return Err(format!("UI_DEV_SETUP: {}", std::io::Error::last_os_error()));
153        }
154        if unsafe { libc::ioctl(raw, UI_DEV_CREATE as _) } < 0 {
155            return Err(format!(
156                "UI_DEV_CREATE: {}",
157                std::io::Error::last_os_error()
158            ));
159        }
160        Ok(Self { fd })
161    }
162
163    /// A keyboard carrying the whole `KEY_*` block; the `BTN_*` codes above it
164    /// are left out, or the device reads as a pad rather than a keyboard.
165    pub fn keyboard() -> Result<Self, String> {
166        Self::open("Selkies Virtual Keyboard", 0x0001, |fd| unsafe {
167            set(fd, UI_SET_EVBIT, EV_KEY as c_ulong)?;
168            for code in 1..BTN_MISC {
169                set(fd, UI_SET_KEYBIT, code as c_ulong)?;
170            }
171            Ok(())
172        })
173    }
174
175    /// A pointer with both motion kinds: absolute for a placed position,
176    /// relative for a locked pointer whose delta must stay a delta.
177    pub fn pointer() -> Result<Self, String> {
178        Self::open("Selkies Virtual Pointer", 0x0002, |fd| unsafe {
179            set(fd, UI_SET_EVBIT, EV_KEY as c_ulong)?;
180            set(fd, UI_SET_EVBIT, EV_REL as c_ulong)?;
181            set(fd, UI_SET_EVBIT, EV_ABS as c_ulong)?;
182            for code in [BTN_LEFT, BTN_RIGHT, BTN_MIDDLE] {
183                set(fd, UI_SET_KEYBIT, code as c_ulong)?;
184            }
185            for code in [REL_X, REL_Y, REL_WHEEL, REL_HWHEEL] {
186                set(fd, UI_SET_RELBIT, code as c_ulong)?;
187            }
188            for code in [ABS_X, ABS_Y] {
189                set(fd, UI_SET_ABSBIT, code as c_ulong)?;
190                let abs = UinputAbsSetup {
191                    code,
192                    absinfo: AbsInfo {
193                        maximum: ABS_RANGE,
194                        ..AbsInfo::default()
195                    },
196                };
197                if libc::ioctl(fd, UI_ABS_SETUP as _, &abs as *const _) < 0 {
198                    return Err(format!(
199                        "UI_ABS_SETUP {code}: {}",
200                        std::io::Error::last_os_error()
201                    ));
202                }
203            }
204            Ok(())
205        })
206    }
207
208    /// Write one event and the `SYN_REPORT` that closes its report.
209    pub fn emit(&self, kind: u16, code: u16, value: i32) -> Result<(), String> {
210        let events = [
211            InputEvent {
212                sec: 0,
213                usec: 0,
214                kind,
215                code,
216                value,
217            },
218            InputEvent {
219                sec: 0,
220                usec: 0,
221                kind: EV_SYN,
222                code: SYN_REPORT,
223                value: 0,
224            },
225        ];
226        let bytes = unsafe {
227            std::slice::from_raw_parts(events.as_ptr() as *const u8, std::mem::size_of_val(&events))
228        };
229        let written = unsafe {
230            libc::write(
231                self.fd.as_raw_fd(),
232                bytes.as_ptr() as *const libc::c_void,
233                bytes.len(),
234            )
235        };
236        if written < 0 {
237            return Err(format!("uinput write: {}", std::io::Error::last_os_error()));
238        }
239        Ok(())
240    }
241}
242
243impl Drop for Device {
244    fn drop(&mut self) {
245        unsafe { libc::ioctl(self.fd.as_raw_fd(), UI_DEV_DESTROY as _) };
246    }
247}
248
249/// The keyboard and pointer one host session injects through.
250pub struct Pair {
251    pub keyboard: Device,
252    pub pointer: Device,
253}
254
255impl Pair {
256    /// Both devices, or `None` where the kernel cannot serve them, which is
257    /// what sends injection down to the libei and portal rungs.
258    pub fn open() -> Option<Self> {
259        if !available() {
260            return None;
261        }
262        match (Device::keyboard(), Device::pointer()) {
263            (Ok(keyboard), Ok(pointer)) => Some(Self { keyboard, pointer }),
264            (keyboard, pointer) => {
265                for err in [keyboard.err(), pointer.err()].into_iter().flatten() {
266                    eprintln!("[HostCapture] uinput unavailable: {err}");
267                }
268                None
269            }
270        }
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    /// The events a device emits reach its consumer byte for byte.
279    ///
280    /// Needs the `/dev/uinput` emulator preloaded, which stands in for a kernel
281    /// this machine has no node for, so it is not part of the default run:
282    /// `LD_PRELOAD=<selkies>/tests/tools/uinput_shim.so UINPUT_SHIM_STREAM=/tmp/ui.bin
283    /// cargo test --lib uinput -- --ignored`
284    ///
285    /// The emulator's ioctl log is a buffered `FILE*` this process cannot flush,
286    /// so the assertion is on the event stream, which it writes straight through.
287    #[test]
288    #[ignore]
289    fn emitted_events_reach_the_device_byte_for_byte() {
290        let path = std::env::var("UINPUT_SHIM_STREAM").expect("UINPUT_SHIM_STREAM");
291        let keyboard = Device::keyboard().expect("the emulator serves /dev/uinput");
292        keyboard.emit(EV_KEY, 30, 1).unwrap();
293        keyboard.emit(EV_KEY, 30, 0).unwrap();
294        drop(keyboard);
295        let raw = std::fs::read(&path).expect("event stream");
296        let size = std::mem::size_of::<InputEvent>();
297        let mut events: Vec<(u16, u16, i32)> = Vec::new();
298        let mut at = 0;
299        while at + size <= raw.len() {
300            let c = &raw[at..at + size];
301            events.push((
302                u16::from_ne_bytes([c[16], c[17]]),
303                u16::from_ne_bytes([c[18], c[19]]),
304                i32::from_ne_bytes([c[20], c[21], c[22], c[23]]),
305            ));
306            at += size;
307        }
308        assert_eq!(
309            events,
310            vec![
311                (EV_KEY, 30, 1),
312                (EV_SYN, SYN_REPORT, 0),
313                (EV_KEY, 30, 0),
314                (EV_SYN, SYN_REPORT, 0),
315            ],
316            "stream was {events:?}"
317        );
318    }
319
320    #[test]
321    fn ioctls_and_structs_match_the_kernel_abi() {
322        assert_eq!(UI_DEV_CREATE, 0x5501);
323        assert_eq!(UI_DEV_DESTROY, 0x5502);
324        assert_eq!(UI_SET_EVBIT, 0x4004_5564);
325        assert_eq!(UI_SET_KEYBIT, 0x4004_5565);
326        assert_eq!(UI_SET_RELBIT, 0x4004_5566);
327        assert_eq!(UI_SET_ABSBIT, 0x4004_5567);
328        assert_eq!(std::mem::size_of::<UinputSetup>(), 92);
329        assert_eq!(std::mem::size_of::<UinputAbsSetup>(), 28);
330        assert_eq!(std::mem::size_of::<InputEvent>(), 24);
331        assert_eq!(std::mem::offset_of!(UinputAbsSetup, absinfo), 4);
332        assert_eq!(std::mem::offset_of!(UinputSetup, name), 8);
333        assert_eq!(std::mem::offset_of!(UinputSetup, ff_effects_max), 88);
334        assert_eq!(UI_DEV_SETUP, 0x405c_5503);
335        assert_eq!(UI_ABS_SETUP, 0x401c_5504);
336    }
337}