use super::*;
use crate::sandbox::{EchoSandbox, Fuel};
use crate::tools::exec::{ExecSetup, ExecTool, Shell};
use kernel::{Address, Payload, RunId, Tool, ToolCall, ToolName};
use serde_json::Value;
const TEST_MEMORY_BYTES: usize = 0x1000_0000;
#[test]
#[ignore = "writes AppContainer profiles and disposable ACLs; explicit Windows Actions acceptance only"]
fn native_windows_disposable_production_axes_and_cleanup() {
assert_eq!(std::env::var("SPRAWLING_DISPOSABLE_NATIVE").unwrap(), "1");
let work = tempfile::tempdir().unwrap();
let host = tempfile::tempdir().unwrap();
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let address = listener.local_addr().unwrap();
assert!(std::net::TcpStream::connect(address).is_ok());
std::fs::copy(
std::env::current_exe().unwrap(),
work.path().join("probe.exe"),
)
.unwrap();
std::fs::write(
work.path().join("native-input.json"),
serde_json::to_vec(&serde_json::json!({
"outside": host.path().join("escape"), "address": address.to_string(),
"memory": TEST_MEMORY_BYTES,
}))
.unwrap(),
)
.unwrap();
let mut host_allocation = Vec::<u8>::new();
host_allocation
.try_reserve_exact(TEST_MEMORY_BYTES)
.unwrap();
drop(host_allocation);
let owner = RunId::from_bytes([0x71; 16]);
let backlog = crate::Backlog::with_window(crate::PollBudget::new(3_000, 5)).with_shares(
Shares::CpuAndMemory {
limit: std::num::NonZeroU64::new(u64::try_from(TEST_MEMORY_BYTES).unwrap()).unwrap(),
},
);
let tool = ExecTool::new(
ExecSetup {
workdir: work.path().to_path_buf(),
mounts: Vec::new(),
python_wasm: None,
shell: Shell::Absent,
fuel: Fuel(1),
env_passthrough: Vec::new(),
domain: Address::parse("work").unwrap(),
run: owner,
policy: crate::PolicyCell::new(kernel::RunPolicy::of(kernel::Mode::Work)).reader(),
},
Box::new(EchoSandbox::new()),
backlog.clone(),
)
.unwrap()
.confined(crate::tools::exec::confinement::Confined::with_arm(
crate::tools::exec::confinement::Confinement::WindowsJobObject,
Some(std::env::temp_dir()),
));
let call = ToolCall {
id: "native-production".to_owned(), name: ToolName::parse("exec").unwrap(),
args: Payload::new(serde_json::json!({"arm":{"program":{
"path":".\\probe.exe", "args":["--exact", "tools::exec::native_windows::tests::native_windows_child_axes", "--ignored", "--nocapture"]
}}}).as_object().unwrap().clone()).unwrap(),
};
let outcome = tool.invoke(&call).unwrap();
let result = serde_json::to_value(outcome.result).unwrap();
assert_eq!(result["exit_code"], 0, "{result}");
assert_eq!(
(
&result["memory_ceiling"]["state"],
&result["memory_ceiling"]["limit_bytes"]
),
(
&Value::from("hit"),
&Value::from(u64::try_from(TEST_MEMORY_BYTES).unwrap())
),
"the probe's refused allocation is reported as reaching the ceiling: {result}"
);
assert!(
result["stdout"]
.as_str()
.unwrap()
.contains("NATIVE_FIVE_AXES_PASSED"),
"{result}"
);
assert!(!host.path().join("escape").exists());
assert!(!work.path().join("native-copy-marker").exists());
assert!(
backlog
.processes()
.unwrap()
.values()
.all(|run| run.pids.is_empty())
);
assert!(
std::net::TcpStream::connect(address).is_ok(),
"harness/provider network remains available"
);
}
#[test]
#[ignore = "child probe entered only by native_windows_disposable_production_axes_and_cleanup"]
fn native_windows_child_axes() {
use std::os::windows::process::CommandExt;
let input: Value =
serde_json::from_slice(&std::fs::read("native-input.json").unwrap()).unwrap();
std::fs::write("native-copy-marker", "copy write permitted").unwrap();
let temp = std::env::temp_dir();
std::fs::write(temp.join("native-temp-probe"), "temp write permitted")
.unwrap_or_else(|err| panic!("the redirected TEMP {} is writable: {err}", temp.display()));
assert!(std::fs::write(input["outside"].as_str().unwrap(), "escape").is_err());
let address = input["address"].as_str().unwrap().parse().unwrap();
assert!(
std::net::TcpStream::connect_timeout(&address, std::time::Duration::from_millis(200))
.is_err()
);
let bytes = usize::try_from(input["memory"].as_u64().unwrap()).unwrap();
let mut allocation = Vec::<u8>::new();
assert!(
allocation.try_reserve_exact(bytes).is_err(),
"committed Job memory is a hard ceiling"
);
assert!(
Command::new(std::env::current_exe().unwrap())
.creation_flags(0x0100_0000)
.arg("--list")
.spawn()
.is_err(),
"breakaway is refused"
);
let mut descendant = Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"tools::exec::native_windows::tests::native_windows_child_waits",
"--ignored",
])
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.spawn()
.unwrap();
assert!(descendant.try_wait().unwrap().is_none());
println!("NATIVE_DESCENDANT_PID={}", descendant.id());
println!("NATIVE_FIVE_AXES_PASSED");
}
#[test]
#[ignore = "descendant probe owned and terminated by the native Job"]
fn native_windows_child_waits() {
std::thread::park_timeout(std::time::Duration::from_secs(60));
std::fs::write("native-survived", "must not survive its owner").unwrap();
}
#[test]
#[ignore = "writes AppContainer profiles and disposable ACLs; explicit Windows Actions acceptance only"]
fn native_windows_disposable_cancellation_owns_the_tree() {
assert_eq!(std::env::var("SPRAWLING_DISPOSABLE_NATIVE").unwrap(), "1");
let copy = tempfile::tempdir().unwrap();
let exe = copy.path().join("probe.exe");
std::fs::copy(std::env::current_exe().unwrap(), &exe).unwrap();
let backlog = crate::Backlog::with_window(crate::PollBudget::new(1, 1));
let owner = RunId::from_bytes([0x72; 16]);
let scope = Address::parse("work").unwrap();
let mut command = Command::new(exe);
command.env_clear().current_dir(copy.path()).args([
"--exact",
"tools::exec::native_windows::tests::native_windows_child_waits",
"--ignored",
]);
let started = backlog
.run_native(owner, &scope, "native cancellation".to_owned(), command)
.unwrap();
assert!(matches!(started, crate::Started::Backgrounded { .. }));
assert_eq!(backlog.release(owner), 1);
for _ in 0..200 {
backlog.harvest(owner).unwrap();
if backlog.standing(&scope).unwrap().is_empty() {
assert!(!copy.path().join("native-survived").exists());
return;
}
std::thread::park_timeout(std::time::Duration::from_millis(5));
}
panic!("native cancellation must reap its owned Job");
}
#[test]
fn native_memory_is_unrequested_without_a_user_ceiling() {
for shares in [Shares::Unset, Shares::Cpu] {
assert_eq!(limits(shares).unwrap().memory, None);
}
}
#[test]
#[ignore = "writes AppContainer profiles and disposable ACLs; explicit Windows Actions acceptance only"]
fn native_windows_disposable_argv_and_unrequested_memory() {
use std::os::windows::ffi::OsStringExt;
assert_eq!(std::env::var("SPRAWLING_DISPOSABLE_NATIVE").unwrap(), "1");
let copy = tempfile::tempdir().unwrap();
let directory = copy.path().join("path with spaces");
std::fs::create_dir(&directory).unwrap();
let source = directory.join("probe.rs");
let exe = directory.join("probe.exe");
std::fs::write(&source, super::child::SOURCE).unwrap();
let built = Command::new("rustc")
.arg(&source)
.arg("-o")
.arg(&exe)
.output()
.unwrap();
assert!(built.status.success(), "{built:?}");
let args = vec![
vec![],
vec![32, 9],
vec![34, 92, 34],
vec![92, 92],
vec![0xd800, 32, 0xdc00],
vec![97, 92, 34, 98, 92],
];
let backlog = crate::Backlog::with_window(crate::PollBudget::new(10_000, 5));
let scope = Address::parse("work").unwrap();
let mut command = Command::new(&exe);
command
.env_clear()
.current_dir(&directory)
.args(args.iter().map(|units| OsString::from_wide(units)));
let result = backlog
.run_native(
RunId::from_bytes([0x74; 16]),
&scope,
"argv round trip".to_owned(),
command,
)
.unwrap();
let crate::Started::Settled {
exit,
stdout,
stderr,
..
} = result
else {
panic!("argv child must settle: {result:?}");
};
assert_eq!(exit, crate::Exit::Ended { code: 0 }, "{stderr}");
assert_eq!(
serde_json::from_str::<Vec<Vec<u16>>>(&stdout).unwrap(),
args
);
let home = tempfile::tempdir().unwrap();
let protected = home.path().join("protected");
std::fs::create_dir(&protected).unwrap();
std::fs::write(protected.join("input"), "read only").unwrap();
let prepared = Command::new("pwsh").env("NATIVE_ACL_FIXTURE", &protected)
.args(["-NoProfile", "-NonInteractive", "-Command", "$acl=Get-Acl -LiteralPath $env:NATIVE_ACL_FIXTURE; $acl.SetAccessRuleProtection($true,$true); Set-Acl -LiteralPath $env:NATIVE_ACL_FIXTURE -AclObject $acl"])
.output().unwrap();
assert!(prepared.status.success(), "{prepared:?}");
let before = Command::new("pwsh")
.env("NATIVE_ACL_FIXTURE", home.path())
.args([
"-NoProfile",
"-NonInteractive",
"-Command",
"Get-Acl -LiteralPath @($env:NATIVE_ACL_FIXTURE, (Join-Path $env:NATIVE_ACL_FIXTURE 'protected'), (Join-Path $env:NATIVE_ACL_FIXTURE 'protected/input')) | ForEach-Object { $_.Sddl.Replace('D:PAI','D:P').Replace('D:AI','D:') }",
])
.output()
.unwrap();
assert!(before.status.success(), "{before:?}");
let backlog = crate::Backlog::with_window(crate::PollBudget::new(100, 5));
let copies = [tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap()];
for (index, copy) in copies.iter().enumerate() {
let index = u8::try_from(index).unwrap();
let probe = copy.path().join("probe.exe");
std::fs::copy(&exe, &probe).unwrap();
let mut command = Command::new(probe);
command
.env_clear()
.env("CARGO_HOME", home.path())
.current_dir(copy.path())
.args(["grant", &index.to_string()]);
assert!(matches!(
backlog
.run_native(
RunId::from_bytes([index + 0x80; 16]),
&scope,
"concurrent read-execute grant".to_owned(),
command
)
.unwrap(),
crate::Started::Backgrounded { .. }
));
}
for index in 0..2_u8 {
std::fs::write(home.path().join(index.to_string()), "open").unwrap();
let owner = RunId::from_bytes([index + 0x80; 16]);
let mut finished = None;
for _ in 0..2_000 {
if let Some(result) = backlog.harvest(owner).unwrap().into_iter().next() {
finished = Some(result);
break;
}
std::thread::park_timeout(std::time::Duration::from_millis(5));
}
let result = finished.expect("declared home reader settles");
assert_eq!(
(result.exit, result.stdout.trim(), result.stderr),
(crate::Exit::Ended { code: 0 }, "read only", String::new())
);
backlog.release(owner);
}
assert!(!home.path().join("forbidden").exists());
let after = Command::new("pwsh")
.env("NATIVE_ACL_FIXTURE", home.path())
.args([
"-NoProfile",
"-NonInteractive",
"-Command",
"Get-Acl -LiteralPath @($env:NATIVE_ACL_FIXTURE, (Join-Path $env:NATIVE_ACL_FIXTURE 'protected'), (Join-Path $env:NATIVE_ACL_FIXTURE 'protected/input')) | ForEach-Object { $_.Sddl.Replace('D:PAI','D:P').Replace('D:AI','D:') }",
])
.output()
.unwrap();
assert!(after.status.success(), "{after:?}");
assert_eq!(
after.stdout, before.stdout,
"both unique SID grants are revoked"
);
for (index, shares) in [Shares::Unset, Shares::Cpu].into_iter().enumerate() {
let backlog =
crate::Backlog::with_window(crate::PollBudget::new(10_000, 5)).with_shares(shares);
let mut command = Command::new(&exe);
command.env_clear().current_dir(&directory).arg("allocate");
let result = backlog
.run_native(
RunId::from_bytes([u8::try_from(index).unwrap(); 16]),
&scope,
"unrequested memory".to_owned(),
command,
)
.unwrap();
assert_eq!(
result,
crate::Started::Settled {
exit: crate::Exit::Ended { code: 0 },
stdout: "2348810240\n".to_owned(),
stderr: String::new(),
ceiling: None,
}
);
}
let backlog = crate::Backlog::with_window(crate::PollBudget::new(10_000, 5));
let mut command = Command::new(&exe);
command.env_clear().current_dir(&directory).arg("nul");
let result = backlog
.run_native(
RunId::from_bytes([0x90; 16]),
&scope,
"child opens the null device".to_owned(),
command,
)
.unwrap();
assert_eq!(
result,
crate::Started::Settled {
exit: crate::Exit::Ended { code: 0 },
stdout: "DEFAULT_CAPTURE=Ok(Some(0))\nNUL_DEVICE=Ok(())\n".to_owned(),
stderr: String::new(),
ceiling: None,
},
"an AppContainer child opens NUL itself (D59)"
);
}