Skip to content
Draft
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
48 changes: 9 additions & 39 deletions crates/rocm-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ pub use runtime::{
runtime_path_text_is_absolute_for_platform, runtime_paths_equivalent,
runtime_python_activation_hint, runtime_python_activation_script, runtime_python_bin_dir_name,
runtime_python_env_bin_dir, runtime_python_executable_in_env, runtime_python_executable_name,
runtime_rocm_library_filename, shell_command_for_host,
runtime_rocm_library_filename, shell_command_for_host, user_runtime_dir,
};
pub use uv::{
DEFAULT_UV_TIMEOUT_SECS, UV_CACHE_DIR_ENV, UV_CACHE_DIR_OVERRIDE_ENV, UvCacheSource,
Expand Down Expand Up @@ -5241,8 +5241,8 @@ pub struct RocmCliConfig {
fn default_dashboard_socket() -> String {
// Choose a socket location whose *parent* directory is always user-owned so
// that run_unix can tighten it to 0o700 without EPERM. See
// `dashboard_socket_path` for the precedence. This resolver is mirrored in
// `rocm-dash-core` so the canonical `rocm` config and a standalone
// `runtime::user_runtime_dir` for the precedence. This resolver is mirrored
// in `rocm-dash-core` so the canonical `rocm` config and a standalone
// `rocm-dash` config resolve to the same place; keep the two in sync.
let path = dashboard_socket_path(
std::env::var_os("XDG_RUNTIME_DIR"),
Expand All @@ -5266,48 +5266,18 @@ fn default_dashboard_socket() -> String {
/// 2. `$HOME/.rocm/data/telemetry` — standard per-user data dir.
/// 3. `temp_dir()/rocm-<user>` — user-named subdir so the parent is something we
/// create and own, not `/tmp` itself.
///
/// The tier chain itself lives in [`user_runtime_dir`], which the Lemonade
/// engine also uses to synthesize a runtime directory for its child process.
/// Only tier 2 needs the `telemetry` leaf: tiers 1 and 3 are already per-user
/// runtime directories, so the socket sits directly in them.
fn dashboard_socket_path(
xdg_runtime_dir: Option<std::ffi::OsString>,
home: Option<std::ffi::OsString>,
user: Option<String>,
temp_dir: std::path::PathBuf,
) -> std::path::PathBuf {
use std::path::PathBuf;
xdg_runtime_dir
.filter(|v| !v.is_empty())
.map(|d| PathBuf::from(d).join("rocmdashd.sock"))
.or_else(|| {
home.filter(|v| !v.is_empty()).map(|h| {
PathBuf::from(h)
.join(".rocm")
.join("data")
.join("telemetry")
.join("rocmdashd.sock")
})
})
.unwrap_or_else(|| {
let raw = user.unwrap_or_else(|| "user".to_owned());
// Sanitize: keep only alphanumeric, hyphen, and underscore so a path
// separator or `..` in the env var cannot escape the subdirectory.
let sanitized: String = raw
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect();
let sanitized = if sanitized.is_empty() {
"user".to_owned()
} else {
sanitized
};
temp_dir
.join(format!("rocm-{sanitized}"))
.join("rocmdashd.sock")
})
user_runtime_dir(xdg_runtime_dir, home, user, temp_dir, "telemetry", "").join("rocmdashd.sock")
}

fn default_dashboard_listen() -> String {
Expand Down
74 changes: 74 additions & 0 deletions crates/rocm-core/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,80 @@ pub fn default_cache_dir() -> Option<PathBuf> {
.or_else(|| project_dirs().map(|dirs| dirs.cache_dir().to_path_buf()))
}

/// Resolve a writable, user-owned directory for per-user runtime state from
/// explicit environment inputs.
///
/// Runtime state (sockets, lock files, an engine's scratch area) needs a
/// directory whose *parent* is user-owned, so it can be tightened to mode
/// `0o700` without the `EPERM` that results from trying to `chmod` a shared,
/// root-owned `/tmp` (mode `1777`). Precedence:
///
/// 1. `$XDG_RUNTIME_DIR` — already mode `0700` on systemd systems, ideal.
/// 2. `$HOME/.rocm/data/<home_subdir>` — standard per-user data dir.
/// 3. `temp_dir()/rocm-<user>/<temp_subdir>` — user-named subdir so the parent
/// is something we create and own, not `/tmp` itself.
///
/// Tiers 2 and 3 are not exotic: `$XDG_RUNTIME_DIR` is populated by
/// `pam_systemd` at login, so it is absent for every non-login process (cron
/// jobs, CI runners, `systemd-run`, a bare container exec).
///
/// The environment is taken as arguments rather than read here so the
/// precedence is testable without mutating process-global env vars, which is
/// `unsafe` and racy under parallel tests in edition 2024.
///
/// `home_subdir` and `temp_subdir` name the caller's own leaf directory in
/// tiers 2 and 3; either may be empty when the caller owns that whole tier.
/// They are separate parameters because tier 2 lands inside the shared
/// `.rocm/data` tree, where every component needs its own leaf, whereas tier 3
/// is already under a rocm-private `rocm-<user>` directory.
pub fn user_runtime_dir(
xdg_runtime_dir: Option<OsString>,
home: Option<OsString>,
user: Option<String>,
temp_dir: PathBuf,
home_subdir: &str,
temp_subdir: &str,
) -> PathBuf {
if let Some(runtime_dir) = xdg_runtime_dir.filter(|value| !value.is_empty()) {
return PathBuf::from(runtime_dir);
}
if let Some(home) = home.filter(|value| !value.is_empty()) {
return join_subdir(PathBuf::from(home).join(".rocm").join("data"), home_subdir);
}
let user_dir = format!("rocm-{}", sanitized_user_path_component(user));
join_subdir(temp_dir.join(user_dir), temp_subdir)
}

fn join_subdir(mut base: PathBuf, subdir: &str) -> PathBuf {
if !subdir.is_empty() {
base.push(subdir);
}
base
}

/// Reduce a user name to a single safe path component: keep only alphanumeric,
/// hyphen, and underscore so a path separator or `..` in the env var cannot
/// escape the intended subdirectory. An absent or fully-stripped name yields
/// `user`.
fn sanitized_user_path_component(user: Option<String>) -> String {
let sanitized: String = user
.unwrap_or_default()
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect();
if sanitized.is_empty() {
"user".to_owned()
} else {
sanitized
}
}

pub fn managed_runtime_cache_dir(root: &Path) -> PathBuf {
normalize_runtime_path_for_host(root).join("cache")
}
Expand Down
6 changes: 6 additions & 0 deletions crates/rocm-dash-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ pub struct DaemonConfig {
/// 2. `$HOME/.rocm/data/telemetry` — standard per-user data dir.
/// 3. `temp_dir()/rocm-<user>` — user-named subdir so the parent is something
/// the daemon creates and owns, not `/tmp` itself.
///
/// In `rocm-core` the tier chain is shared as `runtime::user_runtime_dir`. This
/// crate keeps its own copy on purpose: it is a standalone library with a
/// deliberately lean dependency set, and depending on `rocm-core` to share ~30
/// lines would pull in that crate's whole graph. Keep the two in sync — the
/// tests below mirror `rocm-core`'s so a divergence is caught.
fn default_socket() -> String {
let path = socket_path(
std::env::var_os("XDG_RUNTIME_DIR"),
Expand Down
Loading
Loading