fix(spawner): resolve privileged wrappers to absolute paths + log spawn context #131
1 changed files with 67 additions and 1 deletions
|
|
@ -185,6 +185,41 @@ fn priv_wrap(
|
|||
}
|
||||
}
|
||||
|
||||
/// Directories searched to resolve a bare wrapper name (`sudo`, `jexec`, `mdo`)
|
||||
/// to an absolute path. A daemon must not depend on an inherited `PATH` for its
|
||||
/// privileged wrappers: under `daemon(8)`/rc the `PATH` is often empty or
|
||||
/// reordered, so `execvp` either misses the binary (`ENOENT`) or hits a
|
||||
/// non-executable same-named entry first and returns `EACCES` — the exact spawn
|
||||
/// failure seen on FreeBSD even though the same command runs fine from a shell.
|
||||
const WRAPPER_SEARCH_DIRS: &[&str] = &[
|
||||
"/usr/local/sbin",
|
||||
"/usr/local/bin",
|
||||
"/usr/sbin",
|
||||
"/usr/bin",
|
||||
"/sbin",
|
||||
"/bin",
|
||||
];
|
||||
|
||||
/// Resolve a program name to an absolute executable path. Names already
|
||||
/// containing a `/` are returned unchanged. Bare names are looked up in
|
||||
/// `WRAPPER_SEARCH_DIRS`, returning the first regular executable file found.
|
||||
/// Falls back to the original name (letting the OS report the error) when
|
||||
/// nothing matches.
|
||||
fn resolve_program(program: &str) -> String {
|
||||
if program.contains('/') {
|
||||
return program.to_string();
|
||||
}
|
||||
for dir in WRAPPER_SEARCH_DIRS {
|
||||
let candidate = Path::new(dir).join(program);
|
||||
if let Ok(meta) = std::fs::metadata(&candidate) {
|
||||
if meta.is_file() && meta.permissions().mode() & 0o111 != 0 {
|
||||
return candidate.to_string_lossy().into_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
program.to_string()
|
||||
}
|
||||
|
||||
fn jail_stage_root(jail: &JailConfig) -> Option<&str> {
|
||||
jail.root_path.as_deref().or(jail.path.as_deref())
|
||||
}
|
||||
|
|
@ -259,7 +294,15 @@ async fn spawn_prepared_child(
|
|||
prepared: &PreparedSpawnCommand,
|
||||
agent_config: &AgentSpawnConfig,
|
||||
) -> Result<(Child, Option<ChildStdout>), SpawnerError> {
|
||||
let mut cmd = Command::new(&prepared.program);
|
||||
let program = resolve_program(&prepared.program);
|
||||
info!(
|
||||
program = %program,
|
||||
requested = %prepared.program,
|
||||
args = ?prepared.argv,
|
||||
path = %std::env::var("PATH").unwrap_or_default(),
|
||||
"spawning agent subprocess"
|
||||
);
|
||||
let mut cmd = Command::new(&program);
|
||||
cmd.args(&prepared.argv)
|
||||
.envs(&prepared.env)
|
||||
.stdin(Stdio::null())
|
||||
|
|
@ -788,6 +831,29 @@ mod jail_tests {
|
|||
v.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_program_passes_through_absolute_paths() {
|
||||
assert_eq!(resolve_program("/usr/sbin/jexec"), "/usr/sbin/jexec");
|
||||
assert_eq!(resolve_program("./rel/path"), "./rel/path");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_program_absolutizes_a_known_bare_name() {
|
||||
// `sh` exists and is executable on any unix test host. Resolution must
|
||||
// yield an absolute path under one of the search dirs.
|
||||
let resolved = resolve_program("sh");
|
||||
assert!(
|
||||
resolved.starts_with('/') && resolved.ends_with("/sh"),
|
||||
"expected absolute path to sh, got {resolved}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_program_falls_back_to_bare_name_when_missing() {
|
||||
let missing = "definitely-not-a-real-wrapper-xyz";
|
||||
assert_eq!(resolve_program(missing), missing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_jail_returns_command_unchanged() {
|
||||
let (exe, a) = jail_wrap(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue