Skip to content
Open
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
31 changes: 16 additions & 15 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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"
Expand All @@ -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"] }
Expand Down
7 changes: 6 additions & 1 deletion crates/memtrack/src/ebpf/tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(())
Expand Down
3 changes: 3 additions & 0 deletions crates/memtrack/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand Down
3 changes: 3 additions & 0 deletions src/binary_pins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
13 changes: 12 additions & 1 deletion src/cli/setup.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -95,6 +96,7 @@ async fn setup_executor(
executor.name()
);
executor.setup(system_info, setup_cache_dir).await?;
executor.grant_privileges()?;
}
}
Ok(())
Expand All @@ -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!(
Expand Down
126 changes: 125 additions & 1 deletion src/executor/helpers/run_with_sudo.rs
Original file line number Diff line number Diff line change
@@ -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<u64> {
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")
Expand Down Expand Up @@ -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<u8> {
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()
));
}
}
Loading