scx_forge_agent/sudo.rs
1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: GPL-2.0-only
3//! Resolve how the harness gains root to load the scheduler, and build the
4//! privileged commands. Ported from the Python harness's `setup_sudo`.
5//!
6//! Precedence:
7//! 1. already root -> run directly, no sudo
8//! 2. `$SUDO_ASKPASS` set -> `sudo -A` (use the caller's askpass)
9//! 3. password file (from the spec via `resolve()`, or from
10//! `$SCX_SUDO_PASSWORD_FILE`) -> generate an askpass shim that prints the
11//! file's contents and use `sudo -A`; the password stays in the file and
12//! never appears in argv or the process table. The shim path is exported
13//! to spawned sudo commands only (via `Command::env`), never to the
14//! agent's own environment.
15//! 4. otherwise -> `sudo -n` (passwordless / cached credentials)
16
17use std::os::unix::fs::PermissionsExt;
18use std::path::{Path, PathBuf};
19use std::process::Command;
20
21use anyhow::{Context, Result};
22
23/// A generated askpass shim, removed from disk when dropped (replaces atexit).
24struct TempShim {
25 path: PathBuf,
26}
27
28impl Drop for TempShim {
29 fn drop(&mut self) {
30 let _ = std::fs::remove_file(&self.path);
31 }
32}
33
34/// Resolved sudo strategy: the argv prefix plus any owned askpass shim.
35pub struct Sudo {
36 /// e.g. `[]` (root), `["sudo", "-A"]`, or `["sudo", "-n"]`.
37 prefix: Vec<String>,
38 /// Askpass shim exported to spawned sudo commands via `SUDO_ASKPASS`.
39 /// `None` when sudo is not used, or when the caller's own `SUDO_ASKPASS`
40 /// is inherited from the environment.
41 askpass: Option<PathBuf>,
42 _shim: Option<TempShim>,
43}
44
45/// Single-quote a path for safe embedding in a /bin/sh script.
46fn shell_quote(s: &str) -> String {
47 format!("'{}'", s.replace('\'', "'\\''"))
48}
49
50impl Sudo {
51 /// Decide the sudo strategy from euid, the environment, and an optional
52 /// password file resolved from the spec (takes precedence over
53 /// `$SCX_SUDO_PASSWORD_FILE`).
54 pub fn resolve(password_file: Option<&Path>) -> Result<Sudo> {
55 if unsafe { libc::geteuid() } == 0 {
56 return Ok(Sudo {
57 prefix: Vec::new(),
58 askpass: None,
59 _shim: None,
60 });
61 }
62 if std::env::var_os("SUDO_ASKPASS").is_some() {
63 return Ok(Sudo {
64 prefix: vec!["sudo".into(), "-A".into()],
65 askpass: None,
66 _shim: None,
67 });
68 }
69 let pass_file = password_file
70 .map(Path::to_path_buf)
71 .or_else(|| std::env::var_os("SCX_SUDO_PASSWORD_FILE").map(PathBuf::from));
72 if let Some(pass_file) = pass_file {
73 let pf = pass_file;
74 if !pf.is_file() {
75 anyhow::bail!("SCX_SUDO_PASSWORD_FILE not found: {}", pf.display());
76 }
77 let pf_abs = std::fs::canonicalize(&pf)
78 .with_context(|| format!("canonicalize {}", pf.display()))?;
79 // askpass helper: sudo runs this and reads the password from stdout.
80 let shim = std::env::temp_dir().join(format!("scx-askpass-{}.sh", std::process::id()));
81 std::fs::write(
82 &shim,
83 format!(
84 "#!/bin/sh\nexec cat {}\n",
85 shell_quote(&pf_abs.to_string_lossy())
86 ),
87 )
88 .with_context(|| format!("write askpass shim {}", shim.display()))?;
89 std::fs::set_permissions(&shim, std::fs::Permissions::from_mode(0o700))
90 .with_context(|| format!("chmod askpass shim {}", shim.display()))?;
91 return Ok(Sudo {
92 prefix: vec!["sudo".into(), "-A".into()],
93 askpass: Some(shim.clone()),
94 _shim: Some(TempShim { path: shim }),
95 });
96 }
97 Ok(Sudo {
98 prefix: vec!["sudo".into(), "-n".into()],
99 askpass: None,
100 _shim: None,
101 })
102 }
103
104 /// Export the askpass shim (if any) to a spawned sudo command.
105 fn apply_env(&self, c: &mut Command) {
106 if let Some(askpass) = &self.askpass {
107 c.env("SUDO_ASKPASS", askpass);
108 }
109 }
110
111 /// Build a `Command` running `program` (with `args`) as root.
112 pub fn command(&self, program: &str, args: &[String]) -> Command {
113 if self.prefix.is_empty() {
114 let mut c = Command::new(program);
115 c.args(args);
116 c
117 } else {
118 let mut c = Command::new(&self.prefix[0]);
119 c.args(&self.prefix[1..]);
120 c.arg(program);
121 c.args(args);
122 self.apply_env(&mut c);
123 c
124 }
125 }
126
127 /// Validate that sudo authenticates now (`sudo <prefix> -v`). No-op as root.
128 /// Returns Err with sudo's stderr on failure.
129 pub fn authenticate(&self) -> Result<()> {
130 if self.prefix.is_empty() {
131 return Ok(());
132 }
133 let mut c = Command::new(&self.prefix[0]);
134 c.args(&self.prefix[1..]).arg("-v");
135 self.apply_env(&mut c);
136 let out = c.output().context("spawn sudo -v")?;
137 if out.status.success() {
138 Ok(())
139 } else {
140 let err = String::from_utf8_lossy(&out.stderr);
141 anyhow::bail!("{}", err.trim());
142 }
143 }
144}