use crate::persona::TpmVersion;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SwtpmSpec {
pub version: TpmVersion,
pub state_dir: PathBuf,
pub socket: PathBuf,
}
impl SwtpmSpec {
#[must_use]
pub fn derive(run_id: &str, work_root: &Path, version: TpmVersion) -> Self {
let run_dir = work_root.join(run_id);
Self {
version,
state_dir: run_dir.join("tpm-state"),
socket: run_dir.join("swtpm.sock"),
}
}
}
#[derive(Debug)]
pub enum SwtpmInstance {
NoTpm {
spec: SwtpmSpec,
},
Live {
spec: SwtpmSpec,
child: Child,
},
}
impl SwtpmInstance {
pub fn spawn(spec: &SwtpmSpec) -> Result<Self, SwtpmError> {
Self::spawn_with_binary(spec, "swtpm")
}
pub fn spawn_with_binary(spec: &SwtpmSpec, binary: &str) -> Result<Self, SwtpmError> {
if matches!(spec.version, TpmVersion::None) {
return Ok(Self::NoTpm { spec: spec.clone() });
}
fs::create_dir_all(&spec.state_dir).map_err(|e| SwtpmError::WorkDirInaccessible {
path: spec.state_dir.clone(),
kind: e.to_string(),
})?;
let mut cmd = Command::new(binary);
if binary == "swtpm" {
cmd.arg("socket");
if matches!(spec.version, TpmVersion::Tpm20) {
cmd.arg("--tpm2");
}
cmd.args([
"--tpmstate",
&format!("dir={}", spec.state_dir.display()),
"--ctrl",
&format!("type=unixio,path={}", spec.socket.display()),
"--log",
"level=0",
]);
} else {
cmd.arg("30");
}
let child = cmd.spawn().map_err(|e| SwtpmError::SpawnFailed {
binary: binary.to_string(),
kind: e.to_string(),
})?;
Ok(Self::Live {
spec: spec.clone(),
child,
})
}
#[must_use]
pub fn socket_path(&self) -> Option<&Path> {
match self {
Self::NoTpm { .. } => None,
Self::Live { spec, .. } => Some(&spec.socket),
}
}
#[must_use]
pub fn spec(&self) -> &SwtpmSpec {
match self {
Self::NoTpm { spec } | Self::Live { spec, .. } => spec,
}
}
#[must_use]
pub fn is_no_tpm(&self) -> bool {
matches!(self, Self::NoTpm { .. })
}
}
impl Drop for SwtpmInstance {
fn drop(&mut self) {
if let Self::Live { child, .. } = self {
let _ = child.kill();
let _ = child.wait();
}
}
}
#[derive(Debug, Error)]
pub enum SwtpmError {
#[error("swtpm state dir {path} inaccessible: {kind}")]
WorkDirInaccessible {
path: PathBuf,
kind: String,
},
#[error("failed to spawn {binary}: {kind}. Is swtpm installed? Debian: `apt install swtpm`")]
SpawnFailed {
binary: String,
kind: String,
},
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn derive_socket_path_is_under_work_root() {
let root = PathBuf::from("/tmp/hwsim-work");
let spec = SwtpmSpec::derive("run-42", &root, TpmVersion::Tpm20);
assert_eq!(spec.socket, root.join("run-42").join("swtpm.sock"));
assert_eq!(spec.state_dir, root.join("run-42").join("tpm-state"));
}
#[test]
fn derive_preserves_tpm_version() {
let root = PathBuf::from("/tmp/x");
assert_eq!(
SwtpmSpec::derive("r", &root, TpmVersion::None).version,
TpmVersion::None,
);
assert_eq!(
SwtpmSpec::derive("r", &root, TpmVersion::Tpm12).version,
TpmVersion::Tpm12,
);
assert_eq!(
SwtpmSpec::derive("r", &root, TpmVersion::Tpm20).version,
TpmVersion::Tpm20,
);
}
#[test]
fn spawn_with_tpm_none_returns_no_tpm_without_io() {
let tmp = tempfile::tempdir().unwrap();
let spec = SwtpmSpec::derive("run-none", tmp.path(), TpmVersion::None);
let inst = SwtpmInstance::spawn(&spec).unwrap();
assert!(inst.is_no_tpm());
assert!(inst.socket_path().is_none());
assert!(
!spec.state_dir.exists(),
"NoTpm must not create the state dir: {:?}",
spec.state_dir
);
}
#[test]
fn spawn_with_fake_binary_creates_state_dir_and_returns_live() {
let tmp = tempfile::tempdir().unwrap();
let spec = SwtpmSpec::derive("run-live", tmp.path(), TpmVersion::Tpm20);
let inst = SwtpmInstance::spawn_with_binary(&spec, "sleep").unwrap();
assert!(!inst.is_no_tpm());
assert!(spec.state_dir.exists(), "state dir should be created");
assert_eq!(inst.socket_path(), Some(spec.socket.as_path()));
drop(inst); }
#[test]
fn spawn_failure_yields_named_error() {
let tmp = tempfile::tempdir().unwrap();
let spec = SwtpmSpec::derive("run-fail", tmp.path(), TpmVersion::Tpm20);
let err =
SwtpmInstance::spawn_with_binary(&spec, "/definitely/not/a/binary-xyz123").unwrap_err();
assert!(
matches!(err, SwtpmError::SpawnFailed { .. }),
"expected SpawnFailed, got {err:?}"
);
}
#[test]
fn work_dir_inaccessible_when_parent_is_a_file() {
let tmp = tempfile::tempdir().unwrap();
let blocker = tmp.path().join("blocked");
fs::write(&blocker, b"not-a-dir").unwrap();
let spec = SwtpmSpec {
version: TpmVersion::Tpm20,
state_dir: blocker.join("tpm-state"),
socket: blocker.join("swtpm.sock"),
};
let err = SwtpmInstance::spawn_with_binary(&spec, "sleep").unwrap_err();
assert!(
matches!(err, SwtpmError::WorkDirInaccessible { .. }),
"expected WorkDirInaccessible, got {err:?}"
);
}
#[test]
fn drop_guard_terminates_child() {
use std::time::Duration;
let tmp = tempfile::tempdir().unwrap();
let spec = SwtpmSpec::derive("run-drop", tmp.path(), TpmVersion::Tpm20);
let inst = SwtpmInstance::spawn_with_binary(&spec, "sleep").unwrap();
let pid = match &inst {
SwtpmInstance::Live { child, .. } => child.id(),
SwtpmInstance::NoTpm { .. } => panic!("should be live"),
};
drop(inst);
std::thread::sleep(Duration::from_millis(100));
let alive = std::process::Command::new("kill")
.args(["-0", &pid.to_string()])
.status()
.map(|s| s.success())
.unwrap_or(false);
assert!(!alive, "drop-guard should have killed PID {pid}");
}
}