Skip to content

Commit cecd3f8

Browse files
committed
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
1 parent 623f210 commit cecd3f8

2 files changed

Lines changed: 133 additions & 29 deletions

File tree

src/executor/helpers/run_with_sudo.rs

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,21 @@ pub fn can_elevate_without_prompt() -> bool {
4343
is_root_user() || (is_sudo_available() && sudo_runs_without_password())
4444
}
4545

46-
fn should_prompt_for_sudo_password(
47-
stdin_is_terminal: bool,
48-
sudo_runs_without_password: bool,
49-
) -> bool {
50-
stdin_is_terminal && !sudo_runs_without_password
46+
#[cfg(unix)]
47+
fn has_controlling_terminal() -> bool {
48+
std::fs::File::open("/dev/tty")
49+
.map(|tty| tty.is_terminal())
50+
.unwrap_or(false)
51+
}
52+
53+
#[cfg(not(unix))]
54+
fn has_controlling_terminal() -> bool {
55+
false
5156
}
5257

5358
/// Validate sudo access, prompting the user for their password if necessary
5459
fn validate_sudo_access() -> Result<()> {
55-
let needs_password = should_prompt_for_sudo_password(
56-
IsTerminal::is_terminal(&std::io::stdin()),
57-
sudo_runs_without_password(),
58-
);
60+
let needs_password = has_controlling_terminal() && !sudo_runs_without_password();
5961

6062
if needs_password {
6163
suspend_progress_bar(|| {
@@ -128,23 +130,3 @@ where
128130

129131
Ok(())
130132
}
131-
132-
#[cfg(test)]
133-
mod tests {
134-
use super::should_prompt_for_sudo_password;
135-
136-
#[test]
137-
fn prompts_with_interactive_stdin_when_sudo_requires_password() {
138-
assert!(should_prompt_for_sudo_password(true, false));
139-
}
140-
141-
#[test]
142-
fn skips_prompt_without_interactive_stdin() {
143-
assert!(!should_prompt_for_sudo_password(false, false));
144-
}
145-
146-
#[test]
147-
fn skips_prompt_when_sudo_does_not_need_password() {
148-
assert!(!should_prompt_for_sudo_password(true, true));
149-
}
150-
}

tests/sudo_prompt.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#![cfg(target_os = "linux")]
2+
3+
use std::{
4+
env, fs, io,
5+
os::unix::{fs::PermissionsExt, process::CommandExt},
6+
process::{Command, Stdio},
7+
};
8+
9+
#[test]
10+
fn validates_sudo_with_piped_stdin_and_redirected_stdout() {
11+
if nix::unistd::Uid::current().is_root() {
12+
return;
13+
}
14+
let temp_dir = tempfile::tempdir().unwrap();
15+
let bin_dir = temp_dir.path().join("bin");
16+
fs::create_dir(&bin_dir).unwrap();
17+
18+
let sudo_path = bin_dir.join("sudo");
19+
fs::write(
20+
&sudo_path,
21+
r##"#!/usr/bin/env bash
22+
set -eu
23+
printf '%s\n' "$*" >> "$CODSPEED_TEST_SUDO_LOG"
24+
25+
case "$1" in
26+
--version)
27+
exit 0
28+
;;
29+
--non-interactive)
30+
if [[ "$2" == true ]]; then
31+
exit 1
32+
fi
33+
if [[ ! -e "$CODSPEED_TEST_SUDO_VALIDATED" ]]; then
34+
echo "sudo validation was skipped" >&2
35+
exit 1
36+
fi
37+
exit 0
38+
;;
39+
--validate)
40+
: > "$CODSPEED_TEST_SUDO_VALIDATED"
41+
exit 0
42+
;;
43+
esac
44+
45+
exit 1
46+
"##,
47+
)
48+
.unwrap();
49+
fs::set_permissions(&sudo_path, fs::Permissions::from_mode(0o755)).unwrap();
50+
51+
let log_path = temp_dir.path().join("sudo.log");
52+
let validated_path = temp_dir.path().join("validated");
53+
let stdout_path = temp_dir.path().join("runner.stdout");
54+
let stderr_path = temp_dir.path().join("runner.stderr");
55+
let path = format!(
56+
"{}:{}",
57+
bin_dir.display(),
58+
env::var_os("PATH").unwrap().to_string_lossy()
59+
);
60+
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""#;
61+
62+
let mut master_fd = -1;
63+
let mut slave_fd = -1;
64+
let result = unsafe {
65+
libc::openpty(
66+
&mut master_fd,
67+
&mut slave_fd,
68+
std::ptr::null_mut(),
69+
std::ptr::null(),
70+
std::ptr::null(),
71+
)
72+
};
73+
assert_eq!(result, 0, "openpty failed: {}", io::Error::last_os_error());
74+
75+
let slave_fd_for_child = slave_fd;
76+
let mut command = Command::new("bash");
77+
command
78+
.args(["-c", shell])
79+
.current_dir(env!("CARGO_MANIFEST_DIR"))
80+
.env("PATH", path)
81+
.env("CODSPEED_BIN", env!("CARGO_BIN_EXE_codspeed"))
82+
.env("CODSPEED_ISOLATION", "true")
83+
.env("CODSPEED_PROFILER_ENABLED", "false")
84+
.env("CODSPEED_TEST_SUDO_LOG", &log_path)
85+
.env("CODSPEED_TEST_SUDO_VALIDATED", &validated_path)
86+
.env("CODSPEED_TEST_STDOUT", &stdout_path)
87+
.env("CODSPEED_TEST_STDERR", &stderr_path)
88+
.stdin(Stdio::piped())
89+
.stdout(Stdio::null())
90+
.stderr(Stdio::null());
91+
unsafe {
92+
command.pre_exec(move || {
93+
if libc::setsid() == -1 {
94+
return Err(io::Error::last_os_error());
95+
}
96+
if libc::ioctl(slave_fd_for_child, libc::TIOCSCTTY as _, 0) == -1 {
97+
return Err(io::Error::last_os_error());
98+
}
99+
Ok(())
100+
});
101+
}
102+
103+
let mut child = command.spawn().unwrap();
104+
drop(child.stdin.take());
105+
let status = child.wait().unwrap();
106+
107+
unsafe {
108+
libc::close(master_fd);
109+
libc::close(slave_fd);
110+
}
111+
112+
assert!(
113+
status.success(),
114+
"bash example failed: {}",
115+
fs::read_to_string(stderr_path).unwrap_or_default()
116+
);
117+
let sudo_invocations = fs::read_to_string(log_path).unwrap();
118+
assert!(
119+
sudo_invocations.lines().any(|line| line == "--validate"),
120+
"sudo --validate was not invoked; calls: {sudo_invocations}"
121+
);
122+
}

0 commit comments

Comments
 (0)