diff --git a/Cargo.lock b/Cargo.lock index d408fb0a..8cfc472e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -481,6 +481,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "caps" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0" +dependencies = [ + "libc", +] + [[package]] name = "cargo-platform" version = "0.1.9" @@ -693,6 +702,7 @@ dependencies = [ "axoupdater", "base64", "bincode", + "caps", "clap", "console", "crc32fast", @@ -752,6 +762,7 @@ dependencies = [ "url", "uuid", "which 8.0.2", + "xattr", ] [[package]] @@ -1681,7 +1692,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2", "system-configuration", "tokio", "tower-service", @@ -3054,7 +3065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -3088,7 +3099,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2", "thiserror 2.0.18", "tokio", "tracing", @@ -3126,7 +3137,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2", "tracing", "windows-sys 0.60.2", ] @@ -4226,16 +4237,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.3" @@ -4679,7 +4680,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", - "socket2 0.6.3", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] diff --git a/Cargo.toml b/Cargo.toml index 4a82e7b3..fbc5e300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ indicatif = "0.18" console = "0.16" async-trait = "0.1.89" libc = { workspace = true } +xattr = "1.6" bincode = "1.3.3" # Pinned to 1.x: 2.0 changes the wire format and serde integration object = "0.39" linux-perf-data = { git = "https://github.com/mstange/linux-perf-data.git", rev = "da5bce4b9fb724e84b1eea0cb6ab9c8a291bc676", features = [ @@ -54,10 +55,8 @@ memmap2 = "0.9.10" nix = { version = "0.31.3", features = ["fs", "time", "user"] } futures = "0.3.32" runner-shared = { path = "crates/runner-shared" } -memtrack = { path = "crates/memtrack", default-features = false } exec-harness = { path = "crates/exec-harness" } instrument-hooks-bindings = { path = "crates/instrument-hooks-bindings" } -ipc-channel = { workspace = true } shellexpand = { version = "3.1.2", features = ["tilde"] } addr2line = "0.26" gimli = "0.33" @@ -71,8 +70,12 @@ which = "8.0.2" crc32fast = "1.5.0" samply = { git = "https://github.com/CodSpeedHQ/samply-codspeed", rev = "81ba2c346e71" } +# Memory profiling (memtrack) and the capability handling around it are Linux-only. [target.'cfg(target_os = "linux")'.dependencies] procfs = "0.18" +caps = "0.5" +memtrack = { path = "crates/memtrack", default-features = false } +ipc-channel = { workspace = true } [dev-dependencies] temp-env = { version = "0.3.6", features = ["async_closure"] } diff --git a/crates/memtrack/src/ebpf/tracker.rs b/crates/memtrack/src/ebpf/tracker.rs index ec9c3b82..e5814106 100644 --- a/crates/memtrack/src/ebpf/tracker.rs +++ b/crates/memtrack/src/ebpf/tracker.rs @@ -76,6 +76,8 @@ impl Tracker { Ok(rx) } + /// Bump RLIMIT_MEMLOCK for kernels older than 5.11. Newer kernels account BPF + /// memory against the cgroup, so a denied raise (no CAP_SYS_RESOURCE) is harmless. fn bump_memlock_rlimit() -> Result<()> { let rlimit = libc::rlimit { rlim_cur: libc::RLIM_INFINITY, @@ -84,7 +86,10 @@ impl Tracker { let ret = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlimit) }; if ret != 0 { - anyhow::bail!("Failed to increase rlimit"); + let err = std::io::Error::last_os_error(); + warn!( + "Could not raise RLIMIT_MEMLOCK ({err}); continuing since kernels >= 5.11 don't require it" + ); } Ok(()) diff --git a/crates/memtrack/src/main.rs b/crates/memtrack/src/main.rs index ea918c77..b14487eb 100644 --- a/crates/memtrack/src/main.rs +++ b/crates/memtrack/src/main.rs @@ -100,6 +100,9 @@ fn track_command( } })) } else { + // Without IPC, nothing toggles the tracking_enabled map, so events would + // be dropped by the eBPF is_enabled() check. Enable it up front. + tracker_arc.lock().unwrap().enable()?; None }; diff --git a/src/binary_pins.rs b/src/binary_pins.rs index be739ff3..bef7fdf9 100644 --- a/src/binary_pins.rs +++ b/src/binary_pins.rs @@ -113,6 +113,7 @@ const MEMTRACK_INSTALLER: BinaryPin = BinaryPin { url_template: "https://github.com/CodSpeedHQ/codspeed/releases/download/memtrack-v{version}/memtrack-installer.sh", sha256: "67f30ebe17d5da4246b51d8663394026385d95203ff09e81289772159e969603", }; +#[cfg(target_os = "linux")] pub const MEMTRACK_VERSION: &str = MEMTRACK_INSTALLER.version; const EXEC_HARNESS_INSTALLER: BinaryPin = BinaryPin { @@ -134,6 +135,8 @@ const MONGO_TRACER_INSTALLER: BinaryPin = BinaryPin { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PinnedBinary { ValgrindDeb(ValgrindTarget), + // Only installed by the Linux-only memory executor. + #[cfg_attr(not(target_os = "linux"), allow(dead_code))] MemtrackInstaller, ExecHarnessInstaller, MongoTracerInstaller, diff --git a/src/cli/setup.rs b/src/cli/setup.rs index 76aca014..c27b945c 100644 --- a/src/cli/setup.rs +++ b/src/cli/setup.rs @@ -1,5 +1,6 @@ use crate::executor::{ - Executor, ExecutorSupport, ToolInstallStatus, get_all_executors, get_executor_from_mode, + Executor, ExecutorSupport, PrivilegeStatus, ToolInstallStatus, get_all_executors, + get_executor_from_mode, }; use crate::prelude::*; use crate::runner_mode::RunnerMode; @@ -95,6 +96,7 @@ async fn setup_executor( executor.name() ); executor.setup(system_info, setup_cache_dir).await?; + executor.grant_privileges()?; } } Ok(()) @@ -118,6 +120,15 @@ pub fn status(modes: &[RunnerMode]) -> Result<()> { tool_status.tool_name, version ); + match executor.privilege_status() { + Some(PrivilegeStatus::Satisfied { detail }) => { + info!(" {} privileges: {}", check_mark(), detail); + } + Some(PrivilegeStatus::Missing { message }) => { + info!(" {} privileges: {}", cross_mark(), message); + } + None => {} + } } ToolInstallStatus::IncorrectVersion { version, message } => { info!( diff --git a/src/executor/helpers/run_with_sudo.rs b/src/executor/helpers/run_with_sudo.rs index 33d40f14..cda3472c 100644 --- a/src/executor/helpers/run_with_sudo.rs +++ b/src/executor/helpers/run_with_sudo.rs @@ -1,18 +1,67 @@ use crate::executor::helpers::command::CommandBuilder; use crate::{local_logger::suspend_progress_bar, prelude::*}; +#[cfg(target_os = "linux")] +use std::path::Path; use std::{ ffi::OsStr, io::IsTerminal, process::{Command, Stdio}, }; -fn is_root_user() -> bool { +pub fn is_root_user() -> bool { #[cfg(unix)] return nix::unistd::Uid::current().is_root(); #[cfg(not(unix))] return false; } +/// Decode the permitted capability set from a `security.capability` xattr value +/// as a [`caps::Capability::bitmask`]-compatible mask. +/// +/// Layout (little-endian `vfs_cap_data`): a `magic_etc` word encoding the +/// revision, followed by one (v1) or two (v2/v3) `{permitted, inheritable}` +/// 32-bit word pairs. The permitted words are concatenated low-to-high into the +/// returned 64-bit mask. Returns `None` for an unknown revision or truncated data. +#[cfg(target_os = "linux")] +fn permitted_caps_from_xattr(data: &[u8]) -> Option { + const VFS_CAP_REVISION_MASK: u32 = 0xFF00_0000; + const VFS_CAP_REVISION_1: u32 = 0x0100_0000; + const VFS_CAP_REVISION_2: u32 = 0x0200_0000; + const VFS_CAP_REVISION_3: u32 = 0x0300_0000; + + let magic = u32::from_le_bytes(data.get(0..4)?.try_into().ok()?); + let words = match magic & VFS_CAP_REVISION_MASK { + VFS_CAP_REVISION_1 => 1, + VFS_CAP_REVISION_2 | VFS_CAP_REVISION_3 => 2, + _ => return None, + }; + + let mut permitted: u64 = 0; + for word in 0..words { + let offset = 4 + word * 8; // skip magic, then {permitted, inheritable} pairs + let value = u32::from_le_bytes(data.get(offset..offset + 4)?.try_into().ok()?); + permitted |= (value as u64) << (32 * word); + } + Some(permitted) +} + +/// Whether `binary` holds every capability in `required` (a mask built from +/// [`caps::Capability::bitmask`]) in its permitted file-capability set. +#[cfg(target_os = "linux")] +pub fn binary_has_capabilities(binary: &Path, required: u64) -> bool { + let Ok(Some(value)) = xattr::get(binary, "security.capability") else { + return false; + }; + permitted_caps_from_xattr(&value).is_some_and(|permitted| permitted & required == required) +} + +/// Whether a privileged command can run without being wrapped in sudo: true when +/// the caller is already root or the binary carries the needed file capabilities. +#[cfg(target_os = "linux")] +pub fn has_sufficient_privileges(is_root: bool, has_caps: bool) -> bool { + is_root || has_caps +} + fn is_sudo_available() -> bool { Command::new("sudo") .arg("--version") @@ -106,3 +155,78 @@ where Ok(()) } + +#[cfg(all(test, target_os = "linux"))] +mod tests { + use super::*; + use caps::Capability; + use std::io::Write; + + fn mask(caps: &[Capability]) -> u64 { + caps.iter().fold(0, |acc, c| acc | c.bitmask()) + } + + /// Build a `VFS_CAP_REVISION_2` xattr value granting `caps` in the permitted set. + fn vfs_cap_data_v2(caps: &[Capability]) -> Vec { + let permitted = mask(caps); + let mut data = Vec::new(); + data.extend_from_slice(&0x0200_0000u32.to_le_bytes()); // magic_etc + data.extend_from_slice(&(permitted as u32).to_le_bytes()); // permitted lo + data.extend_from_slice(&0u32.to_le_bytes()); // inheritable lo + data.extend_from_slice(&((permitted >> 32) as u32).to_le_bytes()); // permitted hi + data.extend_from_slice(&0u32.to_le_bytes()); // inheritable hi + data + } + + #[test] + fn has_sufficient_privileges_truth_table() { + assert!(has_sufficient_privileges(true, false)); // root + assert!(has_sufficient_privileges(false, true)); // has caps + assert!(!has_sufficient_privileges(false, false)); // unprivileged + } + + #[test] + fn decodes_permitted_caps_across_both_words() { + // CAP_DAC_READ_SEARCH/CAP_SYS_ADMIN live in the low word, CAP_PERFMON/CAP_BPF in the high word. + let required = mask(&[ + Capability::CAP_BPF, + Capability::CAP_PERFMON, + Capability::CAP_SYS_ADMIN, + Capability::CAP_DAC_READ_SEARCH, + ]); + let permitted = permitted_caps_from_xattr(&vfs_cap_data_v2(&[ + Capability::CAP_BPF, + Capability::CAP_PERFMON, + Capability::CAP_SYS_ADMIN, + Capability::CAP_DAC_READ_SEARCH, + ])) + .unwrap(); + assert_eq!(permitted & required, required); + } + + #[test] + fn rejects_unknown_revision_and_truncated_data() { + assert!(permitted_caps_from_xattr(&[]).is_none()); + assert!(permitted_caps_from_xattr(&0xDEAD_BEEFu32.to_le_bytes()).is_none()); + // Revision claims two words but only the magic is present. + assert!(permitted_caps_from_xattr(&0x0200_0000u32.to_le_bytes()).is_none()); + } + + #[test] + fn plain_binary_has_no_capabilities() { + let mut file = tempfile::NamedTempFile::new().unwrap(); + file.write_all(b"\x7fELF").unwrap(); + assert!(!binary_has_capabilities( + file.path(), + Capability::CAP_BPF.bitmask() + )); + } + + #[test] + fn missing_path_has_no_capabilities() { + assert!(!binary_has_capabilities( + Path::new("/nonexistent/codspeed-memtrack"), + Capability::CAP_BPF.bitmask() + )); + } +} diff --git a/src/executor/memory/executor.rs b/src/executor/memory/executor.rs index ae748ddb..a028bdee 100644 --- a/src/executor/memory/executor.rs +++ b/src/executor/memory/executor.rs @@ -1,12 +1,13 @@ use crate::executor::ExecutorName; use crate::executor::ExecutorSupport; +use crate::executor::PrivilegeStatus; use crate::executor::ToolStatus; use crate::executor::helpers::command::CommandBuilder; use crate::executor::helpers::env::{build_path_env, get_base_injected_env}; use crate::executor::helpers::get_bench_command::get_bench_command; use crate::executor::helpers::run_command_with_log_pipe::run_command_with_log_pipe_and_callback; use crate::executor::helpers::run_with_env::wrap_with_env; -use crate::executor::helpers::run_with_sudo::wrap_with_sudo; +use crate::executor::helpers::run_with_sudo::{has_sufficient_privileges, is_root_user}; use crate::executor::shared::fifo::RunnerFifo; use crate::executor::{ExecutionContext, Executor}; use crate::instruments::mongo_tracer::MongoTracer; @@ -27,7 +28,10 @@ use std::rc::Rc; use tempfile::NamedTempFile; use tokio::time::{Duration, timeout}; -use super::setup::{MEMTRACK_COMMAND, get_memtrack_status, install_memtrack}; +use super::setup::{ + MEMTRACK_COMMAND, ensure_memtrack_capabilities, get_memtrack_status, has_memtrack_capabilities, + install_memtrack, +}; pub struct MemoryExecutor; @@ -68,6 +72,21 @@ impl MemoryExecutor { Ok((ipc_server, cmd_builder, env_file)) } + + /// memtrack needs elevated privileges to load its eBPF programs. Those come + /// from running as root or from file capabilities granted at setup time; we + /// never wrap the run in sudo. Bail with actionable guidance when neither is + /// present, instead of letting libbpf fail with a cryptic permission error. + fn ensure_privileges() -> Result<()> { + if has_sufficient_privileges(is_root_user(), has_memtrack_capabilities()) { + return Ok(()); + } + + bail!( + "{MEMTRACK_COMMAND} needs elevated privileges to load its eBPF programs.\n\ + Run `codspeed setup --mode memory` to grant the required capabilities (one-time sudo), or run as root." + ); + } } #[async_trait(?Send)] @@ -80,6 +99,22 @@ impl Executor for MemoryExecutor { Some(get_memtrack_status()) } + fn privilege_status(&self) -> Option { + if is_root_user() { + return Some(PrivilegeStatus::Satisfied { + detail: "running as root".to_string(), + }); + } + if has_memtrack_capabilities() { + return Some(PrivilegeStatus::Satisfied { + detail: "capabilities granted".to_string(), + }); + } + Some(PrivilegeStatus::Missing { + message: "capabilities missing, run `codspeed setup --mode memory`".to_string(), + }) + } + fn support_level(&self, system_info: &SystemInfo) -> ExecutorSupport { match &system_info.os { SupportedOs::Linux(_) => ExecutorSupport::FullySupported, @@ -95,6 +130,10 @@ impl Executor for MemoryExecutor { install_memtrack().await } + fn grant_privileges(&self) -> Result<()> { + ensure_memtrack_capabilities() + } + async fn run( &mut self, execution_context: &ExecutionContext, @@ -103,8 +142,10 @@ impl Executor for MemoryExecutor { // Create the results/ directory inside the profile folder to avoid having memtrack create it with wrong permissions std::fs::create_dir_all(execution_context.profile_folder.join("results"))?; + Self::ensure_privileges()?; + let (ipc, cmd_builder, _env_file) = Self::build_memtrack_command(execution_context)?; - let cmd = wrap_with_sudo(cmd_builder)?.build(); + let cmd = cmd_builder.build(); debug!("cmd: {cmd:?}"); let runner_fifo = RunnerFifo::new()?; diff --git a/src/executor/memory/setup.rs b/src/executor/memory/setup.rs index bab8b6d9..ff35784a 100644 --- a/src/executor/memory/setup.rs +++ b/src/executor/memory/setup.rs @@ -1,12 +1,95 @@ use crate::binary_installer::ensure_binary_installed; use crate::binary_pins::{self, PinnedBinary}; +use crate::executor::helpers::run_with_sudo::{ + binary_has_capabilities, is_root_user, run_with_sudo, +}; use crate::executor::{ToolInstallStatus, ToolStatus}; use crate::prelude::*; +use caps::Capability; +use std::path::PathBuf; use std::process::Command; pub const MEMTRACK_COMMAND: &str = "codspeed-memtrack"; pub const MEMTRACK_CODSPEED_VERSION: &str = binary_pins::MEMTRACK_VERSION; +/// Capabilities memtrack needs to load its eBPF programs without sudo: read +/// tracefs ids, create uprobes, attach tracepoints, load BPF, and raise +/// RLIMIT_MEMLOCK on pre-5.11 kernels. See `2026-06-15-memtrack-setcap-setup.md`. +const MEMTRACK_REQUIRED_CAPS: &[Capability] = &[ + Capability::CAP_DAC_READ_SEARCH, + Capability::CAP_SYS_ADMIN, + Capability::CAP_PERFMON, + Capability::CAP_BPF, + Capability::CAP_SYS_RESOURCE, +]; + +/// `setcap` text form of [`MEMTRACK_REQUIRED_CAPS`]. Kept as an explicit string +/// because it must match libcap's grammar exactly. +const MEMTRACK_SETCAP_SPEC: &str = + "cap_bpf,cap_perfmon,cap_dac_read_search,cap_sys_admin,cap_sys_resource+ep"; + +fn memtrack_required_caps_mask() -> u64 { + MEMTRACK_REQUIRED_CAPS + .iter() + .fold(0, |acc, c| acc | c.bitmask()) +} + +fn memtrack_path() -> Option { + which::which(MEMTRACK_COMMAND).ok() +} + +/// Whether the installed memtrack binary already carries the required capabilities. +pub fn has_memtrack_capabilities() -> bool { + memtrack_path() + .is_some_and(|path| binary_has_capabilities(&path, memtrack_required_caps_mask())) +} + +/// Grant memtrack the capabilities it needs to run without sudo. +/// +/// Best-effort and idempotent: a no-op when running as root or when the caps are +/// already present. Otherwise runs `setcap` (a single sudo prompt) and re-verifies. +/// Failures are surfaced as warnings rather than aborting, since the run-time +/// privilege guard enforces the requirement and reports it clearly. +pub fn ensure_memtrack_capabilities() -> Result<()> { + if is_root_user() { + debug!("Running as root, memtrack does not need file capabilities"); + return Ok(()); + } + + let Some(path) = memtrack_path() else { + warn!("Could not locate {MEMTRACK_COMMAND} to grant capabilities"); + return Ok(()); + }; + + if binary_has_capabilities(&path, memtrack_required_caps_mask()) { + debug!("{MEMTRACK_COMMAND} already has the required capabilities"); + return Ok(()); + } + + info!("Granting memtrack the capabilities required for sudo-less memory profiling"); + let setcap_args = [ + MEMTRACK_SETCAP_SPEC.to_string(), + path.to_string_lossy().into_owned(), + ]; + if let Err(e) = run_with_sudo("setcap", setcap_args) { + warn!( + "Failed to grant capabilities to {MEMTRACK_COMMAND} ({e}). \ + Memory profiling will require running as root." + ); + return Ok(()); + } + + if !binary_has_capabilities(&path, memtrack_required_caps_mask()) { + warn!( + "Capabilities did not stick on {}. The filesystem may not support file \ + capabilities (e.g. nosuid, overlayfs, NFS). Memory profiling will require running as root.", + path.display() + ); + } + + Ok(()) +} + pub fn get_memtrack_status() -> ToolStatus { let tool_name = MEMTRACK_COMMAND.to_string(); diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 1980ed8c..4564e299 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -4,6 +4,7 @@ pub mod config; mod execution_context; pub(crate) mod helpers; mod interfaces; +#[cfg(target_os = "linux")] mod memory; pub mod orchestrator; mod shared; @@ -22,6 +23,7 @@ pub use execution_context::ExecutionContext; pub use interfaces::ExecutorName; pub use orchestrator::Orchestrator; +#[cfg(target_os = "linux")] use memory::executor::MemoryExecutor; use std::path::Path; use valgrind::executor::ValgrindExecutor; @@ -49,16 +51,22 @@ pub fn get_executor_from_mode( #[allow(deprecated)] RunnerMode::Instrumentation | RunnerMode::Simulation => Box::new(ValgrindExecutor), RunnerMode::Walltime => Box::new(WallTimeExecutor::new(walltime_profiler)), + #[cfg(target_os = "linux")] RunnerMode::Memory => Box::new(MemoryExecutor), + #[cfg(not(target_os = "linux"))] + RunnerMode::Memory => panic!("the memory executor is only supported on Linux"), } } pub fn get_all_executors() -> Vec> { - vec![ + #[cfg_attr(not(target_os = "linux"), allow(unused_mut))] + let mut executors: Vec> = vec![ Box::new(ValgrindExecutor), Box::new(WallTimeExecutor::new(None)), - Box::new(MemoryExecutor), - ] + ]; + #[cfg(target_os = "linux")] + executors.push(Box::new(MemoryExecutor)); + executors } /// Installation status of a tool required by an executor. @@ -76,6 +84,17 @@ pub enum ToolInstallStatus { NotInstalled, } +/// Readiness of any elevated privileges an executor needs beyond a plain install +/// (e.g. file capabilities). Reported alongside [`ToolStatus`] in `setup status`. +/// Only the Linux-only memory executor produces these today. +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +pub enum PrivilegeStatus { + /// Privileges are in place; `detail` explains how (root, capabilities granted, …). + Satisfied { detail: String }, + /// Privileges are missing; `message` tells the user how to obtain them. + Missing { message: String }, +} + /// How well a given executor runs on a given [`SupportedOs`]. #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum ExecutorSupport { @@ -94,6 +113,13 @@ pub trait Executor { /// Report the installation status of the tool(s) this executor depends on. fn tool_status(&self) -> Option; + /// Report whether the elevated privileges this executor needs are in place. + /// Defaults to `None` for executors that need none. Only consulted once the + /// tool itself is installed. + fn privilege_status(&self) -> Option { + None + } + /// Declare how well this executor runs on the host system. Drives whether `setup()` is invoked /// (only when [`ExecutorSupport::FullySupported`]) and whether we bail out of running the /// executor at all (on [`ExecutorSupport::Unsupported`]). @@ -107,6 +133,13 @@ pub trait Executor { Ok(()) } + /// Grant any elevated privileges this executor needs (e.g. file capabilities). + /// Runs after [`setup`](Self::setup) and may prompt for sudo. Defaults to a + /// no-op for executors that need none. + fn grant_privileges(&self) -> Result<()> { + Ok(()) + } + /// Runs the executor async fn run( &mut self, @@ -139,6 +172,7 @@ pub async fn run_executor( executor .setup(&orchestrator.system_info, setup_cache_dir) .await?; + executor.grant_privileges()?; } } } diff --git a/src/executor/tests.rs b/src/executor/tests.rs index df30eb57..9a08399a 100644 --- a/src/executor/tests.rs +++ b/src/executor/tests.rs @@ -398,6 +398,7 @@ mod memory { let executor = MemoryExecutor; let system_info = SystemInfo::new().unwrap(); executor.setup(&system_info, None).await.unwrap(); + executor.grant_privileges().unwrap(); }) .await;