Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,41 @@ Use the narrowest owning directory. Fixture explanations and component
READMEs should stay with their fixtures/components; general guides should be
linked from the root README and live under `docs/`.

## Test a guest without TEE hardware

Development images include the extensible `dstack-tee-simulator`. Its default
platform backend is TDX. On a VM launched with `no_tee`, that backend exposes
the Linux interfaces used by the normal guest software:

- configfs-tsm quote generation under `/sys/kernel/config/tsm/report`;
- RTMR extension files used by `tdx-attest`;
- a CCEL boot-event fixture.

This keeps `dstack-prepare`, measurement, encrypted-storage setup, the guest
agent, and attestation APIs on their production code paths. The generated quote
has an intentionally invalid signature, so a production verifier or KMS must
reject it.

The service uses the default TDX backend. For direct simulator development, the
same selection can be made explicitly with `dstack-tee-simulator --platform
tdx`. New TEE platforms are implemented as separate backends behind the shared
simulator lifecycle.

Build or install a `dstack-dev-*` image, then deploy without KMS or gateway:

```bash
dstack deploy ./docker-compose.yml \
--name no-tee-dev \
--image dstack-dev-VERSION \
--no-kms \
--no-tee
```

The simulator package is installed only in development images. This mode
provides no hardware isolation and must never be used with production
workloads or secrets. Real quote generation, hardware isolation, and KMS
authorization still require TDX or another supported TEE.

## Commit Convention

This project uses [Conventional Commits](https://www.conventionalcommits.org/). Please format your commit messages as:
Expand Down
41 changes: 41 additions & 0 deletions dstack/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions dstack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ members = [
"guest-agent",
"guest-agent/rpc",
"guest-agent-simulator",
"tee-simulator",
"vmm",
"vmm/rpc",
"gateway",
Expand Down Expand Up @@ -118,6 +119,7 @@ chrono = "0.4.40"
clap = { version = "4.5.32", features = ["derive", "string"] }
dashmap = "6.1.0"
fs-err = "3.1.0"
fuser = "0.16.0"
path-absolutize = "3.1.1"
futures = "0.3.31"
git-version = "0.3.9"
Expand Down
28 changes: 28 additions & 0 deletions dstack/tee-simulator/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
#
# SPDX-License-Identifier: Apache-2.0

[package]
name = "dstack-tee-simulator"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true

[[bin]]
name = "dstack-tee-simulator"
path = "src/main.rs"

[dependencies]
anyhow.workspace = true
cc-eventlog.workspace = true
clap.workspace = true
fuser.workspace = true
libc.workspace = true
sd-notify.workspace = true
sha2.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true

[dev-dependencies]
dcap-qvl.workspace = true
105 changes: 105 additions & 0 deletions dstack/tee-simulator/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
//
// SPDX-License-Identifier: Apache-2.0

use std::path::{Path, PathBuf};

use anyhow::{bail, Context, Result};
use clap::{Parser, ValueEnum};
use fuser::{Filesystem, MountOption, Session};
use tracing::info;

mod tdx;

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
enum TeePlatform {
#[default]
Tdx,
}

#[derive(Parser)]
#[command(about = "Development-only simulator for Linux TEE guest ABIs")]
struct Args {
/// TEE platform ABI to simulate.
#[arg(long, value_enum, default_value = "tdx")]
platform: TeePlatform,

/// Override the platform backend's default mountpoint.
#[arg(long)]
mountpoint: Option<PathBuf>,
}

/// Platform-specific simulator backend.
///
/// Adding another TEE platform only requires a backend implementation and a
/// CLI enum variant; the FUSE lifecycle and safety checks remain shared.
trait TeeBackend {
type Fs: Filesystem;

const PLATFORM: &'static str;
const DEFAULT_MOUNTPOINT: &'static str;

fn create_filesystem() -> Result<Self::Fs>;
fn prepare_mountpoint(mountpoint: &Path) -> Result<()>;
fn real_tee_available() -> bool;
}

fn run_backend<B: TeeBackend>(mountpoint_override: Option<PathBuf>) -> Result<()> {
let default_mountpoint = Path::new(B::DEFAULT_MOUNTPOINT);
let mountpoint = mountpoint_override.as_deref().unwrap_or(default_mountpoint);

if mountpoint == default_mountpoint && B::real_tee_available() {
bail!(
"refusing to start the {} simulator when a real TEE device is present",
B::PLATFORM
);
}
B::prepare_mountpoint(mountpoint)?;

let fs = B::create_filesystem()?;
let mut session = Session::new(
fs,
mountpoint,
&[
MountOption::FSName(format!("dstack-tee-simulator-{}", B::PLATFORM)),
MountOption::DefaultPermissions,
MountOption::NoSuid,
MountOption::NoDev,
MountOption::NoExec,
],
)
.with_context(|| format!("failed to mount FUSE at {}", mountpoint.display()))?;

info!(
platform = B::PLATFORM,
mountpoint = %mountpoint.display(),
"development TEE simulator ready"
);
sd_notify::notify(true, &[sd_notify::NotifyState::Ready])
.context("failed to notify systemd")?;
session.run().context("fuse session failed")?;
Ok(())
}

fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
let args = Args::parse();

match args.platform {
TeePlatform::Tdx => run_backend::<tdx::TdxBackend>(args.mountpoint),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn defaults_to_tdx() {
let args = Args::try_parse_from(["dstack-tee-simulator"]).unwrap();
assert_eq!(args.platform, TeePlatform::Tdx);
assert!(args.mountpoint.is_none());
}
}
Loading
Loading