#[cfg(unix)]
use std::time::{Duration, Instant};
use processkit::Command;
#[cfg(unix)]
use processkit::Mechanism;
use processkit::ProcessGroup;
use crate::common::*;
#[tokio::test]
#[ignore = "spawns real subprocesses to compare environments"]
async fn inherit_env_whitelists_parent_env() {
let with_marker = print_env()
.env("PK_ITEM8_MARKER", "present")
.output_string()
.await
.expect("run env printer");
assert!(with_marker.is_success());
assert!(
with_marker.stdout().contains("PK_ITEM8_MARKER"),
"explicit env should reach the child"
);
let whitelisted = print_env()
.inherit_env(if cfg!(windows) {
vec!["PATH", "SystemRoot"]
} else {
vec!["PATH"]
})
.output_string()
.await
.expect("run env printer");
assert!(whitelisted.is_success(), "result: {whitelisted:?}");
assert!(
whitelisted.stdout().to_uppercase().contains("PATH="),
"whitelisted PATH should be present: {:?}",
whitelisted.stdout()
);
assert!(
!whitelisted.stdout().contains("PK_ITEM8_MARKER"),
"non-whitelisted vars must not leak"
);
}
#[cfg(windows)]
#[tokio::test]
#[ignore = "spawns a real subprocess to compare environments"]
async fn inherit_env_matches_windows_names_case_insensitively() {
let result = print_env()
.inherit_env(["path", "systemroot"]) .output_string()
.await
.expect("run env printer");
assert!(result.is_success(), "result: {result:?}");
assert!(
result.stdout().to_uppercase().contains("PATH="),
"lowercase allow-list entry must still copy PATH: {:?}",
result.stdout()
);
}
#[cfg(unix)]
#[tokio::test]
#[ignore = "spawns a real subprocess in a new session"]
async fn setsid_spawns_and_stays_contained() {
let group = ProcessGroup::new().expect("create group");
let process = group
.start(&sleep_secs(30).setsid())
.await
.expect("setsid child spawns (EPERM would mean the pgroup coordination broke)");
let pid = process.pid().expect("pid") as i32;
drop(group);
let start = Instant::now();
let _ = tokio::time::timeout(Duration::from_secs(10), process.wait())
.await
.expect("setsid child outlived the group drop — containment broke")
.expect("wait");
assert!(
start.elapsed() < Duration::from_secs(5),
"setsid child was not reaped promptly (took {:?})",
start.elapsed()
);
assert!(
unsafe { libc::kill(pid, 0) != 0 },
"pid still probes alive after reap"
);
}
#[cfg(unix)]
#[tokio::test]
#[ignore = "drops privileges; meaningful only as root"]
async fn uid_gid_drop_privileges() {
if unsafe { libc::geteuid() } != 0 {
eprintln!("skipping: privilege drop requires root");
return;
}
let result = Command::new("id").arg("-u").uid(1).gid(1).run().await;
match ProcessGroup::new().expect("probe group").mechanism() {
Mechanism::CgroupV2 => {
assert!(
result.is_err(),
"uid drop on the cgroup mechanism is documented to fail the \
spawn, got {result:?}"
);
}
_ => {
let out = result.expect("run id -u as uid 1");
assert_eq!(out.trim(), "1", "child should report the dropped uid");
}
}
}
#[cfg(unix)]
#[tokio::test]
#[ignore = "sets supplementary groups; meaningful only as root"]
async fn groups_set_supplementary_groups() {
if unsafe { libc::geteuid() } != 0 {
eprintln!("skipping: setting supplementary groups requires root");
return;
}
let out = Command::new("id")
.arg("-G")
.groups([1, 2])
.run()
.await
.expect("run id -G with supplementary groups set");
let ids: std::collections::HashSet<&str> = out.split_whitespace().collect();
assert!(
ids.contains("1") && ids.contains("2"),
"the requested supplementary groups should be present: id -G = {out:?}"
);
}
#[cfg(unix)]
#[tokio::test]
#[ignore = "drops privileges with supplementary groups; meaningful only as root"]
async fn groups_with_uid_drop_respects_the_cgroup_caveat() {
if unsafe { libc::geteuid() } != 0 {
eprintln!("skipping: privilege drop requires root");
return;
}
let result = Command::new("id")
.arg("-u")
.uid(1)
.gid(1)
.groups([1])
.run()
.await;
match ProcessGroup::new().expect("probe group").mechanism() {
Mechanism::CgroupV2 => assert!(
result.is_err(),
"uid drop with groups on the cgroup mechanism must fail the spawn, got {result:?}"
),
_ => {
let out = result.expect("run id -u as uid 1 with groups");
assert_eq!(out.trim(), "1", "child should report the dropped uid");
}
}
}
#[cfg(windows)]
#[tokio::test]
#[ignore = "exercises the non-unix unsupported gate"]
async fn windows_unix_only_builders_are_unsupported() {
for (command, what) in [
(Command::new("cmd").args(["/c", "exit 0"]).uid(1000), "uid"),
(Command::new("cmd").args(["/c", "exit 0"]).gid(1000), "gid"),
(
Command::new("cmd").args(["/c", "exit 0"]).groups([1000]),
"groups",
),
(
Command::new("cmd").args(["/c", "exit 0"]).setsid(),
"setsid",
),
] {
let err = command
.output_string()
.await
.expect_err("a privilege request must not be silently skipped");
assert!(
matches!(err, processkit::Error::Unsupported { .. }),
"expected Unsupported for {what}, got {err:?}"
);
}
}
#[cfg(windows)]
#[tokio::test]
#[ignore = "spawns a real subprocess with CREATE_NO_WINDOW under a job"]
async fn windows_create_no_window_spawns_in_group() {
let group = ProcessGroup::new().expect("create group");
let process = group
.start(&two_line_echo().create_no_window())
.await
.expect("spawn with CREATE_NO_WINDOW");
let result = process.output_string().await.expect("collect");
assert!(result.is_success(), "result: {result:?}");
assert!(result.stdout().contains("first"));
}