atagen

monolith

public
Star0
Watch0

Commit 706b42a51787

tarball

atagen <boss@atagen.co>2026-10-19 10:59 UTC

1 file changed+69-11
xwayland: close a STRING relay that stops moving data

A selection owner that kept the pipe open without writing, or an X client that never read, left the relay's calloop source and both fds registered until the connection died. A timer now removes the live source after 30 s without progress.
Modified file: crates/monolith/src/xwayland.rs+69 additions−11 deletions
@@ -9,6 +9,7 @@ use crate::state::State;
99 use monolith_config::SelectionAccess;
1010 use smithay::desktop::Window;
1111 use smithay::reexports::calloop::generic::Generic;
12+use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
1213 use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction, RegistrationToken};
1314 use smithay::reexports::rustix::fs::{OFlags, fcntl_setfl};
1415 use smithay::reexports::rustix::io::{FdFlags, fcntl_getfd, fcntl_setfd};
@@ -26,6 +27,7 @@ use smithay::wayland::selection::primary_selection::{
2627 use smithay::wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState};
2728 use smithay::xwayland::xwm::{Reorder, ResizeEdge, WmWindowProperty, WmWindowType, XwmId};
2829 use smithay::xwayland::{X11Surface, X11Wm, XWayland, XWaylandEvent, XwmHandler};
30+use std::cell::Cell;
2931 use std::fs::File;
3032 use std::io::{ErrorKind, Read, Write};
3133 use std::os::fd::AsFd;
@@ -33,6 +35,8 @@ use std::os::fd::OwnedFd;
3335 use std::os::linux::net::SocketAddrExt;
3436 use std::os::unix::net::{SocketAddr, UnixListener};
3537 use std::process::Stdio;
38+use std::rc::Rc;
39+use std::time::{Duration, Instant};
3640
3741 const UTF8_TEXT: [&str; 4] = [
3842 "text/plain;charset=utf-8",
@@ -45,6 +49,9 @@ const UTF8_TEXT: [&str; 4] = [
4549 /// grow the compositor without bound.
4650 const RELAY_CAP: usize = 16 << 20;
4751
52+/// How long a STRING relay may go without moving data before it is closed.
53+const RELAY_IDLE: Duration = Duration::from_secs(30);
54+
4855 fn utf8_source(mimes: &[String]) -> Option<&str> {
4956 UTF8_TEXT.into_iter().find(|t| mimes.iter().any(|m| m == t))
5057 }
@@ -65,17 +72,29 @@ fn latin1(text: &[u8]) -> Vec<u8> {
6572 .collect()
6673 }
6774
75+/// The live source of a STRING relay and when it last moved data, so a timer
76+/// can close one that stalled.
77+struct Relay {
78+ source: Cell<Option<RegistrationToken>>,
79+ moved: Cell<Instant>,
80+}
81+
6882 /// Hands back the write end of a pipe whose UTF-8 contents reach `out` as
6983 /// Latin-1, which is what ICCCM says STRING holds. Text past `RELAY_CAP` is
70-/// dropped along with the transfer.
84+/// dropped along with the transfer, and so is a transfer that moves nothing
85+/// for `RELAY_IDLE`.
7186 fn latin1_relay(handle: &LoopHandle<'static, State>, out: OwnedFd) -> std::io::Result<OwnedFd> {
7287 let (read, write) = std::io::pipe()?;
7388 fcntl_setfl(&read, OFlags::NONBLOCK)?;
7489 fcntl_setfl(&out, OFlags::NONBLOCK)?;
90+ let relay = Rc::new(Relay {
91+ source: Cell::new(None),
92+ moved: Cell::new(Instant::now()),
93+ });
7594 let mut out = Some(File::from(out));
7695 let mut text = Vec::new();
77- let next = handle.clone();
78- handle
96+ let (next, shared) = (handle.clone(), relay.clone());
97+ let token = handle
7998 .insert_source(
8099 Generic::new(read, Interest::READ, Mode::Level),
81100 move |_, read, _| {
@@ -83,33 +102,54 @@ fn latin1_relay(handle: &LoopHandle<'static, State>, out: OwnedFd) -> std::io::R
83102 loop {
84103 match (&**read).read(&mut chunk) {
85104 Ok(0) => break,
86- Ok(n) if text.len() + n > RELAY_CAP => return Ok(PostAction::Remove),
87- Ok(n) => text.extend_from_slice(&chunk[..n]),
105+ Ok(n) if text.len() + n > RELAY_CAP => {
106+ shared.source.set(None);
107+ return Ok(PostAction::Remove);
108+ }
109+ Ok(n) => {
110+ text.extend_from_slice(&chunk[..n]);
111+ shared.moved.set(Instant::now());
112+ }
88113 Err(e) if e.kind() == ErrorKind::Interrupted => {}
89114 Err(e) if e.kind() == ErrorKind::WouldBlock => {
90115 return Ok(PostAction::Continue);
91116 }
92- Err(_) => return Ok(PostAction::Remove),
117+ Err(_) => {
118+ shared.source.set(None);
119+ return Ok(PostAction::Remove);
120+ }
93121 }
94122 }
123+ shared.source.set(None);
95124 if let Some(out) = out.take() {
96- write_when_ready(&next, out, latin1(&text));
125+ write_when_ready(&next, &shared, out, latin1(&text));
97126 }
98127 Ok(PostAction::Remove)
99128 },
100129 )
101130 .map_err(|e| std::io::Error::other(e.to_string()))?;
131+ relay.source.set(Some(token));
132+ expire_when_stalled(handle, relay);
102133 Ok(write.into())
103134 }
104135
105-fn write_when_ready(handle: &LoopHandle<'static, State>, out: File, data: Vec<u8>) {
136+fn write_when_ready(
137+ handle: &LoopHandle<'static, State>,
138+ relay: &Rc<Relay>,
139+ out: File,
140+ data: Vec<u8>,
141+) {
106142 let mut sent = 0;
143+ let shared = relay.clone();
107144 let inserted = handle.insert_source(
108145 Generic::new(out, Interest::WRITE, Mode::Level),
109146 move |_, out, _| {
110147 while sent < data.len() {
111148 match (&**out).write(&data[sent..]) {
112- Ok(n) => sent += n,
149+ Ok(n) => {
150+ sent += n;
151+ shared.moved.set(Instant::now());
152+ }
113153 Err(e) if e.kind() == ErrorKind::Interrupted => {}
114154 Err(e) if e.kind() == ErrorKind::WouldBlock => {
115155 return Ok(PostAction::Continue);
@@ -117,14 +157,32 @@ fn write_when_ready(handle: &LoopHandle<'static, State>, out: File, data: Vec<u8
117157 Err(_) => break,
118158 }
119159 }
160+ shared.source.set(None);
120161 Ok(PostAction::Remove)
121162 },
122163 );
123- if let Err(e) = inserted {
124- tracing::warn!("cannot relay STRING to X11: {e}");
164+ match inserted {
165+ Ok(token) => relay.source.set(Some(token)),
166+ Err(e) => tracing::warn!("cannot relay STRING to X11: {e}"),
125167 }
126168 }
127169
170+fn expire_when_stalled(handle: &LoopHandle<'static, State>, relay: Rc<Relay>) {
171+ let owner = handle.clone();
172+ let _ = handle.insert_source(Timer::from_duration(RELAY_IDLE), move |_, _, _| {
173+ let Some(token) = relay.source.get() else {
174+ return TimeoutAction::Drop;
175+ };
176+ let idle = relay.moved.get().elapsed();
177+ if idle < RELAY_IDLE {
178+ return TimeoutAction::ToDuration(RELAY_IDLE - idle);
179+ }
180+ relay.source.set(None);
181+ owner.remove(token);
182+ TimeoutAction::Drop
183+ });
184+}
185+
128186 pub struct Xwayland {
129187 pub display: u32,
130188 listeners: Vec<UnixListener>,