use std::fs::{self, File, OpenOptions};
use std::io::{self, Read as _, Write as _};
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256};
use crate::DaemonRecord;
const VERSION: u32 = 1;
#[cfg(test)]
mod tests;
const MAX_BYTES: u64 = 4096;
#[derive(Serialize, Deserialize)]
struct Evidence {
version: u32,
instance_id: String,
artifact_id: String,
state_location_id: String,
}
#[derive(Debug)]
pub struct ExecutionLifetime {
file: File,
execution_admission: File,
owns_admission_lock: bool,
owner_pid: u32,
}
impl Drop for ExecutionLifetime {
fn drop(&mut self) {
if self.owner_pid == std::process::id() {
let _ = self.file.unlock();
if self.owns_admission_lock {
let _ = self.execution_admission.unlock();
}
}
}
}
#[derive(Debug)]
pub struct ExecutionMaintenance {
file: File,
root: PathBuf,
owner_pid: u32,
}
impl Drop for ExecutionMaintenance {
fn drop(&mut self) {
if self.owner_pid == std::process::id() {
let _ = self.file.unlock();
}
}
}
fn admission_file(root: &Path) -> io::Result<File> {
let root = directory(root, true)?;
let path = root
.join("daemon-execution-lifetimes")
.join("admission.lock");
let mut options = OpenOptions::new();
options.read(true).write(true).create(true).truncate(false);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.custom_flags(libc::O_NOFOLLOW);
}
let file = options.open(path)?;
if !file.metadata()?.is_file() {
return Err(invalid());
}
Ok(file)
}
impl ExecutionMaintenance {
pub fn publish_coordinator(&self, root: &Path, record: &DaemonRecord) -> io::Result<()> {
if root.canonicalize()? != self.root {
return Err(invalid());
}
let _lifetime = ExecutionLifetime::publish(root, record, self.file.try_clone()?, false)?;
Ok(())
}
pub fn remove_daemon_images(&self) -> io::Result<()> {
let _image_guard = super::try_acquire_daemon_image_cleanup_lock(&self.root)
.map_err(|_| invalid())?
.ok_or_else(|| io::Error::new(io::ErrorKind::WouldBlock, "image publication active"))?;
let images = self.root.join("daemon-images");
match fs::symlink_metadata(&images) {
Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {
fs::remove_dir_all(images)
}
Ok(_) => Err(invalid()),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error),
}
}
pub fn acquire(root: &Path) -> io::Result<Self> {
let file = admission_file(root)?;
file.try_lock().map_err(io::Error::from)?;
Ok(Self {
file,
root: root.canonicalize()?,
owner_pid: std::process::id(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutionLifetimeStatus {
Live,
Released,
Missing,
Unverifiable,
}
fn evidence_path(root: &Path, instance_id: &str) -> PathBuf {
let digest = Sha256::digest(instance_id.as_bytes());
root.join("daemon-execution-lifetimes")
.join(format!("{}.json", hex::encode(digest)))
}
fn invalid() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
"daemon execution lifetime evidence is unverifiable",
)
}
fn directory(root: &Path, create: bool) -> io::Result<PathBuf> {
let root = root.canonicalize()?;
let path = root.join("daemon-execution-lifetimes");
if create {
match fs::create_dir(&path) {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(error),
}
}
if !fs::symlink_metadata(&path)?.is_dir()
|| path.canonicalize()?.parent() != Some(root.as_path())
{
return Err(invalid());
}
#[cfg(unix)]
{
if create {
File::open(&root)?.sync_all()?;
}
}
Ok(root)
}
impl ExecutionLifetime {
pub fn begin(root: &Path, record: &DaemonRecord) -> io::Result<Self> {
let execution_admission = admission_file(root)?;
execution_admission
.try_lock_shared()
.map_err(io::Error::from)?;
Self::publish(root, record, execution_admission, true)
}
fn publish(
root: &Path,
record: &DaemonRecord,
execution_admission: File,
owns_admission_lock: bool,
) -> io::Result<Self> {
let root = directory(root, true)?;
let evidence = Evidence {
version: VERSION,
instance_id: record.instance_id.clone(),
artifact_id: record.artifact_id.as_ref().ok_or_else(invalid)?.to_string(),
state_location_id: record.state_location_id.clone().ok_or_else(invalid)?,
};
if evidence.instance_id.is_empty() || evidence.state_location_id.is_empty() {
return Err(invalid());
}
let path = evidence_path(&root, &record.instance_id);
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(path)?;
try_lock_evidence(&file).map_err(|_| invalid())?;
let bytes = serde_json::to_vec(&evidence).map_err(|_| invalid())?;
if bytes.len() as u64 > MAX_BYTES {
return Err(invalid());
}
file.write_all(&bytes)?;
file.sync_all()?;
#[cfg(unix)]
File::open(root.join("daemon-execution-lifetimes"))?.sync_all()?;
Ok(Self {
file,
execution_admission,
owns_admission_lock,
owner_pid: std::process::id(),
})
}
}
#[must_use]
pub fn execution_lifetime_status(
root: &Path,
instance_id: &str,
artifact_id: &str,
state_location_id: &str,
) -> ExecutionLifetimeStatus {
match observe(root, instance_id, artifact_id, state_location_id) {
Ok(status) => status,
Err(error) if error.kind() == io::ErrorKind::NotFound => ExecutionLifetimeStatus::Missing,
Err(_) => ExecutionLifetimeStatus::Unverifiable,
}
}
#[cfg(not(windows))]
fn try_lock_evidence(file: &File) -> Result<(), fs::TryLockError> {
file.try_lock()
}
#[cfg(windows)]
fn try_lock_evidence(file: &File) -> Result<(), fs::TryLockError> {
use std::os::windows::io::AsRawHandle as _;
use windows_sys::Win32::Storage::FileSystem::{
LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY, LockFileEx,
};
let mut overlapped = windows_sys::Win32::System::IO::OVERLAPPED {
Anonymous: windows_sys::Win32::System::IO::OVERLAPPED_0 {
Anonymous: windows_sys::Win32::System::IO::OVERLAPPED_0_0 {
Offset: u32::try_from(MAX_BYTES).expect("bounded evidence"),
OffsetHigh: 0,
},
},
..Default::default()
};
let locked = unsafe {
LockFileEx(
file.as_raw_handle(),
LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
0,
1,
0,
&raw mut overlapped,
)
};
if locked != 0 {
return Ok(());
}
let error = io::Error::last_os_error();
if error.raw_os_error()
== Some(
i32::try_from(windows_sys::Win32::Foundation::ERROR_LOCK_VIOLATION)
.expect("Win32 error fits i32"),
)
{
Err(fs::TryLockError::WouldBlock)
} else {
Err(fs::TryLockError::Error(error))
}
}
fn observe(
root: &Path,
instance: &str,
artifact: &str,
location: &str,
) -> io::Result<ExecutionLifetimeStatus> {
let root = directory(root, false)?;
let path = evidence_path(&root, instance);
let metadata = fs::symlink_metadata(&path)?;
if !metadata.is_file() || metadata.len() > MAX_BYTES {
return Err(invalid());
}
let mut options = OpenOptions::new();
options.read(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.custom_flags(libc::O_NOFOLLOW);
}
let mut file = options.open(path)?;
let mut bytes = Vec::new();
(&mut file).take(MAX_BYTES + 1).read_to_end(&mut bytes)?;
if bytes.len() as u64 > MAX_BYTES {
return Err(invalid());
}
let evidence: Evidence = serde_json::from_slice(&bytes).map_err(|_| invalid())?;
if evidence.version != VERSION
|| evidence.instance_id != instance
|| evidence.artifact_id != artifact
|| evidence.state_location_id != location
{
return Err(invalid());
}
match try_lock_evidence(&file) {
Ok(()) => Ok(ExecutionLifetimeStatus::Released),
Err(fs::TryLockError::WouldBlock) => Ok(ExecutionLifetimeStatus::Live),
Err(fs::TryLockError::Error(error)) => Err(error),
}
}