Skip to content
Closed
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
29 changes: 20 additions & 9 deletions src/executor/helpers/run_with_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,31 @@ pub fn wrap_with_env(
mut cmd_builder: CommandBuilder,
extra_env: &HashMap<String, String>,
) -> Result<(CommandBuilder, NamedTempFile)> {
let env_file = create_env_file(extra_env)?;

// Create bash command that sources the env file and runs the original command
let original_command = cmd_builder.as_command_line();
let bash_command = format!(
"source {} && {}",
env_file.path().display(),
original_command
);
let (bash_command, env_file) =
prefix_command_with_env(&cmd_builder.as_command_line(), extra_env)?;
cmd_builder.wrap("bash", ["-c", &bash_command]);

Ok((cmd_builder, env_file))
}

/// Prefixes a shell command with a `source` of the forwarded environment.
///
/// Unlike [`wrap_with_env`], the returned value is the raw `source <file> && <command>`
/// snippet without a `bash -c` wrapper, for callers that already run their argument
/// through a shell. The `source` must be the innermost layer, below any setuid or
/// file-capability binary in the chain: such binaries put the process in glibc's
/// secure-execution mode, which strips `LD_*` variables from the environment before
/// `main()`. Re-sourcing the environment after them restores those variables for the
/// benchmark.
pub fn prefix_command_with_env(
command: &str,
extra_env: &HashMap<String, String>,
) -> Result<(String, NamedTempFile)> {
let env_file = create_env_file(extra_env)?;
let wrapped = format!("source {} && {}", env_file.path().display(), command);
Ok((wrapped, env_file))
}

fn create_env_file(extra_env: &HashMap<String, String>) -> Result<NamedTempFile> {
let system_env = get_exported_system_env()?;
let base_injected_env = extra_env
Expand Down
14 changes: 10 additions & 4 deletions src/executor/memory/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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_env::prefix_command_with_env;
use crate::executor::helpers::run_with_sudo::is_root_user;
use crate::executor::shared::fifo::RunnerFifo;
use crate::executor::{ExecutionContext, Executor};
Expand Down Expand Up @@ -53,23 +53,29 @@ impl MemoryExecutor {
// Setup memtrack IPC server
let (ipc_server, server_name) = ipc::IpcOneShotServer::new()?;

// memtrack runs its command argument through `bash -c`, so the environment
// must be sourced there rather than around memtrack itself: memtrack holds
// file capabilities and runs in glibc secure-execution mode, which strips
// LD_* variables. Sourcing inside the benchmark shell restores them below
// that boundary.
let bench_command = get_bench_command(&execution_context.config)?;
let (bench_command, env_file) = prefix_command_with_env(&bench_command, &extra_env)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Privilege Drop Hides Env File

When the runner starts memtrack as root and memtrack drops its child bash -c to SUDO_UID/SUDO_GID, the child now has to source a NamedTempFile created by the runner. That temp file is owner-only by default, so the benchmark shell can fail with Permission denied before restoring the forwarded environment; the old outer wrapper read the file before crossing into memtrack's dropped child process.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/executor/memory/executor.rs
Line: 62

Comment:
**Privilege Drop Hides Env File**

When the runner starts memtrack as root and memtrack drops its child `bash -c` to `SUDO_UID`/`SUDO_GID`, the child now has to `source` a `NamedTempFile` created by the runner. That temp file is owner-only by default, so the benchmark shell can fail with `Permission denied` before restoring the forwarded environment; the old outer wrapper read the file before crossing into memtrack's dropped child process.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex


// Build the memtrack command
let mut cmd_builder = CommandBuilder::new(MEMTRACK_COMMAND);
cmd_builder.arg("track");
cmd_builder.arg("--output");
cmd_builder.arg(execution_context.profile_folder.join("results"));
cmd_builder.arg("--ipc-server");
cmd_builder.arg(server_name);
cmd_builder.arg(get_bench_command(&execution_context.config)?);
cmd_builder.arg(bench_command);

// Set working directory if specified
if let Some(cwd) = &execution_context.config.working_directory {
let abs_cwd = canonicalize(cwd)?;
cmd_builder.current_dir(abs_cwd);
}

let (cmd_builder, env_file) = wrap_with_env(cmd_builder, &extra_env)?;

Ok((ipc_server, cmd_builder, env_file))
}

Expand Down
35 changes: 35 additions & 0 deletions src/executor/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,4 +476,39 @@ fi
})
.await;
}

// codspeed-memtrack holds file capabilities, so a non-root user executing it
// enters glibc secure-execution mode (AT_SECURE=1), which strips LD_* variables
// from the environment before main(). Forwarding the environment *around*
// memtrack instead of *into* the benchmark it spawns therefore loses
// LD_LIBRARY_PATH, and the benchmark loads the wrong shared libraries (e.g. neqo
// picking up system NSS over a locally built newer one, panicking with
// UnsupportedVersion). PATH survives this because it is not an LD_* variable.
#[test_log::test(tokio::test)]
async fn test_memory_executor_forwards_ld_library_path() {
let custom_lib = "/custom/test/lib";
let current = std::env::var("LD_LIBRARY_PATH").unwrap_or_default();
let modified = match current.is_empty() {
true => custom_lib.to_string(),
false => format!("{custom_lib}:{current}"),
};

let cmd = format!(
r#"
if ! echo "$LD_LIBRARY_PATH" | grep -q "{custom_lib}"; then
echo "FAIL: LD_LIBRARY_PATH does not contain {custom_lib}"
echo "Got LD_LIBRARY_PATH: $LD_LIBRARY_PATH"
exit 1
fi
"#
);
let config = memory_config(&cmd);
let (execution_context, _temp_dir) = create_test_setup(config).await;
let (_permit, _lock, mut executor) = get_memory_executor().await;

temp_env::async_with_vars(&[("LD_LIBRARY_PATH", Some(&modified))], async {
executor.run(&execution_context, &None).await.unwrap();
})
.await;
}
}
Loading