use std::fs::{self, OpenOptions}; use std::io::{self, Write}; use std::ops::Range; use std::os::linux::net::SocketAddrExt; use std::os::unix::ffi::OsStrExt; use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; use std::os::unix::net::{SocketAddr, UnixListener}; use std::path::{Path, PathBuf}; use tracing::warn; // Identity checks prevent accidental unlinking, not races with hostile same-UID code. #[derive(Debug)] struct CreatedPath { path: PathBuf, device: u64, inode: u64, } impl CreatedPath { fn record(path: PathBuf, metadata: &fs::Metadata) -> Self { Self { path, device: metadata.dev(), inode: metadata.ino(), } } fn remove(&mut self) { match fs::symlink_metadata(&self.path) { Ok(metadata) if metadata.dev() == self.device && metadata.ino() == self.inode => { if let Err(error) = fs::remove_file(&self.path) { warn!( path = %self.path.display(), %error, "could not remove X11 path this process created" ); } } Ok(_) => warn!( path = %self.path.display(), "refusing to remove an X11 path that no longer matches the one this process created" ), Err(error) if error.kind() == io::ErrorKind::NotFound => {} Err(error) => warn!( path = %self.path.display(), %error, "could not inspect X11 path during cleanup" ), } } } impl Drop for CreatedPath { fn drop(&mut self) { self.remove(); } } fn validate_shared_directory(path: &Path) -> io::Result<()> { let metadata = fs::symlink_metadata(path)?; // SAFETY: geteuid has no pointer arguments or caller-side preconditions. let uid = unsafe { libc::geteuid() }; if !metadata.is_dir() || metadata.file_type().is_symlink() || (metadata.uid() != 0 && metadata.uid() != uid) { return Err(io::Error::new( io::ErrorKind::PermissionDenied, format!("untrusted X11 directory: {}", path.display()), )); } if metadata.mode() & 0o022 != 0 && metadata.mode() & 0o1000 == 0 { return Err(io::Error::new( io::ErrorKind::PermissionDenied, format!("shared X11 directory is not sticky: {}", path.display()), )); } Ok(()) } #[derive(Debug)] pub(crate) struct Reservation { display: u32, listeners: Vec, socket_path: Option, lock_path: Option, socket_directory: PathBuf, } impl Reservation { // Leave pre-existing locks and socket paths untouched. pub fn allocate() -> io::Result { #[cfg(test)] { // Owned inputs, so an ordinary test never competes for a real slot in // `/tmp/.X11-unix` nor depends on that directory existing. let (sockets, locks) = crate::xwayland::test_hooks::allocator_root_for_reservation() .unwrap_or_else(|| { let root = crate::xwayland::test_hooks::default_allocator_root(); (root.clone(), root) }); Self::allocate_in(&sockets, &locks, 0..64) } #[cfg(not(test))] Self::allocate_in(Path::new("/tmp/.X11-unix"), Path::new("/tmp"), 0..64) } fn allocate_in( socket_directory: &Path, lock_directory: &Path, candidates: Range, ) -> io::Result { validate_shared_directory(socket_directory)?; validate_shared_directory(lock_directory)?; for display in candidates { let path = lock_directory.join(format!(".X{display}-lock")); let mut file = match OpenOptions::new() .write(true) .create_new(true) .mode(0o644) .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW) .open(&path) { Ok(file) => file, Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue, Err(error) => return Err(error), }; let lock_path = CreatedPath::record(path, &file.metadata()?); if let Err(error) = writeln!(file, "{:>10}", std::process::id()).and_then(|()| file.flush()) { drop(file); drop(lock_path); return Err(error); } drop(file); let mut reservation = Self { display, listeners: Vec::new(), socket_path: None, lock_path: Some(lock_path), socket_directory: socket_directory.to_owned(), }; match reservation.bind_same_display() { Ok(()) => return Ok(reservation), Err(error) if error.kind() == io::ErrorKind::AddrInUse => { drop(reservation); } Err(error) => return Err(error), } } Err(io::Error::new( io::ErrorKind::AddrInUse, "no unused X11 display in the configured internal candidate range", )) } pub fn display_number(&self) -> u32 { self.display } pub fn display_name(&self) -> String { format!(":{}", self.display) } pub fn listeners(&self) -> &[UnixListener] { &self.listeners } // Remove source clones and stop the server before closing the reservation. pub fn close_listeners(&mut self) { self.listeners.clear(); self.socket_path = None; } pub fn bind_same_display(&mut self) -> io::Result<()> { #[cfg(test)] if crate::xwayland::test_hooks::fire(&crate::xwayland::test_hooks::FAIL_REBIND_AFTER) { return Err(io::Error::new( io::ErrorKind::AddrInUse, "injected X11 display rebind failure", )); } if !self.listeners.is_empty() || self.socket_path.is_some() { return Err(io::Error::new( io::ErrorKind::AlreadyExists, "X11 listeners are already bound", )); } validate_shared_directory(&self.socket_directory)?; let path = self.socket_directory.join(format!("X{}", self.display)); let filesystem = UnixListener::bind(&path)?; let owned_path = CreatedPath::record(path.clone(), &fs::symlink_metadata(&path)?); let abstract_address = SocketAddr::from_abstract_name(path.as_os_str().as_bytes())?; let abstract_socket = match UnixListener::bind_addr(&abstract_address) { Ok(listener) => listener, Err(error) => { // Drop the recorded path first so cleanup happens in order, once. drop(owned_path); drop(filesystem); return Err(error); } }; self.listeners = vec![filesystem, abstract_socket]; self.socket_path = Some(owned_path); Ok(()) } fn release_display(&mut self) { self.close_listeners(); self.lock_path = None; } } impl Drop for Reservation { fn drop(&mut self) { self.release_display(); } } #[cfg(test)] mod tests { use super::*; use std::os::unix::fs::PermissionsExt; fn shared_root(name: &str) -> PathBuf { let root = std::env::temp_dir().join(format!( "ferese-xwayland-alloc-{name}-{}", std::process::id() )); let _ = fs::remove_dir_all(&root); fs::create_dir_all(&root).unwrap(); for directory in ["sockets", "locks"] { fs::create_dir(root.join(directory)).unwrap(); fs::set_permissions(root.join(directory), fs::Permissions::from_mode(0o1777)).unwrap(); } root } #[test] fn allocates_a_display_with_two_listener_transports() { let root = shared_root("two-transports"); let reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); assert!(reservation.listeners().len() >= 2); let socket = root .join("sockets") .join(format!("X{}", reservation.display_number())); assert!(socket.exists(), "conventional socket path must exist"); assert!( root.join("locks") .join(format!(".X{}-lock", reservation.display_number())) .exists() ); } #[test] fn skips_an_occupied_display_without_reclaiming_it() { let root = shared_root("occupied"); let sockets = root.join("sockets"); let locks = root.join("locks"); let first = Reservation::allocate_in(&sockets, &locks, 0..64).unwrap(); let taken = first.display_number(); let second = Reservation::allocate_in(&sockets, &locks, 0..64).unwrap(); assert_ne!(second.display_number(), taken); assert!(second.display_number() > taken); assert!(locks.join(format!(".X{taken}-lock")).exists()); assert!(sockets.join(format!("X{taken}")).exists()); } #[test] fn an_existing_lock_file_is_left_untouched() { let root = shared_root("stale-lock"); let locks = root.join("locks"); let stale = locks.join(".X3-lock"); fs::write(&stale, b"9999\n").unwrap(); let reservation = Reservation::allocate_in(&root.join("sockets"), &locks, 0..64).unwrap(); assert_ne!( reservation.display_number(), 3, "occupied number must be skipped" ); assert_eq!( fs::read(&stale).unwrap(), b"9999\n", "stale lock must not be reclaimed" ); } #[test] fn cleanup_removes_paths_this_process_created() { let root = shared_root("cleanup"); let reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); let display = reservation.display_number(); let socket = root.join("sockets").join(format!("X{display}")); let lock = root.join("locks").join(format!(".X{display}-lock")); drop(reservation); assert!(!socket.exists(), "owned socket path must be removed"); assert!(!lock.exists(), "owned lock path must be removed"); } #[test] fn close_listeners_keeps_the_display_lock_owned() { let root = shared_root("close-listeners"); let mut reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); let display = reservation.display_number(); reservation.close_listeners(); assert!(reservation.listeners().is_empty()); assert!( root.join("locks") .join(format!(".X{display}-lock")) .exists() ); assert!(!root.join("sockets").join(format!("X{display}")).exists()); } #[test] fn retry_rebinds_the_same_display_number() { let root = shared_root("retry"); let mut reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); let display = reservation.display_number(); reservation.close_listeners(); reservation.bind_same_display().unwrap(); assert_eq!(reservation.display_number(), display); assert!(!reservation.listeners().is_empty()); assert!(root.join("sockets").join(format!("X{display}")).exists()); } #[test] fn binding_twice_is_rejected() { let root = shared_root("double-bind"); let mut reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); reservation.close_listeners(); reservation.bind_same_display().unwrap(); let error = reservation.bind_same_display().unwrap_err(); assert_eq!(error.kind(), io::ErrorKind::AlreadyExists); } #[test] fn an_untrusted_socket_directory_is_rejected() { let root = std::env::temp_dir().join(format!("ferese-xwayland-untrusted-{}", std::process::id())); let _ = fs::remove_dir_all(&root); fs::create_dir_all(&root).unwrap(); fs::set_permissions(&root, fs::Permissions::from_mode(0o777)).unwrap(); let locks = root.join("locks"); fs::create_dir(&locks).unwrap(); fs::set_permissions(&locks, fs::Permissions::from_mode(0o1777)).unwrap(); let error = Reservation::allocate_in(&root, &locks, 0..64).unwrap_err(); assert_eq!(error.kind(), io::ErrorKind::PermissionDenied); assert!(error.to_string().contains("not sticky")); } #[test] fn a_missing_socket_directory_is_an_actionable_error() { let root = shared_root("missing-dir"); let error = Reservation::allocate_in(&root.join("absent"), &root.join("locks"), 0..64).unwrap_err(); assert_eq!(error.kind(), io::ErrorKind::NotFound); } #[test] fn exhausting_the_candidate_range_reports_no_free_display() { let root = shared_root("exhausted"); let locks = root.join("locks"); for display in 10..13 { fs::write(locks.join(format!(".X{display}-lock")), b"1\n").unwrap(); } let error = Reservation::allocate_in(&root.join("sockets"), &locks, 10..13).unwrap_err(); assert_eq!(error.kind(), io::ErrorKind::AddrInUse); assert!(error.to_string().contains("no unused X11 display")); } }