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> {
|
fn jail_stage_root(jail: &JailConfig) -> Option<&str> {
|
||||||
jail.root_path.as_deref().or(jail.path.as_deref())
|
jail.root_path.as_deref().or(jail.path.as_deref())
|
||||||
}
|
}
|
||||||
|
|
@ -259,7 +294,15 @@ async fn spawn_prepared_child(
|
||||||
prepared: &PreparedSpawnCommand,
|
prepared: &PreparedSpawnCommand,
|
||||||
agent_config: &AgentSpawnConfig,
|
agent_config: &AgentSpawnConfig,
|
||||||
) -> Result<(Child, Option<ChildStdout>), SpawnerError> {
|
) -> 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)
|
cmd.args(&prepared.argv)
|
||||||
.envs(&prepared.env)
|
.envs(&prepared.env)
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
|
|
@ -788,6 +831,29 @@ mod jail_tests {
|
||||||
v.iter().map(|s| s.to_string()).collect()
|
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]
|
#[test]
|
||||||
fn no_jail_returns_command_unchanged() {
|
fn no_jail_returns_command_unchanged() {
|
||||||
let (exe, a) = jail_wrap(
|
let (exe, a) = jail_wrap(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue