#[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;
#[cfg(feature = "kubernetes")]
pub mod kubernetes;
#[cfg(feature = "local")]
pub mod local;
#[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"))]
impl DeadlineReport {
pub(crate) fn bounded_program(deadline: std::time::Duration) -> String {
format!(
"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_once('\n')
.filter(|(nonce, _)| !nonce.is_empty() && nonce.chars().all(|c| c.is_ascii_hexdigit()));
let Some((nonce, rest)) = announced 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), "abc123\nboom\nabc123Killed\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), "abc123\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 a_command_that_finished_as_the_killer_fired_keeps_its_result() {
let Bounded::Ran { killed, stderr } = DeadlineReport::read(Some(0), "abc123\nboom\nabc123")
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("wait $sleeper; trap '' TERM; kill -KILL"),
"past its sleep the killer ignores the stop, so its report is never cut off: {program}"
);
assert!(
program.contains("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}"
);
}
#[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"
);
}
}