Skip to main content

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}