Isolate nested IPC sockets and define session ownership policy

This commit is contained in:
Samuel Onoja committed 2026-10-07 08:03:30 +01:00
1 parent 97026a0db7
commit ce024d4794
42 files changed
+635 -147

No files matched your search

+98 -12
View File
@@ -8,7 +8,7 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::mpsc::{SyncSender, sync_channel};
use std::time::{Duration, Instant};
use std::{env, fs, io, thread};
use std::{fs, io, thread};
use ferese_core::LayoutMode;
use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
@@ -78,6 +78,41 @@ pub(crate) struct IpcSocketGuard {
path: PathBuf,
device: u64,
inode: u64,
_instance: Option<tempfile::TempDir>,
}
pub(crate) struct Endpoint {
pub(crate) path: PathBuf,
instance: Option<tempfile::TempDir>,
}
impl Endpoint {
pub(crate) fn desktop() -> io::Result<Self> {
Self::desktop_at(ferese_ipc::socket::runtime_default()?)
}
fn desktop_at(path: PathBuf) -> io::Result<Self> {
prepare_parent(&path)?;
Ok(Self { path, instance: None })
}
pub(crate) fn instance() -> io::Result<Self> {
Self::instance_at(ferese_ipc::socket::runtime_default()?)
}
fn instance_at(default: PathBuf) -> io::Result<Self> {
let root = default.parent().expect("runtime control socket has a parent");
prepare_directory(root)?;
let instances = root.join("instances");
prepare_directory(&instances)?;
let instance = tempfile::Builder::new()
.prefix(&format!("{}-", std::process::id()))
.tempdir_in(instances)?;
Ok(Self {
path: instance.path().join("control.sock"),
instance: Some(instance),
})
}
}
impl Drop for IpcSocketGuard {
@@ -90,8 +125,11 @@ impl Drop for IpcSocketGuard {
}
}
pub(crate) fn init(event_loop: &mut EventLoop<'static, Ferese>) -> Result<ScreenshotInit, Box<dyn std::error::Error>> {
let path = socket_path()?;
pub(crate) fn init(
event_loop: &mut EventLoop<'static, Ferese>,
endpoint: Endpoint,
) -> Result<ScreenshotInit, Box<dyn std::error::Error>> {
let Endpoint { path, instance } = endpoint;
prepare_parent(&path)?;
let listener = bind_listener(&path)?;
fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
@@ -100,6 +138,7 @@ pub(crate) fn init(event_loop: &mut EventLoop<'static, Ferese>) -> Result<Screen
path: path.clone(),
device: metadata.dev(),
inode: metadata.ino(),
_instance: instance,
};
let (sender, receiver): (channel::SyncSender<IpcEvent>, channel::Channel<IpcEvent>) =
@@ -1531,16 +1570,17 @@ fn workspace_arg(args: &Value) -> Result<u32, CommandError> {
.ok_or_else(|| CommandError::new("invalid_argument", "index must be a positive 32-bit integer"))
}
fn socket_path() -> Result<PathBuf, io::Error> {
env::var_os("XDG_RUNTIME_DIR")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.map(|directory| directory.join("ferese/control.sock"))
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "XDG_RUNTIME_DIR is not set"))
}
fn prepare_parent(socket: &Path) -> Result<(), io::Error> {
let parent = socket.parent().expect("the Ferese control socket always has a parent");
prepare_directory(parent)
}
fn prepare_directory(parent: &Path) -> Result<(), io::Error> {
match fs::create_dir(parent) {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(error),
}
match fs::symlink_metadata(parent) {
Ok(metadata) => {
if !metadata.is_dir() || metadata.file_type().is_symlink() {
@@ -1556,7 +1596,6 @@ fn prepare_parent(socket: &Path) -> Result<(), io::Error> {
));
}
}
Err(error) if error.kind() == io::ErrorKind::NotFound => fs::create_dir(parent)?,
Err(error) => return Err(error),
}
@@ -1613,6 +1652,7 @@ fn effective_uid() -> u32 {
#[cfg(test)]
mod tests {
use std::env;
use std::io::{Read, Write};
use std::net::Shutdown;
use std::os::unix::fs::PermissionsExt;
@@ -1818,6 +1858,52 @@ mod tests {
fs::remove_dir_all(root).unwrap();
}
#[test]
fn nested_endpoints_reserve_private_unique_directories_and_preserve_the_default() {
let runtime = tempfile::tempdir().unwrap();
let default = runtime.path().join("ferese/control.sock");
let drm = Endpoint::desktop_at(default.clone()).unwrap();
let host = bind_listener(&drm.path).unwrap();
let first = Endpoint::instance_at(default.clone()).unwrap();
let second = Endpoint::instance_at(default.clone()).unwrap();
assert_eq!(drm.path, default);
assert_ne!(first.path, second.path);
assert_eq!(
first.path.parent().unwrap().parent().unwrap(),
default.parent().unwrap().join("instances")
);
for endpoint in [&first, &second] {
assert_eq!(
fs::metadata(endpoint.path.parent().unwrap())
.unwrap()
.permissions()
.mode()
& 0o777,
0o700
);
}
let first_directory = first.path.parent().unwrap().to_owned();
drop(first);
assert!(!first_directory.exists());
assert!(second.path.parent().unwrap().is_dir());
assert!(default.symlink_metadata().unwrap().file_type().is_socket());
assert_eq!(bind_listener(&default).unwrap_err().kind(), io::ErrorKind::AddrInUse);
drop(host);
}
#[test]
fn nested_endpoint_rejects_an_instances_symlink() {
let runtime = tempfile::tempdir().unwrap();
let default = runtime.path().join("ferese/control.sock");
let _drm = Endpoint::desktop_at(default.clone()).unwrap();
let outside = runtime.path().join("outside");
fs::create_dir(&outside).unwrap();
fs::set_permissions(&outside, fs::Permissions::from_mode(0o755)).unwrap();
std::os::unix::fs::symlink(&outside, default.parent().unwrap().join("instances")).unwrap();
assert!(Endpoint::instance_at(default).is_err());
assert_eq!(fs::metadata(outside).unwrap().permissions().mode() & 0o777, 0o755);
}
#[test]
fn listener_reclaims_only_a_stale_owned_socket() {
let root = unique_test_directory("stale-socket");