use crate::contract::ids::{
AdmissionProgramHash, AttemptId, BackendProfileHash, BoundaryPlanHash, Digest32,
};
use crate::contract::lowering::LoweringSchedule;
use crate::contract::report::Outcome;
use serde::{Deserialize, Serialize};
pub const MAGIC: [u8; 8] = *b"BVZLNCH1";
pub const PROTO_VERSION: u16 = 1;
pub const HEADER_LEN: usize = 8 + 2 + 4 + 32;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum EnvelopeReject {
Truncated,
BadMagic,
UnsupportedVersion {
found: u16,
},
LengthMismatch,
DigestMismatch,
}
impl std::fmt::Display for EnvelopeReject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Truncated => write!(f, "frame shorter than the declared envelope/body"),
Self::BadMagic => write!(f, "frame magic does not match the launcher protocol tag"),
Self::UnsupportedVersion { found } => {
write!(f, "unsupported launcher protocol version {found}")
}
Self::LengthMismatch => {
write!(f, "frame length does not match the declared body length")
}
Self::DigestMismatch => write!(f, "frame body digest does not match the header digest"),
}
}
}
impl std::error::Error for EnvelopeReject {}
#[must_use]
pub fn frame(body: &[u8]) -> Vec<u8> {
let body_len = u32::try_from(body.len()).unwrap_or(u32::MAX);
let digest = batpak::event::hash::compute_hash(body);
let mut out = Vec::with_capacity(HEADER_LEN + body.len());
out.extend_from_slice(&MAGIC);
out.extend_from_slice(&PROTO_VERSION.to_le_bytes());
out.extend_from_slice(&body_len.to_le_bytes());
out.extend_from_slice(&digest);
out.extend_from_slice(body);
out
}
pub fn parse_and_verify(bytes: &[u8]) -> Result<&[u8], EnvelopeReject> {
if bytes.len() < HEADER_LEN {
return Err(EnvelopeReject::Truncated);
}
if bytes[0..8] != MAGIC {
return Err(EnvelopeReject::BadMagic);
}
let mut ver = [0u8; 2];
ver.copy_from_slice(&bytes[8..10]);
let found = u16::from_le_bytes(ver);
if found != PROTO_VERSION {
return Err(EnvelopeReject::UnsupportedVersion { found });
}
let mut len = [0u8; 4];
len.copy_from_slice(&bytes[10..14]);
let body_len = u32::from_le_bytes(len) as usize;
let mut header_digest = [0u8; 32];
header_digest.copy_from_slice(&bytes[14..HEADER_LEN]);
let Some(total) = HEADER_LEN.checked_add(body_len) else {
return Err(EnvelopeReject::LengthMismatch);
};
if bytes.len() != total {
return Err(EnvelopeReject::LengthMismatch);
}
let body = &bytes[HEADER_LEN..total];
if batpak::event::hash::compute_hash(body) != header_digest {
return Err(EnvelopeReject::DigestMismatch);
}
Ok(body)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DescriptorRole {
ReadRoot,
WriteRoot,
TargetExe,
CgroupDir,
Stdin,
Stdout,
Stderr,
ControlChannel,
}
impl DescriptorRole {
#[must_use]
pub fn is_singleton(self) -> bool {
!matches!(self, Self::ReadRoot | Self::WriteRoot)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DescriptorKind {
Directory,
Regular,
Socket,
Pipe,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DescriptorShape {
pub kind: DescriptorKind,
pub writable: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DescriptorSlotV1 {
pub slot_index: u32,
pub role: DescriptorRole,
pub expected: DescriptorShape,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum TableReject {
DuplicateSlotIndex {
slot_index: u32,
},
DuplicateSingletonRole {
role: DescriptorRole,
},
}
impl std::fmt::Display for TableReject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DuplicateSlotIndex { slot_index } => {
write!(f, "descriptor table has two slots at index {slot_index}")
}
Self::DuplicateSingletonRole { role } => {
write!(f, "descriptor table has two {role:?} descriptors")
}
}
}
}
impl std::error::Error for TableReject {}
pub fn validate_table(table: &[DescriptorSlotV1]) -> Result<(), TableReject> {
use std::collections::BTreeSet;
let mut seen_index: BTreeSet<u32> = BTreeSet::new();
for slot in table {
if !seen_index.insert(slot.slot_index) {
return Err(TableReject::DuplicateSlotIndex {
slot_index: slot.slot_index,
});
}
}
let mut seen_singleton: BTreeSet<DescriptorRole> = BTreeSet::new();
for slot in table {
if slot.role.is_singleton() && !seen_singleton.insert(slot.role) {
return Err(TableReject::DuplicateSingletonRole { role: slot.role });
}
}
Ok(())
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UserNsRequest {}
impl UserNsRequest {
#[must_use]
pub fn new() -> Self {
Self {}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NetworkNsRequest {}
impl NetworkNsRequest {
#[must_use]
pub fn new() -> Self {
Self {}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SeccompRequest {
pub deny_new_tasks: bool,
pub deny_inet_sockets: bool,
}
impl SeccompRequest {
#[must_use]
pub fn deny_new_tasks() -> Self {
Self {
deny_new_tasks: true,
deny_inet_sockets: false,
}
}
#[must_use]
pub fn denies_anything(self) -> bool {
self.deny_new_tasks || self.deny_inet_sockets
}
#[must_use]
pub fn with_inet_socket_deny(mut self) -> Self {
self.deny_inet_sockets = true;
self
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TargetSpecV1 {
pub argv: Vec<String>,
pub envp: Vec<(String, String)>,
pub exe_slot: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_namespace: Option<UserNsRequest>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network_namespace: Option<NetworkNsRequest>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seccomp: Option<SeccompRequest>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LoweringWireEntryV1 {
pub id: String,
pub version: u32,
pub phase_code: u8,
pub param_digest: Digest32,
pub decl_digest: Digest32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LoweringWireV1 {
pub entries: Vec<LoweringWireEntryV1>,
}
impl LoweringWireV1 {
#[must_use]
pub fn from_schedule(schedule: &LoweringSchedule) -> Self {
let entries = schedule
.entries()
.iter()
.map(|e| LoweringWireEntryV1 {
id: e.id().as_str().to_owned(),
version: e.version().get(),
phase_code: e.phase().code(),
param_digest: *e.param_digest(),
decl_digest: *e.decl_digest(),
})
.collect();
Self { entries }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LinuxLaunchBodyV1 {
pub attempt_id: AttemptId,
pub plan_id: BoundaryPlanHash,
pub h_a: AdmissionProgramHash,
pub h_p: BackendProfileHash,
pub h_l: Digest32,
pub lowering: LoweringWireV1,
pub descriptor_table: Vec<DescriptorSlotV1>,
pub target: TargetSpecV1,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum EncodeError {
Canonical(String),
}
impl std::fmt::Display for EncodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Canonical(e) => write!(f, "could not canonically encode the launch body: {e}"),
}
}
}
impl std::error::Error for EncodeError {}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum DecodeError {
Envelope(EnvelopeReject),
Canonical(String),
}
impl std::fmt::Display for DecodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Envelope(r) => write!(f, "launch frame envelope rejected: {r}"),
Self::Canonical(e) => write!(f, "launch body did not canonically decode: {e}"),
}
}
}
impl std::error::Error for DecodeError {}
impl From<EnvelopeReject> for DecodeError {
fn from(r: EnvelopeReject) -> Self {
Self::Envelope(r)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LinuxLaunchPlanV1 {
pub body: LinuxLaunchBodyV1,
}
impl LinuxLaunchPlanV1 {
pub fn encode(&self) -> Result<Vec<u8>, EncodeError> {
let body_bytes = batpak::canonical::to_bytes(&self.body)
.map_err(|e| EncodeError::Canonical(e.to_string()))?;
Ok(frame(&body_bytes))
}
pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
let body_bytes = parse_and_verify(bytes)?;
let body: LinuxLaunchBodyV1 = batpak::canonical::from_bytes(body_bytes)
.map_err(|e| DecodeError::Canonical(e.to_string()))?;
Ok(Self { body })
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum PhaseResult {
Applied,
NotRequired,
Refused,
Faulted,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SetupPhase {
Identity,
Visibility,
AmbientAuthority,
Confinement,
}
impl SetupPhase {
#[must_use]
pub fn resolved_state(self) -> LauncherState {
match self {
Self::Identity => LauncherState::IdentityPhaseResolved,
Self::Visibility => LauncherState::VisibilityPhaseResolved,
Self::AmbientAuthority => LauncherState::AmbientAuthorityPhaseResolved,
Self::Confinement => LauncherState::ConfinementPhaseResolved,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum RefusalReason {
MissingPrimitive,
IdentityMismatch,
PlanInvalid,
HandleMismatch,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum LauncherState {
LauncherStarted,
IdentityVerified,
PlanVerified,
HandlesVerified,
ChildCreated,
IdentityPhaseResolved,
VisibilityPhaseResolved,
AmbientAuthorityPhaseResolved,
ConfinementPhaseResolved,
ReadyToExec,
ExecSucceeded,
SetupRefused,
SetupFaulted,
}
impl LauncherState {
#[must_use]
pub fn non_terminal_progression() -> &'static [Self] {
&Self::PROGRESSION
}
const PROGRESSION: [Self; 10] = [
Self::LauncherStarted,
Self::IdentityVerified,
Self::PlanVerified,
Self::HandlesVerified,
Self::ChildCreated,
Self::IdentityPhaseResolved,
Self::VisibilityPhaseResolved,
Self::AmbientAuthorityPhaseResolved,
Self::ConfinementPhaseResolved,
Self::ReadyToExec,
];
#[must_use]
pub fn is_terminal(self) -> bool {
matches!(
self,
Self::ExecSucceeded | Self::SetupRefused | Self::SetupFaulted
)
}
fn rank(self) -> Option<usize> {
Self::PROGRESSION.iter().position(|s| *s == self)
}
}
#[must_use]
pub fn is_valid_transition(from: LauncherState, to: LauncherState) -> bool {
if from.is_terminal() {
return false;
}
if matches!(
to,
LauncherState::SetupRefused | LauncherState::SetupFaulted
) {
return true;
}
if to == LauncherState::ExecSucceeded {
return from == LauncherState::ReadyToExec;
}
match (from.rank(), to.rank()) {
(Some(f), Some(t)) => t == f + 1,
_ => false,
}
}
#[must_use]
pub fn can_exec(state: LauncherState) -> bool {
state == LauncherState::ReadyToExec
}
#[must_use]
pub fn outcome_class(terminal: LauncherState) -> Option<Outcome> {
match terminal {
LauncherState::ExecSucceeded => Some(Outcome::Completed),
LauncherState::SetupRefused => Some(Outcome::Unsupported),
LauncherState::SetupFaulted => Some(Outcome::SupervisorFault),
LauncherState::LauncherStarted
| LauncherState::IdentityVerified
| LauncherState::PlanVerified
| LauncherState::HandlesVerified
| LauncherState::ChildCreated
| LauncherState::IdentityPhaseResolved
| LauncherState::VisibilityPhaseResolved
| LauncherState::AmbientAuthorityPhaseResolved
| LauncherState::ConfinementPhaseResolved
| LauncherState::ReadyToExec => None,
}
}
#[must_use]
pub fn phase_resolution_consistent(
scheduled: &[LoweringWireEntryV1],
observed: &[LoweringWireEntryV1],
result: PhaseResult,
) -> bool {
match result {
PhaseResult::NotRequired => scheduled.is_empty() && observed.is_empty(),
PhaseResult::Applied => !scheduled.is_empty() && observed == scheduled,
PhaseResult::Refused | PhaseResult::Faulted => true,
}
}
#[must_use]
pub fn confinement_installed(
scheduled_confinement_action_count: usize,
confinement_result: PhaseResult,
) -> bool {
scheduled_confinement_action_count > 0 && confinement_result == PhaseResult::Applied
}
#[must_use]
pub fn ready_to_exec(
child_created: bool,
phases: [(SetupPhase, PhaseResult); 4],
observed_schedule_digest: Digest32,
h_l: Digest32,
) -> bool {
if !child_created {
return false;
}
if observed_schedule_digest != h_l {
return false;
}
let mut ambient_applied = false;
for (phase, result) in phases {
match result {
PhaseResult::Applied | PhaseResult::NotRequired => {}
PhaseResult::Refused | PhaseResult::Faulted => return false,
}
if matches!(phase, SetupPhase::AmbientAuthority) && result == PhaseResult::Applied {
ambient_applied = true;
}
}
ambient_applied
}