Skip to main content

podbox/
compositor.rs

1use std::collections::{HashSet, VecDeque};
2use std::io::{IoSlice, IoSliceMut};
3use std::os::fd::{AsRawFd, 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::{Duration, Instant};
9
10use anyhow::{Context, Result};
11use nix::sys::socket::{ControlMessage, ControlMessageOwned, MsgFlags, recvmsg, sendmsg};
12
13/// Register SIGTERM/SIGINT handlers that set `shutdown`.
14fn setup_signal_handler(shutdown: Arc<AtomicBool>) -> Result<()> {
15    for sig in [signal_hook::consts::SIGTERM, signal_hook::consts::SIGINT] {
16        signal_hook::flag::register(sig, Arc::clone(&shutdown))?;
17    }
18    Ok(())
19}
20
21use crate::config::Config;
22
23const MAX_CONNECTIONS: usize = 128;
24
25struct FirewallState {
26    blocked_interfaces: HashSet<String>,
27}
28
29impl FirewallState {
30    fn new(blocked_interfaces: Vec<String>) -> Self {
31        Self {
32            blocked_interfaces: blocked_interfaces.into_iter().collect(),
33        }
34    }
35}
36
37/// Run the Wayland firewall proxy for a container.
38///
39/// Listens on `$XDG_RUNTIME_DIR/podbox/{name}-wayland.sock`, accepts
40/// connections from the container, bridges each to the host compositor's
41/// Wayland socket, and filters blocked interfaces from `wl_registry::global`
42/// events on the host→client path.
43pub fn run_compositor(config: &Config, name: &str) -> Result<()> {
44    let xdg_runtime = std::env::var("XDG_RUNTIME_DIR")
45        .or_else(|_| {
46            let uid = nix::unistd::getuid().as_raw();
47            Ok::<_, std::env::VarError>(format!("/run/user/{uid}"))
48        })
49        .context("XDG_RUNTIME_DIR not set")?;
50
51    let wayland_display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".into());
52    let host_socket = Path::new(&xdg_runtime).join(&wayland_display);
53
54    if !host_socket.exists() {
55        anyhow::bail!(
56            "Host Wayland socket not found at {} (WAYLAND_DISPLAY={})",
57            host_socket.display(),
58            wayland_display
59        );
60    }
61
62    let socket_path = Path::new(&xdg_runtime)
63        .join("podbox")
64        .join(format!("{name}-wayland.sock"));
65
66    let _ = std::fs::remove_file(&socket_path);
67    std::fs::create_dir_all(socket_path.parent().context("socket path has no parent")?)?;
68
69    let shutdown = Arc::new(AtomicBool::new(false));
70    setup_signal_handler(Arc::clone(&shutdown))?;
71
72    let listener = UnixListener::bind(&socket_path).with_context(|| {
73        format!(
74            "Failed to bind Wayland proxy socket at {}",
75            socket_path.display()
76        )
77    })?;
78    // Non-blocking + periodic tick so SIGTERM/SIGINT ends the accept loop
79    // promptly instead of blocking in accept(2) until systemd's
80    // TimeoutStopSec SIGKILL (90s stall on every container stop).
81    listener.set_nonblocking(true)?;
82
83    let blocked = config.wayland.blocked_interfaces.clone();
84
85    let mut connections = 0;
86    loop {
87        if shutdown.load(Ordering::Relaxed) || connections >= MAX_CONNECTIONS {
88            break;
89        }
90
91        let stream = match listener.accept() {
92            Ok((s, _)) => s,
93            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
94                std::thread::sleep(Duration::from_millis(200));
95                continue;
96            }
97            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
98            Err(e) => {
99                tracing::error!("compositor: accept failed: {e}");
100                break;
101            }
102        };
103        connections += 1;
104
105        stream.set_nonblocking(false)?;
106
107        let host_conn = match UnixStream::connect(&host_socket) {
108            Ok(s) => s,
109            Err(e) => {
110                tracing::error!("compositor: failed to connect to host Wayland socket: {e}");
111                continue;
112            }
113        };
114
115        let state = Arc::new(Mutex::new(FirewallState::new(blocked.clone())));
116        let done = Arc::new(AtomicBool::new(false));
117
118        let client_clone = stream.try_clone()?;
119        let host_clone = host_conn.try_clone()?;
120        let state_c2h = Arc::clone(&state);
121        let done_c2h = Arc::clone(&done);
122
123        std::thread::spawn(move || {
124            if let Err(e) = bridge_loop(stream, host_clone, state_c2h, &done_c2h, true) {
125                tracing::error!("compositor: client→host bridge error: {e}");
126            }
127            done_c2h.store(true, Ordering::Relaxed);
128        });
129
130        let state_h2c = state;
131        let done_h2c = done;
132
133        std::thread::spawn(move || {
134            if let Err(e) = bridge_loop(host_conn, client_clone, state_h2c, &done_h2c, false) {
135                tracing::error!("compositor: host→client bridge error: {e}");
136            }
137            done_h2c.store(true, Ordering::Relaxed);
138        });
139    }
140
141    Ok(())
142}
143
144/// Token-bucket rate-limit check.
145/// Returns `true` if the message is allowed through.
146fn rate_allow(bucket: &mut f64, last_refill: &mut Instant) -> bool {
147    const RATE: f64 = 10_000.0;
148    let now = Instant::now();
149    let elapsed = now.duration_since(*last_refill).as_secs_f64();
150    *bucket = (*bucket + elapsed * RATE).min(RATE);
151    *last_refill = now;
152    if *bucket >= 1.0 {
153        *bucket -= 1.0;
154        true
155    } else {
156        false
157    }
158}
159
160/// Bidirectional byte-stream bridge between two Unix sockets.
161///
162/// For the host→client direction, `is_client_to_host = false`, and the
163/// bridge intercepts `wl_registry::global` events (opcode 0, string
164/// payload at offset 12) to filter interfaces on the blocklist.
165///
166/// File descriptors received via `SCM_RIGHTS` are attributed per message:
167/// each read's fds are keyed to the absolute stream offset at which the
168/// read ended, and attach to the Wayland message whose completion boundary
169/// matches. libwayland transmits an fd in the same datagram as the message
170/// bytes referencing it, so the batch delivered by the completing read
171/// belongs to that message — never to earlier messages completed by the
172/// same or prior reads, and a dropped message only closes the fds it owns.
173fn bridge_loop(
174    in_socket: UnixStream,
175    out_socket: UnixStream,
176    state: Arc<Mutex<FirewallState>>,
177    done: &AtomicBool,
178    is_client_to_host: bool,
179) -> Result<()> {
180    let mut read_buf = [0u8; 16384];
181    let mut cmsg_buffer = vec![0u8; 4096];
182    let mut bytes_cache = Vec::with_capacity(32768);
183    // Fds grouped by the absolute stream offset of the read that delivered
184    // them. Batches are consumed (FIFO) by messages as they complete.
185    let mut fd_batches: VecDeque<(usize, Vec<OwnedFd>)> = VecDeque::new();
186    // Absolute stream offset of bytes_cache[0].
187    let mut base_offset: usize = 0;
188
189    // Token-bucket rate limiter for host→client direction.
190    // Protects the guest from slow-client memory exhaustion.
191    let mut bucket: f64 = 10_000.0;
192    let mut last_refill = Instant::now();
193
194    loop {
195        if done.load(Ordering::Relaxed) {
196            break;
197        }
198
199        let msg_bytes = {
200            let mut iov = [IoSliceMut::new(&mut read_buf)];
201            let msg = match recvmsg::<()>(
202                in_socket.as_raw_fd(),
203                &mut iov,
204                Some(&mut cmsg_buffer),
205                MsgFlags::empty(),
206            ) {
207                Ok(m) => m,
208                Err(e) if e == nix::errno::Errno::EINTR => continue,
209                Err(e) => {
210                    done.store(true, Ordering::Relaxed);
211                    let _ = in_socket.shutdown(std::net::Shutdown::Both);
212                    let _ = out_socket.shutdown(std::net::Shutdown::Both);
213                    return Err(e.into());
214                }
215            };
216
217            let bytes = msg.bytes;
218            if bytes == 0 {
219                break;
220            }
221
222            let mut read_fds: Vec<OwnedFd> = Vec::new();
223            if let Ok(cmsgs) = msg.cmsgs() {
224                for cmsg in cmsgs {
225                    if let ControlMessageOwned::ScmRights(fds) = cmsg {
226                        for fd in fds {
227                            read_fds.push(crate::process::adopt_scm_fd(fd));
228                        }
229                    }
230                }
231            }
232
233            // Key this read's fds to where the read ends in the stream:
234            // the message completed exactly there owns them.
235            if !read_fds.is_empty() {
236                fd_batches.push_back((base_offset + bytes_cache.len() + bytes, read_fds));
237            }
238
239            bytes
240        };
241
242        bytes_cache.extend_from_slice(&read_buf[..msg_bytes]);
243
244        // Process complete Wayland messages from the coalesced buffer.
245        let mut consumed = 0;
246        while consumed + 8 <= bytes_cache.len() {
247            let header = &bytes_cache[consumed..consumed + 8];
248            let size_and_opcode = u32::from_ne_bytes(header[4..8].try_into().unwrap());
249            let msg_size = (size_and_opcode >> 16) as usize;
250            let opcode = (size_and_opcode & 0xFFFF) as u16;
251
252            if msg_size < 8 {
253                done.store(true, Ordering::Relaxed);
254                let _ = in_socket.shutdown(std::net::Shutdown::Both);
255                let _ = out_socket.shutdown(std::net::Shutdown::Both);
256                anyhow::bail!("Invalid Wayland message size: {msg_size}");
257            }
258
259            if consumed + msg_size > bytes_cache.len() {
260                break;
261            }
262
263            let message_bytes = &bytes_cache[consumed..consumed + msg_size];
264            let should_drop =
265                !is_client_to_host && is_blocked_global(message_bytes, opcode, &state);
266
267            let message_end_abs = base_offset + consumed + msg_size;
268            let mut msg_fds = take_fd_batches(&mut fd_batches, message_end_abs);
269
270            if should_drop {
271                // Dropping `msg_fds` closes them — correct: they belonged to
272                // the blocked message alone.
273                drop(msg_fds);
274            } else if is_client_to_host || rate_allow(&mut bucket, &mut last_refill) {
275                forward_message(&out_socket, message_bytes, &mut msg_fds)?;
276            } else {
277                drop(fd_batches);
278                done.store(true, Ordering::Relaxed);
279                let _ = in_socket.shutdown(std::net::Shutdown::Both);
280                let _ = out_socket.shutdown(std::net::Shutdown::Both);
281                tracing::warn!("compositor: rate-limited, closing connection");
282                return Ok(());
283            }
284
285            consumed += msg_size;
286        }
287
288        bytes_cache.drain(..consumed);
289        base_offset += consumed;
290    }
291
292    // Signal shutdown to the sibling thread
293    done.store(true, Ordering::Relaxed);
294    let _ = in_socket.shutdown(std::net::Shutdown::Both);
295    let _ = out_socket.shutdown(std::net::Shutdown::Both);
296    Ok(())
297}
298
299/// Take ownership of every fd batch delivered by reads ending at or before
300/// `message_end` (absolute stream offset). These are the fds carried by the
301/// Wayland message completing at `message_end`: libwayland sends an fd in
302/// the same datagram as the message bytes referencing it.
303fn take_fd_batches<T>(fd_batches: &mut VecDeque<(usize, Vec<T>)>, message_end: usize) -> Vec<T> {
304    let mut out = Vec::new();
305    while let Some((read_end, _)) = fd_batches.front() {
306        if *read_end > message_end {
307            break;
308        }
309        let (_, fds) = fd_batches.pop_front().expect("front checked");
310        out.extend(fds);
311    }
312    out
313}
314
315/// Check whether a host→client message is a `wl_registry::global` event
316/// announcing a blocked interface.
317fn is_blocked_global(message_bytes: &[u8], opcode: u16, state: &Mutex<FirewallState>) -> bool {
318    // wl_registry::global (opcode 0) format:
319    //   8 bytes header (object_id, size+opcode=0)
320    //   4 bytes name (u32)
321    //   4 bytes interface string length (u32, includes NUL)
322    //   N bytes interface string (padded to 4 bytes)
323    //   4 bytes version (u32)
324    if opcode != 0 || message_bytes.len() < 16 {
325        return false;
326    }
327
328    let str_len = u32::from_ne_bytes(message_bytes[12..16].try_into().unwrap()) as usize;
329
330    // Guard against integer overflow on 32-bit platforms
331    if message_bytes
332        .len()
333        .checked_sub(16)
334        .is_none_or(|rem| rem < str_len)
335    {
336        return false;
337    }
338
339    if str_len < 2 {
340        return false;
341    }
342
343    // Exclude the null terminator at the end.
344    let interface_bytes = &message_bytes[16..16 + str_len - 1];
345    let interface_name = match std::str::from_utf8(interface_bytes) {
346        Ok(s) => s,
347        Err(_) => return false,
348    };
349
350    let guard = state.lock().unwrap_or_else(|e| e.into_inner());
351    guard.blocked_interfaces.contains(interface_name)
352}
353
354/// Forward a single Wayland message (with any accumulated fds) to the
355/// output socket.
356fn forward_message(
357    out_socket: &UnixStream,
358    message_bytes: &[u8],
359    pending_fds: &mut Vec<OwnedFd>,
360) -> Result<()> {
361    let iov = [IoSlice::new(message_bytes)];
362
363    if pending_fds.is_empty() {
364        sendmsg::<()>(out_socket.as_raw_fd(), &iov, &[], MsgFlags::empty(), None)?;
365    } else {
366        let raw_fds: Vec<RawFd> = pending_fds.iter().map(|f| f.as_raw_fd()).collect();
367        let cmsg = ControlMessage::ScmRights(&raw_fds);
368        sendmsg::<()>(
369            out_socket.as_raw_fd(),
370            &iov,
371            &[cmsg],
372            MsgFlags::empty(),
373            None,
374        )?;
375        pending_fds.clear();
376    }
377
378    Ok(())
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    fn make_global(object_id: u32, name: u32, interface: &str, version: u32) -> Vec<u8> {
386        let raw = interface.as_bytes();
387        let str_len = raw.len().checked_add(1).unwrap();
388        let padded_len = str_len.next_multiple_of(4);
389        let msg_size = u32::try_from(8 + 4 + 4 + padded_len + 4).unwrap();
390
391        let mut buf = Vec::with_capacity(msg_size as usize);
392        buf.extend_from_slice(&object_id.to_ne_bytes());
393        buf.extend_from_slice(&(msg_size << 16).to_ne_bytes());
394        buf.extend_from_slice(&name.to_ne_bytes());
395        buf.extend_from_slice(&u32::try_from(str_len).unwrap().to_ne_bytes());
396        buf.extend_from_slice(raw);
397        buf.push(0);
398        while buf.len() < (8 + 4 + 4 + padded_len) {
399            buf.push(0);
400        }
401        buf.extend_from_slice(&version.to_ne_bytes());
402        buf
403    }
404
405    fn make_message(object_id: u32, size: u32, opcode: u16) -> Vec<u8> {
406        let mut buf = Vec::with_capacity(size as usize);
407        buf.extend_from_slice(&object_id.to_ne_bytes());
408        buf.extend_from_slice(&((size << 16) | u32::from(opcode)).to_ne_bytes());
409        while buf.len() < size as usize {
410            buf.push(0);
411        }
412        buf
413    }
414
415    fn blocked_state() -> Mutex<FirewallState> {
416        Mutex::new(FirewallState::new(vec![
417            "zwlr_screencopy_manager_v1".into(),
418            "ext_foreign_toplevel_list_v1".into(),
419        ]))
420    }
421
422    fn empty_state() -> Mutex<FirewallState> {
423        Mutex::new(FirewallState::new(vec![]))
424    }
425
426    #[test]
427    fn blocks_screencopy_interface() {
428        let data = make_global(2, 42, "zwlr_screencopy_manager_v1", 1);
429        assert!(is_blocked_global(&data, 0, &blocked_state()));
430    }
431
432    #[test]
433    fn blocks_foreign_toplevel() {
434        let data = make_global(2, 43, "ext_foreign_toplevel_list_v1", 1);
435        assert!(is_blocked_global(&data, 0, &blocked_state()));
436    }
437
438    #[test]
439    fn allows_safe_interface() {
440        let data = make_global(2, 44, "wl_compositor", 6);
441        assert!(!is_blocked_global(&data, 0, &blocked_state()));
442    }
443
444    #[test]
445    fn allows_wl_shm() {
446        let data = make_global(2, 1, "wl_shm", 1);
447        assert!(!is_blocked_global(&data, 0, &blocked_state()));
448    }
449
450    #[test]
451    fn blocks_nothing_when_empty_blocklist() {
452        let data = make_global(2, 42, "zwlr_screencopy_manager_v1", 1);
453        assert!(!is_blocked_global(&data, 0, &empty_state()));
454    }
455
456    #[test]
457    fn ignores_non_registry_opcode() {
458        let data = make_message(2, 16, 1);
459        assert!(!is_blocked_global(&data, 1, &blocked_state()));
460    }
461
462    #[test]
463    fn ignores_short_payload() {
464        let data = make_message(2, 12, 0);
465        assert!(!is_blocked_global(&data, 0, &blocked_state()));
466    }
467
468    #[test]
469    fn ignores_empty_interface_string() {
470        let mut data = make_message(2, 16, 0);
471        data[12..16].copy_from_slice(&0u32.to_ne_bytes());
472        assert!(!is_blocked_global(&data, 0, &blocked_state()));
473    }
474
475    #[test]
476    fn allows_partial_name_prefix_match() {
477        let data = make_global(2, 42, "zwlr_screencopy", 1);
478        assert!(!is_blocked_global(&data, 0, &blocked_state()));
479    }
480
481    #[test]
482    fn allows_similar_but_not_blocked() {
483        let data = make_global(2, 99, "zwlr_layer_shell_v1", 1);
484        assert!(!is_blocked_global(&data, 0, &blocked_state()));
485    }
486
487    #[test]
488    fn rate_allow_accepts_first_message() {
489        let mut bucket = 10_000.0;
490        let mut last = Instant::now();
491        assert!(rate_allow(&mut bucket, &mut last));
492    }
493
494    #[test]
495    fn rate_allow_drains_bucket() {
496        let mut bucket = 2.0;
497        let mut last = Instant::now();
498        assert!(rate_allow(&mut bucket, &mut last));
499        assert!(rate_allow(&mut bucket, &mut last));
500        assert!(!rate_allow(&mut bucket, &mut last));
501    }
502
503    #[test]
504    fn rate_allow_refills_over_time() {
505        let mut bucket = 0.0;
506        let mut last = Instant::now();
507        assert!(!rate_allow(&mut bucket, &mut last));
508        // Simulate a small delay
509        std::thread::sleep(std::time::Duration::from_millis(1));
510        let mut bucket = 0.0;
511        let mut last_refill = Instant::now();
512        std::thread::sleep(std::time::Duration::from_millis(2));
513        assert!(rate_allow(&mut bucket, &mut last_refill));
514    }
515
516    // ---- FD batch attribution ----
517
518    /// Fake fd tokens: attribution logic is offset-based, so plain integers
519    /// stand in for real descriptors (no drop side effects).
520    fn batches(items: &[(usize, &[u32])]) -> VecDeque<(usize, Vec<u32>)> {
521        items
522            .iter()
523            .map(|&(end, fds)| (end, fds.to_vec()))
524            .collect()
525    }
526
527    #[test]
528    fn fd_batch_attaches_to_completing_message() {
529        // Read ended at 24 — exactly where message [16..24) completes.
530        let mut q = batches(&[(24, &[7, 8])]);
531        let fds = take_fd_batches(&mut q, 24);
532        assert_eq!(fds, vec![7u32, 8]);
533        assert!(q.is_empty());
534    }
535
536    #[test]
537    fn fd_batch_waits_for_its_own_message() {
538        // Review case: message A [0..12) completed by read #1, then read #2
539        // ends at 24 completing B [12..24) and delivering fds. The fds must
540        // ride with B, not with the earlier-processed A.
541        let mut q = batches(&[(24, &[9])]);
542        assert!(take_fd_batches(&mut q, 12).is_empty());
543        assert_eq!(take_fd_batches(&mut q, 24), vec![9u32]);
544    }
545
546    #[test]
547    fn split_message_gets_fds_from_tail_read() {
548        // wl_shm.create_pool split across reads: header in read #1, tail +
549        // fd in read #2 ending at 40. Only when the message completes at 40
550        // do its fds become available.
551        let mut q = batches(&[(40, &[5])]);
552        assert!(take_fd_batches(&mut q, 20).is_empty());
553        assert_eq!(take_fd_batches(&mut q, 40), vec![5u32]);
554    }
555
556    #[test]
557    fn blocked_message_closes_only_its_own_fds() {
558        // Blocked global [0..28) owns the batch from read #1; a later valid
559        // message's batch (read #2, end 60) must survive the drop.
560        let mut q = batches(&[(28, &[3]), (60, &[4])]);
561        let dropped = take_fd_batches(&mut q, 28);
562        assert_eq!(dropped, vec![3u32]); // caller drops these
563        assert_eq!(take_fd_batches(&mut q, 60), vec![4u32]); // survivor intact
564    }
565}