#[cfg(any(feature = "aws", feature = "kubernetes"))]
pub mod agent_protocol;
#[cfg(feature = "aws")]
pub mod aws;
#[cfg(feature = "azure")]
pub mod azure;
#[cfg(feature = "gcp")]
pub mod gcp_agent_platform;
#[cfg(feature = "kubernetes")]
pub mod kubernetes;
#[cfg(feature = "local")]
pub mod local;
#[cfg(feature = "aws")]
mod refusal;
#[cfg(any(feature = "azure", feature = "local"))]
pub(crate) const MAX_DEADLINE: std::time::Duration = std::time::Duration::from_secs(24 * 60 * 60);
#[cfg(any(feature = "azure", feature = "local"))]
pub(crate) const DEADLINE_GRACE: std::time::Duration = std::time::Duration::from_secs(10);
#[cfg(any(feature = "azure", feature = "local"))]
pub(crate) struct DeadlineReport;
#[cfg(any(feature = "azure", feature = "local"))]
const NONCE_HEXITS: usize = 32;
#[cfg(any(feature = "azure", feature = "local"))]
impl DeadlineReport {
pub(crate) fn bounded_program(deadline: std::time::Duration) -> String {
format!(
"unset nonce command_pid killer_pid sleeper status; \
command -v setsid >/dev/null 2>&1 || exit {unboundable}; \
nonce=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \\n') || exit {unboundable}; \
printf '%s\\n' \"$nonce\" >&2; \
setsid \"$@\" & command_pid=$!; \
( sleep {deadline} & sleeper=$!; trap 'kill \"$sleeper\" 2>/dev/null; exit' TERM; \
wait \"$sleeper\"; \
trap '' TERM; kill -KILL -\"$command_pid\" 2>/dev/null && printf %s \"$nonce\" >&2 ) & killer_pid=$!; \
wait \"$command_pid\"; status=$?; \
kill \"$killer_pid\" 2>/dev/null; wait \"$killer_pid\"; \
exit \"$status\"",
unboundable = Self::UNBOUNDABLE_EXIT_CODE,
deadline = deadline_seconds(deadline)
)
}
const UNBOUNDABLE_EXIT_CODE: i32 = 126;
const KILLED_EXIT_CODE: i32 = 137;
pub(crate) fn read(exit_code: Option<i32>, stderr: &str) -> Bounded {
let announced = stderr.split('\n').enumerate().find_map(|(index, line)| {
let line = line.strip_suffix('\r').unwrap_or(line);
let is_nonce =
line.len() == NONCE_HEXITS && line.chars().all(|c| c.is_ascii_hexdigit());
is_nonce.then(|| {
let after = stderr
.split('\n')
.skip(index + 1)
.collect::<Vec<_>>()
.join("\n");
(line, after)
})
});
let Some((nonce, rest)) = announced.as_ref().map(|(n, r)| (*n, r.as_str())) else {
return Bounded::NotRun {
reason: if exit_code == Some(Self::UNBOUNDABLE_EXIT_CODE) {
"the session image cannot hold a command to a deadline: `setsid` and \
`/dev/urandom` are required"
.to_string()
} else {
format!(
"the session could not start a bounded command: {}",
stderr.trim()
)
},
};
};
match rest.find(nonce) {
Some(at) => Bounded::Ran {
killed: exit_code == Some(Self::KILLED_EXIT_CODE),
stderr: format!("{}{}", &rest[..at], &rest[at + nonce.len()..]),
},
None => Bounded::Ran {
killed: false,
stderr: rest.to_string(),
},
}
}
}
#[cfg(any(feature = "azure", feature = "local"))]
pub(crate) enum Bounded {
NotRun { reason: String },
Ran { killed: bool, stderr: String },
}
#[cfg(any(feature = "azure", feature = "local"))]
fn deadline_seconds(deadline: std::time::Duration) -> String {
let millis = deadline.as_millis().max(1);
if millis % 1000 == 0 {
(millis / 1000).to_string()
} else {
format!("{}.{:03}", millis / 1000, millis % 1000)
}
}
#[cfg(any(feature = "azure", feature = "local"))]
pub(crate) fn guard_for(
deadline: std::time::Duration,
) -> crate::error::Result<std::time::Duration> {
let refuse = |reason: &str| {
alien_error::AlienError::new(crate::error::ErrorData::SandboxCommandFailed {
failure: "invalidRequest".to_string(),
reason: reason.to_string(),
})
};
if deadline < std::time::Duration::from_millis(1) {
return Err(refuse(
"a command deadline must be at least one millisecond",
));
}
if deadline > MAX_DEADLINE {
return Err(refuse(&format!(
"a command deadline must be at most {} hours",
MAX_DEADLINE.as_secs() / 3600
)));
}
Ok(deadline + DEADLINE_GRACE)
}
#[cfg(all(test, any(feature = "azure", feature = "local")))]
mod tests {
use super::*;
#[test]
fn the_deadline_reaches_the_shell_to_the_millisecond() {
assert_eq!(deadline_seconds(std::time::Duration::from_secs(30)), "30");
assert_eq!(
deadline_seconds(std::time::Duration::from_millis(1500)),
"1.500"
);
assert_eq!(
deadline_seconds(std::time::Duration::from_millis(500)),
"0.500"
);
}
#[test]
fn only_the_session_can_report_a_deadline() {
let killed = match DeadlineReport::read(
Some(137),
"a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\nboom\na1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4Killed\n",
) {
Bounded::Ran { killed, stderr } => {
assert_eq!(stderr, "boom\nKilled\n");
killed
}
Bounded::NotRun { reason } => panic!("the command ran: {reason}"),
};
assert!(killed);
assert!(matches!(
DeadlineReport::read(
Some(0),
"a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\nboom\ndeadbeef\n"
),
Bounded::Ran { killed: false, .. }
));
}
#[test]
fn a_session_that_cannot_bound_a_command_runs_nothing() {
let Bounded::NotRun { reason } = DeadlineReport::read(Some(126), "") else {
panic!("an unboundable session must not look like a command that ran");
};
assert!(reason.contains("setsid"), "{reason}");
let Bounded::NotRun { reason } = DeadlineReport::read(Some(127), "sh: not found\n") else {
panic!("stderr with no announcement is not a command that ran");
};
assert!(reason.contains("could not start"), "{reason}");
}
#[test]
fn the_announcement_is_found_by_shape_rather_than_by_position() {
let traced = format!("+ unset nonce command_pid\n+ printf\na1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\nboom\na1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4Killed\n");
let Bounded::Ran { killed, stderr } = DeadlineReport::read(Some(137), &traced) else {
panic!("the trace must not hide the announcement");
};
assert!(killed, "the killer's repeat still reports the kill");
assert_eq!(
stderr, "boom\nKilled\n",
"what precedes the announcement was written before the command started, so it is the \
session's own noise rather than the command's — and one of those lines is the trace \
of the announcement itself"
);
let crlf = format!("a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\r\nboom\r\na1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4Killed\r\n");
assert!(
matches!(
DeadlineReport::read(Some(137), &crlf),
Bounded::Ran { killed: true, .. }
),
"a carriage return is not part of the nonce"
);
assert!(
matches!(
DeadlineReport::read(Some(137), "ab\nboom\nabc\n"),
Bounded::NotRun { .. }
),
"a hex fragment is not a nonce"
);
}
#[test]
fn a_command_that_finished_as_the_killer_fired_keeps_its_result() {
let Bounded::Ran { killed, stderr } = DeadlineReport::read(
Some(0),
"a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\nboom\na1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4",
) else {
panic!("the command ran");
};
assert!(
!killed,
"a command that exited 0 was not ended by the deadline, whatever the signal reached"
);
assert_eq!(stderr, "boom\n", "the announcement is removed either way");
}
#[test]
fn the_bounded_program_kills_and_reports_only_what_it_killed() {
let program = DeadlineReport::bounded_program(std::time::Duration::from_millis(1500));
assert!(program.contains("/dev/urandom"), "{program}");
assert!(program.contains("command -v setsid"), "{program}");
assert!(
program.contains("setsid \"$@\" & command_pid=$!"),
"{program}"
);
assert!(program.contains("sleep 1.500"), "{program}");
assert!(
program.contains("kill -KILL -\"$command_pid\""),
"the kill reaches the command's process group, not one pid: {program}"
);
assert!(
program.contains("2>/dev/null && printf %s \"$nonce\""),
"the repeat follows a signal that was delivered: {program}"
);
assert!(
program.contains("kill \"$killer_pid\" 2>/dev/null; wait \"$killer_pid\""),
"the killer is stopped and then awaited, whatever the command's exit: {program}"
);
assert!(
program.contains(r#"wait "$sleeper"; trap '' TERM; kill -KILL"#),
"past its sleep the killer ignores the stop, so its report is never cut off, and the \
pid is quoted so an inherited IFS cannot split it into words that are not children: \
{program}"
);
assert!(
program.contains(r#"trap 'kill "$sleeper" 2>/dev/null; exit' TERM"#),
"a stopped killer reaps its own sleeper, so none outlives the command: {program}"
);
assert!(
!program.contains("\"$nonce\" &") && program.contains("( sleep"),
"the killer is a subshell: the nonce reaches it as a variable, never as an argument \
the command could read from /proc: {program}"
);
}
#[test]
#[cfg(unix)]
fn the_wrapper_never_hands_its_nonce_to_the_command() {
use std::os::unix::fs::PermissionsExt;
let bin = std::env::temp_dir().join(format!("alien-sandbox-{}", std::process::id()));
std::fs::create_dir_all(&bin).expect("a directory for the stand-in");
let setsid = bin.join("setsid");
std::fs::write(&setsid, "#!/bin/sh\nexec \"$@\"\n").expect("the stand-in is written");
std::fs::set_permissions(&setsid, std::fs::Permissions::from_mode(0o755))
.expect("the stand-in is executable");
let path = format!(
"{}:{}",
bin.display(),
std::env::var("PATH").unwrap_or_default()
);
let run = std::process::Command::new("/bin/sh")
.arg("-c")
.arg(DeadlineReport::bounded_program(
std::time::Duration::from_secs(5),
))
.arg("sh")
.arg("printenv")
.arg("nonce")
.env("PATH", path)
.env("nonce", "inherited-from-the-session")
.output()
.expect("a shell runs");
std::fs::remove_dir_all(&bin).ok();
let announced = String::from_utf8_lossy(&run.stderr);
let announced = announced.lines().next().unwrap_or_default().to_string();
assert!(
announced.len() == 32 && announced.chars().all(|c| c.is_ascii_hexdigit()),
"the session has to reach the point of drawing a nonce, or this proves nothing: \
stderr {:?}",
String::from_utf8_lossy(&run.stderr)
);
let seen = String::from_utf8_lossy(&run.stdout);
assert!(
seen.trim().is_empty(),
"the command must inherit no `nonce` at all, and it saw {seen:?}"
);
}
#[tokio::test]
async fn a_deadline_outside_what_the_backends_can_honour_is_refused() {
let too_fine = guard_for(std::time::Duration::from_micros(500))
.expect_err("half a millisecond cannot be bounded");
assert!(
too_fine.to_string().contains("invalidRequest"),
"{too_fine}"
);
let too_long = guard_for(std::time::Duration::MAX)
.expect_err("a deadline with no representable instant cannot be waited out");
assert!(
too_long.to_string().contains("invalidRequest"),
"{too_long}"
);
assert_eq!(
guard_for(std::time::Duration::from_secs(30)).expect("an ordinary deadline"),
std::time::Duration::from_secs(30) + DEADLINE_GRACE,
"the guard is the deadline plus the grace"
);
}
}