Skip to content

Commit 39b07f8

Browse files
committed
feat(os): add extensible dstack-tee-simulator for no-TEE dev VMs
Add a FUSE-based TEE simulator (default TDX backend) that exposes the configfs-tsm quote, RTMR, and CCEL interfaces used by the existing guest path. Packaged only into development images.
1 parent a7b4c2b commit 39b07f8

10 files changed

Lines changed: 793 additions & 1 deletion

File tree

CONTRIBUTING.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,41 @@ Use the narrowest owning directory. Fixture explanations and component
3232
READMEs should stay with their fixtures/components; general guides should be
3333
linked from the root README and live under `docs/`.
3434

35+
## Test a guest without TEE hardware
36+
37+
Development images include the extensible `dstack-tee-simulator`. Its default
38+
platform backend is TDX. On a VM launched with `no_tee`, that backend exposes
39+
the Linux interfaces used by the normal guest software:
40+
41+
- configfs-tsm quote generation under `/sys/kernel/config/tsm/report`;
42+
- RTMR extension files used by `tdx-attest`;
43+
- a CCEL boot-event fixture.
44+
45+
This keeps `dstack-prepare`, measurement, encrypted-storage setup, the guest
46+
agent, and attestation APIs on their production code paths. The generated quote
47+
has an intentionally invalid signature, so a production verifier or KMS must
48+
reject it.
49+
50+
The service uses the default TDX backend. For direct simulator development, the
51+
same selection can be made explicitly with `dstack-tee-simulator --platform
52+
tdx`. New TEE platforms are implemented as separate backends behind the shared
53+
simulator lifecycle.
54+
55+
Build or install a `dstack-dev-*` image, then deploy without KMS or gateway:
56+
57+
```bash
58+
dstack deploy ./docker-compose.yml \
59+
--name no-tee-dev \
60+
--image dstack-dev-VERSION \
61+
--no-kms \
62+
--no-tee
63+
```
64+
65+
The simulator package is installed only in development images. This mode
66+
provides no hardware isolation and must never be used with production
67+
workloads or secrets. Real quote generation, hardware isolation, and KMS
68+
authorization still require TDX or another supported TEE.
69+
3570
## Commit Convention
3671

3772
This project uses [Conventional Commits](https://www.conventionalcommits.org/). Please format your commit messages as:

dstack/Cargo.lock

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dstack/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ members = [
3333
"guest-agent",
3434
"guest-agent/rpc",
3535
"guest-agent-simulator",
36+
"tee-simulator",
3637
"vmm",
3738
"vmm/rpc",
3839
"gateway",
@@ -122,6 +123,7 @@ chrono = "0.4.40"
122123
clap = { version = "4.5.32", features = ["derive", "string"] }
123124
dashmap = "6.1.0"
124125
fs-err = "3.1.0"
126+
fuser = "0.16.0"
125127
path-absolutize = "3.1.1"
126128
futures = "0.3.31"
127129
git-version = "0.3.9"

dstack/tee-simulator/Cargo.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
[package]
6+
name = "dstack-tee-simulator"
7+
version.workspace = true
8+
authors.workspace = true
9+
edition.workspace = true
10+
license.workspace = true
11+
12+
[[bin]]
13+
name = "dstack-tee-simulator"
14+
path = "src/main.rs"
15+
16+
[dependencies]
17+
anyhow.workspace = true
18+
cc-eventlog.workspace = true
19+
clap.workspace = true
20+
fuser.workspace = true
21+
libc.workspace = true
22+
sd-notify.workspace = true
23+
sha2.workspace = true
24+
tracing.workspace = true
25+
tracing-subscriber.workspace = true
26+
27+
[dev-dependencies]
28+
dcap-qvl.workspace = true

dstack/tee-simulator/src/main.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
use std::path::{Path, PathBuf};
6+
7+
use anyhow::{bail, Context, Result};
8+
use clap::{Parser, ValueEnum};
9+
use fuser::{Filesystem, MountOption, Session};
10+
use tracing::info;
11+
12+
mod tdx;
13+
14+
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
15+
enum TeePlatform {
16+
#[default]
17+
Tdx,
18+
}
19+
20+
#[derive(Parser)]
21+
#[command(about = "Development-only simulator for Linux TEE guest ABIs")]
22+
struct Args {
23+
/// TEE platform ABI to simulate.
24+
#[arg(long, value_enum, default_value = "tdx")]
25+
platform: TeePlatform,
26+
27+
/// Override the platform backend's default mountpoint.
28+
#[arg(long)]
29+
mountpoint: Option<PathBuf>,
30+
}
31+
32+
/// Platform-specific simulator backend.
33+
///
34+
/// Adding another TEE platform only requires a backend implementation and a
35+
/// CLI enum variant; the FUSE lifecycle and safety checks remain shared.
36+
trait TeeBackend {
37+
type Fs: Filesystem;
38+
39+
const PLATFORM: &'static str;
40+
const DEFAULT_MOUNTPOINT: &'static str;
41+
42+
fn create_filesystem() -> Result<Self::Fs>;
43+
fn prepare_mountpoint(mountpoint: &Path) -> Result<()>;
44+
fn real_tee_available() -> bool;
45+
}
46+
47+
fn run_backend<B: TeeBackend>(mountpoint_override: Option<PathBuf>) -> Result<()> {
48+
let default_mountpoint = Path::new(B::DEFAULT_MOUNTPOINT);
49+
let mountpoint = mountpoint_override.as_deref().unwrap_or(default_mountpoint);
50+
51+
if mountpoint == default_mountpoint && B::real_tee_available() {
52+
bail!(
53+
"refusing to start the {} simulator when a real TEE device is present",
54+
B::PLATFORM
55+
);
56+
}
57+
B::prepare_mountpoint(mountpoint)?;
58+
59+
let fs = B::create_filesystem()?;
60+
let mut session = Session::new(
61+
fs,
62+
mountpoint,
63+
&[
64+
MountOption::FSName(format!("dstack-tee-simulator-{}", B::PLATFORM)),
65+
MountOption::DefaultPermissions,
66+
MountOption::NoSuid,
67+
MountOption::NoDev,
68+
MountOption::NoExec,
69+
],
70+
)
71+
.with_context(|| format!("failed to mount FUSE at {}", mountpoint.display()))?;
72+
73+
info!(
74+
platform = B::PLATFORM,
75+
mountpoint = %mountpoint.display(),
76+
"development TEE simulator ready"
77+
);
78+
sd_notify::notify(true, &[sd_notify::NotifyState::Ready])
79+
.context("failed to notify systemd")?;
80+
session.run().context("fuse session failed")?;
81+
Ok(())
82+
}
83+
84+
fn main() -> Result<()> {
85+
tracing_subscriber::fmt()
86+
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
87+
.init();
88+
let args = Args::parse();
89+
90+
match args.platform {
91+
TeePlatform::Tdx => run_backend::<tdx::TdxBackend>(args.mountpoint),
92+
}
93+
}
94+
95+
#[cfg(test)]
96+
mod tests {
97+
use super::*;
98+
99+
#[test]
100+
fn defaults_to_tdx() {
101+
let args = Args::try_parse_from(["dstack-tee-simulator"]).unwrap();
102+
assert_eq!(args.platform, TeePlatform::Tdx);
103+
assert!(args.mountpoint.is_none());
104+
}
105+
}

0 commit comments

Comments
 (0)