Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Control socket: frame and bound requests, refuse NUL, contain panics
One line on the control socket could take the desktop down. A NUL byte
in `shortcut bind` reached xkbcommon's CString::new(..).unwrap(), and the
panic unwinding out of the extern "C" IPC callback aborted the process.
`pointer-swipe 3 0 0 4000000000` looped four billion times on the main
thread. Requests were a single 4 KiB read, so a longer command, or one
written in pieces, was cut short and the prefix run, spawn included. An
idle connection held its thread forever.
- read_command frames a request: its first line, ended by the newline,
the client closing, or a 50 ms pause after some bytes (clients that send
neither still work). It is capped at 64 KiB and refused past that,
refused if it contains NUL, and the connection is dropped after 5 s of
silence.
- handle_ipc_event runs each command under catch_unwind. A panic is now an
error reply plus a log line.
- parse_trigger refuses a NUL itself.
- The dispatcher's numbers go through parse_finite (no NaN or inf into
pointer or camera math), and injected swipe and pinch steps are clamped
to 1000.
- The status and stream sockets read their subscription line with a total
deadline and a size cap (read_line_bounded). Their per-read timeout let
a client that trickled bytes hold the thread serving every other
subscriber while its line grew without bound.
- sleep_lock now ends its `lock` request with a newline.
Verified in a shadow. The installed build died on the NUL line; this one
replies with an error and stays up. A 4e9-step swipe answers in 0.02s,
NaN is refused, and a 70 KB spawn is refused and never runs. A
newline-less command is answered after the 50 ms pause, an idle
connection blocks nobody, and a status subscriber is served at once
while another client trickles bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 13 +++
src/server/global_shortcuts.rs | 13 +++
src/server/ipc_server.rs | 223 +++++++++++++++++++++++++++++++++++++++--
src/server/sleep_lock.rs | 1 +
src/server/status_server.rs | 22 ++--
src/server/stream_server.rs | 16 ++-
src/server/window_manager.rs | 82 ++++++++++-----
7 files changed, 313 insertions(+), 57 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index d1fe030b..76f553f3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -978,6 +978,19 @@ headless seat has no keyboard and Chromium crashes in
- **Control socket** `/tmp/cce-{WAYLAND_DISPLAY}.sock` (`ipc_server.rs`): line-oriented
request/reply over a Unix socket. `ccectl` / `cce_ctl.rs` is the client.
+ `read_command` frames a request (since 2026-10-02): the first line, ended
+ by its newline, the client closing, or a 50 ms pause after some bytes
+ (clients that send neither still work), at most 64 KiB — an overlong one
+ is refused, never cut short and run as the old single 4 KiB `read` did —
+ and a connection silent for 5 s is dropped. A NUL byte is refused (it
+ reached xkbcommon's `CString::new(..).unwrap()` from `shortcut bind`), and
+ `handle_ipc_event` runs each command under `catch_unwind`, since a panic
+ unwinding out of that `extern "C"` callback aborts the whole session.
+ Numbers parse through `parse_finite` (no NaN/inf into pointer or camera
+ math) and injected swipe/pinch steps clamp to `MAX_INJECTED_STEPS`. The
+ status and stream sockets read their subscription line through
+ `read_line_bounded` (total deadline and size cap): a per-read timeout let a
+ byte-a-second client hold the thread that serves every other subscriber.
- **Status socket** `/tmp/cce-status-{WAYLAND_DISPLAY}.sock` (`status_server.rs`): runs
on its own thread; a client sends one subscription line (`layout`, `title`,
`modifiers`, `dismiss`, …) and receives text lines on every
diff --git a/src/server/global_shortcuts.rs b/src/server/global_shortcuts.rs
index 04d0713d..11d968b2 100644
--- a/src/server/global_shortcuts.rs
+++ b/src/server/global_shortcuts.rs
@@ -93,6 +93,12 @@ pub fn parse_trigger(s: &str) -> Result<Trigger, String> {
};
mods |= bit;
}
+ // xkbcommon builds a CString from the name and unwraps it, so a NUL
+ // panics. The control socket refuses NUL already; this keeps the parser
+ // safe on its own.
+ if key.contains('\0') {
+ return Err(format!("unknown key {key:?}"));
+ }
let keysym: u32 = xkbcommon::xkb::keysym_from_name(key, xkbcommon::xkb::KEYSYM_CASE_INSENSITIVE).into();
if keysym == 0 {
return Err(format!("unknown key {key:?}"));
@@ -199,6 +205,13 @@ pub fn ipc(wm: &mut WindowManager, args: &[&str]) -> String {
mod tests {
use super::*;
+ #[test]
+ fn a_nul_in_the_key_name_is_an_error_not_a_panic() {
+ // xkbcommon's keysym_from_name unwraps a CString of the name; this
+ // used to abort the compositor from `shortcut bind`.
+ assert!(parse_trigger("CTRL+a\0b").is_err());
+ }
+
#[test]
fn parses_spec_triggers() {
let t = parse_trigger("CTRL+SHIFT+space").unwrap();
diff --git a/src/server/ipc_server.rs b/src/server/ipc_server.rs
index 55cab3e8..7c54b9d4 100644
--- a/src/server/ipc_server.rs
+++ b/src/server/ipc_server.rs
@@ -150,15 +150,124 @@ fn socket_peer_pid(stream: &UnixStream) -> i32 {
}
}
+/// One subscription line from a socket client, within `limit` bytes and an
+/// overall `deadline`. The status and stream sockets read their client's
+/// first line on a thread that serves everyone else too, with `read_line`
+/// and a per-read timeout only: a client trickling a byte per timeout held
+/// that thread (every status-bar update, or every new stream subscriber)
+/// indefinitely, and grew the line without bound inside the compositor.
+/// None on timeout, overflow, EOF before any byte, or a read error.
+pub fn read_line_bounded(stream: &UnixStream, limit: usize, deadline: std::time::Duration) -> Option<String> {
+ let until = std::time::Instant::now() + deadline;
+ let mut buf: Vec<u8> = Vec::new();
+ let mut chunk = [0u8; 512];
+ let mut reader = stream;
+ loop {
+ let left = until.checked_duration_since(std::time::Instant::now())?;
+ if left.is_zero() {
+ return None;
+ }
+ stream.set_read_timeout(Some(left)).ok()?;
+ match reader.read(&mut chunk) {
+ Ok(0) if buf.is_empty() => return None,
+ Ok(0) => break,
+ Ok(n) => {
+ buf.extend_from_slice(&chunk[..n]);
+ if let Some(end) = buf.iter().position(|&b| b == b'\n') {
+ buf.truncate(end);
+ break;
+ }
+ if buf.len() > limit {
+ return None;
+ }
+ }
+ Err(_) => return None,
+ }
+ }
+ if buf.len() > limit {
+ return None;
+ }
+ Some(String::from_utf8_lossy(&buf).into_owned())
+}
+
+/// Longest command accepted. The old single `read` into 4096 bytes cut a
+/// longer command — or one written in pieces — short and RAN the prefix
+/// (`spawn` included); now an overlong command is refused whole.
+const MAX_COMMAND: usize = 64 * 1024;
+/// How long a connection may sit without sending anything. Each one holds a
+/// thread, and the old read had no timeout, so idle connections leaked them.
+const FIRST_BYTE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
+/// Once some bytes are in, a pause this long ends the command. Clients send
+/// one line and then wait for the reply, but not all of them send the
+/// newline or close their side, and those must not stall.
+const QUIET_GAP: std::time::Duration = std::time::Duration::from_millis(50);
+
+#[derive(Debug, PartialEq)]
+enum Request {
+ Command(String),
+ /// Nothing arrived (a probe, an idle connection, EOF at once).
+ Empty,
+ TooLong,
+ /// A NUL byte: no command contains one, and it reached a CString
+ /// conversion (`shortcut bind`'s keysym lookup) whose panic aborted the
+ /// compositor.
+ Nul,
+}
+
+/// Read one command: up to its newline, the client closing its side, or a
+/// pause of `QUIET_GAP` after the first bytes — never more than
+/// `MAX_COMMAND`, never waiting more than `FIRST_BYTE_TIMEOUT` for a start.
+fn read_command(stream: &mut UnixStream) -> Request {
+ let _ = stream.set_read_timeout(Some(FIRST_BYTE_TIMEOUT));
+ let mut buf: Vec<u8> = Vec::new();
+ let mut chunk = [0u8; 4096];
+ loop {
+ match stream.read(&mut chunk) {
+ Ok(0) => break,
+ Ok(n) => {
+ buf.extend_from_slice(&chunk[..n]);
+ if buf.contains(&b'\n') {
+ break;
+ }
+ if buf.len() > MAX_COMMAND {
+ return Request::TooLong;
+ }
+ let _ = stream.set_read_timeout(Some(QUIET_GAP));
+ }
+ Err(e) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => break,
+ Err(_) => return Request::Empty,
+ }
+ }
+ let line = match buf.iter().position(|&b| b == b'\n') {
+ Some(end) => &buf[..end],
+ None => &buf[..],
+ };
+ if line.len() > MAX_COMMAND {
+ return Request::TooLong;
+ }
+ if line.contains(&0) {
+ return Request::Nul;
+ }
+ let cmd = String::from_utf8_lossy(line).trim().to_string();
+ if cmd.is_empty() {
+ Request::Empty
+ } else {
+ Request::Command(cmd)
+ }
+}
+
fn handle_client(mut stream: UnixStream, tx: IpcSender) {
let peer_pid = socket_peer_pid(&stream);
- let mut buf = [0u8; 4096];
- match stream.read(&mut buf) {
- Ok(0) => {}
- Ok(n) => {
- let s = String::from_utf8_lossy(&buf[..n]);
- let cmd = s.trim().to_string();
- if !cmd.is_empty() {
+ match read_command(&mut stream) {
+ Request::Empty => {}
+ Request::TooLong => {
+ let _ = stream.write_all(format!("error: command longer than {MAX_COMMAND} bytes, not run\n").as_bytes());
+ }
+ Request::Nul => {
+ let _ = stream.write_all(b"error: command contains a NUL byte, not run\n");
+ }
+ Request::Command(cmd) => {
+ {
// Commands answer from the IPC drain and so are quick; a
// second is a generous leash that still surfaces a wedged
// compositor. `screenshot` is the exception: its reply now
@@ -187,8 +296,104 @@ fn handle_client(mut stream: UnixStream, tx: IpcSender) {
}
}
}
- Err(e) => {
- log::error!("[ipc] stream read error: {}", e);
+ }
+}
+
+#[cfg(test)]
+mod framing_tests {
+ use super::*;
+ use std::time::{Duration, Instant};
+
+ fn send(bytes: &[u8], close: bool) -> Request {
+ let (mut client, mut server) = UnixStream::pair().unwrap();
+ client.write_all(bytes).unwrap();
+ if close {
+ client.shutdown(std::net::Shutdown::Write).unwrap();
}
+ let got = read_command(&mut server);
+ drop(client);
+ got
+ }
+
+ #[test]
+ fn a_command_ends_at_its_newline_its_close_or_a_pause() {
+ assert_eq!(send(b"windows --json\n", false), Request::Command("windows --json".into()));
+ assert_eq!(send(b"lock", true), Request::Command("lock".into()));
+ // No newline and no close (the old sleep_lock request did this): the
+ // quiet gap ends it rather than the 5s first-byte timeout.
+ let t = Instant::now();
+ assert_eq!(send(b"lock", false), Request::Command("lock".into()));
+ assert!(t.elapsed() < Duration::from_secs(1));
+ // Only the first line is the command.
+ assert_eq!(send(b"spawn foo\nspawn bar\n", false), Request::Command("spawn foo".into()));
+ }
+
+ #[test]
+ fn a_command_written_in_pieces_arrives_whole() {
+ let (mut client, mut server) = UnixStream::pair().unwrap();
+ let writer = std::thread::spawn(move || {
+ client.write_all(b"spawn ").unwrap();
+ std::thread::sleep(Duration::from_millis(10));
+ client.write_all(b"cce-terminal\n").unwrap();
+ client
+ });
+ assert_eq!(read_command(&mut server), Request::Command("spawn cce-terminal".into()));
+ drop(writer.join().unwrap());
+ }
+
+ #[test]
+ fn an_overlong_command_is_refused_not_cut_short() {
+ // The old reader ran the first 4096 bytes of this.
+ let mut long = b"spawn ".to_vec();
+ long.extend(std::iter::repeat(b'x').take(MAX_COMMAND + 10));
+ long.push(b'\n');
+ let (mut client, mut server) = UnixStream::pair().unwrap();
+ let writer = std::thread::spawn(move || {
+ let _ = client.write_all(&long);
+ client
+ });
+ assert_eq!(read_command(&mut server), Request::TooLong);
+ drop(server);
+ drop(writer.join().unwrap());
+ // 5000 bytes, over the old 4096, is now one whole command.
+ let mid = format!("spawn {}\n", "y".repeat(5000));
+ assert_eq!(send(mid.as_bytes(), false), Request::Command(mid.trim().to_string()));
+ }
+
+ #[test]
+ fn a_nul_byte_is_refused() {
+ assert_eq!(send(b"shortcut bind /s/1 x CTRL+a\0b\n", false), Request::Nul);
+ }
+
+ #[test]
+ fn a_silent_or_trickling_subscriber_is_cut_off_on_time() {
+ let (_client, server) = UnixStream::pair().unwrap();
+ let t = Instant::now();
+ assert_eq!(read_line_bounded(&server, 256, Duration::from_millis(100)), None);
+ assert!(t.elapsed() < Duration::from_millis(500));
+
+ // A byte every 30ms never finishes a line; the TOTAL deadline ends it
+ // (the old per-read timeout never fired, each read being on time).
+ let (mut client, server) = UnixStream::pair().unwrap();
+ let trickle = std::thread::spawn(move || {
+ for _ in 0..40 {
+ if client.write_all(b"a").is_err() {
+ break;
+ }
+ std::thread::sleep(Duration::from_millis(30));
+ }
+ });
+ let t = Instant::now();
+ assert_eq!(read_line_bounded(&server, 256, Duration::from_millis(200)), None);
+ assert!(t.elapsed() < Duration::from_millis(600), "took {:?}", t.elapsed());
+ drop(server);
+ trickle.join().unwrap();
+
+ let (mut client, server) = UnixStream::pair().unwrap();
+ client.write_all(b"layout\n").unwrap();
+ assert_eq!(read_line_bounded(&server, 256, Duration::from_millis(200)).as_deref(), Some("layout"));
+ let (mut client, server) = UnixStream::pair().unwrap();
+ client.write_all(&[b'z'; 300]).unwrap();
+ assert_eq!(read_line_bounded(&server, 256, Duration::from_millis(200)), None, "over the size cap");
}
}
diff --git a/src/server/sleep_lock.rs b/src/server/sleep_lock.rs
index 0b845fdd..cf5be52a 100644
--- a/src/server/sleep_lock.rs
+++ b/src/server/sleep_lock.rs
@@ -93,6 +93,7 @@ fn request(socket: &str, command: &str) -> std::io::Result<String> {
let mut stream = std::os::unix::net::UnixStream::connect(socket)?;
stream.set_read_timeout(Some(Duration::from_secs(5)))?;
stream.write_all(command.as_bytes())?;
+ stream.write_all(b"\n")?;
let mut reply = String::new();
stream.read_to_string(&mut reply)?;
Ok(reply)
diff --git a/src/server/status_server.rs b/src/server/status_server.rs
index f3c4b561..f5b411a5 100644
--- a/src/server/status_server.rs
+++ b/src/server/status_server.rs
@@ -8,7 +8,7 @@
// The main loop sends updates through an mpsc channel. The server thread
// owns the socket and handles all I/O independently of the Wayland event loop.
-use std::io::{BufRead, Write};
+use std::io::Write;
use std::os::fd::{AsRawFd, OwnedFd};
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::mpsc;
@@ -502,18 +502,14 @@ fn status_server_main(rx: mpsc::Receiver<StatusMsg>, wake: Arc<OwnedFd>, display
}
fn read_subscription(stream: &UnixStream) -> Subscription {
- let mut reader = std::io::BufReader::new(stream);
- let mut line = String::new();
- // Bounded blocking read: a subscriber writes its one line right after
- // connecting, so this returns at once in practice; the timeout is for a
- // client that connects and says nothing.
- stream
- .set_read_timeout(Some(std::time::Duration::from_millis(200)))
- .ok();
- match reader.read_line(&mut line) {
- Ok(_) => Subscription::from_str(&line),
- Err(e) => {
- log::error!("[status] failed to read subscription: {}", e);
+ // A subscriber writes its one line right after connecting, so this
+ // returns at once in practice. The bound is for one that does not: this
+ // thread pushes every status update, so the wait is capped in total time
+ // and size, not per read (`read_line_bounded`).
+ match crate::ipc_server::read_line_bounded(stream, 256, std::time::Duration::from_millis(200)) {
+ Some(line) => Subscription::from_str(&line),
+ None => {
+ log::error!("[status] no subscription line within 200ms / 256 bytes");
Subscription::Unknown
}
}
diff --git a/src/server/stream_server.rs b/src/server/stream_server.rs
index 815da803..ace36c08 100644
--- a/src/server/stream_server.rs
+++ b/src/server/stream_server.rs
@@ -20,7 +20,7 @@
// thread only try_send()s, so a stalled client skips frames (backpressure =
// frame dropping) and can never block the compositor.
-use std::io::{BufRead, Write};
+use std::io::Write;
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
@@ -89,15 +89,11 @@ fn accept_loop(hub: StreamHub, display_socket: Option<String>) {
for stream in listener.incoming() {
let Ok(stream) = stream else { continue };
- // Subscription line, with a timeout so a silent connect can't park.
- let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(5)));
- let mut line = String::new();
- {
- let mut reader = std::io::BufReader::new(&stream);
- if reader.read_line(&mut line).is_err() {
- continue;
- }
- }
+ // Subscription line: bounded in size and in TOTAL time, since this
+ // accept thread serves every subscriber in turn.
+ let Some(line) = crate::ipc_server::read_line_bounded(&stream, 256, std::time::Duration::from_secs(2)) else {
+ continue;
+ };
let Some(query) = line.trim().strip_prefix("window ").map(str::trim) else {
continue;
};
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index daaaeeca..b5e7795a 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -164,6 +164,18 @@ fn borrowable(saved: &SavedWindowState, program: Option<&str>) -> bool {
/// `create_restore_placeholders` draws no plate for them: Ubisoft Connect's
/// stood a minute over the empty desk every login (2026-09-26), waiting for
/// a window nothing had started.
+/// A control-socket number: `str::parse::<f64>` accepts "NaN", "inf" and
+/// "infinity", which no command means and which would reach pointer and
+/// camera math as positions (cce-remote filters the same for its frames).
+fn parse_finite(s: &str) -> Result<f64, ()> {
+ s.parse::<f64>().ok().filter(|v| v.is_finite()).ok_or(())
+}
+
+/// Most synthetic steps one `pointer-swipe` / `pointer-pinch` may take. The
+/// steps run in one loop on the main thread, so a count like 4000000000 held
+/// the whole session frozen; a real gesture is tens of events.
+const MAX_INJECTED_STEPS: u32 = 1000;
+
fn relaunchable(cmdline: &str) -> bool {
!cmdline.trim().is_empty() && !is_windows_path(cmdline)
}
@@ -5432,7 +5444,7 @@ impl WindowManager {
}
"pan-by" => {
if parts.len() < 3 { return "error: missing dx or dy\n".to_string(); }
- if let (Ok(dx), Ok(dy)) = (parts[1].parse::<f64>(), parts[2].parse::<f64>()) {
+ if let (Ok(dx), Ok(dy)) = (parse_finite(parts[1]), parse_finite(parts[2])) {
self.desk_pan_x += dx;
self.desk_pan_y += dy;
if matches!(self.state, WindowManagerState::Idle) {
@@ -5446,7 +5458,7 @@ impl WindowManager {
}
"pan-to" => {
if parts.len() < 3 { return "error: missing x or y\n".to_string(); }
- if let (Ok(x), Ok(y)) = (parts[1].parse::<f64>(), parts[2].parse::<f64>()) {
+ if let (Ok(x), Ok(y)) = (parse_finite(parts[1]), parse_finite(parts[2])) {
self.desk_pan_x = x;
self.desk_pan_y = y;
if matches!(self.state, WindowManagerState::Idle) {
@@ -5496,7 +5508,7 @@ impl WindowManager {
}
"set-zoom" => {
if parts.len() < 2 { return "error: missing zoom factor\n".to_string(); }
- if let Ok(factor) = parts[1].parse::<f64>() {
+ if let Ok(factor) = parse_finite(parts[1]) {
let new_zoom = factor.clamp(0.1, 10.0);
let (mut viewport_w, mut viewport_h) = (1920.0, 1080.0);
let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
@@ -5524,7 +5536,7 @@ impl WindowManager {
}
"set-coords" => {
if parts.len() < 3 { return "error: missing x or y\n".to_string(); }
- if let (Ok(x), Ok(y)) = (parts[1].parse::<f64>(), parts[2].parse::<f64>()) {
+ if let (Ok(x), Ok(y)) = (parse_finite(parts[1]), parse_finite(parts[2])) {
if let Some(seat) = self.first_seat() {
if let crate::seat::Focus::Window(fw) = (*seat).focused {
(*fw).virtual_x = x;
@@ -5540,7 +5552,7 @@ impl WindowManager {
"set-coords-of" => {
if parts.len() < 4 { return "error: missing app_id, x, or y\n".to_string(); }
let app_id_query = parts[1];
- if let (Ok(x), Ok(y)) = (parts[2].parse::<f64>(), parts[3].parse::<f64>()) {
+ if let (Ok(x), Ok(y)) = (parse_finite(parts[2]), parse_finite(parts[3])) {
let mut found = false;
for &w in self.windows.iter() {
if !w.is_null() && !(*w).closed && !(*w).minimized && matches!((*w).state, crate::window::WindowState::Mapped) {
@@ -6253,18 +6265,18 @@ impl WindowManager {
self.layout.desktop_cell_color = crate::config::parse_hex_color_rgba(val);
}
"desktop_grid_scale" | "grid_cell_size" => {
- if let Ok(v) = val.parse::<f64>() {
+ if let Ok(v) = parse_finite(val) {
self.layout.desktop_cell_width = v;
self.layout.desktop_cell_height = v;
}
}
"grid_cell_width" => {
- if let Ok(v) = val.parse::<f64>() {
+ if let Ok(v) = parse_finite(val) {
self.layout.desktop_cell_width = v;
}
}
"grid_cell_height" => {
- if let Ok(v) = val.parse::<f64>() {
+ if let Ok(v) = parse_finite(val) {
self.layout.desktop_cell_height = v;
}
}
@@ -6335,7 +6347,7 @@ impl WindowManager {
let key = parts[2];
let val = parts[3];
if key == "scroll-factor" {
- if let Ok(factor) = val.parse::<f64>() {
+ if let Ok(factor) = parse_finite(val) {
if factor < 0.0 {
return "error: scroll factor cannot be negative\n".to_string();
}
@@ -6372,7 +6384,7 @@ impl WindowManager {
// (`Cursor::inject_*`), so grabs/ops/focus behave exactly as with hardware.
"pointer-move-to" => {
if parts.len() < 3 { return "error: usage: pointer-move-to <x> <y>\n".to_string(); }
- if let (Ok(x), Ok(y)) = (parts[1].parse::<f64>(), parts[2].parse::<f64>()) {
+ if let (Ok(x), Ok(y)) = (parse_finite(parts[1]), parse_finite(parts[2])) {
self.for_each_cursor(|cursor| cursor.inject_motion_to(x, y));
"ok\n".to_string()
} else {
@@ -6381,7 +6393,7 @@ impl WindowManager {
}
"pointer-move-by" => {
if parts.len() < 3 { return "error: usage: pointer-move-by <dx> <dy>\n".to_string(); }
- if let (Ok(dx), Ok(dy)) = (parts[1].parse::<f64>(), parts[2].parse::<f64>()) {
+ if let (Ok(dx), Ok(dy)) = (parse_finite(parts[1]), parse_finite(parts[2])) {
self.for_each_cursor(|cursor| cursor.inject_motion_by(dx, dy));
"ok\n".to_string()
} else {
@@ -6414,8 +6426,8 @@ impl WindowManager {
// them (already sign-flipped), and the view drag undoes that.
let finger = parts.iter().any(|p| *p == "finger");
let natural = parts.iter().any(|p| *p == "natural");
- let dy = parts[1].parse::<f64>();
- let dx = parts.get(2).filter(|p| **p != "finger" && **p != "natural").map(|v| v.parse::<f64>()).unwrap_or(Ok(0.0));
+ let dy = parse_finite(parts[1]);
+ let dx = parts.get(2).filter(|p| **p != "finger" && **p != "natural").map(|v| parse_finite(v)).unwrap_or(Ok(0.0));
if let (Ok(dy), Ok(dx)) = (dy, dx) {
self.for_each_cursor(|cursor| {
cursor.inject_natural = natural;
@@ -6433,7 +6445,7 @@ impl WindowManager {
let usage = "error: usage: pointer-swipe <fingers> <dx> <dy> [steps] | begin <fingers> | update <dx> <dy> | end\n";
if parts.len() >= 2 && matches!(parts[1], "begin" | "update" | "end") {
let stage = parts[1].to_string();
- let num = |i: usize| parts.get(i).and_then(|v| v.parse::<f64>().ok());
+ let num = |i: usize| parts.get(i).and_then(|v| parse_finite(v).ok());
let (fingers, dx, dy) = match parts[1] {
"begin" => (num(2).map(|f| f as u32).unwrap_or(3), 0.0, 0.0),
"update" => match (num(2), num(3)) {
@@ -6448,9 +6460,9 @@ impl WindowManager {
}
if parts.len() < 4 { return usage.to_string(); }
let fingers = parts[1].parse::<u32>();
- let dx = parts[2].parse::<f64>();
- let dy = parts[3].parse::<f64>();
- let steps = parts.get(4).map(|v| v.parse::<u32>()).unwrap_or(Ok(10));
+ let dx = parse_finite(parts[2]);
+ let dy = parse_finite(parts[3]);
+ let steps = parts.get(4).map(|v| v.parse::<u32>()).unwrap_or(Ok(10)).map(|n| n.clamp(1, MAX_INJECTED_STEPS));
if let (Ok(fingers), Ok(dx), Ok(dy), Ok(steps)) = (fingers, dx, dy, steps) {
self.for_each_cursor(|cursor| cursor.inject_swipe(fingers, dx, dy, steps));
"ok\n".to_string()
@@ -6463,15 +6475,15 @@ impl WindowManager {
// pointer-pinch begin | update <scale> [rotation] | end (paced by the caller)
if parts.len() < 2 { return "error: usage: pointer-pinch <scale> [rotation] [steps] | begin | update <scale> [rotation] | end\n".to_string(); }
if matches!(parts[1], "begin" | "update" | "end") {
- let scale = parts.get(2).and_then(|v| v.parse::<f64>().ok()).unwrap_or(1.0);
- let rotation = parts.get(3).and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.0);
+ let scale = parts.get(2).and_then(|v| parse_finite(v).ok()).unwrap_or(1.0);
+ let rotation = parts.get(3).and_then(|v| parse_finite(v).ok()).unwrap_or(0.0);
let stage = parts[1].to_string();
self.for_each_cursor(|cursor| cursor.inject_pinch_stage(&stage, scale, rotation));
return "ok\n".to_string();
}
- let scale = parts[1].parse::<f64>();
- let rotation = parts.get(2).map(|v| v.parse::<f64>()).unwrap_or(Ok(0.0));
- let steps = parts.get(3).map(|v| v.parse::<u32>()).unwrap_or(Ok(10));
+ let scale = parse_finite(parts[1]);
+ let rotation = parts.get(2).map(|v| parse_finite(v)).unwrap_or(Ok(0.0));
+ let steps = parts.get(3).map(|v| v.parse::<u32>()).unwrap_or(Ok(10)).map(|n| n.clamp(1, MAX_INJECTED_STEPS));
if let (Ok(scale), Ok(rotation), Ok(steps)) = (scale, rotation, steps) {
self.for_each_cursor(|cursor| cursor.inject_pinch(scale, rotation, steps));
"ok\n".to_string()
@@ -6522,7 +6534,7 @@ impl WindowManager {
if parts.len() < 4 {
return "error: usage: place-next <app_id> <x> <y>\n".to_string();
}
- let (x, y) = match (parts[2].parse::<f64>(), parts[3].parse::<f64>()) {
+ let (x, y) = match (parse_finite(parts[2]), parse_finite(parts[3])) {
(Ok(x), Ok(y)) => (x, y),
_ => return "error: x/y must be numbers\n".to_string(),
};
@@ -6542,7 +6554,7 @@ impl WindowManager {
if parts.len() < 4 {
return "error: usage: place-next-cell <app_id> <x> <y>\n".to_string();
}
- let (x, y) = match (parts[2].parse::<f64>(), parts[3].parse::<f64>()) {
+ let (x, y) = match (parse_finite(parts[2]), parse_finite(parts[3])) {
(Ok(x), Ok(y)) => (x, y),
_ => return "error: x/y must be numbers\n".to_string(),
};
@@ -7005,7 +7017,18 @@ unsafe extern "C" fn handle_ipc_event(fd: std::os::raw::c_int, _mask: u32, data:
// reply.
(*wm).pending_ipc_reply = Some(req.reply_tx);
(*wm).pending_ipc_peer_pid = req.peer_pid;
- let reply = (*wm).process_ipc_command(&req.command);
+ // A panic here would unwind out of this extern "C" callback,
+ // which aborts the process — the whole desktop — over one bad
+ // command. Contain it to the command: the caller gets an error.
+ let reply = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+ (*wm).process_ipc_command(&req.command)
+ })) {
+ Ok(reply) => reply,
+ Err(_) => {
+ log::error!("[ipc] command panicked and was abandoned: {:?}", req.command);
+ "error: the compositor failed running that command\n".to_string()
+ }
+ };
(*wm).pending_ipc_peer_pid = 0;
if let Some(tx) = (*wm).pending_ipc_reply.take() {
let _ = tx.send(reply);
@@ -7922,6 +7945,15 @@ mod tests {
String::from_utf8(out.stdout).unwrap().split('\0').filter(|s| !s.is_empty()).map(String::from).collect()
}
+ #[test]
+ fn control_socket_numbers_must_be_finite() {
+ assert_eq!(parse_finite("1.5"), Ok(1.5));
+ assert_eq!(parse_finite("-20"), Ok(-20.0));
+ for bad in ["NaN", "nan", "inf", "-inf", "infinity", "1e999", "x", ""] {
+ assert!(parse_finite(bad).is_err(), "{bad:?}");
+ }
+ }
+
#[test]
fn a_restore_relaunches_the_saved_argv_exactly() {
// Every argument comes back from the shell as it went in: the