Skip to main content

podbox/
compositor.rs

1use std::collections::HashSet;
2use std::io::{IoSlice, IoSliceMut};
3use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
4use std::os::unix::net::{UnixListener, UnixStream};
5use std::path::Path;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8use std::time::Instant;
9
10use anyhow::{Context, Result};
11use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, Signal, sigaction};
12use nix::sys::socket::{ControlMessage, ControlMessageOwned, MsgFlags, recvmsg, sendmsg};
13
14/// Set by SIGTERM/SIGINT handler to request clean compositor shutdown.
15static SHUTDOWN_REQUESTED: AtomicBool = AtomicBool::new(false);
16
17/// Register SIGTERM/SIGINT handlers that set `SHUTDOWN_REQUESTED`.
18fn setup_signal_handler() {
19    extern "C" fn handle_signal(_: i32) {
20        SHUTDOWN_REQUESTED.store(true, Ordering::Relaxed);
21    }
22    let sig_action = SigAction::new(
23        SigHandler::Handler(handle_signal),
24        SaFlags::empty(),
25        SigSet::empty(),
26    );
27    // SAFETY: handler only writes to an AtomicBool (signal-safe on Linux).
28    unsafe {
29        let _ = sigaction(Signal::SIGTERM, &sig_action);
30        let _ = sigaction(Signal::SIGINT, &sig_action);
31    }
32}
33
34use crate::config::Config;
35
36const MAX_CONNECTIONS: usize = 128;
37
38struct FirewallState {
39    blocked_interfaces: HashSet<String>,
40}
41
42impl FirewallState {
43    fn new(blocked_interfaces: Vec<String>) -> Self {
44        Self {
45            blocked_interfaces: blocked_interfaces.into_iter().collect(),
46        }
47    }
48}
49
50/// Run the Wayland firewall proxy for a container.
51///
52/// Listens on `$XDG_RUNTIME_DIR/podbox/{name}-wayland.sock`, accepts
53/// connections from the container, bridges each to the host compositor's
54/// Wayland socket, and filters blocked interfaces from `wl_registry::global`
55/// events on the host→client path.
56pub fn run_compositor(config: &Config, name: &str) -> Result<()> {
57    let xdg_runtime = std::env::var("XDG_RUNTIME_DIR")
58        .or_else(|_| {
59            let uid = nix::unistd::getuid().as_raw();
60            Ok::<_, std::env::VarError>(format!("/run/user/{uid}"))
61        })
62        .context("XDG_RUNTIME_DIR not set")?;
63
64    let wayland_display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".into());
65    let host_socket = Path::new(&xdg_runtime).join(&wayland_display);
66
67    if !host_socket.exists() {
68        anyhow::bail!(
69            "Host Wayland socket not found at {} (WAYLAND_DISPLAY={})",
70            host_socket.display(),
71            wayland_display
72        );
73    }
74
75    let socket_path = Path::new(&xdg_runtime)
76        .join("podbox")
77        .join(format!("{name}-wayland.sock"));
78
79    let _ = std::fs::remove_file(&socket_path);
80    std::fs::create_dir_all(socket_path.parent().context("socket path has no parent")?)?;
81
82    setup_signal_handler();
83
84    let listener = UnixListener::bind(&socket_path).with_context(|| {
85        format!(
86            "Failed to bind Wayland proxy socket at {}",
87            socket_path.display()
88        )
89    })?;
90
91    let blocked = config.wayland.blocked_interfaces.clone();
92
93    let mut connections = 0;
94    loop {
95        if SHUTDOWN_REQUESTED.load(Ordering::Relaxed) || connections >= MAX_CONNECTIONS {
96            break;
97        }
98
99        let stream = match listener.accept() {
100            Ok((s, _)) => s,
101            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
102            Err(e) => {
103                tracing::error!("compositor: accept failed: {e}");
104                break;
105            }
106        };
107        connections += 1;
108
109        let host_conn = match UnixStream::connect(&host_socket) {
110            Ok(s) => s,
111            Err(e) => {
112                tracing::error!("compositor: failed to connect to host Wayland socket: {e}");
113                continue;
114            }
115        };
116
117        let state = Arc::new(Mutex::new(FirewallState::new(blocked.clone())));
118        let done = Arc::new(AtomicBool::new(false));
119
120        let client_clone = stream.try_clone()?;
121        let host_clone = host_conn.try_clone()?;
122        let state_c2h = Arc::clone(&state);
123        let done_c2h = Arc::clone(&done);
124
125        std::thread::spawn(move || {
126            if let Err(e) = bridge_loop(stream, host_clone, state_c2h, &done_c2h, true) {
127                tracing::error!("compositor: client→host bridge error: {e}");
128            }
129            done_c2h.store(true, Ordering::Relaxed);
130        });
131
132        let state_h2c = state;
133        let done_h2c = done;
134
135        std::thread::spawn(move || {
136            if let Err(e) = bridge_loop(host_conn, client_clone, state_h2c, &done_h2c, false) {
137                tracing::error!("compositor: host→client bridge error: {e}");
138            }
139            done_h2c.store(true, Ordering::Relaxed);
140        });
141    }
142
143    Ok(())
144}
145
146/// Token-bucket rate-limit check.
147/// Returns `true` if the message is allowed through.
148fn rate_allow(bucket: &mut f64, last_refill: &mut Instant) -> bool {
149    const RATE: f64 = 10_000.0;
150    let now = Instant::now();
151    let elapsed = now.duration_since(*last_refill).as_secs_f64();
152    *bucket = (*bucket + elapsed * RATE).min(RATE);
153    *last_refill = now;
154    if *bucket >= 1.0 {
155        *bucket -= 1.0;
156        true
157    } else {
158        false
159    }
160}
161
162/// Bidirectional byte-stream bridge between two Unix sockets.
163///
164/// For the host→client direction, `is_client_to_host = false`, and the
165/// bridge intercepts `wl_registry::global` events (opcode 0, string
166/// payload at offset 12) to filter interfaces on the blocklist.
167///
168/// File descriptors received via `SCM_RIGHTS` are forwarded with the
169/// first Wayland message from the same `recvmsg` batch.
170fn bridge_loop(
171    in_socket: UnixStream,
172    out_socket: UnixStream,
173    state: Arc<Mutex<FirewallState>>,
174    done: &AtomicBool,
175    is_client_to_host: bool,
176) -> Result<()> {
177    let mut read_buf = [0u8; 16384];
178    let mut cmsg_buffer = vec![0u8; 4096];
179    let mut bytes_cache = Vec::with_capacity(32768);
180    let mut pending_fds: Vec<OwnedFd> = Vec::new();
181
182    // Token-bucket rate limiter for host→client direction.
183    // Protects the guest from slow-client memory exhaustion.
184    let mut bucket: f64 = 10_000.0;
185    let mut last_refill = Instant::now();
186
187    loop {
188        if done.load(Ordering::Relaxed) {
189            break;
190        }
191
192        let msg_bytes = {
193            let mut iov = [IoSliceMut::new(&mut read_buf)];
194            let msg = match recvmsg::<()>(
195                in_socket.as_raw_fd(),
196                &mut iov,
197                Some(&mut cmsg_buffer),
198                MsgFlags::empty(),
199            ) {
200                Ok(m) => m,
201                Err(e) if e == nix::errno::Errno::EINTR => continue,
202                Err(e) => {
203                    done.store(true, Ordering::Relaxed);
204                    let _ = in_socket.shutdown(std::net::Shutdown::Both);
205                    let _ = out_socket.shutdown(std::net::Shutdown::Both);
206                    return Err(e.into());
207                }
208            };
209
210            let bytes = msg.bytes;
211            if bytes == 0 {
212                break;
213            }
214
215            if let Ok(cmsgs) = msg.cmsgs() {
216                for cmsg in cmsgs {
217                    if let ControlMessageOwned::ScmRights(fds) = cmsg {
218                        for fd in fds {
219                            // SAFETY: fds received via SCM_RIGHTS are owned by the receiver.
220                            let owned = unsafe { OwnedFd::from_raw_fd(fd) };
221                            pending_fds.push(owned);
222                        }
223                    }
224                }
225            }
226
227            bytes
228        };
229
230        bytes_cache.extend_from_slice(&read_buf[..msg_bytes]);
231
232        // Process complete Wayland messages from the coalesced buffer.
233        let mut consumed = 0;
234        while consumed + 8 <= bytes_cache.len() {
235            let header = &bytes_cache[consumed..consumed + 8];
236            let size_and_opcode = u32::from_ne_bytes(header[4..8].try_into().unwrap());
237            let msg_size = (size_and_opcode >> 16) as usize;
238            let opcode = (size_and_opcode & 0xFFFF) as u16;
239
240            if msg_size < 8 {
241                done.store(true, Ordering::Relaxed);
242                let _ = in_socket.shutdown(std::net::Shutdown::Both);
243                let _ = out_socket.shutdown(std::net::Shutdown::Both);
244                anyhow::bail!("Invalid Wayland message size: {}", msg_size);
245            }
246
247            if consumed + msg_size > bytes_cache.len() {
248                break;
249            }
250
251            let message_bytes = &bytes_cache[consumed..consumed + msg_size];
252            let should_drop =
253                !is_client_to_host && is_blocked_global(message_bytes, opcode, &state);
254
255            if should_drop {
256                pending_fds.clear();
257            } else if is_client_to_host || rate_allow(&mut bucket, &mut last_refill) {
258                forward_message(&out_socket, message_bytes, &mut pending_fds)?;
259            } else {
260                pending_fds.clear();
261                done.store(true, Ordering::Relaxed);
262                let _ = in_socket.shutdown(std::net::Shutdown::Both);
263                let _ = out_socket.shutdown(std::net::Shutdown::Both);
264                tracing::warn!("compositor: rate-limited, closing connection");
265                return Ok(());
266            }
267
268            consumed += msg_size;
269        }
270
271        bytes_cache.drain(..consumed);
272    }
273
274    // Signal shutdown to the sibling thread
275    done.store(true, Ordering::Relaxed);
276    let _ = in_socket.shutdown(std::net::Shutdown::Both);
277    let _ = out_socket.shutdown(std::net::Shutdown::Both);
278    Ok(())
279}
280
281/// Check whether a host→client message is a `wl_registry::global` event
282/// announcing a blocked interface.
283fn is_blocked_global(message_bytes: &[u8], opcode: u16, state: &Mutex<FirewallState>) -> bool {
284    // wl_registry::global (opcode 0) format:
285    //   8 bytes header (object_id, size+opcode=0)
286    //   4 bytes name (u32)
287    //   4 bytes interface string length (u32, includes NUL)
288    //   N bytes interface string (padded to 4 bytes)
289    //   4 bytes version (u32)
290    if opcode != 0 || message_bytes.len() < 16 {
291        return false;
292    }
293
294    let str_len = u32::from_ne_bytes(message_bytes[12..16].try_into().unwrap()) as usize;
295
296    // Guard against integer overflow on 32-bit platforms
297    if message_bytes
298        .len()
299        .checked_sub(16)
300        .is_none_or(|rem| rem < str_len)
301    {
302        return false;
303    }
304
305    if str_len < 2 {
306        return false;
307    }
308
309    // Exclude the null terminator at the end.
310    let interface_bytes = &message_bytes[16..16 + str_len - 1];
311    let interface_name = match std::str::from_utf8(interface_bytes) {
312        Ok(s) => s,
313        Err(_) => return false,
314    };
315
316    let guard = state.lock().unwrap_or_else(|e| e.into_inner());
317    guard.blocked_interfaces.contains(interface_name)
318}
319
320/// Forward a single Wayland message (with any accumulated fds) to the
321/// output socket.
322fn forward_message(
323    out_socket: &UnixStream,
324    message_bytes: &[u8],
325    pending_fds: &mut Vec<OwnedFd>,
326) -> Result<()> {
327    let iov = [IoSlice::new(message_bytes)];
328
329    if pending_fds.is_empty() {
330        sendmsg::<()>(out_socket.as_raw_fd(), &iov, &[], MsgFlags::empty(), None)?;
331    } else {
332        let raw_fds: Vec<RawFd> = pending_fds.iter().map(|f| f.as_raw_fd()).collect();
333        let cmsg = ControlMessage::ScmRights(&raw_fds);
334        sendmsg::<()>(
335            out_socket.as_raw_fd(),
336            &iov,
337            &[cmsg],
338            MsgFlags::empty(),
339            None,
340        )?;
341        pending_fds.clear();
342    }
343
344    Ok(())
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    fn make_global(object_id: u32, name: u32, interface: &str, version: u32) -> Vec<u8> {
352        let raw = interface.as_bytes();
353        let str_len = raw.len().checked_add(1).unwrap();
354        let padded_len = str_len.next_multiple_of(4);
355        let msg_size = u32::try_from(8 + 4 + 4 + padded_len + 4).unwrap();
356
357        let mut buf = Vec::with_capacity(msg_size as usize);
358        buf.extend_from_slice(&object_id.to_ne_bytes());
359        buf.extend_from_slice(&(msg_size << 16).to_ne_bytes());
360        buf.extend_from_slice(&name.to_ne_bytes());
361        buf.extend_from_slice(&u32::try_from(str_len).unwrap().to_ne_bytes());
362        buf.extend_from_slice(raw);
363        buf.push(0);
364        while buf.len() < (8 + 4 + 4 + padded_len) {
365            buf.push(0);
366        }
367        buf.extend_from_slice(&version.to_ne_bytes());
368        buf
369    }
370
371    fn make_message(object_id: u32, size: u32, opcode: u16) -> Vec<u8> {
372        let mut buf = Vec::with_capacity(size as usize);
373        buf.extend_from_slice(&object_id.to_ne_bytes());
374        buf.extend_from_slice(&((size << 16) | u32::from(opcode)).to_ne_bytes());
375        while buf.len() < size as usize {
376            buf.push(0);
377        }
378        buf
379    }
380
381    fn blocked_state() -> Mutex<FirewallState> {
382        Mutex::new(FirewallState::new(vec![
383            "zwlr_screencopy_manager_v1".into(),
384            "ext_foreign_toplevel_list_v1".into(),
385        ]))
386    }
387
388    fn empty_state() -> Mutex<FirewallState> {
389        Mutex::new(FirewallState::new(vec![]))
390    }
391
392    #[test]
393    fn blocks_screencopy_interface() {
394        let data = make_global(2, 42, "zwlr_screencopy_manager_v1", 1);
395        assert!(is_blocked_global(&data, 0, &blocked_state()));
396    }
397
398    #[test]
399    fn blocks_foreign_toplevel() {
400        let data = make_global(2, 43, "ext_foreign_toplevel_list_v1", 1);
401        assert!(is_blocked_global(&data, 0, &blocked_state()));
402    }
403
404    #[test]
405    fn allows_safe_interface() {
406        let data = make_global(2, 44, "wl_compositor", 6);
407        assert!(!is_blocked_global(&data, 0, &blocked_state()));
408    }
409
410    #[test]
411    fn allows_wl_shm() {
412        let data = make_global(2, 1, "wl_shm", 1);
413        assert!(!is_blocked_global(&data, 0, &blocked_state()));
414    }
415
416    #[test]
417    fn blocks_nothing_when_empty_blocklist() {
418        let data = make_global(2, 42, "zwlr_screencopy_manager_v1", 1);
419        assert!(!is_blocked_global(&data, 0, &empty_state()));
420    }
421
422    #[test]
423    fn ignores_non_registry_opcode() {
424        let data = make_message(2, 16, 1);
425        assert!(!is_blocked_global(&data, 1, &blocked_state()));
426    }
427
428    #[test]
429    fn ignores_short_payload() {
430        let data = make_message(2, 12, 0);
431        assert!(!is_blocked_global(&data, 0, &blocked_state()));
432    }
433
434    #[test]
435    fn ignores_empty_interface_string() {
436        let mut data = make_message(2, 16, 0);
437        data[12..16].copy_from_slice(&0u32.to_ne_bytes());
438        assert!(!is_blocked_global(&data, 0, &blocked_state()));
439    }
440
441    #[test]
442    fn allows_partial_name_prefix_match() {
443        let data = make_global(2, 42, "zwlr_screencopy", 1);
444        assert!(!is_blocked_global(&data, 0, &blocked_state()));
445    }
446
447    #[test]
448    fn allows_similar_but_not_blocked() {
449        let data = make_global(2, 99, "zwlr_layer_shell_v1", 1);
450        assert!(!is_blocked_global(&data, 0, &blocked_state()));
451    }
452
453    #[test]
454    fn rate_allow_accepts_first_message() {
455        let mut bucket = 10_000.0;
456        let mut last = Instant::now();
457        assert!(rate_allow(&mut bucket, &mut last));
458    }
459
460    #[test]
461    fn rate_allow_drains_bucket() {
462        let mut bucket = 2.0;
463        let mut last = Instant::now();
464        assert!(rate_allow(&mut bucket, &mut last));
465        assert!(rate_allow(&mut bucket, &mut last));
466        assert!(!rate_allow(&mut bucket, &mut last));
467    }
468
469    #[test]
470    fn rate_allow_refills_over_time() {
471        let mut bucket = 0.0;
472        let mut last = Instant::now();
473        assert!(!rate_allow(&mut bucket, &mut last));
474        // Simulate a small delay
475        std::thread::sleep(std::time::Duration::from_millis(1));
476        let mut bucket = 0.0;
477        let mut last_refill = Instant::now();
478        std::thread::sleep(std::time::Duration::from_millis(2));
479        assert!(rate_allow(&mut bucket, &mut last_refill));
480    }
481}