From 49c0417dae3a7acc8c2d083dc506e85efa9328c8 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Fri, 17 Jul 2026 19:54:50 +0200 Subject: [PATCH 1/2] fix(memtrack): prompt sudo on interactive stdin Use the controlling input terminal to decide whether sudo can prompt for a password. Keep the non-interactive wrapper safe after credentials are validated.\n\nFixes COD-3153 --- src/executor/helpers/run_with_sudo.rs | 33 +++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/executor/helpers/run_with_sudo.rs b/src/executor/helpers/run_with_sudo.rs index ce5b6bd3..72a59a4d 100644 --- a/src/executor/helpers/run_with_sudo.rs +++ b/src/executor/helpers/run_with_sudo.rs @@ -43,10 +43,19 @@ pub fn can_elevate_without_prompt() -> bool { is_root_user() || (is_sudo_available() && sudo_runs_without_password()) } +fn should_prompt_for_sudo_password( + stdin_is_terminal: bool, + sudo_runs_without_password: bool, +) -> bool { + stdin_is_terminal && !sudo_runs_without_password +} + /// Validate sudo access, prompting the user for their password if necessary fn validate_sudo_access() -> Result<()> { - let needs_password = - IsTerminal::is_terminal(&std::io::stdout()) && !sudo_runs_without_password(); + let needs_password = should_prompt_for_sudo_password( + IsTerminal::is_terminal(&std::io::stdin()), + sudo_runs_without_password(), + ); if needs_password { suspend_progress_bar(|| { @@ -119,3 +128,23 @@ where Ok(()) } + +#[cfg(test)] +mod tests { + use super::should_prompt_for_sudo_password; + + #[test] + fn prompts_with_interactive_stdin_when_sudo_requires_password() { + assert!(should_prompt_for_sudo_password(true, false)); + } + + #[test] + fn skips_prompt_without_interactive_stdin() { + assert!(!should_prompt_for_sudo_password(false, false)); + } + + #[test] + fn skips_prompt_when_sudo_does_not_need_password() { + assert!(!should_prompt_for_sudo_password(true, true)); + } +} From 29159409552609184e7356e7313db09060667fd7 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Fri, 17 Jul 2026 20:07:53 +0200 Subject: [PATCH 2/2] fix(memtrack): detect sudo prompts via controlling tty Use /dev/tty so piped stdin still prompts when a controlling terminal is available. Add a Bash regression covering piped stdin and redirected stdout.\n\nRefs COD-3153 --- src/executor/helpers/run_with_sudo.rs | 40 +++------ tests/sudo_prompt.rs | 122 ++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 29 deletions(-) create mode 100644 tests/sudo_prompt.rs diff --git a/src/executor/helpers/run_with_sudo.rs b/src/executor/helpers/run_with_sudo.rs index 72a59a4d..bc9c8462 100644 --- a/src/executor/helpers/run_with_sudo.rs +++ b/src/executor/helpers/run_with_sudo.rs @@ -43,19 +43,21 @@ pub fn can_elevate_without_prompt() -> bool { is_root_user() || (is_sudo_available() && sudo_runs_without_password()) } -fn should_prompt_for_sudo_password( - stdin_is_terminal: bool, - sudo_runs_without_password: bool, -) -> bool { - stdin_is_terminal && !sudo_runs_without_password +#[cfg(unix)] +fn has_controlling_terminal() -> bool { + std::fs::File::open("/dev/tty") + .map(|tty| tty.is_terminal()) + .unwrap_or(false) +} + +#[cfg(not(unix))] +fn has_controlling_terminal() -> bool { + false } /// Validate sudo access, prompting the user for their password if necessary fn validate_sudo_access() -> Result<()> { - let needs_password = should_prompt_for_sudo_password( - IsTerminal::is_terminal(&std::io::stdin()), - sudo_runs_without_password(), - ); + let needs_password = has_controlling_terminal() && !sudo_runs_without_password(); if needs_password { suspend_progress_bar(|| { @@ -128,23 +130,3 @@ where Ok(()) } - -#[cfg(test)] -mod tests { - use super::should_prompt_for_sudo_password; - - #[test] - fn prompts_with_interactive_stdin_when_sudo_requires_password() { - assert!(should_prompt_for_sudo_password(true, false)); - } - - #[test] - fn skips_prompt_without_interactive_stdin() { - assert!(!should_prompt_for_sudo_password(false, false)); - } - - #[test] - fn skips_prompt_when_sudo_does_not_need_password() { - assert!(!should_prompt_for_sudo_password(true, true)); - } -} diff --git a/tests/sudo_prompt.rs b/tests/sudo_prompt.rs new file mode 100644 index 00000000..85a9484d --- /dev/null +++ b/tests/sudo_prompt.rs @@ -0,0 +1,122 @@ +#![cfg(target_os = "linux")] + +use std::{ + env, fs, io, + os::unix::{fs::PermissionsExt, process::CommandExt}, + process::{Command, Stdio}, +}; + +#[test] +fn validates_sudo_with_piped_stdin_and_redirected_stdout() { + if nix::unistd::Uid::current().is_root() { + return; + } + let temp_dir = tempfile::tempdir().unwrap(); + let bin_dir = temp_dir.path().join("bin"); + fs::create_dir(&bin_dir).unwrap(); + + let sudo_path = bin_dir.join("sudo"); + fs::write( + &sudo_path, + r##"#!/usr/bin/env bash +set -eu +printf '%s\n' "$*" >> "$CODSPEED_TEST_SUDO_LOG" + +case "$1" in + --version) + exit 0 + ;; + --non-interactive) + if [[ "$2" == true ]]; then + exit 1 + fi + if [[ ! -e "$CODSPEED_TEST_SUDO_VALIDATED" ]]; then + echo "sudo validation was skipped" >&2 + exit 1 + fi + exit 0 + ;; + --validate) + : > "$CODSPEED_TEST_SUDO_VALIDATED" + exit 0 + ;; +esac + +exit 1 +"##, + ) + .unwrap(); + fs::set_permissions(&sudo_path, fs::Permissions::from_mode(0o755)).unwrap(); + + let log_path = temp_dir.path().join("sudo.log"); + let validated_path = temp_dir.path().join("validated"); + let stdout_path = temp_dir.path().join("runner.stdout"); + let stderr_path = temp_dir.path().join("runner.stderr"); + let path = format!( + "{}:{}", + bin_dir.display(), + env::var_os("PATH").unwrap().to_string_lossy() + ); + let shell = r#"printf 'piped input\n' | "$CODSPEED_BIN" run --mode walltime --skip-setup --skip-upload --allow-empty -- true > "$CODSPEED_TEST_STDOUT" 2> "$CODSPEED_TEST_STDERR""#; + + let mut master_fd = -1; + let mut slave_fd = -1; + let result = unsafe { + libc::openpty( + &mut master_fd, + &mut slave_fd, + std::ptr::null_mut(), + std::ptr::null(), + std::ptr::null(), + ) + }; + assert_eq!(result, 0, "openpty failed: {}", io::Error::last_os_error()); + + let slave_fd_for_child = slave_fd; + let mut command = Command::new("bash"); + command + .args(["-c", shell]) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .env("PATH", path) + .env("CODSPEED_BIN", env!("CARGO_BIN_EXE_codspeed")) + .env("CODSPEED_ISOLATION", "true") + .env("CODSPEED_PROFILER_ENABLED", "false") + .env("CODSPEED_TEST_SUDO_LOG", &log_path) + .env("CODSPEED_TEST_SUDO_VALIDATED", &validated_path) + .env("CODSPEED_TEST_STDOUT", &stdout_path) + .env("CODSPEED_TEST_STDERR", &stderr_path) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + unsafe { + command.pre_exec(move || { + if libc::setsid() == -1 { + return Err(io::Error::last_os_error()); + } + if libc::ioctl(slave_fd_for_child, libc::TIOCSCTTY as _, 0) == -1 { + return Err(io::Error::last_os_error()); + } + Ok(()) + }); + } + + let mut child = command.spawn().unwrap(); + drop(child.stdin.take()); + let status = child.wait().unwrap(); + + unsafe { + libc::close(master_fd); + libc::close(slave_fd); + } + + assert!( + status.success(), + "bash example failed: {}", + fs::read_to_string(stderr_path).unwrap_or_default() + ); + let sudo_invocations = fs::read_to_string(log_path).unwrap(); + assert!( + sudo_invocations.lines().any(|line| line == "--validate"), + "sudo --validate was not invoked; calls: {sudo_invocations}" + ); +}