Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 49 additions & 13 deletions src/ipc.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
//! IPC via Unix domain socket.
//!
//! The daemon listens on `$XDG_RUNTIME_DIR/ashell.sock`.
//! The daemon listens on `$XDG_RUNTIME_DIR/ashell.sock`, or on
//! `$TMPDIR/ashell-<uid>/ashell.sock` when the runtime dir is unusable.
//! The same binary acts as a client via `ashell msg <command>`.

use std::fmt;
use std::fs::DirBuilder;
use std::io::{BufRead, BufReader, Read, Write};
use std::os::unix::fs::DirBuilderExt;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::str::FromStr;
Expand Down Expand Up @@ -162,17 +165,38 @@ impl FromStr for IpcCommand {
}
}

pub fn socket_path() -> PathBuf {
let uid = unsafe { libc::getuid() };
match xdg::get_runtime_dir() {
Some(dir) => [dir, PathBuf::from("ashell.sock")],
None => [
std::env::temp_dir(),
PathBuf::from(format!("ashell-{uid}.sock")),
],
/// `$XDG_RUNTIME_DIR/ashell.sock`, else a private (0700, ours, not a symlink)
/// per-user dir in the temp dir so other users can't squat or spoof it.
fn socket_path(create_dir: bool) -> Result<PathBuf> {
if let Some(dir) = xdg::get_runtime_dir() {
return Ok(dir.join("ashell.sock"));
}
.iter()
.collect()

let uid = unsafe { libc::geteuid() };
let dir = std::env::temp_dir().join(format!("ashell-{uid}"));
if create_dir {
log::warn!(
"XDG_RUNTIME_DIR is unset or invalid, falling back to {} for the IPC socket",
dir.display()
);
match DirBuilder::new().mode(0o700).create(&dir) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(e) => {
return Err(anyhow::Error::new(e).context(format!("create {}", dir.display())));
}
}
}

let metadata =
std::fs::symlink_metadata(&dir).with_context(|| format!("stat {}", dir.display()))?;
if !xdg::is_private_dir(&metadata) {
return Err(anyhow!(
"{} is not a directory owned by you with mode 0700",
dir.display()
));
}
Ok(dir.join("ashell.sock"))
}

// ---------------------------------------------------------------------------
Expand All @@ -181,7 +205,7 @@ pub fn socket_path() -> PathBuf {

/// Run the IPC client: connect to the daemon, send a command, print the response.
pub fn run_client(cmd: &IpcCommand) -> Result<()> {
let path = socket_path();
let path = socket_path(false).context("locate IPC socket (is ashell running?)")?;
let mut stream = UnixStream::connect(&path)
.with_context(|| format!("connect to {} — is ashell running?", path.display()))?;

Expand Down Expand Up @@ -223,7 +247,7 @@ enum ListenerError {
/// remove the file or bind a new listener — otherwise we'd orphan the
/// primary's fd and break `ashell msg` until it's restarted.
fn create_listener() -> std::result::Result<UnixListener, ListenerError> {
let path = socket_path();
let path = socket_path(true).map_err(ListenerError::Other)?;

match UnixStream::connect(&path) {
Ok(_) => return Err(ListenerError::AlreadyRunning),
Expand Down Expand Up @@ -287,6 +311,12 @@ fn handle_connection(mut stream: UnixStream) -> Option<IpcCommand> {
}
}

/// Defence in depth: the socket dir is already private.
fn is_same_user(stream: &tokio::net::UnixStream) -> bool {
let uid = unsafe { libc::geteuid() };
stream.peer_cred().is_ok_and(|cred| cred.uid() == uid)
}

fn init_listener() -> Option<tokio::net::UnixListener> {
let std_listener = match create_listener() {
Ok(l) => l,
Expand Down Expand Up @@ -320,7 +350,13 @@ pub fn subscription() -> Subscription<IpcCommand> {
};
let (request, listener) = match listener.accept().await {
Ok((stream, _)) => {
let same_user = is_same_user(&stream);
let request = match stream.into_std() {
Ok(mut std_stream) if !same_user => {
log::warn!("IPC: rejected connection from another user");
write_response(&mut std_stream, "error permission denied");
None
}
Ok(std_stream) => handle_connection(std_stream),
Err(e) => {
log::error!("IPC stream conversion error: {e}");
Expand Down
12 changes: 7 additions & 5 deletions src/xdg.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::env;
use std::fs::Metadata;
use std::os::linux::fs::MetadataExt;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
Expand All @@ -10,10 +11,11 @@ use std::path::PathBuf;
pub fn get_runtime_dir() -> Option<PathBuf> {
let runtime_dir = PathBuf::from(env::var_os("XDG_RUNTIME_DIR")?);
let metadata = runtime_dir.metadata().ok()?;
(runtime_dir.is_absolute() && is_private_dir(&metadata)).then_some(runtime_dir)
}

/// A directory owned by the effective user with mode 0700.
pub fn is_private_dir(metadata: &Metadata) -> bool {
let uid = unsafe { libc::geteuid() };
(runtime_dir.is_absolute()
&& metadata.is_dir()
&& metadata.st_uid() == uid
&& metadata.permissions().mode() & 0o777 == 0o700)
.then_some(runtime_dir)
metadata.is_dir() && metadata.st_uid() == uid && metadata.permissions().mode() & 0o777 == 0o700
}
4 changes: 3 additions & 1 deletion website/docs/configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ ashell msg volume-up --no-osd

The socket is created at `$XDG_RUNTIME_DIR/ashell.sock`. When
`$XDG_RUNTIME_DIR` is unset or unusable, ashell falls back to
`$TMPDIR/ashell-<uid>.sock` (typically `/tmp/ashell-1000.sock`).
`$TMPDIR/ashell-<uid>/ashell.sock` (typically `/tmp/ashell-1000/ashell.sock`).
That directory must be owned by you with mode `0700`; otherwise ashell runs
without IPC and `ashell msg` refuses to connect.

If another ashell instance already owns the socket, the new instance still
starts but runs without IPC and logs a warning.
Loading